645 if gbFirst in ClientDataSet1.GetGroupState (1) then Text := Sender.AsString else Text := ‘’; end; Defining Aggregates Another feature of the ClientDataSet component is support for aggregates. An aggregate is a calculated value based on multiple records, such as the sum or the average value of a field for the entire table or a group of records (defined with the grouping logic I’ve just discussed). Aggregates are maintained; that is, they are recalculated immediately if one of the records changes. For example, the total of an invoice can be maintained automatically while the user types in the invoice items. NOTE Aggregates are maintained incrementally, not by recalculating all the values every time one value changes. Aggregate updates take advantage of the deltas tracked by the ClientDataSet. For example, to update a sum when a field is changed, the ClientDataSet subtracts the old value from the aggregate and adds the new value. Only two calculations are needed, even if there are thousands of rows in that aggregate group. For this reason, aggregate updates are instantaneous. FIGURE 14.11: The CdsCalcs example demonstrates that by writing a little code, you can have the DBGrid control visually show the grouping defined in the ClientDataSet. ClientDataSet and MyBase Copyright ©2001 SYBEX, Inc., Alameda, CA www.sybex.com 646 There are two ways to define aggregates. You can use the Aggregates property of the ClientDataSet, which is a collection, or you can define aggregate fields using the Fields editor. In both cases, you define the aggregate expression, give it a name, and connect it to an index and a grouping level (unless you want to apply it to the entire table). Here is the Aggregates collection of the CdsCalcs example: object ClientDataSet1: TClientDataSet Aggregates = < item Active = True AggregateName = ‘Count’ Expression = ‘COUNT (NAME)’ GroupingLevel = 1 IndexName = ‘ClientDataSet1Index1’ Visible = False end item Active = True AggregateName = ‘TotalPopulation’ Expression = ‘SUM (POPULATION)’ Visible = False end> AggregatesActive = True Notice in the last line above that you must activate the support for aggregates, in addition to activating each specific aggregate you want to use. Disabling aggregates is important, because having too many of them can slow down a program. The alternative approach, as I mentioned, is to use the Fields editor, select the New Field command of its shortcut menu, and choose the Aggregate option (available, along with the InternalCalc option, only in a ClientDataSet). This is the definition of an aggregate field: object ClientDataSet1: TClientDataSet object ClientDataSet1TotalArea: TAggregateField FieldName = ‘TotalArea’ ReadOnly = True Visible = True Active = True DisplayFormat = ‘###,###,###’ Expression = ‘SUM(AREA)’ GroupingLevel = 1 IndexName = ‘ClientDataSet1Index1’ end The aggregate fields are displayed in the Fields editor in a separate group, as you can see in Figure 14.12. The advantage of using an aggregate field, compared to a plain aggregate, is that you can define the display format and hook the field directly to a data-aware control, Chapter 14 • Client/Server Programming Copyright ©2001 SYBEX, Inc., Alameda, CA www.sybex.com 647 such as a DBEdit in the CdsCalcs example. Because the aggregate is connected to a group, as soon as you select a record of a different group, the output will be automatically updated. Also, if you change the data, the total will immediately show the new value. To use plain aggregates, instead, you have to write a little code, as in the following example (notice that the Value of the aggregate is a variant): procedure TForm1.Button1Click(Sender: TObject); begin Label1.Caption := ‘Area: ‘ + ClientDataSet1TotalArea.DisplayText + #13’Population : ‘ + FormatFloat (‘###,###,###’, ClientDataSet1.Aggregates [1].Value) + #13’Number : ‘ + IntToStr (ClientDataSet1.Aggregates [0].Value); end; Manipulating Updates One of the core ideas behind the ClientDataSet component is that it is used as a local cache to collect some input from a user and then send a batch of update requests to the database. The component has both a list of the changes to apply to the database server, stored in the same format used by the ClientDataSet (accessible though the Delta property), and a com- plete updates log that you can manipulate with a few methods (including an Undo capability). The Status of the Records The component lets us monitor what’s going on within the data packets. The UpdateStatus method returns one of the following indicators for the current record: type TUpdateStatus = (usUnmodified, usModified, usInserted, usDeleted); FIGURE 14.12: The bottom portion of the Fields editor of a Client- DataSet displays aggregate fields. ClientDataSet and MyBase Copyright ©2001 SYBEX, Inc., Alameda, CA www.sybex.com 648 To check the status of every record in the client dataset easily, you can add a string-type calculated field to the dataset (I’ve called it ClientDataSet1Status) and compute its value with the following OnCalcFields event handler: procedure TForm1.ClientDataSet1CalcFields(DataSet: TDataSet); begin ClientDataSet1Status.AsString := GetEnumName (TypeInfo(TUpdateStatus), Integer (ClientDataSet1.UpdateStatus)); end; This method (based on the RTTI GetEnumName function) converts the current value of the TUpdateStatus enumeration to a string, with the effect you can see in Figure 14.13. Accessing the Delta Beyond examining the status of each record, the best way to understand which changes have occurred in a given ClientDataSet (but haven’t been uploaded to the server) is to look at the delta, the list of changes waiting to be applied to the server. This property is defined as follows: property Delta: OleVariant; The format used by the Delta property is the same as that used to transmit the data from the client to the server. What we can do, then, is add another ClientDataSet component to an application and connect it to the data in the Delta property of the first client dataset: if ClientDataSet1.ChangeCount > 0 then begin ClientDataSet2.Data := ClientDataSet1.Delta; ClientDataSet2.Open; In the CdsDelta example, I’ve added a data module with the two ClientDataSet compo- nents and an actual source of data, a SQLDataSet mapped to InterBase’s EMPLOYEE demo FIGURE 14.13: The CdsDelta program displays the status of each record of a ClientDataSet. Chapter 14 • Client/Server Programming Copyright ©2001 SYBEX, Inc., Alameda, CA www.sybex.com 649 table. Both client datasets have the extra status calculated field, with a slightly more generic version than the code discussed earlier, because the event handler is shared between them. TIP To create persistent fields for the ClientDataSet hooked to the delta (at run time), I’ve tem- porarily connected it, at design time, to the same provider of the main ClientDataSet. The structure of the delta, in fact, is the same of the dataset it refers to. After creating the persis- tent fields, I’ve removed the connection. The form of this application has a page control with two pages, each with a DBGrid, one for the actual data and one for the delta. Some code hides or shows the second tab depending on the existence of data in the change log, as returned by the ChangeCount method, and updates the delta when the corresponding tab is selected. The core of the code used to handle the delta is very similar to the last code snippet above, and you can study the example source code on the CD to see more details. You can see the change log of the CdsDelta application in Figure 14.14. Notice that the delta dataset has two entries for each modified record: the original values and the modified fields, unless this is a new or deleted record, as indicated by its status. TIP You can also filter the delta dataset (or any other ClientDataSet) depending on its update sta- tus, using the StatusFilter property. This allows you to show new, updated, and deleted records in separate grids or in a grid filtered by selecting an option in a TabControl. Undo and SavePoint Because the update data is stored in the local memory (in the delta), besides applying the updates and sending them to the application server, we can reject them, removing entries FIGURE 14.14: The CdsDelta example allows you to see the temporary update requests stored in the Delta property of the ClientDataSet. ClientDataSet and MyBase Copyright ©2001 SYBEX, Inc., Alameda, CA www.sybex.com 650 from the delta. The ClientDataSet component has a specific UndoLastChange method to accomplish this. The parameter of this method allows you to follow the undo operation (the name of this parameter is FollowChange). This means the client dataset will move to the record that has been restored by the undo operation. Here is the code connected to the Undo button of the CdsDelta example: procedure TForm1.ButtonUndoClick(Sender: TObject); begin DmCds.cdsEmployee.UndoLastChange (True); end; An extension of the undo support is the possibility to save a sort of bookmark of the change log position (the current status) and to restore it later by undoing all successive changes. The SavePoint property can be used either to save the number of changes in the log or to reset the log to a past situation. Notice, anyway, that you can only remove records from the change log, not reinsert changes. In other words, the ChangeLog refers to a position in a log, so it can only go back to a position in which there were fewer records! This position is just a number of changes, so if you undo some changes and then do more edits, that number of changes will become meaningless. Enabling and Disabling Logging Keeping track of changes makes sense if you need to send the updated data back to a server database. In local applications with data stored to a MyBase file, keeping this log around can become useless and consumes memory. For this reason, you can disable logging altogether with the LogChanges property. You can also call the MergeChangesLog method to remove all current editing from the change log. This makes sense if the dataset doesn’t directly originate by a provider but was built with custom code, or in case you want to add or edit the data programmatically, without having to send it to the back-end database server. TIP The ClientDataSet in Delphi 6 has a new property, DisableStringTrim, which allows you to keep trailing spaces in field values. In past versions, in fact, string fields were invariably trimmed, which creates trouble with some databases. Updating the Data Now that we have a better understanding of what goes on during local updates, we can try to make this program work by sending the local update (stored in the delta) back to the database server. To apply all the updates from a dataset at once, pass -1 to the ApplyUpdates method. Chapter 14 • Client/Server Programming Copyright ©2001 SYBEX, Inc., Alameda, CA www.sybex.com 651 If the provider (or actually the Resolver component inside it) has trouble applying an update, it triggers the OnReconcileError event. This can take place because of a concurrent update by two different people. As we tend to use optimistic locking in client/server applications, this should be regarded as a normal situation. The OnReconcileError event allows you to modify the Action parameter (passed by refer- ence), which determines how the server should behave: procedure TForm1.ClientDataSet1ReconcileError(DataSet: TClientDataSet; E: EReconcileError; UpdateKind: TUpdateKind; var Action: TReconcileAction); This method has three parameters: the client dataset component (in case more than one client application is interacting with the application server), the exception that caused the error (with the error message), and the kind of operation that failed (ukModify, ukInsert, or ukDelete). The return value, which you’ll store in the Action parameter, can be any one of the following: type TReconcileAction = (raSkip, raAbort, raMerge, raCorrect, raCancel, raRefresh); • The raSkip value specifies that the server should skip the conflicting record, leaving it in the delta (this is the default value). • The raAbort value tells the server to abort the entire update operation and not even try to apply the remaining changes listed in the delta. • The raMerge value tells the server to merge the data of the client with the data on the server, applying only the modified fields of this client (and keeping the other fields modified by other clients). • The raCorrect value tells the server to replace its data with the current client data, overriding all field changes already done by other clients. • The raCancel value cancels the update request, removing the entry from the delta and restoring the values originally fetched from the database (thus ignoring changes done by other clients). • The raRefresh value tells the server to dump the updates in the client delta and to replace them with the values currently on the server (thus keeping the changes done by other clients). If you want to test a collision, you can simply launch two copies of the client application, change the same record in both clients, and then post the updates from both. We’ll do this later to generate an error, but let’s first see how to handle the OnReconcileError event. This is actually a simple thing to accomplish, but only because we’ll receive a little help. Since building a specific form to handle an OnReconcileError event is very common, Delphi ClientDataSet and MyBase Copyright ©2001 SYBEX, Inc., Alameda, CA www.sybex.com 652 already provides such a form in the Object Repository. Simply go to the Dialogs page and select the Reconcile Error Dialog item. This unit exports a function you can directly use to initialize and display the dialog box, as I’ve done in the CdsDelta example: procedure TDmCds.cdsEmployeeReconcileError (DataSet: TCustomClientDataSet; E: EReconcileError; UpdateKind: TUpdateKind; var Action: TReconcileAction); begin Action := HandleReconcileError(DataSet, UpdateKind, E); end; WARNING As the source code of the Reconcile Error Dialog unit suggests, you should use the Project Options dialog to remove this form from the list of automatically created forms (if you don’t, an error will occur when you compile the project). Of course, you need to do this only if you haven’t set up Delphi to skip the automatic form creation. The HandleReconcileError function simply creates the form of the dialog box and shows it, as you can see in the code provided by Borland: function HandleReconcileError(DataSet: TDataSet; UpdateKind: TUpdateKind; ReconcileError: EReconcileError): TReconcileAction; var UpdateForm: TReconcileErrorForm; begin UpdateForm := TReconcileErrorForm.CreateForm(DataSet, UpdateKind, ReconcileError); with UpdateForm do try if ShowModal = mrOK then begin Result := TReconcileAction(ActionGroup.Items.Objects[ ActionGroup.ItemIndex]); if Result = raCorrect then SetFieldValues(DataSet); end else Result := raAbort; finally Free; end; end; The Reconc unit, which hosts the Reconcile Error dialog, contains over 350 lines of code, so we can’t describe it in detail. However, you should be able to understand the source code by studying it carefully. Alternatively, you can simply use it without caring about how every- thing works. Chapter 14 • Client/Server Programming Copyright ©2001 SYBEX, Inc., Alameda, CA www.sybex.com 653 The dialog box will appear in case of an error, reporting the requested change that caused the conflict and allowing the user to choose one of the possible TReconcileAction values. You can see an example in Figure 14.15. TIP When you call ApplyUpdates, you start a rather complex update sequence, discussed in more detail in Chapter 17 for multitier architectures. In short, the delta is sent to the provider, which fires the OnUpdateData event and then receives a BeforeUpdateRecord event for every record to update. These are two chances you have to take a look at the changes and force specific operations on the database server. MyBase (or the Briefcase Model) The last capability of the ClientDataSet component I want to discuss in this chapter is its support for mapping memory data to local files, building stand-alone applications. The same technique can be applied in multitier applications to use the client program even when you’re not physically connected to the application server. In this case, you can save all the data you expect to need in a local file for travel with a laptop (perhaps visiting client sites). You’ll use the client program to access the local version of the data, edit the data normally, and when you reconnect, apply all the updates you’ve performed while disconnected. To map a ClientDataSet to a local file you only need to set its FileName property, which requires an absolute pathname. To build a minimal MyBase program (called MyBase1), all FIGURE 14.15: The Reconcile Error dialog provided by Delphi in the Object Repository and used by the CdsDelta example ClientDataSet and MyBase Copyright ©2001 SYBEX, Inc., Alameda, CA www.sybex.com 654 you need is a ClientDataSet component hooked to a file and with a few fields defined (in the FieldDefs property): object ClientDataSet1: TClientDataSet FileName = ‘C:\md6code\14\MyBase1\test’ FieldDefs = < item Name = ‘one’ DataType = ftString Size = 20 end item Name = ‘two’ DataType = ftSmallint end> StoreDefs = True end At this point you can use the Create DataSet command of the local menu of the ClientDataSet at design time, or call its CreateDataSet method at run time, to physically create the file for the table. As you make changes and close the application, the data will be automatically saved to the file. (You might want to disable the change log, though, to reduce the size of this data.) The dataset, in any case, also has a SaveToFile method and a LoadFromFile method you can use in your code. MyBase1, my example program, shown in Figure 14.16, doesn’t require any database server or database connection to work. It needs only your own program and the Midas.dll file, but you can even get rid of it by including the MidasLib unit in the project. And the program doesn’t require any actual Pascal code, either. TIP MyBase generally saves the datasets in XML format, although the internal CDS format is still available. I’ll explore this format in detail when I discuss XML in Chapter 23, “XML and SOAP.” For the moment, suffice to say this is a text-based format (so it is less space-efficient than the internal format), which can be manipulated programmatically but immediately makes some sense even if you try reading it. FIGURE 14.16: The MyBase1 example, which saves data directly to a MyBase file Chapter 14 • Client/Server Programming Copyright ©2001 SYBEX, Inc., Alameda, CA www.sybex.com [...]... www.sybex.com 67 8 Chapter 15 • InterBase and IBX As I’ve mentioned, the IBX datasets in Delphi 6 can be tied directly to a generator, simplifying the overall picture quite a lot Thanks to the specific property editor (shown in Figure 15.8), in fact, connecting a field of the dataset to the generator becomes trivial FIGURE 15.8: The editor of the GeneratorField property of the IBX datasets in Delphi 6 Notice... Windows Interactive SQL (WISQL) Version 6 includes a much more powerful front-end application, called IBConsole This is a full-fledged Windows program (built with Delphi) that allows you to administer, configure, test, and query an InterBase server, whether local or remote Copyright ©2001 SYBEX, Inc., Alameda, CA www.sybex.com Getting Started with InterBase 6 661 IBConsole is a simple and complete system... in Delphi 6 is the ability to set up the automatic behavior of a generator as a sort of auto-incremental field This is accomplished by setting the GeneratorField property using its specific property editor An example of this is discussed later in this chapter in the section “Generators and IDs.” IBX Administrative Components A new page of Delphi 6 Component palette, InterBase Admin, hosts InterBase 6. .. server properties, and all connected users You can see an example of server properties in Figure 15 .7 and the code for extracting the users in the following code fragment FIGURE 15 .7: The assorted server information displayed by the IbxMon application Copyright ©2001 SYBEX, Inc., Alameda, CA www.sybex.com 67 6 Chapter 15 • InterBase and IBX // grab the users data IBSecurityService1.DisplayUsers; // display... reason, InterBase 6 supports 64 -bit generators How do you generate the unique values for these IDs when multiple clients are running? Keeping a table with a latest value is going to create troubles, as multiple concurrent transactions (from different users) will see the same values If you don’t use tables, you can use a Copyright ©2001 SYBEX, Inc., Alameda, CA www.sybex.com Real-World Blocks 67 7 database-independent...ClientDataSet and MyBase 65 5 The MyBase support in Delphi 6 also includes the possibility of extracting the XML representation of a memory dataset by using the XMLData property In Delphi 5, you could obtain the same by saving the ClientDataSet in XML format in a memory stream Abstract Data Types in MyBase The ClientDataSet component supports most data types provided by Delphi, including nested data... Inc., Alameda, CA www.sybex.com Using InterBase Express 67 1 ‘examples\database\employee.gdb’; finally Reg.CloseKey; Reg.Free; end; EmpDS.DataSet.Open; end; The source code actually contains alternate code for using the database installed in the Data subfolder of the Borland Shared folder, used for Delphi sample databases Notice also that InterBase 6 places the sample databases in a different subfolder... the same time, the output of the IbxMon program will list the details about the UpdSql2 program’s interaction with InterBase, as you can see in Figure 15 .6 Copyright ©2001 SYBEX, Inc., Alameda, CA www.sybex.com Using InterBase Express 67 5 FIGURE 15 .6: The output of the IbxMon example, based on the IBMonitor component Getting More System Data The IbxMon example doesn’t only monitor the InterBase connection,... doesn’t change too often! From Delphi you can activate a stored procedure returning a result set by using either a Query or a StoredProc component With a Query, you can use the following SQL code: select * from MaxSalOfTheDay (‘01/01/1990’) Copyright ©2001 SYBEX, Inc., Alameda, CA www.sybex.com Server-Side Programming 66 5 Triggers (and Generators) Triggers behave more or less like Delphi events and are automatically... The IBX components include custom dataset components and a few others The dataset components inherit from the base TDataSet class, can use all the common Delphi data-aware Copyright ©2001 SYBEX, Inc., Alameda, CA www.sybex.com Using InterBase Express 66 7 controls, provide a field editor and all the usual design-time features, and can be used in the Data Module Designer, but they don’t require the BDE . include in your Delphi programs. Server-Side Programming Copyright ©2001 SYBEX, Inc., Alameda, CA www.sybex.com 66 4 Stored Procedures Stored procedures are like the global functions of a Delphi unit. library Borland is introducing in Delphi 6 and of the ClientDataSet component and MyBase technology. There is certainly more we can say about client/server programming in Delphi, and in the next chapter. language very close to the SQL standard. Getting Started with InterBase 6 Copyright ©2001 SYBEX, Inc., Alameda, CA www.sybex.com 66 0 • Advanced programming capabilities, with positional triggers, selectable