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.
Defining a ClientDataSet's Structure at Design-Time
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.
The following steps demonstrate how to define a ClientDataSet's structure
using TFields as design-time:
- Place a ClientDataSet from the Data Access page of the Component Palette
onto a form.
- Right-click the ClientDataSet and select Fields Editor. The empty Fields
Editor is shown in the following figure

- 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 Calculated Virtual Field
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.
- Double-click the OnCalcFields event handler to add this event handler. In
Delphi or Kylix, complete this event handler as shown here
code:
procedure TDataModule2.ClientDataSet1CalcFields(DataSet: TDataSet);
begin
if (not ClientDataSet1.FieldbyName('Price').IsNull) and
(not ClientDataSet1.FieldbyName('Quantity').IsNull) then
ClientDataSet1.FieldByName('Total Price').Value :=
ClientDataSet1.FieldbyName('Price').Value *
ClientDataSet1.FieldByName('Quantity').Value;
end;
Adding a Virtual Aggregate Field
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.

Notice that just below the main menu there is a Label and a DBLabel. The
DBLabel is associated with the Total Parts aggregate field, and it is used to
display the sum of the values entered in the Quantity field of the
ClientDataSet. The DBNavigator and the DBGrid that appear on this main form are
associated with the ClientDataSet through a DataSource. This ClientDataSet is created at runtime, if
it does not already exist. This is done from code executed from the main form's
OnCreate event handler, shown here:
procedure TForm1.FormCreate(Sender: TObject);
begin
DataModule2.ClientDataSet1.FileName :=
ExtractFilePath(Application.ExeName) + 'parts.xml';
if not FileExists(DataModule2.ClientDataSet1.FileName) then
DataModule2.ClientDataSet1.CreateDataSet
else
DataModule2.ClientDataSet1.Open;
end;
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.

Creating Nested DataSet
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).
- For each nested dataset, add a new field, using the same technique that you
use for the other data fields, but set its Data Type to DataSet.
- For each DataSet field that you add to your first ClientDataSet, add an
additional ClientDataSet. Associate each of these secondary ClientDataSets with
one of the primary ClientDataSet's DataSet fields using the secondary
ClientDataSet's DataSetField property.
- 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.

Creating a ClientDataSet's Structure at Runtime using TFields
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.
procedure TForm1.FormCreate(Sender: TObject);
begin
with ClientDataSet1 do
begin
with TStringField.Create(Self) do
begin
Name := 'ClientDataSet1FirstName';
FieldKind := fkData;
FieldName := 'FieldName';
Size := 72;
DataSet := ClientDataSet1;
end; //FieldName
with TMemoField.Create(Self) do
begin
Name := 'ClientDataSet1LastName';
FieldKind := fkData;
FieldName := 'Last Name';
DataSet := ClientDataSet1;
end; //Last Name
ClientDataSet1.CreateDataSet
end;
end;
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.
TFields and FieldDefs are Different
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.
Creating a ClientDataSet's Structure Using TFields at Runtime
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.
An Example
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.
About the Author
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.
Click
here for a
listing of upcoming seminars, workshops, and conferences where Cary Jensen is
presenting.
Copyright
) 2002 Cary Jensen, Jensen Data Systems, Inc.
ALL RIGHTS RESERVED. NO PART OF THIS DOCUMENT CAN BE COPIED IN ANY FORM WITHOUT
THE EXPRESS, WRITTEN CONSENT OF THE AUTHOR.
Connect with Us