Defining a ClientDataSet's Structure Using FieldDefs

By: Cary Jensen

Abstract: When creating a ClientDataSet's memory store on-the-fly, you must explicitly define the structure of your table. This article shows you how to do it at both runtime and design-time using FieldDefs.

The ClientDataSet is an in-memory data store that lets you to view, edit, and navigate data. Because these operations are performed on data held in memory, they tend to be performed very quickly. 

This is the second article in a series designed to detail the use of the ClientDatSet. In the last installment, I provided you with a basic overview of ClientDataSet, with particular attention paid to how a ClientDataSet gets its data from a DataSetProvider. You use a ClientDataSet with a DataSetProvider when you obtain your data through a remote database management system (RDBMS) or a local database engine, such as the Borland Database Engine (BDE). Instead of using a DataSetProvider, it is possible to load and save the data held by a ClientDataSet from the local file system. Borland calls this mechanism MyBase. 

As you learned in the preceding article in this series, a ClientDataSet loaded through a DataSetProvider get its metadata, the data that defines the fields of the dataset (commonly referred to as a table's structure), through the DataSetProvider. This metadata is produced by the DataSetProvider, based on the DataSet to which it points. 

When a ClientDataSet gets its data from a local file using MyBase, the metadata is read from this file. However, neither mechanism is available when you create the in-memory dataset on-the-fly, at runtime. In these situations, it is necessary for you to explicitly define the structure of the ClientDataSet. Defining this metadata can be done either at design-time or at runtime. Once the metadata is defined, you create the in-memory dataset by calling the ClientDataSet's CreateDataSet method, or by using the ClientDataSet's component editor in the designer.

There are two ways to define the metadata of a ClientDataset. You can use the FieldDefs property of the ClientDataSet, or you can create TFields and associate them with the ClientDataSet. Creating the metadata definitions using FieldDefs is the most common. However, FieldDefs does not permit you to create virtual fields, such as calculated or aggregate fields. Similarly, using FieldDefs does not allow you to easily create nested datasets. Nested datasets represent one-to-many (sometimes called master-detail or parent-child) associations in your data. In this article you will learn how to use FieldDefs. The next article in this series will discuss the use of TFields to define the structure of a ClientDataSet.

Defining a Table's Structure Using FieldDefs

You can configure FieldDefs either at design time or at runtime. To define the structure of a client dataset at design time, you use the FieldDefs collection editor to create individual FieldDef instances. You then use the Object Inspector to configure each FieldDef, defining the field name, data type, size, or precision, among other options. At runtime, you define your FieldDef objects by calling the FieldDefs AddFieldDef or Add methods. This section begins by demonstrating how to create your ClientDataSet's structure at design-time. Defining the table structure at runtime is shown later in this article.

Creating FieldDefs at Design-time

You create FieldDefs at design-time using the FieldDefs collection editor. To display this collection editor, select the FieldDefs property of a ClientDataSet in the Object Inspector and click the displayed ellipsis button. The FieldDefs collection editor is shown in the following figure.

Using the FieldDefs collection editor, click the Add New button (or press Ins) once for each field that you want to include in your ClientDataSet. Each click of the Add New button (or press of Ins) will create a new FieldDef instance, which will be displayed in the collection editor. For example, if you add five new FieldDefs to the FieldDefs collection editor, it will look something like that shown in the following figure.  

You must configure each FieldDef that is added to the FieldDefs collection editor before the dataset can be created. To configure a FieldDef, select the FieldDef you want to configure in the collection editor or the Object TreeView, and then use the Object Inspector to set its properties. The following is how the Object Inspector looks when a FieldDef is selected. (Notice that the Attributes property has been expanded to display its subproperties.)

At a minimum, you must set the DataType property of each FieldDef. You will also want to set the Name property. The Name property defines the name of the corresponding field that will be created. 

Other properties you will often set include the Size property, which you define for String, BCD (binary coded decimal), byte, and VarByte fields, and the precision property for BCD fields. Similarly, if a particular field requires a value before the record to which it is associated can be posted, set the faRequired subproperty of the Attributes property to True. For information on the other properties of the TFieldDef class, see the online help.

After setting the necessary properties of each FieldDef, you can create the ClientDataSet. This can be done either at design-time or runtime. To create the ClientDataSet at design-time, right-click the ClientDataSet and select Create DataSet, as shown in the following figure.

Creating the dataset at design-time creates an in-memory table, but does not actually create a physical file on disk. You save a physical file by right-clicking the ClientDataSet and selecting one of the save options, such as Save to MyBase Xml table or Save to binary MyBase file. 

If you create your physical file at design-time, you will then likely need to deploy that file, along with any other required files. As a result, many ClientDataSet users create the ClientDataSet at runtime. As mentioned earlier in this article, this task is performed by calling the ClientDataSet's CreateDataSet method. For example, consider the following event handler, which might be associated with the OnCreate event handler of the form to which it is associated.

procedure TForm1.FormCreate(Sender: TObject);
const
  DataFile = 'mydata.xml';
begin
ClientDataSet1.FileName := ExtractFilePath(Application.ExeName) + DataFile;
if FileExists(ClientDataSet1.FileName) then
  ClientDataSet1.Open
else
  ClientDataSet1.CreateDataSet;
end;

This code begins by defining the FileName property of the ClientDataSet, pointing to a file named mydata.xml in the application directory. Next, it tests to see if this file already exists. If it does, it opens the ClientDataSet, loading the specified file's metadata and data into memory. If the file does not exist, it is created through a call to CreateDataSet. When CreateDataSet is called, the in-memory structure is created based on the FieldDefs property of the ClientDataSet.

Creating FieldDefs at Runtime

Being able to create FieldDefs at design-time is an important feature, in that the Object Inspector provides you with assistance in defining the various properties of each FieldDef you add. However, there may be times when you do not know the structure of the dataset that you need until runtime. In those cases, you must define the FieldDefs property at runtime.

As mentioned earlier in this article, there are two methods that you can use to configure the FieldDefs property at runtime. The easiest technique is to use the Add method of the TFieldDefs class. The following is the syntax of Add:

procedure Add(const Name: String; DataType: TFieldType; 
  Size: Integer = 0; Required: Boolean = False);

This method has two required parameters and two optional parameters. The first parameter is the name of the FieldDef and the second is its type. If you need to set the Size property, as is the case with fields of type ftString and ftBCD, set the Size property to the size of the field. For required fields, set the fourth property to a Boolean True.

The following code sample creates an in-memory table with five fields.

procedure TForm1.FormCreate(Sender: TObject);
const
  DataFile = 'mydata.xml';
begin
ClientDataSet2.FileName := 
  ExtractFilePath(Application.ExeName) + DataFile;
if FileExists(ClientDataSet2.FileName) then
  ClientDataSet2.Open
else
  begin
    with ClientDataSet2.FieldDefs do
    begin
      Clear;
      Add('ID',ftInteger, 0, True);
      Add('First Name',ftString, 20);
      Add('Last Name',ftString, 25);
      Add('Date of Birth',ftDate);
      Add('Active',ftBoolean);
    end; //with ClientDataSet2.FieldDefs
    ClientDataSet2.CreateDataSet;
  end; //else
end;  

Like the previous code listing, this code begins by defining the name of the data file, and then testing whether or not it already exists. When it does not exist, the Add method of the FieldDefs property is used to define the table structure, after which the in-memory dataset is created using the CreateDataSet method.

If you consider how the Object Inspector looks when an individual FieldDef is selected in the FieldDefs collection editor, you will notice that the Add method is rather limited. Specifically, using the Add method you cannot create hidden fields, readonly fields, or BCD fields where you define precision. For these more complicated types of FieldDef definitions, you will need to use the AddFieldDef method of the FieldDefs property. The following is the syntax of AddFieldDef:

function AddFieldDef: TFieldDef;

As you can see from this syntax, this method returns a TFieldDef instance. Set the properties of this instance to configure the FieldDef. The following code sample shows you how to do this.

procedure TForm1.FormCreate(Sender: TObject);
const
  DataFile = 'mydata.xml';
begin
ClientDataSet2.FileName := 
  ExtractFilePath(Application.ExeName) + DataFile;
if FileExists(ClientDataSet2.FileName) then
  ClientDataSet2.Open
else
  begin
    with ClientDataSet2.FieldDefs do
    begin
      Clear;
      with AddFieldDef do
      begin
        Name := 'ID';
        DataType := ftInteger;
      end; //with AddFieldDef do
      with AddFieldDef do
      begin
        Name := 'First Name';
        DataType := ftString;
        Size := 20;
      end; //with AddFieldDef do
      with AddFieldDef do
      begin
        Name := 'Last Name';
        DataType := ftString;
        Size := 25;
      end; //with AddFieldDef do
      with AddFieldDef do
      begin
        Name := 'Date of Birth';
        DataType := ftDate;
      end; //with AddFieldDef do
      with AddFieldDef do
      begin
        Name := 'Active';
        DataType := ftBoolean;
      end; //with AddFieldDef do
    end; //with ClientDataSet2.FieldDefs
    ClientDataSet2.CreateDataSet;
  end; //else
end;

Saving Data

If you have assigned a file name to the FileName property of a ClientDataSet whose in-memory table you create using CreateDataSet, and post at least one new record to the dataset, a physical file will be written to disk when you close or destroy the ClientDataSet. This happens automatically. Alternative, you can call the SaveToFile method of the ClientDataSet to explicitly save your data to a physical file. The following is the syntax of SaveToFile

procedure SaveToFile(const FileName: string = ''; 
 Format TDataPacketFormat=dfBinary);

As you can see, both of the parameters of this method are optional. If you omit the first parameter, the ClientDataSet saves to a file whose name is assigned to the FileName property. If you omit the second parameter, the type of file that is written to disk will depend on the file extension of the file to which you are saving the data. If the extension is XML, an XML MyBase file is created. Otherwise, a binary MyBase file is written. You can override this behavior by specifying the type of file you want to write. If you pass dfBinary as the second parameter, a binary MyBase file is created. To create an XML MyBase file when the file extension of the file name is not XML, use dfXML.

On more than one occasion I have noticed that the XML MyBase file is not written to disk correctly if you do not explicitly call SaveToFile. Therefore, even though a ClientDataSet can save its data automatically, I make a habit of explicitly calling SaveToFile before closing or destroying a ClientDataSet.

An Example

An example application that demonstrates the use of the FieldDefs methods AddFieldDefs and Add can be downloaded from Code Central. The following is how the main form of this application looks after File | Create or Load is selected from the main menu.

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 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.



Server Response from: ETNASC01