Abstract: This article demonstrates how to define a ClientDataSet's structure at both design-time and runtime using TFields. How to create virtual and nested dataset fields is also demonstrated.
In the last installment of The Professional Developer, I described how to define the structure of a ClientDataSet using the ClientDataSet's FieldDefs property. This structure is used to create the in-memory data store when you call the ClientDataSet's CreateDataSet method. The metadata describing this structure, and any data subsequently entered into the ClientDataSet, will be saved to disk when the ClientDataSet's SaveToFile method is invoked.
While the FieldDefs property provides you with a convenient and valuable mechanism for defining a ClientDataSet's structure, it has several short-comings. Specifically, you cannot use FieldDefs to create virtual fields, which include calculated fields, lookup fields, and aggregate fields. In addition, creating nested datasets (one-to-many relationships) through FieldDefs is problematic. Specifically, while I have found it possible to create nested datasets using FieldDefs, I have not been able to successfully save and then later reload these nested datasets into a ClientDataSets. Only the TFields method appears to create nested datasets that can be reliably saved to the ClientDataSet's native local file formats and later re-loaded into memory.
Like the FieldDefs method of defining the structure of a ClientDataSet, you can define a ClientDataSet's structure using TFields either at design-time or at runtime. Since the design-time technique is the easiest to demonstrate, this article with start with it. Defining a ClientDataSet's structure using TFields at runtime is shown later in this article.
You define the TFields that represent the structure of a ClientDataSet at design-time using the Fields Editor. Unfortunately, this process is a bit more tedious than that using FieldDefs. Specifically, using the FieldDefs collection editor you can quickly add one or more FieldDef definitions, each of which defines the characteristic of a corresponding field in the ClientDataSets's structure. Using the TFields method, you must add one field at a time. All this really means is that it takes a little longer to define a ClientDataSet's structure using TFields than it does using FieldDefs.
Although using the TFields method of defining a ClientDataSet's structure is more time consuming, it has the advantage of permitting you to define both the fields of a table's structure for the purpose of storing data, as well as to define virtual fields. Virtual fields are used define dataset fields whose values are calculated at runtime -- the values are not physically stored.
Place a ClientDataSet from the Data Access page of the Component Palette onto a form.
Right-click the Fields Editor and select New Field (or simply press the INS key). The New Field dialog box is displayed, as shown in the following figure.
Enter PartNo in the Name field, and Integer in the Type field. Leave the Field Type radio button set to the default, which is Data. Your New Field dialog box should now look something like the following.
Click OK to accept this new field. The newly added field should now appear in the Fields Editor.
Repeat steps 3 through 5 to add three more fields to the table structure. For the first field, set Name to Description, Type to String, and Size to 80. For the second field, set Name to Price and Type to Currency. For the third field, set Name to Quantity and Type to Integer. When you are done, the Fields Editor should look something like the following.
Adding a virtual field to a ClientDataSet's structure at design-time is only slightly more complicated than adding a data field. This added complexity involves setting additional properties and/or adding additional event handlers.
Let's begin by adding a calculated field. Calculated fields require both a new field whose type is Calculated, and an OnCalcFields event handler, which is associated with the ClientDataSet itself. This event handler is used to calculate the value that will be displayed in this virtual field.
Note: This example demonstrates the addition of a calculated virtual field, which is available for most TDataSet descendents. Alternatively, these same basic steps can be used to add an InternalCalc field, which is a special calculated field associated with ClientDataSets. InternalCalc virtual fields can be more efficient than Calculated virtual fields, since they need to be re-calculated less often than calculated fields.
Begin by right-clicking the Fields Editor and selecting New Field (or press INS).
Using the New Fields dialog box, set Name to Total Price, Type to Currency, and Field Type to Calculated. Click OK to add the new field.
Now select the ClientDataSet in the Object Inspector or the Object TreeView, and display the Events page of the Object Inspector.
Aggregate fields, which can be used to perform a number of automatic calculations across one or more records of your data, do not require event handlers, but do require that the ClientDataSet have at least one index. The following steps will walk you through adding an index, as well as an aggregate field that will use the index. A more complete discussion of ClientDataSet indexes will appear in a later article in this series.
With the ClientDataSet selected in the Object Inspector, choose the IndexDefs property and double-click the ellipsis button that appears. Using the IndexDefs collection editor, click the Add New button once.
With this newly adding IndexDef selected in the IndexDefs collection editor, use the Object Inspector to set its Name property to PNIndex, and its Fields property to PartNo.
Select the ClientDataSet in the Object Inspector once again. Set its IndexName property to PNIndex and its AggregatesActive property to True.
We are now ready to add the aggregate field. Double-click the ClientDataSet to display the Fields Editor (alternatively, you can right-click the ClientDataSet and select Fields Editor from the displayed context menu).
Right-click the Fields Editor and select New Field.
Set Name to Total Parts and Data Type to Aggregate. Select OK to close the New Field dialog box. The aggregate virtual field is displayed in its own section of the Fields Editor, as shown in the following figure.
Select the Total Parts aggregate field in the Fields Editor. Then, using the Object Inspector, set the Expression property to Sum(Quantity), the IndexName property to PXIndex, and Active to True.
That's all it takes. All you need to do now is call the CreateDataSet method at runtime (or alternatively, right-click the ClientDataSet at design-time and select Create DataSet). Of course, if you want to actually see the resulting ClientDataSet, you will also have to hook it up to one or more data-aware controls.
The use of the TField definitions described here are demonstrated in the FieldDemo project, which you can download from Code Central. The following is the main form of this project.
As you can see from this code, the ClientDataSet in this example resides on a data module. Upon startup, this form calculates the name of the file in which the ClientDataSet's data can be stored. It then tests to see if this file already exists. If it does not, CreateDataSet is called, otherwise the ClientDataSet is opened.
The following figure shows this form at runtime, after some records have been added.
Nested datasets represent one-to-many relationships. Imagine, for instance, that you have a ClientDataSet designed to hold information about your customers. Imagine further that for each customer you want to be able to store one or more phone numbers. There are three techniques that developers often use to provide this feature. The first, and least flexible technique, is to add a fixed number of fields to the ClientDataSet to hold the possible phone numbers. For example, one for a business number, another for the a home number, and a third for a mobile phone number. The problem with this approach is that you have to decide, in advance, the maximum number of phone numbers that you can store for any given customer.
The second technique is to create a separate file to hold customer phone numbers. This file would have to include one or more fields that define a link between a given customer and their phone numbers (such as a unique customer identification number), as well as fields for holding the type of phone number and the phone number itself. Using this approach, you can store any number of phone numbers for each customer.
The third technique is to create a nested dataset. A nested dataset is created by adding a Field of DataSet type to a ClientDataSet's structure. This dataset field is then assigned to the DataSetField property of a second client dataset. Using this second ClientDataSet, you can define fields to store the one or more records of related data. In this example it might make sense to add two fields, one to hold the type of phone number (such as, home, cell, fax, and so forth), and a second to hold the phone number itself. Similar to the second technique, nested datasets permit a customer to have any number of phone numbers. On the other hand, unlike the second technique, in which phone numbers are stored in a separate file, there is no need for any fields to link phone numbers to customers, since the phone numbers are actually "nested" within each customer's record.
Here is how you create a nested dataset at design-time.
Using the technique outlined earlier in this article (using the Fields Editor), create one field of data type Data for each regular field in the dataset (such as Customer Name, Title, Address1, Address2, and so forth).
To define the fields of each nested dataset, add fields to each secondary ClientDataSet using its Fields Editor, just as you added fields to the primary ClientDataSet. For example, following the customer/phone numbers example discussed here, the nested dataset fields would include phone type and phone number.
For an example of a project that demonstrates how to create nested datasets at design-time, download the NestedDataSetFields project from Code Central. This project provides an example of how the customer/phone numbers application might be implemented. This project contains a data module that includes two ClientDataSets. One is used to hold the customer information, and it includes a DataSet field called PhoneNumbers. This DataSet field is associated with a second ClientDataSet through the second ClientDataSet's DataSetField property. The Fields Editor for this second ClientDataSet, shown in the following figure, displays its two String fields, one for Phone Type and the other for Phone Number.
In the previous article in this series, where a ClientDataSet's structure was defined using FieldDefs, you learned that you can define the structure of a ClientDataSet both at design-time as well as at runtime. As explained in that article, the advantage of using design-time configuration is that you can use the features of the Object Inspector to assist in the definition of the ClientDataSet's structure. This approach, however, is only useful if you know the structure of your ClientDataSet in advance. If you do not, your only option is to define your structure at runtime.
You define your TFields at runtime using the methods and properties of the appropriate TField or TDataSetField class. Specifically, you call the constructor of the appropriate TField or TDataSetField object, setting the properties of the created object to define its nature. Among the properties of the constructed object, one of the most important is the DataSet property. This property defines to which TDataSet descendant you want the object associated (which will be a ClientDataSet in this case, since we are discussing this type of TDataSet). After creating all of the TFields or TDataSetFields, you call the ClientDataSet's CreateDataSet method. Doing so creates the ClientDataSet's structure based on the TFields to which it is associated.
The following is a simple example of defining a ClientDataSet's structure using TFields.
You can test this code for yourself easy enough. Simply create a project and place on the main form a ClientDataSet, a DataSource, a DBGrid, and a DBNavigator. Assign the DataSet property of the DBGrid and the DBNavigator to the DataSource, assign the DataSet property of the DataSource to ClientDataSet, and ensure that the ClientDataSet is named ClientDataSet1. Finally, add the preceding code to the OnCreate event handler of the form to which these components appear, and run the project.
When your structure is defined using TFields, there is an important behavior that might not be immediately obvious. Specifically, the TFields specified at design-time using the Fields Editor define objects that are created automatically when the form, data module, or frame to which they are associated is created. These objects define the ClientDataSet's structure, which in turn defines the value of the ClientDataSet's FieldDefs property.
This same behavior does not apply when a ClientDataSet's structure is defined using FieldDefs at design-time. Specifically, the TFields of a ClientDataSet whose structure is defined using FieldDefs is defined when the ClientDataSet's CreateDataSet method is invoked. But they are also created when metadata is read from a previously saved ClientDataSet file. If a ClientDataSet is loaded from a saved file, the structure defined in the metadata of the saved file takes precedence. In other words, the FieldDefs property created at design-time is replaced by FieldDefs defined by the saved metadata, and this is used to create the TFields.
When your ClientDataSet's structure is defined using TFields at design-time, metadata in a previously saved ClientDataSet is not used to define the TFields, since they already exist. As a result, when a ClientDataSet's structure is defined using TFields, and you attempt to load previously save data, it is essential that the metadata in the file being loaded be consistent with the defined TFields.
As mentioned in the preceding section, TFields defined at design-time cause the automatic creation of the corresponding TField instances at runtime (as well as FieldDefs). If you define your ClientDataSet's structure at runtime, by calling the constructor of the various TField and TDataSetField objects that you need, you must follow the call to these constructors with a call to the ClientDataSet's CreateDataSet method before the ClientDataSet can be used. This is true even when you intend to load the ClientDataSet from previously saved data.
The reason for this is that, as pointed out in the previous section, ClientDataSet structures defined using TFields do not rely on the metadata of previously saved ClientDataSets. Instead, the structure relies on the TFields and TDataSetFields that have been created for the ClientDataSet. This becomes particularly obvious when you consider that virtual fields are not stored in the files saved by a ClientDataSet. The only way that you can have virtual fields in a ClientDataSet whose structure is defined at runtime is to create these fields using the appropriate constructors, and then call CreateDataSet to build the ClientDataSet's in-memory data store. Only then can a compatible, previously saved data file be loaded into the ClientDataSet.
Here is another way to put it. When you define your ClientDataSet's structure using FieldDefs, you call CreateDataSet only if there is no previously saved data file. If there is a previously saved data file, you simply load it into the ClientDataSet - CreateDataSet does not need to be invoked. The ClientDataSet's structure is based on the saved metadata.
By comparison, when you define your ClientDataSet's structure using TFields at runtime, you always call CreateDataSet (but only after creating and configuring the TField and TDataSetField instances that define the ClientDataSet's structure). This must be done whether or not you want to load previously saved data.
The VideoLibrary project, which can be downloaded from Code Central, includes code that demonstrates how to create data, aggregate, lookup, and nested dataset fields at runtime using TFields. This project, whose running main form is shown in the following figure, includes two primary ClientDataSets. One is used to hold a list of videos and another holds a list of Talent (actors). The ClientDataSet that holds the video information contains two nested datasets: one to hold the list of talent for that particular video and another to hold a list of the video's special features (for instance, a music video found on a DVD).
This project is too complicated to describe adaquately in this limited space (I'll save that discussion for a future article). Instead, I;ll leave it up to you to download the project. In particular, you will want to examine the OnCreate event handler for this project's data module. There you will see how the various data fields, virtual fields, dataset fields, and indexes are created and configured.
Cary Jensen is President of Jensen Data Systems, Inc., a Texas-based training and consulting company that won the 2002 Delphi Informant Magazine Readers Choice award for Best Training. He is the author and presenter for Delphi Developer Days (www.DelphiDeveloperDays.com), an information-packed Delphi (TM) seminar series that tours North America and Europe. Cary is also an award-winning, best-selling co-author of eighteen books, including Building Kylix Applications (2001, Osborne/McGraw-Hill), Oracle JDeveloper (1999, Oracle Press), JBuilder Essentials (1998, Osborne/McGraw-Hill), and Delphi In Depth (1996, Osborne/McGraw-Hill). For information about onsite training and consulting you can contact Cary at cjensen@jensendatasystems.com, or visit his Web site at www.JensenDataSystems.com.
I was searching this, and found it. Clear, concise and smart in the explanation. Thank's a lot!
Great column! ...It would be nice to be able to get all of the projects in this series in one zip file.
Midas.dcu is one of the Borland units. There may be something wrong with your library search path. Try adding MidasLib to the uses clause in the project source file (*.dpr).
This piece of demo does not work."Midas.dcu" is missing.