DataSnap includes many more features than I've covered up to now. Here is a quick tour of some of the more advanced features of the architecture, partially demonstrated by the AppSPlus and ThinPlus examples. Unfortunately, demonstrating every piece of functionality would turn this chapter into an entire book, so I'll limit myself to an overview.
Besides the features discussed in the following sections, the AppSPlus and ThinPlus examples demonstrate the use of a socket connection, limited logging of events and updates on the server side, and direct fetching of a record on the client side. The last feature is accomplished with this call:
procedure TClientForm.ButtonFetchClick(Sender: TObject); begin ButtonFetch.Caption := IntToStr (cds.GetNextPacket); end;
This allows you to get more records than are required by the client user interface (the DBGrid). In other words, you can fetch records directly, without waiting for the user to scroll down in the grid. I suggest you study the details of these complex examples after reading the rest of this section.
If you want to use parameters in a query or stored procedure, then instead of building a custom solution (with a custom method call to the server), you can let Delphi help you. First define the query on the middle tier with a parameter:
select * from customer where Country = :Country
Use the Params property to set the type and default value of the parameter. On the client side, you can use the Fetch Params command from the ClientDataSet's shortcut menu, after connecting the component to the proper provider. At run time, you can call the equivalent FetchParams method of the ClientDataSet component.
Now you can provide a local default value for the parameter by acting on the Params property. The value of the parameter will be sent to the middle tier when you fetch the data. The ThinPlus example refreshes the parameter with the following code:
procedure TFormQuery.btnParamClick(Sender: TObject); begin cdsQuery.Close; cdsQuery.Params.AsString := EditParam.Text; cdsQuery.Open; end;
You can see the secondary form of this example, which shows the result of the parametric query in a grid, in Figure 16.5. The figure also shows some custom data sent by the server, as explained in the section "Customizing the Data Packets."
Because the server has a normal COM interface, you can add more methods or properties to it and call them from the client. Simply open the server's type library editor and use it as with any other COM server. In the AppSPlus example, I've added a custom Login method with the following implementation:
procedure TAppServerPlus.Login(const Name, Password: WideString); begin // TODO: add actual login code... if Password <> Name then raise Exception.Create ('Wrong name/password combination received') else Query.Active := True; ServerForm.Add ('Login:' + Name + '/' + Password); end;
The program performs a simple test, instead of checking the name/password combination against a list of authorizations as a real application should do. Also, disabling the Query doesn't really work, because it can be activated by the provider; disabling the DataSetProvider is a more robust approach. The client has a simple way to access the server: the AppServer property of the remote connection component. Here is a sample call from the ThinPlus example, which takes place in the AfterConnect event of the connection component:
procedure TClientForm.ConnectionAfterConnect(Sender: TObject); begin Connection.AppServer.Login (Edit2.Text, Edit3.Text); end;
Note that you can call extra methods of the COM interface through DCOM and also using a socket-based or HTTP connection. Because the program uses the safecall calling convention, the exception raised on the server is automatically forwarded and displayed on the client side. This way, when a user selects the Connect check box, the event handler used to enable the client datasets is interrupted, and a user with the wrong password won't be able to see the data.
If your middle-tier application exports multiple datasets, you can retrieve them using multiple ClientDataSet components on the client side and connect them locally to form a master/detail structure. This approach will create quite a few problems for the detail dataset unless you retrieve all the records locally.
This solution also makes it quite complex to apply the updates; you cannot usually cancel a master record until all related detail records have been removed, and you cannot add detail records until the new master record is properly in place. (Different servers handle this situation differently, but in most cases where a foreign key is used, this is the standard behavior.) To solve this problem, you can write complex code on the client side to update the records of the two tables according to the specific rules.
A completely different approach is to retrieve a single dataset that already includes the detail as a dataset field, a field of type TDataSetField. To accomplish this, you need to set up the master/detail relation on the server application:
object TableCustomer: TTable DatabaseName = 'DBDEMOS' TableName = 'customer.db' end object TableOrders: TTable DatabaseName = 'DBDEMOS' MasterFields = 'CustNo' MasterSource = DataSourceCust TableName = 'ORDERS.DB' end object DataSourceCust: TDataSource DataSet = TableCustomer end object ProviderCustomer: TDataSetProvider DataSet = TableCustomer end
On the client side, the detail table will show up as an extra field of the ClientDataSet, and the DBGrid control will display it as an extra column with an ellipsis button. Clicking the button will display a secondary form with a grid presenting the detail table (see Figure 16.6). If you need to build a flexible user interface on the client, you can then add a secondary ClientDataSet connected to the dataset field of the master dataset, using the DataSetField property. Simply create persistent fields for the main ClientDataSet and then hook up the property:
Figure 16.6: The ThinPlus example shows how a dataset field can either be displayed in a grid in a floating window or extracted by a ClientDataSet and displayed in a second form. You'll generally do one of the two things, not both!
object cdsDet: TClientDataSet DataSetField = cdsTableOrders end
With this setting, you can show the detail dataset in a separate DBGrid placed as usual in the form (the bottom grid of Figure 16.6) or any other way you like. Note that with this structure, the updates relate only to the master table, and the server should handle the proper update sequence even in complex situations.
I've already mentioned that the ConnectionBroker component can be helpful in case you might want to change the physical connection used by many ClientDataSet components of a single program. By hooking each ClientDataSet to the ConnectionBroker, you can change the physical connection of all the ClientDataSets by updating the physical connection of the broker.
The ThinPlus example uses these settings:
object Connection: TSocketConnection ServerName = 'AppSPlus.AppServerPlus' AfterConnect = ConnectionAfterConnect Address = '127.0.0.1' end object ConnectionBroker1: TConnectionBroker Connection = Connection end object cds: TClientDataSet ConnectionBroker = ConnectionBroker1 end // in the secondary form object cdsQuery: TClientDataSet ConnectionBroker = ClientForm.ConnectionBroker1 end
That's basically all you have to do. To change the physical connection, drop a new DataSnap connection component to the main form and set the Connection property of the broker to it.
I've already mentioned the Options property of the DataSetProvider component, noting that you can use it to add the field properties to the data packet. There are several other options you can use to customize the data packet and the behavior of the client program. Here is a short list:
The SimpleObjectBroker component provides an easy way to locate a server application among several server computers. You provide a list of available computers, and the client will try each of them in order until it finds one that is available.
Moreover, if you enable the LoadBalanced property, the component will randomly choose one of the servers; when many clients use the same configuration, the connections will be automatically distributed among the multiple servers. If this seems like a "poor man's" object broker, consider that some highly expensive load-balancing systems don't offer much more than this.
When multiple clients connect to your server at the same time, you have two options. The first is to create a remote data module object for each of them and let each request be processed in sequence (the default behavior for a COM server with the ciMultiInstance style). Alternatively, you can let the system create a different instance of the server application for every client (ciSingleInstance). This approach requires more resources and more SQL server connections (and possibly licenses).
An alternate approach is offered by DataSnap's support for object pooling. All you need to do to request this feature is add a call to RegisterPooled in the overridden UpdateRegistry method. Combined with the stateless support built in to this architecture, the pooling capability allows you to share some middle-tier objects among a much larger number of clients. A pooling mechanism is built in to COM+, but DataSnap makes it available for HTTP and socket-based connections as well.
The users on the client computers will spend most of their time reading data and typing in updates, and they generally don't continue asking for data and sending updates. When the client is not calling a method of the middle-tier object, this same remote data module can be used for another client. Being stateless, every request reaches the middle tier as a brand-new operation, even when a server is dedicated to a specific client.
There are many ways to include custom information within the data packet handled by the IAppServer interface. The simplest is to handle the OnGetDataSetProperties event of the provider. This event has a Sender parameter, a dataset parameter indicating where the data is coming from, and an OleVariant array Properties parameter, in which you can place the extra information. You need to define one variant array for each extra property and include the name of the extra property, its value, and whether you want the data to return to the server along with the update delta (the IncludeInDelta parameter).
Of course, you can pass properties of the related dataset component, but you can also pass any other value (extra fake properties). In the AppSPlus example, I pass to the client the time the query was executed and its parameters:
procedure TAppServerPlus.ProviderQueryGetDataSetProperties( Sender: TObject; DataSet: TDataSet; out Properties: OleVariant); begin Properties := VarArrayCreate([0,1], varVariant); Properties := VarArrayOf(['Time', Now, True]); Properties := VarArrayOf(['Param', Query.Params.AsString, False]); end;
On the client side, the ClientDataSet component has a GetOptionalParameter method to retrieve the value of the extra property with the given name. The ClientDataSet also has the SetOptionalParameter method to add more properties to the dataset. These values will be saved to disk (in the briefcase model) and eventually sent back to the middle tier (by setting the IncludeInDelta member of the variant array to True). Here is a simple example of the retrieval of the dataset in the previous code:
Caption := 'Data sent at ' + TimeToStr (TDateTime ( cdsQuery.GetOptionalParam('Time'))); Label1.Caption := 'Param ' + cdsQuery.GetOptionalParam('Param');
The effect of this code was visible in Figure 16.5. An alternative and more powerful approach for customizing the data packet sent to the client is to handle the OnGetData event of the provider, which receives the outgoing data packet in the form of a client dataset. Using the methods of this client dataset, you can edit data before it is sent to the client. For example, you might encode some of the data or filter out sensitive records.
|Copyright © 2004-2019 "Delphi Sources". Delphi Programming Guide||