After adding some constraints and field properties to the server, let's return our attention to the client application. The first version was very simple, but now you can add several features to make it work well. In the ThinCli2 example, I've embedded support for checking the record status and accessing the delta information (the updates to be sent back to the server), using some of the ClientDataSet techniques already discussed in Chapter 13. The program also handles reconcile errors and supports the briefcase model.
Keep in mind that while you're using this client to edit the data locally, you'll be reminded of any failure to match the business rules of the application, which are set up on the server side using constraints. The server will also provide you with a default value for the Salary field of a new record and pass along the value of its DisplayFormat property. In Figure 16.3, you can see one of the error messages this client application can display, which it receives from the server. This message is displayed when you're editing the data locally, not when you send it back to the server.
This client program includes a button to apply the updates to the server and a standard reconcile dialog. Here is a summary of the complete sequence of operations related to an update request and the possible error events:
The client program calls the ApplyUpdates method of a ClientDataSet.
The delta is sent to the provider on the middle tier. The provider fires the OnUpdateData event, where you have a chance to look at the requested changes before they reach the database server. At this point you can modify the delta, which is passed in a format compatible with the data of a ClientDataSet.
The provider (technically, a part of the provider called the resolver) applies each row of the delta to the database server. Before applying each update, the provider receives a BeforeUpdateRecord event. If you've set the ResolveToDataSet flag, this update will eventually fire local events of the dataset in the middle tier.
In case of a server error, the provider fires the OnUpdateError event (on the middle tier) and the program has a chance of fixing the error at that level.
If the middle-tier program doesn't fix the error, the corresponding update request remains in the delta. The error is returned to the client side at this point or after a given number of errors have been collected, depending on the value of the MaxErrors parameter of the ApplyUpdates call.
The delta packet with the remaining updates is sent back to the client, firing the OnReconcileError event of the ClientDataSet for each remaining update. In this event handler, the client program can try to fix the problem (possibly prompting the user for help), modifying the update in the delta, and later reissuing it.
You can obtain an updated version of the data, which other users might have modified, by calling the Refresh method of the ClientDataSet. However, this operation can be done only if there are no pending update operations in the cache, because calling Refresh raises an exception when the change log is not empty:
if cds.ChangeCount = 0 then cds.Refresh;
If only some records have been changed, you can refresh the others by calling RefreshRecords. This method refreshes only the current record, but it should be used only if the user hasn't modified the current record. In this case, RefreshRecords leaves the unapplied changes in the change log. As an example, you can refresh a record every time it becomes the active one, unless it has been modified and the changes have not yet been posted to the server:
procedure TForm1.cdsAfterScroll(DataSet: TDataSet); begin if cds.UpdateStatus = usUnModified then cds.RefreshRecord; end;
When the data is subject to frequent changes by many users and each user should see changes right away, you should apply any change immediately in the AfterPost and AfterDelete methods, and call RefreshRecords for the active record (as shown previously) or each of the records visible inside a grid. This code is part of the ClientRefresh example, connected to the AppServ2 server. For debugging purposes, the program also logs the EMP_NO field for each record it refreshes, as you can see in Figure 16.4.
I've done this by adding a button to the ClientRefresh example. The handler of this button moves from the current record to the first visible record of the grid and then to the last visible record. This is accomplished by noting that there are RowCount - 1 rows visible, assuming that the first row is the fixed one hosting the field names. The program doesn't call RefreshRecord every time, because each movement will trigger an AfterScroll event with the code shown previously. The following code refreshes the visible rows, which might be triggered by a timer:
// protected access hack type TMyGrid = class (TDBGrid) end; procedure TForm1.Button2Click(Sender: TObject); var i: Integer; bm: TBookmarkStr; begin // refresh visible rows cds.DisableControls; // start with the current row i := TMyGrid (DbGrid1).Row; bm := cds.Bookmark; try // get back to the first visible record while i > 1 do begin cds.Prior; Dec (i); end; // return to the current record i := TMyGrid(DbGrid1).Row; cds.Bookmark := bm; // go ahead until the grid is complete while i < TMyGrid(DbGrid1).RowCount do begin cds.Next; Inc (i); end; finally // set back everything and refresh cds.Bookmark := bm; cds.EnableControls; end; end;
This approach generates a huge amount of network traffic, so you might want to trigger updates only when there are actual changes. You can implement this process by adding a callback technology to the server, so that it can inform all connected clients that a given record has changed. The client can determine whether it is interested in the change and eventually trigger the update request.