DataSnap Clients and Servers

   

DataSnap Clients and Servers

The best way to understand what DataSnap is and how it works is to actually build an application, consisting of a client and a server. Usually, I start with the DataSnap server to encapsulate and export the datasets. Then, the next step is to build a DataSnap client that connects to this server and displays the data in some way.

Creating a Simple DataSnap Server

To build your first DataSnap server, select File, NewApplication to open a new empty application. The fact that the main form of this application is shown ensures that the DataSnap server will remain loaded (the message loop of the main form keeps the DataSnap server alive ). The Caption property of the main form is set to C++Builder 6 Developer's Guide . However, to be able to easily identify the DataSnap server, I always drop a TLabel on the main form, set its Font property to something that's big and readable (like Comic Sans MS 24pt.), its Transparent property to true , and set the Caption property of the TLabel component to the name of the DataSnap server ( C++Builder 6 DataSnap Server in this case). Resizing the main form and giving it a noticeable background color in the Color property helps to identify it among your other applications, as shown in Figure 20.1.

Figure 20.1. The C++Builder 6 DataSnap Server main form.

graphics/20fig01.jpg

To turn a regular application into a middleware database server, you have to add a remote data module to it. This special data module can be found on the Multitier tab of the Object Repository (see Figure 20.2), so select File, NewOther and go to the Multitier tab.

Figure 20.2. The Remote Data Module icon inside the Object Repository.

graphics/20fig02.gif

The Multitier tab shows several CORBA wizards, a remote data module, and a Transactional Data Module. The Transactional Data Module can be used with Microsoft Transaction Server (MTS) prior to Windows 2000 or COM+ in Windows 2000 and later; it won't be covered here. It's the normal remote data module that you need to select to create your first simple DataSnap server.

When you select the Remote Data Module icon and click the OK button, the New Remote Data Module Object dialog, which is shown in Figure 20.3, opens.

Figure 20.3. The New Remote Data Module Object dialog.

graphics/20fig03.gif

There are a few options you must specify. CoClass Name is the name of the internal class. This must be a name that you can remember, so use SimpleDataSnapServer at this time (one word, because no spaces are allowed). Threading Model is the second option you can set. By default, it is set to Apartment, which is almost always the correct choice. Alternative choices are Single , Free , Both , and Neutral . Although you almost never need to change this option, it's important to know what they all mean.

The Single threading model setting supports only one client request at a time. If more than one client wants to make a request, they must all wait in line. Only one client request is executed and in a single thread. This avoids all possible multithreading issues, but it can kill your performance (unless you never expect more than one client to make a request at the same time).

The Apartment threading model setting assumes that more than one client can make a request at roughly the same time, meaning that more than one request might need to be handled simultaneously . Using the Apartment threading model means that each instance of the remote data module handles one client request at a time. To handle more client requests , a separate thread is created for each request. As a consequence, each request runs in its own separate little apartment (hence the name) and, although instance data is safe, you must be aware of threading issues with global variables . This model can be used with regular BDE datasets, in which you need a TSession component with AutoSessionName set to true to make sure each thread (that is, each request) gets its own unique BDE session.

The Free threading model means that each thread can handle more than one client request at the same time. This approach gets harder because you must guard against not only global variable threading issues, but also against instance data. This threading model can be selected when you're using ADO datasets.

The Both threading model is a variation on the Free threading model, with serialized callbacks to client interfaces. This will not be covered in this chapter.

The final model is the Neutral threading model. This is a new model that's available only under COM+ (in Windows 2000 or XP) and will otherwise map to the Apartment threading model. On Windows 2000 and later, Neutral means that callbacks will always be serialized.

See the "Choosing a Threading Model" section of Chapter 17, "COM Programming," of this book for more information about the different threading models and their consequences.

After the Threading Model option, you can enter a description. What you enter here will end up in the Registry for the ProgID of the application server interface. It is also the help string for the interface in the type library. You can enter anything you want, but I've entered C++Builder 6 Developer's Guide Simple DataSnap Server , as you can see in Figure 20.4.

Figure 20.4. Completed New Remote Data Module Object dialog.

graphics/20fig04.gif

Finally, you might want to let the wizard generate a separate interface for managing events with the Generate Event Support Code option. Managing events in automation objects is not a topic of this chapter, so leave this option unchecked.

After you've completed all options inside the New Remote Data Module Object dialog (see Figure 20.4 for my options), press OK to generate the remote data module. The result is a remote data module that looks very much like a regular data module. Visually, there's no difference, and you can treat it like a regular data module by dropping a TSession component (from the BDE tab) on it and setting the AutoSessionName property of the TSession component to true . (Remember that you need to do this when using BDE and the Apartment threading model, as discussed earlier.)

After you have a TSession component, you can add other components from the Data Access tab of the Component Palette. For example, you can drop a TTable component and set its Name to tblCustomer . Set its DatabaseName property to BCDEMOS and open the drop-down combo box for the TableName property to select the customer.db table.

Now it's time to work on the so-called remote aspects of this data module. Go to the Data Access tab of the Component Palette. Here you'll find a TDataSetProvider component. This component is the key to exporting datasets from a remote data module to the outside world (more specifically to DataSnap client applications). Drop the TDataSetProvider component on the remote data module, set its Name to dspCustomer , and assign its DataSet property to tblCustomer . This means that the TDataSetProvider will provide or export tblCustomer to a DataSnap client application that connects to it (one that you will build in the following section). The RemoteDataModule of SimpleDataSnapServer should now look similar to Figure 20.5.

Figure 20.5. The SimpleDataSnapServer remote data module.

graphics/20fig05.gif

NOTE

Later in this chapter, we'll examine the TDataSetProvider component in more detail. For now, the most important property is the Exported property, which is set to true to indicate that tblCustomer is exported (by default). You can set this property to false to hide the fact that tblCustomer is exported from the remote data module, so clients cannot connect to it. This can be useful for example in a 24x7running middleware data base server where you need to make a backup of certain tables and must ensure that nobody is working on them during the backup. With the Exported property set to false , no one can make a connection to them (until you set it to true again, of course).


Basically, this is all it takes to create a simple DataSnap server. The only thing that's left for you is to save the project (for example using a Save All). I've put the main form in file SimpleDataSnapServerMainForm.cpp , the remote data module will be placed in file SimpleDataSnapServerImpl.cpp , and I've put the project itself in SimpleData SnapServer.bpr . You can save the type library in SimpleDataSnapServer.tlb. After the project is saved, you need to compile and run it. Running the DataSnap server which shows only the main form, of coursewill register it (inside the Windows Registry), so any DataSnap client can find and connect to it. If you ever want to move the DataSnap server to another directory (on the same machine), you only need to move it and immediately run it again, so it re-registers itself for that new location. This is a very convenient way of managing DataSnap server applications.

Later in this chapter, we'll see how to deploy a DataSnap server on another machine (in case you're wondering at this time).

DataSnap Server Registration

Let's examine this DataSnap server application registration process in a little more detail. Open the header file for the source unit SimpleDataSnapServerImpl . Inside SimpleDataSnapServerImpl.h , you'll see a function definition for UpdateRegistry() , which is repeated in Listing 20.1 for your convenience.

Listing 20.1 Function UpdateRegistry
 // Function invoked to (un)register object  //  static HRESULT WINAPI UpdateRegistry(BOOL bRegister)  {    TRemoteDataModuleRegistrar regObj(GetObjectCLSID(), GetProgID(),           GetDescription());    // Disable these flags in order to disable use by socket or Web connections.    // Also set other flags to configure the behavior of your application server.    // For more information, see atlmod.h and atlvcl.cpp.    regObj.Singleton = false;    regObj.EnableWeb = true;    regObj.EnableSocket = true;    return regObj.UpdateRegistry(bRegister);  } 

The UpdateRegistry() function ensures that the DataSnap remote data module is registered (or unregistered) automatically when you want to use the middleware application server as an automation server. Note that the UpdateRegistry() method enables both socket and Web connections (using HTTP). If for some reason you want to disable one of these protocols, you simply have to assign false to the EnableWeb or EnableSocket field of the regObj variable.

Borland's C++Builder documentation even treats this as a security feature: If your DataSnap server is not registered to support the socket or Web connection, it won't be visible to a socket or Web connection component at all. C++Builder 4 MIDAS 2 servers never had any of this and, as a result, you could basically run any automation object on the server using the socket connection component. To prevent that, the C++Builder Socket (and Web) connection components will now show only DataSnap servers that are registered properly.

So far, you haven't written a single line of C++ code for the simple DataSnap server. Let's see what it takes to write a DataSnap client to connect to the simple DataSnap Server.

Creating a DataSnap Client

There are a number of different DataSnap clients that you can develop. These include regular Windows (GUI) applications, ActiveForms, and even Web server applications (using Web Broker or InternetExpress). In fact, just about everything can act as a DataSnap client, as you'll see in a moment. For now, you'll create a simple regular Windows application that will act as the first simple DataSnap client to connect to the simple DataSnap server of the previous section. At this stage, you should not be trying to run the client and the server on separate machines. Instead, get everything up and running on one machine, and then later you can distribute the application on the network.

Select File, NewApplication to start a new C++Builder application.

NOTE

At this time, you might decide to add a data module to it (using File, NewOther, and selecting a data module from the New tab of the Object Repository). To avoid unnecessary screenshots in this book, I skip the data module and use the main form to drop my nonvisual (DataSnap) components as well as my normal visual components in one place.


Before anything else, your DataSnap client must make a connection with the DataSnap server application. This connection can be made using a number of different protocols, such as (D)COM, TCP/IP (sockets), HTTP, and SOAP. The components that implement these connection protocols are TDCOMConnection , TSocketConnection , TWebConnection , and TSOAPConnection , respectively, and will be covered in more detail in the next chapter. For the first SimpleDataSnapClient , you'll use the TDCOMConnection component, so drop one from the DataSnap tab onto the main form of your DataSnap client.

The TDCOMConnection component has a property called ServerName , which holds the name of the DataSnap server you want to connect to. In fact, if you open the drop-down combo box for the ServerName property in the Object Inspector, you'll see a list of all registered DataSnap servers on your local machine. In your case, this list might include only one item ( SimpleDataSnapServer.SimpleDataSnapServer ), but all DataSnap servers that are registered will end up in this list eventually. The names consist of two parts : The part before the dot denotes the application name, and the part after the dot denotes the remote data module name. In the current case, select the SimpleDataSnapServer remote data module of the SimpleDataSnapServer application. After you've selected this ServerName , you'll notice that the ServerGUID property of the TDCOMConnection component also gets a value, as found in the Registry. Developers with a good memory are free to type the ServerGUID property here to automatically get the corresponding ServerName name.

The fun really starts when you double-click the Connected property of the TDCOMConnection component, which will toggle this property value from false to true . To actually make the connection, the DataSnap server will be executed (automatically). This results in the automatic execution and opening of the main form of the SimpleDataSnapServer that you created in the previous section. See Figure 20.6 for the resulting SimpleDataSnapServer at runtime.

Figure 20.6. The SimpleDataSnapServer at runtime.

graphics/20fig06.gif

NOTE

It might appear now that there are two ways to close the DataSnap servereither by assigning false to the Connected property of the TDCOMConnection component or by simply closing down the SimpleDataSnapServer (by clicking the close button of its main form). The former will work, but the latter is not a good idea because a COM Server Warning will try to tell you (see Figure 20.7).

Figure 20.7. COM Server Warning when closing SimpleDataSnapServer the wrong way.

graphics/20fig07.gif

If you still decide to close the DataSnap server this way, the TDCOMConnection component on the DataSnap client main form will still think it's connected. In a real-world situation where a DataSnap server (or a connection to it) is terminated , the same thing will happen: The DataSnap client still thinks it has a connection, but in fact the connection is gone. In the "Implementing Error Handling" section of this chapter, we'll cover the error checking that you must include to be able to survive such circumstances without too many problems.


Double-click the Connected property of the TDCOMConnection component again to close down the DataSnap server. Now that you've seen you can connect to it, it's time to import some of the datasets that are exported by the remote data module, or rather by the TDataSetProvider component on the remote data module. Drop a TClientDataSet component on the main form, and connect its RemoteServer property to the TDCOMConnection component. The TClientDataSet component will obtain its data from the DataSnap server. You now need to specify which provider to usein other words, from which TDataSetProvider you want to import the dataset into the TClientDataSet component. This can be done with the ProviderName property of the TClientDataSet component. Just open the drop-down combo box and you'll see a list of all available provider names; those that have their Exported property set to true . In this case, there is only onethe only TDataSetProvider component that you used on the SimpleDataSnapServer in the previous sectionso select that one ( dspCustomer ).

NOTE

Before you picked a value for the ProviderName property, you closed down the SimpleDataSnapServer . However, when you opened up the drop-down combo box to list all available TDataSetProvider components on the remote data module that currently have their Exported property set to true , there is only one way (for C++Builder and the Object Inspector) to know exactly which of these providers are availableby asking the SimpleDataSnapServer (more specifically, by actively looking at the remote data module and finding out which of the available TDataSetProvider components have their Exported property set to true . And because the SimpleDataSnapServer was down, it has to be started again to present this list to you in the Object Inspector. As a result, the moment you drop-down the combo box of the ProviderName property, the SimpleDataSnapServer will be started again.


After you've selected the RemoteServer and ProviderName , it's time to open (or activate) the TClientDataSet . You can do this by setting the Active property of the TClientDataSet component to true . At that time, the SimpleDataSnapServer is feeding data from the tblCustomer table via the TDataSetProvider component and a (D)COM connection to the TDCOMConnection component, which routes it to the TClientDataSet component on your simple DataSnap client.

Now you can drop a TDataSource component and move to the Data Controls tab of the Component Palette and drop one or more data-aware controls. To keep the example simple, just drop a TDBGrid component. Connect the DataSet property of the TDataSource component to the TClientDataSet , and connect the DataSource property of the TDBGrid component to the TDataSource . Because the TClientDataSet component was just activated, you should immediately see live data at design time, provided by the SimpleDataSnapServer .

In Figure 20.8 you'll see the SimpleDataSnapClient main form so far. Note that I've enabled Component Captions, an option found in the Preferences tab of the Tools, Environment Options dialog.

Figure 20.8. SimpleDataSnapClient at design time.

graphics/20fig08.jpg

Now save your work. Put the main form in the ClientMainForm.cpp file and call the project SimpleDataSnapClient . Now you're ready to compile and run the DataSnap client. Again, you haven't written a single line of C++ code, but rest assuredthat will change soon enough in the upcoming sections.

Using the Briefcase Model

When you run the SimpleDataSnapClient , you see the entire CustomerTable data inside the grid. You can browse through it, change field values, even enter new records or delete records. However, after you close the application, all changes are gone, and you're back at the original dataset inside the C++Builder IDE again. No matter how hard you try; the changes that you make to the visual data seem to affect the data inside the (local) TClientDataSet only, and not the (remote) actual tblCustomer .

What you experience here is actually a feature of the so-called briefcase model . Using this model, you can disconnect the client from the network and still access the data. To do so, save a remote dataset to disk, shut down your machine, and disconnect from the network. You can then boot up again and edit your (local) data without connecting to the network.

When you get back to the network, you can reconnect and update the database. A special mechanism notifies you of database errors and any conflicts that need to be resolved. For instance, if two people edited the same record, you will be notified and given options to resolve any problem.

You don't actually have to be able to reach the server at all times to be able to work with your data. This capability is ideal for laptop users, or for sites that want to keep database traffic to a minimum.

You've already experienced that (apparently) your SimpleDataSnapClient works on the local data inside your TClientDataSet component only. It appears you can even save the data to a local file and load it again. To save the current content of a TClientDataSet , you need to drop a TButton on the main form, set the Name property to btnSave , set Caption to Save , and write the following C++ code for the OnClick event handler:

 void __fastcall TForm1::btnSaveClick(TObject *Sender)  {    ClientDataSet1->SaveToFile("customer.cds",dfBinary);  } 

This saves all records from the TClientDataSet in a file called customer.cds in the current directory. cds stands for ClientDataSet , but you can use your own file and extension names, of course. Note the dfBinary flag that is passed as the second argument to the SaveToFile method of TClientDataSet . This value indicates that I want to save the data in binaryInprise/Borland proprietyformat. Alternately, I could specify to save the data in XML format, passing the dfXML value. An XML file will be much larger (14,108 versus 7,493 bytes for the entire tblCustomer data), but it has the advantage that it can be used by other applications as well. You won't be doing so in this chapter, so I'll stick to the smaller (and more efficient) binary format.

Similarly, to implement the functionality that you can load the customer.cds file again into your TClientDataSet component, you need to drop another TButton component, set its Name property to btnLoad , set Caption to Load , and write the following C++ code for the OnClick event handler:

 void __fastcall TForm1::btnLoadClick(TObject *Sender)  {    ClientDataSet1->LoadFromFile("customer.cds");  } 

Note that the LoadFromFile method of the TClientDataSet component does not need a second argument; it's obviously smart enough to determine whether it's reading a binary or an XML file. And, although the binary file can probably be generated only by another TClientDataSet component, the XML file could actually have been produced by a different application.

Armed with these two buttons , you can now (locally) save the changes to your data and even reload those changeseven if you stop and start the simple DataSnap client application again.

To control whether the TClientDataSet component is connected to the DataSnap server live, you can drop a third TButton component on the form that toggles the Active property of the TClientDataSet component. Set the Name property of this TButton to btnConnect and give the Caption property the value Connect . Now, write the following code for the OnClick event handler, as can be seen in Listing 20.2.

Listing 20.2 btnConnect OnClick Event Handler
 void __fastcall TForm1::btnConnectClick(TObject *Sender)  {    if (ClientDataSet1->Active) // close and disconnect    {      ClientDataSet1->Close();      DCOMConnection1->Close();    }    else // open (will automatically connect)    {  //  DCOMConnection1->Open();      ClientDataSet1->Open();    }  } 

NOTE

Note that to close the connection, you actually have to close the TClientDataSet component and close the TDCOMConnection as well. To open the connection, you need only to open the TClientDataSet component, which will implicitly open the TDCOMConnection as well.


Finally, there's one more thing you really need to do: make sure the TDCOMConnection and TClientDataSet components are not connected to the SimpleDataSnapServer at design time. Otherwise, whenever you open your SimpleDataSnapClient project in the C++Builder IDE again, it will need to make a connection to the SimpleDataSnapServer loading that DataSnap server. And whenfor one reason or another SimpleDataSnapServer is not found on your machine, you will have a hard time loading the SimpleDataSnapClient project. So, I always make sure they are not connected at design time. To do so, you have to assign false to the Connected property of the TDCOMConnection component (which will unload the main form of the SimpleDataSnapServer ) and false to the Active property of the TClientDataSet component (which means you won't see any data at design time anymore).

NOTE

If you try to talk to a DCOM server, but can't reach it, the system will not immediately give up the search. Instead, it can keep trying for a set period of time that rarely exceeds two minutes. During those two minutes, however, the application will be busy and will appear to be locked up. If the application is loaded into the IDE, all C++Builder will appear to be locked up. You can have this problem when you do nothing more than attempt to set the Connected property of the TDCOMConnection component to true .

Note that there is no solution to this problem. This is simply a warning not to leave the Connected property of a TDOMConnection set to true because it can cause your IDE (and machine) to appear hung when you next open the project.


Now, when you recompile and run your SimpleDataSnapClient , it will show up with no data inside the TDBGrid component (see Figure 20.9). This is the time to click the Connect button to connect to the SimpleDataSnapServer and obtain all records (from the database server). However, there are times (for example, when you are on the road or not connected to the machine that runs the SimpleDataSnapServer ), when you cannot connect to the SimpleDataSnapServer . In those cases, you can click the Load button instead and work on the local copy of the records. Note that this local copy is the one that you last saved, and it is updated only when you click the Save button to write the entire contents of the TClientDataSet component to disk.

Figure 20.9. SimpleDataSnapClient at runtime (without SimpleDataSnapServer running).

graphics/20fig09.gif

At this time you might want to write some additional code that disables the Save button until some data is present inside the TClientDataSet component. Otherwise, clicking the Save button has no effect (including not removing or overwriting the current customer.cds fileif you have one).

Another useful enhancement consists of changing the Caption property of the btnConnect buttton from Connect to Disconnect (and back) when connecting. This can be done with the code as shown in Listing 20.3.

Listing 20.3 btnConnect OnClick Event Handler
 void __fastcall TForm1::btnConnectClick(TObject *Sender)  {    if (ClientDataSet1->Active) // close and disconnect    {      ClientDataSet1->Close();      DCOMConnection1->Close();      dynamic_cast<TButton*>(Sender)->Caption = "Connect";    }    else // open (will automatically connect)    {  //  DCOMConnection1->Open();      ClientDataSet1->Open();      dynamic_cast<TButton*>(Sender)->Caption = "Disconnect";    }  } 

Using ApplyUpdates

It's nice to be able to connect or load a local dataset and save it to disk again. But how do you ever apply your updates to the actual (remote) database again? This can be done using the ApplyUpdates method of the TClientDataSet component.

Drop a fourth button on the SimpleDataSnapClient main form, set its Name property to btnApplyUpdates and the Caption property to Apply Updates . The OnClick event handler of the Apply button should get the following code:

 void __fastcall TForm1::btnApplyUpdatesClick(TObject *Sender)  {    ClientDataSet1->ApplyUpdates(0);  } 

The ApplyUpdates method of the TClientDataSet component has one argument: the maximum number of errors that it will allow before it stops applying updates. With a single SimpleDataSnapClient connected to the SimpleDataSnapServer , you will never encounter any problems, so feel free to run your SimpleDataSnapClient now. Click the Connect button to connect to (and load) the SimpleDataSnapServer , and use the Save and Load buttons to store and read the contents of the TClientDataSet component to and from disk. You can even remove your machine from the network and work on your local data for a significant amount of time, which is exactly the idea behind the briefcase model (your laptop being the briefcase). Any changes you make to your local copy will remain visible, and you can apply the changes to the remote database with a click of the Apply Updates buttonwhen you've reconnected to the network with the SimpleDataSnapServer .

Implementing Error Handling

What if two clients, both using the briefcase model, connect to the SimpleDataSnapServer , obtain the entire tblCustomer , and make changes to the first record? According to what you've built so far, both clients could then send the updated record back to the DataSnap server using the ApplyUpdates method of the TClientDataSet component. If both pass zero as value for the MaxErrors argument of ApplyUpdates , the second one to attempt the update will be stopped. The second client could pass a numerical value bigger than zero to indicate a fixed number of errors or conflicts allowed before the update is stopped . However, even if the second client passed -1 as its argument (to indicate that it should continue updating no matter how many errors occur), it will never update the records that have been changed by the previous client. You need reconcile actions to handle updates on already-updated records and fields.

Fortunately, C++Builder contains a very useful dialog especially written for this purpose. Whenever you need to do error reconciliation, you should consider adding this dialog to your DataSnap client application (or write one yourself, but at least do something about it). To use the one available in C++Builder, just select File, New Other, go to the Dialogs tab of the Object Repository and select the Reconcile Error Dialog icon, which can be seen in Figure 20.10.

Figure 20.10. The Reconcile Error Dialog icon inside the Object Repository.

graphics/20fig10.gif

After you select this icon and click OK, a new unit is added to your SimpleDataSnapClient project. This unit contains the definition and implementation of the Update Error dialog that can be used to resolve database update errors (see Figure 20.11).

Figure 20.11. Update (Reconcile) Error dialog at design time.

graphics/20fig11.jpg

After this unit is added to your SimpleDataSnapProject , there is something very important you have to check. First save your work (put the new unit in file ErrorDialog.cpp ).

When or how do you use this special ReconcileErrorForm ? It's actually very simple. For every record for which the update did not succeed (for whatever reason), the OnReconcileError event handler of the TClientDataSet component is called. This event handler of TClientDataSet is defined as follows :

 void __fastcall TForm1::ClientDataSet1ReconcileError(TClientDataSet *DataSet, EReconcileError *E, TUpdateKind UpdateKind,        TReconcileAction &Action)  {  } 

This event handler has four arguments: the TClientDataSet component that raised the error, a specific ReconcileError that contains a message about the cause of the error condition, the UpdateKind that generated the error (insert, delete, or modify), and the Action that should be taken. Action can return the following enum values (the order is based on their actual enum values):

  • raSkip Do not update this record, but leave the unapplied changes in the change log. Be ready to try again next time.

  • raAbort Abort the entire reconcile handling; no more records will be passed to the OnReconcileError event handler.

  • raMerge Merge the updated record with the current record in the (remote) database, only changing (remote) field values if they changed on your side.

  • raCorrect Replace the updated record with a corrected value of the record that you made in the event handler (or inside ReconcileErrorDialog ). This is the option in which user intervention is required.

  • raCancel Undo all changes inside this record, turning it back into the original (local) record.

  • raRefresh Undo all changes inside this record, but reload the record values from the current (remote) database, not from the original local record you had.

The good thing about ReconcileErrorForm is that you don't really need to concern yourself with all this. You only need to pass the arguments from the OnReconcileError event handler in the TClientDataSet component to the HandleReconcileError function from the ErrorDialog .

This can be done in two steps. First, you need to include the ErrorDialog unit header inside the SimpleDataSnapClient main form definition (or the data module, if you decided to use one). Click the ClientMainForm and select File, Include Unit Hdr... to get the Use Unit dialog (see Figure 20.12).

Figure 20.12. Add the ErrorDialog unit header to the ClientMainForm unit.

graphics/20fig12.gif

With the ClientMainForm as your current unit, the Use Unit dialog will list the only other available unit, which is the ErrorDialog . Just select it and click OK.

The second thing you need to do is to write one line of code in the OnReconcileError event handler of the TClientDataSet component to call the HandleReconcileError() function from the ErrorDialog unit (that you just added to your ClientMainForm import list). The HandleReconcileError() function has the same four arguments as the OnReconcileError event handler (not a real coincidence , of course), so it's a matter of passing arguments from one to anothernothing more, nothing less. The OnReconcileError event handler of the TClientDataSet component can be coded similar to Listing 20.4.

Listing 20.4 Completed OnReconcileError Event Handler
 void __fastcall TForm1::ClientDataSet1ReconcileError(TClientDataSet *DataSet, EReconcileError *E, TUpdateKind UpdateKind,        TReconcileAction &Action)  {    Action = HandleReconcileError(this, DataSet, UpdateKind, E);  } 

Demonstrating Reconcile Errors

How does all this work in practice? To test it, you obviously need two (or more) SimpleDataSnapClient applications running simultaneously. For a complete test using the current SimpleDataSnapClient and SimpleDataSnapServer applications, you need to perform the following steps:

  1. Start the first SimpleDataSnapClient and click the Connect button (the SimpleDataSnapServer will now be loaded as well).

  2. Start the second SimpleDataSnapClient and click the Connect button. Data will be obtained from the SimpleDataSnapServer that's already running.

  3. Using the first SimpleDataSnapClient , change the Company field for the first record (for example, change it to "Bob Swart Training Consultancy").

  4. Using the second SimpleDataSnapClient , also change the Company field for the first record (make sure you don't change it to the same value as in the previous stepfor example, change it to eBob42 ).

  5. Click the Apply Updates button of the first SimpleDataSnapClient . All updates will be applied without any problems.

  6. Click the Apply Updates button of the second SimpleDataSnapClient . This time, one or more errors will occur because the first record had its Company field value changed (by the first SimpleDataSnapClient ). The OnReconcileError event handler is called.

  7. Inside the Update Error dialog (see Figure 20.13), you can now experiment with the Reconcile Actions (Abort, Skip, Cancel, Correct, Refresh, and Merge) to get a feel for what they do. Pay special attention to the differences between Skip and Cancel and those between Correct, Refresh, and Merge.

    Figure 20.13. The Reconcile Error dialog in action.

    graphics/20fig13.jpg

Skip moves on to the next record, skipping the requested update (for the time being). The unapplied change will remain in the change log. Cancel also skips the requested update, but it cancels all further updates (in the same update packet). The current update request is skipped in both cases, but Skip continues with other update requests, and Cancel cancels the entire ApplyUpdate request.

Refresh just forgets all updates you made to the record and refreshes the record with the current value from the server database. Merge tries to merge the update record with the record on the server, placing your changes inside the server record. Refresh and Merge will not process the change request any further, so the records are synchronized after Refresh and Merge (whereas the change request can still be redone after a Skip or Cancel).

Correct , the most powerful option actually gives you the option of customizing the update record inside the event handler. For this you need to write some code or enter the values in the dialog yourself.

Creating a DataSnap Master-Detail Server

Time to start a second, more complex example of a DataSnap server. Because we are using different filenames, you can put it in the same directory as the SimpleDataSnapServer. However, feel free to put each DataSnap Server and Client in its own directory (which is easier to maintain if these applications grow in size and complexity). I've listed the steps you need to perform, to make it a bit easier.

  • First, start a new project using File, NewApplication. Save the main form in DataSnapServerMainForm.cpp and the project in DataSnapServer.bpr .

  • Like the first SimpleDataSnapServer example, make sure the main form can be identified as your (second) DataSnap server application (see Figure 20.14). This means just adding a label, an image, or anything that will help identify this main form, so you'll know immediately when it (and hence your second DataSnap server) is running.

    Figure 20.14. Master-detail DataSnap server main form.

    graphics/20fig14.gif

  • Next, start the Remote Data Module Wizard from the Multitier tab of the Object Repository, as you've done before. This time, specify CustomerOrders as CoClass Name, and use the default values for all other options. This will result in a middleware database server with the name DataSnapServer.CustomerOrders , as you'll see when you start to build the DataSnapClient for this server.

  • After you have a new remote data module, drop two TTable components. Set the Name property of one to tblCustomer and the other to tblOrders .

  • For each of these TTable components, set the DatabaseName property to BCDEMOS .

  • Click tblCustomer and select customer.db as the value for the TableName property. Click tblOrders and select orders.db as the value for the TableName property.

You're now ready to define the master-detail relationship between tblCustomer and tblOrders .

  • Drop a TDataSource component on the remote data module. Set its Name to dsCustomer , and its DataSet property to tblCustomers . Select the tblOrders , and set its MasterSource property to the DataSource.

  • Click the elipsis for the MasterFields property of tblOrders . This will show the Field Link Designer. Select CustNo as Available Index, and select CustNo as both the Detail Field and the Master Field. Next, click the Add button to add the Joined Fields (as shown in Figure 20.15).

    Figure 20.15. The Field Link Designer for tblCustomer and tblOrders .

    graphics/20fig15.gif

  • If you click OK again, the Field Link Designer will close and the master-detail relationship between tblCustomer and tblOrders has been created.

Now that you have created the master-detail relationship, it's time to export the tables to the outside world.

Exporting Master-Detail DataSets

In the SimpleDataSnapServer example, you used a single TDataSetProvider component to export tblCustomer from the remote data module. This time, you might feel the urge to use two TDataSetProvider components: one to export tblCustomer and one to export tblOrders from the remote data module. That would export the two tables all right, but not their master-detail relationship. In fact, you would have to redefine the master-detail relationship at the client side again. This might work for a normal application (defining the master-detail relationship at the client side). However, for a multitier application in which a database server provides the data for the tables, this situation has at least two real problems.

First of all, the detail TClientDataSet component on the DataSnapClient will have to fetch and store all detail records from the database server, even if only a few of the detail records are actually needed at the client side (after the master-detail relationship has been established). A potentially large number of records are sent over for nothing, wasting precious bandwidth. Of course, this problem can be overcome by using parameters, sent from the client to the server, but this involves more work and could introduce bugs that are hard to trace.

The second problem in defining the master-detail relationship on the client side has to do with the fact that it's more difficult to apply updates using two separate client datasets. This is caused by the fact that the TClientData component doesn't apply updates for multiple tables in a single transaction, but on a dataset-by-dataset basis (that is, you must make a separate call to ApplyUpdates for each table).

As a result, you should try not to export master-detail datasets as separate entities. Fortunately, TDataSetProvider is able to export two (or more) tables having a master-detail relationship as a single entityprovided you connect the TDataSetProvider component to the master TTable , being the tblCustomer of your DataSnap server. The trick is that the master table will automatically include a DataSetField for the detail records, and only for those detail records that are relevant to the current master record, sending only those records over the wire that are needed.

  • You need only to drop a single TDataSetProvider component (which can be found on the Data Access tab) on the remote data module, set its Name to dspCustomerOrders , and connect its DataSet property to tblCustomer . This will export both tblCustomer and tblOrders (as a nested field) from the remote data module.

  • Save your work again (the assigned name is CustomerOrdersImpl.cpp ). The Remote Data Module should resemble the one of Figure 20.16.

    Figure 20.16. The Remote Data Module with tblCustomer and tblOrders .

    graphics/20fig16.gif

Note again that you didn't have to write a single line of C++ code for the DataSnap server application. Compile the DataSnapServer project and run it to register it on your machine. Now it's time to start working on the DataSnap client application that retrieves this master-detail data.

Creating a DataSnap Master-Detail Client

The new DataSnap server needs a new DataSnap client as well. Start another new application (using File, NewApplication). Save the main form as MainForm.cpp and save the project as DataSnapClient.bpr . Drop a TDCOMConnection component (from the DataSnap tab) on the main form. After you open up the drop-down combo box for the ServerName property of the TDCOMConnection component, you should see both SimpleDataSnapServer.SimpleDataSnapServer (the first example) and DataSnapServer.CustomerOrders (the second example). Obviously, you want to select the DataSnapServer.CustomerOrders as the value for the ServerName property. You can set the Connected property of the TDCOMConnection component to true to test if the DataSnapServer actually gets loaded correctly.

Now, drop a TClientDataSet component (which can be found on the Data Access tab) to retrieve the data via the TDCOMConnection component from the remote data module. Connect the RemoteServer property of the TClientDataSet component to the TDCOMConnection component, which is named DCOMConnection1 by default. Next, you need to select the right provider that's exported from the remote data module. In this case, there is still only one provider (you exported only the tblCustomer using dspCustomerOrders ), so select the only choice you have as the value for the ProviderName property of the TClientDataSet component, which should be dspCustomerOrders .

Now, drop a TDataSource component under the TClientDataSet component (so you know that they'll belong together). Connect the DataSet property of the TDataSource component to the TClientDataSet component. Move over to the Data Controls tab of the Component Palette to drop a TDBGrid component on the form. Connect the DataSource property of the TDBGrid component to the TDataSource component.

To see live data at design time again, you only have to set the Active property of the TClientDataSet component to true and presto! See Figure 20.17 for remote customer data in the C++Builder IDE at design time.

Figure 20.17. The DataSnap client main form showing customer data at design time.

graphics/20fig17.jpg

Using Nested Tables

You might have noticed (from Figure 20.17 for example) that the TDBGrid appears to show data only from TableCustomers . If you scroll all the way to the right of the TDBGrid component, you'll notice one last field called tblOrders . The TDBGrid component apparently cannot show the actual contents of this field because it only displays (DATASET) . Actually, that particular last field named tblOrders is a TDataSetField .

It gets even better when you run the DataSnapClient application and click the tblOrders field inside the DBGrid . This will show an ellipsis, and when you click that ellipsis (or double-click the DATASET field itself), a new pop-up window will appear (see Figure 20.18), showing the detail records belonging to the master record that you just clicked.

Figure 20.18. DataSnap client showing customer data and client detail.

graphics/20fig18.jpg

I have to admit thatat firstit looks nice to have a new pop-up window show the detail records of the particular master record (that you used to double-click the DATASET column). However, after a few minutes the excitement disappears, and I wonder about my clients. Would they like this interface? Wouldn't it be better to display the detail records in another TDBGrid component right under the first one? Your taste may differ , but at least it's possible, like almost anything in C++Builder.

Close the DataSnapClient application if it's still running and return to the C++Builder IDE. Drop another TClientDataSet component on the main form (which will be called ClientDataSet2 by default). This time, you need to look at the DataSetField property of ClientDataSet2 ; the second TClientDataSet component. Somehow, you have to connect this property with the persistent tblOrders field of type TDataSetField . The only problemwhich becomes apparent after you drop down the list of available DataSetField sis that there are no persistent fields, yet.

To use the nested dataset (the detail records), you must create a persistent DataSet field for the nested data. This sounds more difficult than it is because the easy way is just to double-click the first TClientDataSet component ( ClientDataSet1 ) to start the Fields Editor (at design time), right-click in the Fields Editor, and select Add All Fields. This will create persistent fields for every field, including a DataSetField for the nested detail table tblOrders , as can be seen in Figure 20.19.

Figure 20.19. The Fields Editor showing tblCustomer fields.

graphics/20fig19.gif

After tblOrders has been turned into a persistent field, you can drop down the combo box for the DataSetField property of the second TClientDataSet component. The combo box will show the ClientDataSet1.tblOrders as the only possible dataset field to select, so pick it. Note that this ClientDataSet is not connected directly to a remote server, but indirectly because it gets its data from the nested dataset that the first TClientDataSet component received from the remote data server.

You can now drop a second TDataSource component (from the Data Access tab) and a second TDBGrid component (from the Data Controls tab). Connect the second TDataSource ( DataSource2 ) to the second TClientDataSet ( ClientDataSet2 ), and the second TDBGrid (DBGrid) to this second TDataSource (DataSource2) . This will show live detail data at design time (see Figure 20.20).

Figure 20.20. The DataSnap client main form showing customer and orders data at design time.

graphics/20fig20.gif

This is a good solution for both displaying and updating master-detail relationships. Sometimes displaying the detail in a pop-up window might be what you need, and sometimes my solution using a second TClientDataSet component is more suited.

The problem of updating the master-detail relationship is solved by the fact that you now have only one call to ApplyUpdates to make (from the first TClientDataSet componentthe one directly connected to the remote server). This automatically updates the entire nested table.

Understanding DataSnap Bandwidth Bottlenecks

Although even the SimpleDataSnapServer example has some potential (bandwidth) bottlenecks, they will become more noticeable when looking at the master-detail DataSnapServer and DataSnapClient pair.

When a TClientDataSet is set to Active , it makes a request to the TDataSetProvider component on the remote data module to send data over the wire. How much data depends on both the size of the individual records and, of course, the number of records. The latter is determined by the value of the PacketRecords property of the TClientDataSet component. By default, this property is set to -1 , meaning TClientDataSet just says "send me all available records."

This is hardly a problem for a relatively small BCDEMOS example using customers.db (only 55 records) and orders.db (only 205 records). But imagine a real-world customer's table. Surely it would hold more than 55 customers. Even a small table of customers could easily hold a thousand or more records. And what about the orders? A few thousand perhaps? At a hundred bytes or more for each table, that could lead to a few hundred kilobytes to send over the wire as soon as the DataSnapClient connects to the DataSnapServer (and requests all data to be sent). And that's in a small shop, not an airline reservation desk or an online bookstore. I'm sure you understand why this has the potential of being a serious performance bottleneck if not a show stopper, especially with multiple DataSnapClients all talking to the same DataSnapServer over the same wire.

Minimizing Bottlenecks Using PacketRecords

There are a few ways to minimize the impact of this bottleneck. First and most obvious is to change the PacketRecords property to a value other than -1 . Depending on the number of records you want to display at the same time, you might want to set PacketRecords of the first TClientDataSet to 10 or so. This will ensure that only the first 10 records are transferred when the first connection is made. As soon as you start to browse through the TDBGrid component and reach for the 21st record, the TClientDataSet will perform another request to the TDataSetProvider component on the remote data module, to obtain the next set of 10 records. Thus, after two requests, the client shows 20 records inside the TDBGrid . This continues until all records have been moved from the remote data module to the TClientDataSet component inside the DataSnapClient application.

NOTE

You don't need to modify PacketRecords of the second detail TClientDataSet . The nested dataset is already at the client side, contained as DataSetField within the master record itself.


When you looked closely, you might have noted that the scrollbar thumb of the TDBGrid component seems to shrink in size. That's because the first time, the TClientDataSet obtained only 10 records, which are shown in the TDBGrid componentunlike 55 that are shown when all data is obtained (compare Figures 20.20 and 20.21).

Figure 20.21. DataSnapClient showing the first 10 customers with orders in DBGrids .

graphics/20fig21.jpg

So far so good, however, there are a few things you must be aware of when using this solution. If you run the new DataSnapClient, click the grid, and hit Ctrl+End, you expect to scroll down to the last record. And, sure enough, you do. The bad news is that to show you the last record (the 55th, in this case), the TClientDataSet has to make five new requests to the TDataSetProvider component on the remote data module. The first request gets records 1120, and so on, until the fifth request gets records 5155. In other words, to show you the last record, it has to retrieve all records. And again, in this 55-record scenario, that's not a big deal. But imagine thousands of records, where pressing Ctrl+End could lead to a sudden and significant delay in response time.

Minimizing Bottlenecks Using Server Optimization

Apart from the PacketRecords property, which is a client-side optimization technique, it's often far more useful to look at the server side. Remember that the amount of data that is being sent over the wire is the result of multiplying the record size by the number of records. If you did your best by minimizing the number of records, then it's time to look at the record size. Of course, you cannot just hack the tables and try to shrink down the record size, but you can look at the available fields and make a well-planned decision about which fields are exported (provided) from the remote data module and which fields aren't. In all previous examples, you've simply exported the entire dataset using the TDataSetProvider component. In fact, with DataSnapServer you've even explicitly added all fields to both tblCustomer and tblOrders . All this information is sent from the DataSnapServer to the DataSnapClient. If you need all these fields, there's nothing you can do about it. However, more often than not, only some of the available fields are used at the client side. That means you send maybe 10 fields over the wire when you only need 3. Although you can specify at the client-side which fields you want to see (at both the TDBGrid and TClientDataSet levels), this doesn't matter anymore at that time because the fields have already been transferred. You need to make a conscious decision at the server side. It will make a difference, even with a table that contains a large number of records. If you pass only one or two fields (out of perhaps a dozen ), you're sending only a fraction of the table.

Using the PacketRecords property of the TClientDataSet component for client-side optimizations and reducing the number of fields to include at the server side are just two bandwidth optimization techniques that have proven to be very effective in real-world applications. The remaining part of this chapter focuses on some of the specific DataSnap enhancements that are part of C++Builder 6. The first one is a big issue: the stateless nature of the DataSnap server!


   
Top


C++ Builder Developers Guide
C++Builder 5 Developers Guide
ISBN: 0672319721
EAN: 2147483647
Year: 2002
Pages: 253

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net