One of the biggest changes in DataSnap (introduced with MIDAS 3) is that DataSnap applications now support stateless remote data modules (made possible by the DataSnap IAppServer interface). This means you can now share remote data modules without having to write your own custom interfaces (extending the MIDAS 2 IProvider interface) because no state information is maintained . It also means that each client will have to maintain its own state and send it to the server with every request for data. The DataSnap documentation even states that, although each call from the client to the server carries more (state) information, fewer calls are needed, so message traffic is in fact reduced.
To make this change clear, I've compared the events of the TClientDataSet component in C++Builder 4 (MIDAS 2) and C++Builder 6 (DataSnap). New events introduced for the TClientDataSet component in C++Builder 6 are AfterApplyUpdates , AfterExecute , AfterGetParams , AfterGetRecords , AfterPost , AfterRefresh , AfterRowRequest , and AfterScroll , and of course their counterparts BeforeApplyUpdates , BeforeExecute , BeforeGetParams , BeforeGetRecords , BeforePost , BeforeRefresh , BeforeRowRequest , and BeforeScroll . These eight sets of methods are used to send the necessary state information from the client to the server.
A similar list of differences can be seen on the server side, where I've compared the events for the TDataSetProvider component of C++Builder 4 (MIDAS 2) with C++Builder 6 (DataSnap). The new event handlers of TDataSetProvider are AfterApply Updates , AfterExecute , AfterGetParams , AfterGetRecords , and AfterRowRequest . Their counterparts are BeforeApplyUpdates , BeforeExecute , BeforeGetParams , BeforeGetRecords , and BeforeRowRequest . There is also a new OnGetTableName event handler.
The Before events of TDataSetProvider allow necessary state information to be obtained from the client (sent by a corresponding Before event of the TClientDataSet component). The After events of TDataSetProvider enable state information to be passed back to the client (received by the corresponding After events of the TClientDataSet ).
As an example, let's take the case where we used a value of 10 for PacketRecords again. The use of PacketRecords can have a significant side effect: to allow the DataSnap server to send you the next set of records, it has to somehow remember the state of the client. And, because the client didn't tell the server anything (yet), this means that the DataSnap server is actually stateful. As we'll see in the next chapter, not all connection protocols can support a stateful connection. MTS, HTTP, and SOAP will not support this, and hence if you use a value of 10 for the PacketRecords, you will get the same (first) 10 records every time to request new data. Which might even lead to key violations (because the first 10 records are already present in the local ClientDataSet).
So, let's actually build a Stateless DataSnap Server and implement the way in which TClientDataSet and TDataSetProvider components communicate when asking for the next batch of records. First of all, set the FetchOnDemand property of ClientDataSet1 (the master TClientDataSet component) to false . This is very important; otherwise , you'll get a stack error later, as I'll explain in a moment.
With FetchOnDemand set to false , there will be no automatic mechanism to send (fetch) packets of records from the DataSnap server to the client, so we have to implement it ourselves . An example of a situation in which you want to implement (and control) the fetching of packets of records is when you want (or must) make sure the dataset on the server side is positioned correctly before sending the records over. In this section, we'll see exactly how to do that. This technique can also be important when you're working with stateless environments such as MTS, HTTP, or SOAP.
Now, to get the next batch of records from the TDataSetProvider component on a DataSnap server, the TClientDataSet component (with FetchOnDemand set to false ) on the DataSnapClient application should use the BeforeGetRecords event to specify a certain record or position. This is set using the OwnerData parameter of this event handler (given a dataset with a single key field named CustNo , as in your examples), which can be seen in Listing 20.5.
void __fastcall TForm1::ClientDataSet1BeforeGetRecords(TObject *Sender, OleVariant &OwnerData) { TClientDataSet* Master = (TClientDataSet*) Sender; if (Master->Active) { void* Current = Master->GetBookmark(); try { Master->Last(); OwnerData = Master->FieldByName("CustNo")->AsString; Master->GotoBookmark(Current); } __finally { Master->FreeBookmark(Current); } } }
The Master->Last() statement is the one that will generate a stack overflow if the FetchOnDemand property of the TClientDataSet component is set to true . In that case, moving to the last record will actually trigger the TClientDataSet component to fetch (on demand) all records, which will fire this OnBeforeGetRecords event handler again, and so on until you finally get a stack error.
Anyway, just before the TDataSetProvider component on the remote data module from the DataSnapServer sends the requested records, the BeforeGetRecords event handler is called, including the OwnerData value as you passed on the ClientDataSet side (see Listing 20.6).
void __fastcall TCustomerOrders::dspCustomerOrdersBeforeGetRecords( TObject *Sender, OleVariant &OwnerData) { TVariant Variant = OwnerData; if (!VarIsEmpty(Variant)) { TLocateOptions LocateOptions; TDataSet* DataSet = ((TDataSetProvider*) Sender)->DataSet; if (DataSet->Locate("CustNo", Variant, LocateOptions)) DataSet->Next(); } }
Now that both BeforeGetRecords event handlers have fired , it's time to actually send the records from the DataSnapServer remote data module to the DataSnapClient.
After the records are sent, the TDataSetProvider component is able to send some information back to the TClientDataSet (like the number of actual records in the entire dataset on the server side). This could be started using the AfterGetRecords event handler of the TDataSetProvider component as can be seen in Listing 20.7.
void __fastcall TCustomerOrders::dspCustomerOrdersAfterGetRecords( TObject *Sender, OleVariant &OwnerData) { TDataSet* DataSet = ((TDataSetProvider*) Sender)->DataSet; if (DataSet->Active) OwnerData = IntToStr(DataSet->RecordCount); else OwnerData = AnsiString("n/a"); }
Note that you again pass an AnsiString value in the OwnerData parameter (which is of type OleVariant ). Passing AnsiString values always seems to work for me, whereas a direct assignment of DataSet->RecordCount to OwnerData doesn't compile.
Now, the value of OwnerData as passed by the OnAfterGetRecords event handler of the TDataSetProvider component will be picked up at the client side by the OnAfterGetRecords event handler of the TClientDataSet component. Storing the value somewhere is a different matter, so I've just used a ShowMessage dialog to display the number of records at the server side, as you can see in Listing 20.8.
void __fastcall TForm1::ClientDataSet1AfterGetRecords(TObject *Sender, OleVariant &OwnerData) { ShowMessage("Number of records at server: " + WideString(OwnerData)); }
Using the implementations of the OnBeforeGetRecords and OnAfterGetRecords event handlers for both the TClientDataSet (on the client) and the TDataSetProvider (on the server), you can compile the DataSnapServer and DataSnapClient projects and test them. This is your last chance to set the FetchOnDemand property of the TClientDataSet component to false to prevent a stack error, by the way (as I mentioned at the beginning of this section).
To test them, you need to run the DataSnapClient project, which will load the DataSnapServer when connecting to it. As soon as a connection to the DataSnapServer is made, the TClientDataSet makes its first request for data (calling the GetNextPacket method), which results in a call to the OnBeforeGetRecords() from the TClientDataSet , passing nothing because the Active property is still false at that time. So, the OnBeforeGetRecords method of the TDataSetProvider will be called, but with an empty OwnerData argument, which means no further actions are taken. Then, the first 10 (value of PacketRecords ) records are sent by the TDataSetProvider component to the TClientDataSet component. After this, the OnAfterGetRecords event handler of the TDataSetProvider is called, in which it collects the number of records of its DataSet component (the tblCustomer , which has 55 records). The value 55 is passed in OwnerData and is received at the client side when the OnAfterGetRecords event handler of the TClientDataSet is called. This results in a message dialog, seen in Figure 20.22, that shows Number of records at DataSnap server: 55 , just as expected.
The DataSnapClient will now show up with only the first 10 records inside the DBGrid . When you browse through these records until number 10, and you want the next one, you won't get it. Similarly, when you press Ctrl+End, no additional records are fetched . You see only 10 records at the client (the value of PacketRecords ), and you know that 55 exist at the server. Of course, the reason you don't get any more records at this time is because you've set the FetchOnDemand property to false . To get more packets with records, you now have to call the GetNextPacket method manually. This can be done by adding a TButton component to the client main form ( name it btnFetch and set Caption to Fetch , as shown in Figure 20.23) with the following code for the OnClick event handler:
void __fastcall TForm1::btnFetchClick(TObject *Sender) { ClientDataSet1->GetNextPacket(); }
If you click the Fetch button, the next packet of 10 records is retrieved, resulting in a total of 20 records in the client. Clicking Fetch four more times will retrieve the final 35 records. At that time, the DataSet at the DataSnapServer will no longer be active, so you cannot obtain the RecordCount anymore (which is why I had to add the else clause in the OnAfterGetRecords event for the TDataSetProvider ).
In short, the DataSnap server doesn't know anything ”it is stateless; it has to be told the complete state by the clients, and both clients and server can communicate using the OwnerData parameter of some helpful Before and After event handlers. Note that the OwnerData parameter that is used to pass data is of type OleVariant . You can put just about anything in it, but it helps if you know beforehand what to expect (on the other side), which is why I usually try to pass an AnsiString just to be sure.
Apart from the Before and After events, TClientDataSet has two additional events for Post and Scroll , with no direct counterpart on the TDataSetProvider side. Obviously, these routines have only a DataSet as an argument and no OwnerData .
A final word on this: Assigning true to the FetchOnDemand property of the TClient DataSet will ensure that the relevant state information is automatically sent from the client to the server. However, there are situations in which you might want to be in control, in which case you need to rely on the techniques I showed you in this section. It's also important to be able to manage state by yourself (at the client side) when working with stateless protocols such as MTS, HTTP, or SOAP.
Top |