43
Chapter 3: Client Dataset Basics In This Chapter What Is a Client Dataset? Advantages and Disadvantages of Client Datasets Creating Client Datasets Populating and Manipulating Client Datasets Navigating Client Datasets Client Dataset Indexes Filters and Ranges Searching In the preceding two chapters, I discussed dbExpress—a unidirectional database technology. In the real world, most applications support bidirectional scrolling through a dataset. As noted previously, Borland has addressed bidirectional datasets through a technology known as client datasets. This chapter introduces you to the basic operations of client datasets, including how they are a useful standalone tool. Subsequent chapters focus on more advanced client dataset capabilities, including how you can hook a client dataset up to a dbExpress (or other) database connection to create a true multitier application. What Is a Client Dataset? A client dataset, as its name suggests, is a dataset that is located in a client application (as opposed to an application server). The name is a bit of a misnomer, because it seems to indicate that client datasets have no use outside a client/server or multitier application. However, as you'll see in this chapter, client datasets are useful in other types of applications, especially single-tier database applications. Note Client datasets were originally introduced in Delphi 3, and they presented a method for creating multitier applications in Delphi. As their use became more widespread, they were enhanced to support additional single-tier functionality. The base class in VCL/CLX for client datasets is TCustomClientDataSet. Typically, you don't work with TCustomClientDataSet directly, but with its direct descendent, TClientDataSet. (In Chapter 7, "Dataset Providers," I'll introduce you to other descendents of TCustomClientDataSet.) For readability and generalization, I'll refer to client datasets generically in this book as TClientDataSet. Advantages and Disadvantages of Client Datasets Client datasets have a number of advantages, and a couple of perceived disadvantages. The advantages include Memory based. Client datasets reside completely in memory, making them useful for temporary tables. Page 1 of 43 Chapter 3: Client Dataset Basics 10/22/2001 file://J:\Sams\chapters\WB850.html

Client Dataset Basics

Embed Size (px)

Citation preview

Page 1: Client Dataset Basics

Chapter 3: Client Dataset Basics

In This Chapter

z What Is a Client Dataset? z Advantages and Disadvantages of Client Datasets z Creating Client Datasets z Populating and Manipulating Client Datasets z Navigating Client Datasets z Client Dataset Indexes z Filters and Ranges z Searching

In the preceding two chapters, I discussed dbExpress—a unidirectional database technology. In the real world, most applications support bidirectional scrolling through a dataset. As noted previously, Borland has addressed bidirectional datasets through a technology known as client datasets. This chapter introduces you to the basic operations of client datasets, including how they are a useful standalone tool. Subsequent chapters focus on more advanced client dataset capabilities, including how you can hook a client dataset up to a dbExpress (or other) database connection to create a true multitier application.

What Is a Client Dataset?

A client dataset, as its name suggests, is a dataset that is located in a client application (as opposed to an application server). The name is a bit of a misnomer, because it seems to indicate that client datasets have no use outside a client/server or multitier application. However, as you'll see in this chapter, client datasets are useful in other types of applications, especially single-tier database applications.

Note

Client datasets were originally introduced in Delphi 3, and they presented a method for creating multitier applications in Delphi. As their use became more widespread, they were enhanced to support additional single-tier functionality.

The base class in VCL/CLX for client datasets is TCustomClientDataSet. Typically, you don't work with TCustomClientDataSet directly, but with its direct descendent, TClientDataSet. (In Chapter 7, "Dataset Providers," I'll introduce you to other descendents of TCustomClientDataSet.) For readability and generalization, I'll refer to client datasets generically in this book as TClientDataSet.

Advantages and Disadvantages of Client Datasets

Client datasets have a number of advantages, and a couple of perceived disadvantages. The advantages include

z Memory based. Client datasets reside completely in memory, making them useful for temporary tables.

Page 1 of 43Chapter 3: Client Dataset Basics

10/22/2001file://J:\Sams\chapters\WB850.html

Page 2: Client Dataset Basics

z Fast. Because client datasets are RAM based, they are extremely fast.

z Efficient. Client datasets store their data in a very efficient manner, making them resource friendly.

z On-the-fly indexing. Client datasets enable you to create and use indexes on-the-fly, making them extremely versatile.

z Automatic undo support. Client datasets provide multilevel undo support, making it easy to perform what if operations on your data. Undo support is discussed in Chapter 4, "Advanced Client Dataset Operations."

z Maintained aggregates. Client datasets can automatically calculate averages, subtotals, and totals over a group of records. Maintained aggregates are discussed in detail in Chapter 4.

The perceived disadvantages include

z Memory based. This client dataset advantage can also be a disadvantage. Because client datasets reside in RAM, their size is limited by the amount of available RAM.

z Single user. Client datasets are inherently single-user datasets because they are kept in RAM.

When you understand client datasets, you’ll discover that these so-called disadvantages really aren’t detrimental to your application at all. In particular, basing client datasets entirely in RAM has both advantages and disadvantages.

Because they are kept entirely in your computer’s RAM, client datasets are extremely useful for temporary tables, small lookup tables, and other nonpersistent database needs. Client datasets also are fast because they are RAM based. Inserting, deleting, searching, sorting, and traversing in client datasets are lightening fast.

On the flip side, you need to take steps to ensure that client datasets don’t grow too large because you waste precious RAM if you attempt to store huge databases in in-memory datasets. Fortunately, client datasets store their data in a very compact form. (I’ll discuss this in more detail in the "Undo Support" section of Chapter 7.)

Because they are memory based, client datasets are inherently single user. Remote machines do not have access to a client dataset on a local machine. In Chapter 8, "DataSnap," you’ll learn how to connect a client dataset to an application server in a three-tier configuration that supports true multiuser operation.

Creating Client Datasets

Using client datasets in your application is similar to using any other type of dataset because they derive from TDataSet.

You can create client datasets either at design-time or at runtime, as the following sections explain.

Creating a Client Dataset at Design-Time

Page 2 of 43Chapter 3: Client Dataset Basics

10/22/2001file://J:\Sams\chapters\WB850.html

Page 3: Client Dataset Basics

Typically, you create client datasets at design-time. To do so, drop a TClientDataSet component (located on the Data Access tab) on a form or data module. This creates the component, but doesn’t set up any field or index definitions. Name the component cdsEmployee.

To create the field definitions for the client dataset, double-click the TClientDataSet component in the form editor. The standard Delphi field editor is displayed. Right-click the field editor and select New Field... from the pop-up menu to create a new field. The dialog shown in Figure 3.1 appears.

Figure 3.1 Use the New Field dialog to add a field to a dataset.

If you’re familiar with the field editor, you notice a new field type available for client datasets, called Aggregate fields. I’ll discuss Aggregate fields in detail in the following chapter. For now, you should understand that you can add data, lookup, calculated, and internally calculated fields to a client dataset—just as you can for any dataset.

The difference between client datasets and other datasets is that when you create a data field for a typical dataset, all you are doing is creating a persistent field object that maps to a field in the underlying database. For a client dataset, you are physically creating the field in the dataset along with a persistent field object. At design-time, there is no way to create a field in a client dataset without also creating a persistent field object.

Data Fields

Most of the fields in your client datasets will be data fields. A data field represents a field that is physically part of the dataset, as opposed to a calculated or lookup field (which are discussed in the following sections). You can think of calculated and lookup fields as virtual fields because they appear to exist in the dataset, but their data actually comes from another location.

Let’s add a field named ID to our dataset. In the field editor, enter ID in the Name edit control. Tab to the Type combo box and type Integer, or select it from the drop-down list. (The component name has been created for you automatically.) The Size edit control is disabled because Integer values are a fixed-length field. The Field type is preset to Data, which is what we want. Figure 3.2 shows the completed dialog.

Figure 3.2 The New Field dialog after entering information for a new field.

Click OK to add the field to the client dataset. You’ll see the new ID field listed in the field editor.

Now add a second field, called LastName. Right-click the field editor to display the New Field dialog and enter LastName in the Name edit control. In the Type combo, select String. Then, set Size to 30—the size represents the maximum number of characters allowed for the field. Click OK to add the LastName field to the dataset.

Similarly, add a 20-character FirstName field and an Integer Department field.Finally, let's add a Salary field. Open the New Field dialog. In the Name edit control, type Salary. Set the Type to Currency and click OK. (The currency type instructs Delphi to automatically display it with a dollar sign.)

Page 3 of 43Chapter 3: Client Dataset Basics

10/22/2001file://J:\Sams\chapters\WB850.html

Page 4: Client Dataset Basics

If you have performed these steps correctly, the field editor looks like Figure 3.3.

Figure 3.3 The field editor after adding five fields.

That’s enough fields for this dataset. In the next section, I’ll show you how to create a calculated field.

Calculated Fields

Calculated fields, as indicated previously, don’t take up any physical space in the dataset. Instead, they are calculated on-the-fly from other data stored in the dataset. For example, you might create a calculated field that adds the values of two data fields together. In this section, we’ll create two calculated fields: one standard and one internal.

Note

Actually, internal calculated fields do take up space in the dataset, just like a standard data field. For that reason, you can create indexes on them like you would on a data field. Indexes are discussed later in this chapter.

Standard Calculated Fields

In this section, we’ll create a calculated field that computes an annual bonus, which we’ll assume to be five percent of an employee’s salary.

To create a standard calculated field, open the New Field dialog (as you did in the preceding section). Enter a Name of Bonus and a Type of Currency.

In the Field Type radio group, select Calculated. This instructs Delphi to create a calculated field, rather than a data field. Click OK.

That’s all you need to do to create a calculated field. Now, let’s look at internal calculated fields.

Internal Calculated Fields

Creating an internal calculated field is almost identical to creating a standard calculated field. The only difference is that you select InternalCalc as the Field Type in the New Field dialog, instead of Calculated.

Another difference between the two types of calculated fields is that standard calculated fields are calculated on-the-fly every time their value is required, but internal calculated fields are calculated once and their value is stored in RAM. (Of course, internal calculated fields recalculate automatically if the underlying fields that they are calculated from change.)

The dataset’s AutoCalcFields property determines exactly when calculated fields are recomputed. If AutoCalcFields is True (the default value), calculated fields are computed when the dataset is opened,

Page 4 of 43Chapter 3: Client Dataset Basics

10/22/2001file://J:\Sams\chapters\WB850.html

Page 5: Client Dataset Basics

when the dataset enters edit mode, and whenever focus in a form moves from one data-aware control to another and the current record has been modified. If AutoCalcFields is False, calculated fields are computed when the dataset is opened, when the dataset enters edit mode, and when a record is retrieved from an underlying database into the dataset.

There are two reasons that you might want to use an internal calculated field instead of a standard calculated field. If you want to index the dataset on a calculated field, you must use an internal calculated field. (Indexes are discussed in detail later in this chapter.) Also, you might elect to use an internal calculated field if the field value takes a relatively long time to calculate. Because they are calculated once and stored in RAM, internal calculated fields do not have to be computed as often as standard calculated fields.

Let’s add an internal calculated field to our dataset. The field will be called Name, and it will concatenate the FirstName and LastName fields together. We probably will want an index on this field later, so we need to make it an internal calculated field.

Open the New Field dialog, and enter a Name of Name and a Type of String. Set Size to 52 (which accounts for the maximum length of the last name, plus the maximum length of the first name, plus a comma and a space to separate the two).

In the Field Type radio group, select InternalCalc and click OK.

Providing Values for Calculated Fields

At this point, we’ve created our calculated fields. Now we need to provide the code to calculate the values. TClientDataSet, like all Delphi datasets, supports a method named OnCalcFields that we need to provide a body for.

Click the client dataset again, and in the Object Inspector, click the Events tab. Double-click the OnCalcFields event to create an event handler.

We’ll calculate the value of the Bonus field first. Flesh out the event handler so that it looks like this:

procedure TForm1.cdsEmployeeCalcFields(DataSet: TDataSet); begin cdsEmployeeBonus.AsFloat := cdsEmployeeSalary.AsFloat * 0.05; end;

That’s easy—we just take the value of the Salary field, multiply it by five percent (0.05), and store the value in the Bonus field.

Now, let's add the Name field calculation. A first (reasonable) attempt looks like this:

procedure TForm1.cdsEmployeeCalcFields(DataSet: TDataSet); begin cdsEmployeeBonus.AsFloat := cdsEmployeeSalary.AsFloat * 0.05; cdsEmployeeName.AsString := cdsEmployeeLastName.AsString + ’, ’ + cdsEmployeeFirstName.AsString; end;

This works, but it isn't efficient. The Name field calculates every time the Bonus field calculates.

Page 5 of 43Chapter 3: Client Dataset Basics

10/22/2001file://J:\Sams\chapters\WB850.html

Page 6: Client Dataset Basics

However, recall that it isn’t necessary to compute internal calculated fields as often as standard calculated fields. Fortunately, we can check the dataset’s State property to determine whether we need to compute internal calculated fields or not, like this:

procedure TForm1.cdsEmployeeCalcFields(DataSet: TDataSet); begin cdsEmployeeBonus.AsFloat := cdsEmployeeSalary.AsFloat * 0.05; if cdsEmployee.State = dsInternalCalc then cdsEmployeeName.AsString := cdsEmployeeLastName.AsString + ’, ’ + cdsEmployeeFirstName.AsString; end;

Notice that the Bonus field is calculated every time, but the Name field is only calculated when Delphi tells us that it’s time to compute internal calculated fields.

Lookup Fields

Lookup fields are similar, in concept, to calculated fields because they aren’t physically stored in the dataset. However, instead of requiring you to calculate the value of a lookup field, Delphi gets the value from another dataset. Let’s look at an example.

Earlier, we created a Department field in our dataset. Let’s create a new Department dataset to hold department information.

Drop a new TClientDataSet component on your form and name it cdsDepartment. Add two fields: Dept (an integer) and Description (a 30-character string).

Show the field editor for the cdsEmployee dataset by double-clicking the dataset. Open the New Field dialog. Name the field DepartmentName, and give it a Type of String and a Size of 30.

In the Field Type radio group, select Lookup. Notice that two of the fields in the Lookup definition group box are now enabled. In the Key Fields combo, select Department. In the Dataset combo, select cdsDepartment.

At this point, the other two fields in the Lookup definition group box are accessible. In the Lookup Keys combo box, select Dept. In the Result Field combo, select Description. The completed dialog should look like the one shown in Figure 3.4.

Figure 3.4 Adding a lookup field to a dataset.

The important thing to remember about lookup fields is that the Key field represents the field in the base dataset that references the lookup dataset. Dataset refers to the lookup dataset. The Lookup Keys combo box represents the Key field in the lookup dataset. The Result field is the field in the lookup dataset from which the lookup field obtains its value.

To create the dataset at design time, you can right-click the TClientDataSet component and select Create DataSet from the pop-up menu.

Now that you’ve seen how to create a client dataset at design-time, let’s see what’s required to create a

Page 6 of 43Chapter 3: Client Dataset Basics

10/22/2001file://J:\Sams\chapters\WB850.html

Page 7: Client Dataset Basics

client dataset at runtime.

Creating a Client Dataset at Runtime

To create a client dataset at runtime, you start with the following skeletal code:

var CDS: TClientDataSet; begin CDS := TClientDataSet.Create(nil); try // Do something with the client dataset here finally CDS.Free; end; end;

After you create the client dataset, you typically add fields, but you can load the client dataset from a disk instead (as you’ll see later in this chapter in the section titled "Persisting Client Datasets").

Adding Fields to a Client Dataset

To add fields to a client dataset at runtime, you use the client dataset’s FieldDefs property. FieldDefs supports two methods for adding fields: AddFieldDef and Add.

AddFieldDef

TFieldDefs.AddFieldDef is defined like this:

function AddFieldDef: TFieldDef;

As you can see, AddFieldDef takes no parameters and returns a TFieldDef object. When you have the TFieldDef object, you can set its properties, as the following code snippet shows.

var FieldDef: TFieldDef; begin FieldDef := ClientDataSet1.FieldDefs.AddFieldDef; FieldDef.Name := ’Name’; FieldDef.DataType := ftString; FieldDef.Size := 20; FieldDef.Required := True; end;

Add

A quicker way to add fields to a client dataset is to use the TFieldDefs.Add method, which is defined like this:

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

The Add method takes the field name, the data type, the size (for string fields), and a flag indicating

Page 7 of 43Chapter 3: Client Dataset Basics

10/22/2001file://J:\Sams\chapters\WB850.html

Page 8: Client Dataset Basics

whether the field is required as parameters. By using Add, the preceding code snippet becomes the following single line of code:

ClientDataSet1.FieldDefs.Add(’Name’, ftString, 20, True);

Why would you ever want to use AddFieldDef when you could use Add? One reason is that TFieldDef contains several more-advanced properties (such as field precision, whether or not it’s read-only, and a few other attributes) in addition to the four supported by Add. If you want to set these properties for a field, you need to go through the TFieldDef. You should refer to the Delphi documentation for TFieldDef for more details.

Creating the Dataset

After you create the field definitions, you need to create the empty dataset in memory. To do this, call TClientDataSet.CreateDataSet, like this:

ClientDataSet1.CreateDataSet;

As you can see, it’s somewhat easier to create your client datasets at design-time than it is at runtime. However, if you commonly create temporary in-memory datasets, or if you need to create a client dataset in a formless unit, you can create the dataset at runtime with a minimal amount of fuss.

Accessing Fields

Regardless of how you create the client dataset, at some point you need to access field information—whether it's for display, to calculate some values, or to add or modify a new record.

There are several ways to access field information in Delphi. The easiest is to use persistent fields.

Persistent Fields

Earlier in this chapter, when we used the field editor to create fields, we were also creating persistent field objects for those fields. For example, when we added the LastName field, Delphi created a persistent field object named cdsEmployeeLastName.

When you know the name of the field object, you can easily retrieve the contents of the field by using the AsXxx family of methods. For example, to access a field as a string, you would reference the AsString property, like this:

ShowMessage(’The employee’’s last name is ’ + cdsEmployeeLastName.AsString);

To retrieve the employee's salary as a floating-point number, you would reference the AsFloat property:

Bonus := cdsEmployeeSalary.AsFloat * 0.05;

See the VCL/CLX source code and the Delphi documentation for a list of available access properties.

Page 8 of 43Chapter 3: Client Dataset Basics

10/22/2001file://J:\Sams\chapters\WB850.html

Page 9: Client Dataset Basics

Note

You are not limited to accessing a field value in its native format. For example, just because Salary is a currency field doesn’t mean you can’t attempt to access it as a string. The following code displays an employee’s salary as a formatted currency:

ShowMessage(’Your salary is ’ + cdsEmployeeSalary.AsString);

You could access a string field as an integer, for example, if you knew that the field contained an integer value. However, if you try to access a field as an integer (or other data type) and the field doesn’t contain a value that’s compatible with that data type, Delphi raises an exception.

Nonpersistent Fields

If you create a dataset at design-time, you probably won’t have any persistent field objects. In that case, there are a few methods you can use to access a field’s value.

The first is the FieldByName method. FieldByName takes the field name as a parameter and returns a temporary field object. The following code snippet displays an employee’s last name using FieldByName.

ShowMessage(’The employee’’s last name is ’ + ClientDataSet1.FieldByName(’LastName’).AsString);

Caution

If you call FieldByName with a nonexistent field name, Delphi raises an exception.

Another way to access the fields in a dataset is through the FindField method, like this:

if ClientDataSet1.FindField(’LastName’) <> nil then ShowMessage(’Dataset contains a LastName field’);

Using this technique, you can create persistent fields for datasets created at runtime.

var fldLastName: TField; fldFirstName: TField; begin ... fldLastName := cds.FindField(’LastName’); fldFirstName := cds.FindField(’FirstName’); ... ShowMessage(’The last name is ’ + fldLastName.AsString); end;

Page 9 of 43Chapter 3: Client Dataset Basics

10/22/2001file://J:\Sams\chapters\WB850.html

Page 10: Client Dataset Basics

Finally, you can access the dataset’s Fields property. Fields contains a list of TField objects for the dataset, as the following code illustrates:

var Index: Integer; begin for Index := 0 to ClientDataSet1.Fields.Count - 1 do ShowMessage(ClientDataSet1.Fields[Index].AsString); end;

You do not normally access Fields directly. It is generally not safe programming practice to assume, for example, that a given field is the first field in the Fields list. However, there are times when the Fields list comes in handy. For example, if you have two client datasets with the same structure, you could add a record from one dataset to the other using the following code:

var Index: Integer; begin ClientDataSet2.Append; for Index := 0 to ClientDataSet1.Fields.Count - 1 do ClientDataSet2.Fields[Index].AsVariant := ClientDataSet1.Fields[Index].AsVariant; ClientDataSet2.Post; end;

The following section discusses adding records to a dataset in detail.

Populating and Manipulating Client Datasets

After you create a client dataset (either at design-time or at runtime), you want to populate it with data. There are several ways to populate a client dataset: You can populate it manually through code, you can load the dataset’s records from another dataset, or you can load the dataset from a file or a stream. The following sections discuss these methods, as well as how to modify and delete records.

Populating Manually

The most basic way to enter data into a client dataset is through the Append and Insert methods, which are supported by all datasets. The difference between them is that Append adds the new record at the end of the dataset, but Insert places the new record immediately before the current record.

I always use Append to insert new records because it’s slightly faster than Insert. If the dataset is indexed, the new record is automatically sorted in the correct order anyway.

The following code snippet shows how to add a record to a client dataset:

cdsEmployee.Append; // You could use cdsEmployee.Insert; here as well cdsEmployee.FieldByName(’ID’).AsInteger := 5; cdsEmployee.FieldByName(’FirstName’).AsString := ’Eric’; cdsEmployee.Post;

Modifying Records

Page 10 of 43Chapter 3: Client Dataset Basics

10/22/2001file://J:\Sams\chapters\WB850.html

Page 11: Client Dataset Basics

Modifying an existing record is almost identical to adding a new record. Rather than calling Append or Insert to create the new record, you call Edit to put the dataset into edit mode. The following code changes the first name of the current record to Fred.

cdsEmployee.Edit; // Edit the current record cdsEmployee.FieldByName(’FirstName’).AsString := ’Fred’; cdsEmployee.Post;

Deleting Records

To delete the current record, simply call the Delete method, like this:

cdsEmployee.Delete;

If you want to delete all records in the dataset, you can use EmptyDataSet instead, like this:

cdsEmployee.EmptyDataSet;

Populating from Another Dataset

dbExpress datasets are unidirectional and you can’t scroll backward through them. This makes them incompatible with bidirectional, data-aware controls such as TDBGrid. However, TClientDataSet can load its data from another dataset (including dbExpress datasets, BDE datasets, or other client datasets) through a provider. Using this feature, you can load a client dataset from a unidirectional dbExpress dataset, and then connect a TDBGrid to the client dataset, providing bidirectional support.

Indeed, this capability is so powerful and important that it forms the basis for Delphi’s multitier database support.

Populating from a File or Stream: Persisting Client Datasets

Though client datasets are located in RAM, you can save them to a file or a stream and reload them at a later point in time, making them persistent. This is the third method of populating a client dataset.

To save the dataset to a file, use the SaveToFile method, which is defined like this:

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

Similarly, to save the dataset to a stream, you call SaveToStream, which is defined as follows:

procedure SaveToStream(Stream: TStream; Format: TDataPacketFormat = dfBinary);

SaveToFile accepts the name of the file that you’re saving to. If the filename is blank, the data is saved using the FileName property of the client dataset.

Both SaveToFile and SaveToStream take a parameter that indicates the format to use when saving data. Client datasets can be stored in one of three file formats: binary, or either flavor of XML. Table 3.1 lists the possible formats.

Page 11 of 43Chapter 3: Client Dataset Basics

10/22/2001file://J:\Sams\chapters\WB850.html

Page 12: Client Dataset Basics

Table 3.1 Data Packet Formats for Loading and Saving Client Datasets

When client datasets are stored to disk, they are referred to as MyBase files. MyBase stores one dataset per file, or per stream, unless you use nested datasets.

Note

If you’re familiar with Microsoft ADO, you recall that ADO enables you to persist datasets using XML format. The XML formats used by ADO and MyBase are not compatible. In other words, you cannot save an ADO dataset to disk in XML format, and then read it into a client dataset (or vice versa).

Sometimes, you need to determine how many bytes are required to store the data contained in the client dataset. For example, you might want to check to see if there is enough room on a floppy disk before saving the data there, or you might need to preallocate the memory for a stream. In these cases, you can check the DataSize property, like this:

if ClientDataSet1.DataSize > AvailableSpace then ShowMessage(’Not enough room to store the data’);

DataSize always returns the amount of space necessary to store the data in binary format (dfBinary). XML format usually requires more space, perhaps twice as much (or even more).

Note

One way to determine the amount of space that’s required to save the dataset in XML format is to save the dataset to a memory stream, and then obtain the size of the resulting stream.

Example: Creating, Populating, and Manipulating a Client Dataset

The following example illustrates how to create, populate, and manipulate a client dataset at runtime. Code is also provided to save the dataset to disk and to load it.

Value Description dfBinary Data is stored using a proprietary, binary format. dfXML Data is stored in XML format. Extended characters

are represented using an escape sequence. dfXMLUTF8 Data is stored in XML format. Extended characters

are represented using UTF8.

Page 12 of 43Chapter 3: Client Dataset Basics

10/22/2001file://J:\Sams\chapters\WB850.html

Page 13: Client Dataset Basics

Listing 3.1 shows the complete source code for the CDS (ClientDataset) application.

Listing 3.1 CDS—MainForm.pas

unit MainForm; interface uses SysUtils, Types, IdGlobal, Classes, QGraphics, QControls, QForms, QDialogs, QStdCtrls, DB, DBClient, QExtCtrls, QGrids, QDBGrids, QActnList; const MAX_RECS = 10000; type TfrmMain = class(TForm) DataSource1: TDataSource; pnlClient: TPanel; pnlBottom: TPanel; btnPopulate: TButton; btnSave: TButton; btnLoad: TButton; ActionList1: TActionList; btnStatistics: TButton; Populate1: TAction; Statistics1: TAction; Load1: TAction; Save1: TAction; DBGrid1: TDBGrid; lblFeedback: TLabel; procedure FormCreate(Sender: TObject); procedure Populate1Execute(Sender: TObject); procedure Statistics1Execute(Sender: TObject); procedure Save1Execute(Sender: TObject); procedure Load1Execute(Sender: TObject); private { Private declarations } FCDS: TClientDataSet; public { Public declarations } end; var frmMain: TfrmMain; implementation {$R *.xfm} procedure TfrmMain.FormCreate(Sender: TObject); begin FCDS := TClientDataSet.Create(Self); FCDS.FieldDefs.Add(’ID’, ftInteger, 0, True); FCDS.FieldDefs.Add(’Name’, ftString, 20, True); FCDS.FieldDefs.Add(’Birthday’, ftDateTime, 0, True); FCDS.FieldDefs.Add(’Salary’, ftCurrency, 0, True); FCDS.CreateDataSet; DataSource1.DataSet := FCDS;

Page 13 of 43Chapter 3: Client Dataset Basics

10/22/2001file://J:\Sams\chapters\WB850.html

Page 14: Client Dataset Basics

end; procedure TfrmMain.Populate1Execute(Sender: TObject); const FirstNames: array[0 .. 19] of string = (’John’, ’Sarah’, ’Fred’, ’Beth’, ’Eric’, ’Tina’, ’Thomas’, ’Judy’, ’Robert’, ’Angela’, ’Tim’, ’Traci’, ’David’, ’Paula’, ’Bruce’, ’Jessica’, ’Richard’, ’Carla’, ’James’, ’Mary’); LastNames: array[0 .. 11] of string = (’Parker’, ’Johnson’, ’Jones’, ’Thompson’, ’Smith’, ’Baker’, ’Wallace’, ’Harper’, ’Parson’, ’Edwards’, ’Mandel’, ’Stone’); var Index: Integer; t1, t2: DWord; begin RandSeed := 0; t1 := GetTickCount; FCDS.DisableControls; try FCDS.EmptyDataSet; for Index := 1 to MAX_RECS do begin FCDS.Append; FCDS.FieldByName(’ID’).AsInteger := Index; FCDS.FieldByName(’Name’).AsString := FirstNames[Random(20)] + ’ ’ + LastNames[Random(12)]; FCDS.FieldByName(’Birthday’).AsDateTime := StrToDate(’1/1/1950’) + Random(10000); FCDS.FieldByName(’Salary’).AsFloat := 20000.0 + Random(600) * 100; FCDS.Post; end; FCDS.First; finally FCDS.EnableControls; end; t2 := GetTickCount; lblFeedback.Caption := Format(’%d ms to load %.0n records’, [t2 - t1, MAX_RECS * 1.0]); end; procedure TfrmMain.Statistics1Execute(Sender: TObject); var t1, t2: DWord; msLocateID: DWord; msLocateName: DWord; begin FCDS.First; t1 := GetTickCount; FCDS.Locate(’ID’, 9763, []); t2 := GetTickCount; msLocateID := t2 - t1; FCDS.First; t1 := GetTickCount; FCDS.Locate(’Name’, ’Eric Wallace’, []); t2 := GetTickCount; msLocateName := t2 - t1; ShowMessage(Format(’%d ms to locate ID 9763’ + #13’%d ms to locate Eric Wallace’ +

Page 14 of 43Chapter 3: Client Dataset Basics

10/22/2001file://J:\Sams\chapters\WB850.html

Page 15: Client Dataset Basics

#13’%.0n bytes required to store %.0n records’, [msLocateID, msLocateName, FCDS.DataSize * 1.0, MAX_RECS * 1.0])); end; procedure TfrmMain.Save1Execute(Sender: TObject); var t1, t2: DWord; begin t1 := GetTickCount; FCDS.SaveToFile(’C:\Employee.cds’); t2 := GetTickCount; lblFeedback.Caption := Format(’%d ms to save data’, [t2 - t1]); end; procedure TfrmMain.Load1Execute(Sender: TObject); var t1, t2: DWord; begin try t1 := GetTickCount; FCDS.LoadFromFile(’C:\Employee.cds’); t2 := GetTickCount; lblFeedback.Caption := Format(’%d ms to load data’, [t2 - t1]); except FCDS.Open; raise; end; end; end.

There are five methods in this application and each one is worth investigating:

z FormCreate creates the client dataset and its schema at runtime. It would actually be easier to create the dataset at design-time, but I wanted to show you the code required to do this at runtime. The code creates four fields: Employee ID, Name, Birthday, and Salary.

z Populate1Execute loads the client dataset with 10,000 employees made up of random data. At the beginning of the method, I manually set RandSeed to 0 to ensure that multiple executions of the application would generate the same data.

Note

The Delphi Randomizer normally seeds itself with the current date and time. By manually seeding the Randomizer with a constant value, we can ensure that the random numbers generated are consistent every time we run the program.

z The method calculates approximately how long it takes to generate the 10,000 employees, which on my computer is about half of a second.

Page 15 of 43Chapter 3: Client Dataset Basics

10/22/2001file://J:\Sams\chapters\WB850.html

Page 16: Client Dataset Basics

z Statistics1Execute simply measures the length of time required to perform a couple of Locate operations and calculates the amount of space necessary to store the data on disk (again, in binary format). I’ll be discussing the Locate method later in this chapter.

z Save1Execute saves the data to disk under the filename C:\Employee.cds. The .cds extension is standard, although not mandatory, for client datasets that are saved in a binary format. Client datasets stored in XML format generally have the extension .xml.

Note

Please make sure that you click the Save button because the file created (C:\EMPLOYEE.CDS) is used in the rest of the example applications in this chapter, as well as some of the examples in the following chapter.

z Load1Execute loads the data from a file into the client dataset. If LoadFromFile fails (presumably because the file doesn’t exist or is not a valid file format), the client dataset is left in a closed state. For this reason, I reopen the client dataset when an exception is raised.

Figure 3.5 shows the CDS application running on my computer. Note the impressive times posted to locate a record. Even when searching through almost the entire dataset to find ID 9763, it only takes approximately 10 ms on my computer.

Figure 3.5 The CDS application at runtime.

Navigating Client Datasets

A dataset is worthless without a means of moving forward and/or backward through it. Delphi’s datasets provide a large number of methods for traversing a dataset. The following sections discuss Delphi’s support for dataset navigation.

Sequential Navigation

The most basic way to navigate through a dataset is sequentially in either forward or reverse order. For example, you might want to iterate through a dataset when printing a report, or for some other reason. Delphi provides four simple methods to accomplish this:

z First moves to the first record in the dataset. First always succeeds, even if the dataset is empty. If it is empty, First sets the dataset’s EOF (end of file) property to True.

z Next moves to the next record in the dataset (if the EOF property is not already set). If EOF is True, Next will fail. If the call to Next reaches the end of the file, it sets the EOF property to True.

z Last moves to the last record in the dataset. Last always succeeds, even if the dataset is empty. If it is empty, Last sets the dataset’s BOF (beginning of file) property to True.

Page 16 of 43Chapter 3: Client Dataset Basics

10/22/2001file://J:\Sams\chapters\WB850.html

Page 17: Client Dataset Basics

z Prior moves to the preceding record in the dataset (if the BOF property is not already set). If BOF is True, Prior will fail. If the call to Prior reaches the beginning of the file, it sets the BOF property to True.

The following code snippet shows how you can use these methods to iterate through a dataset:

if not ClientDataSet1.IsEmpty then begin ClientDataSet1.First; while not ClientDataSet1.EOF do begin // Process the current record ClientDataSet1.Next; end; ClientDataSet1.Last; while not ClientDataSet1.BOF do begin // Process the current record ClientDataSet1.Prior; end; end;

Random-Access Navigation

In addition to First, Next, Prior, and Last (which provide for sequential movement through a dataset), TClientDataSet provides two ways of moving directly to a given record: bookmarks and record numbers.

Bookmarks

A bookmark used with a client dataset is very similar to a bookmark used with a paper-based book: It marks a location in a dataset so that you can quickly return to it later.

There are three operations that you can perform with bookmarks: set a bookmark, return to a bookmark, and free a bookmark. The following code snippet shows how to do all three:

var Bookmark: TBookmark; begin Bookmark := ClientDataSet1.GetBookmark; try // Do something with ClientDataSet1 here that changes the current record ... ClientDataSet1.GotoBookmark(Bookmark); finally ClientDataSet1.FreeBookmark(Bookmark); end; end;

You can create as many bookmarks as you want for a dataset. However, keep in mind that a bookmark allocates a small amount of memory, so you should be sure to free all bookmarks using FreeBookmark or your application will leak memory.

There is a second set of operations that you can use for bookmarks instead of

Page 17 of 43Chapter 3: Client Dataset Basics

10/22/2001file://J:\Sams\chapters\WB850.html

Page 18: Client Dataset Basics

GetBookmark/GotoBookmark/FreeBookmark. The following code shows this alternate method:

var BookmarkStr: string; begin BookmarkStr := ClientDataSet1.Bookmark; try // Do something with ClientDataSet1 here that changes the current record ... finally ClientDataSet1.Bookmark := BookmarkStr; end; end;

Because the bookmark returned by the property, Bookmark, is a string, you don’t need to concern yourself with freeing the string when you’re done. Like all strings, Delphi automatically frees the bookmark when it goes out of scope.

Record Numbers

Client datasets support a second way of moving directly to a given record in the dataset: setting the RecNo property of the dataset. RecNo is a one-based number indicating the sequential number of the current record relative to the beginning of the dataset.

You can read the RecNo property to determine the current absolute record number, and write the RecNo property to set the current record. There are two important things to keep in mind with respect to RecNo:

z Attempting to set RecNo to a number less than one, or to a number greater than the number of records in the dataset results in an At beginning of table, or an At end of table exception, respectively.

z The record number of any given record is not guaranteed to be constant. For instance, changing the active index on a dataset alters the record number of all records in the dataset.

You can determine the number of records in the dataset by inspecting the dataset’s RecordCount property. When setting RecNo, never attempt to set it to a number higher than RecordCount.

However, when used discriminately, RecNo has its uses. For example, let’s say the user of your application wants to delete all records between the John Smith record and the Fred Jones record. The following code shows how you can accomplish this:

var RecNoJohn: Integer; RecNoFred: Integer; Index: Integer; begin if not ClientDataSet1.Locate(’Name’, ’John Smith’, []) then raise Exception.Create(’Cannot locate John Smith’); RecNoJohn := ClientDataSet1.RecNo; if not ClientDataSet1.Locate(’Name’, ’Fred Jones’, []) then raise Exception.Create(’Cannot locate Fred Jones’); RecNoFred := ClientDataSet1.RecNo;

Page 18 of 43Chapter 3: Client Dataset Basics

10/22/2001file://J:\Sams\chapters\WB850.html

Page 19: Client Dataset Basics

if RecNoJohn < RecNoFred then // Locate John again ClientDataSet1.RecNo := RecNoJohn; for Index := 1 to Abs(RecNoJohn - RecNoFred) + 1 do ClientDataSet1.Delete; end;

This code snippet first locates the two bounding records and remembers their absolute record numbers. Then, it positions the dataset to the lower record number. If Fred occurs before John, the dataset is already positioned at the lower record number.

Because records are sequentially numbered, we can subtract the two record numbers (and add one) to determine the number of records to delete. Deleting a record makes the next record current, so a simple for loop handles the deletion of the records.

Keep in mind that RecNo isn’t usually going to be your first line of attack for moving around in a dataset, but it’s handy to remember that it’s available if you ever need it.

Listing 3.2 contains the complete source code for an application that demonstrates the different navigational methods of client datasets.

Listing 3.2 Navigate—MainForm.pas

unit MainForm; interface uses SysUtils, Classes, QGraphics, QControls, QForms, QDialogs, QStdCtrls, DB, DBClient, QExtCtrls, QActnList, QGrids, QDBGrids, QDBCtrls; type TfrmMain = class(TForm) DataSource1: TDataSource; pnlClient: TPanel; pnlBottom: TPanel; btnFirst: TButton; btnLast: TButton; btnNext: TButton; btnPrior: TButton; DBGrid1: TDBGrid; ClientDataSet1: TClientDataSet; btnSetRecNo: TButton; DBNavigator1: TDBNavigator; btnGetBookmark: TButton; btnGotoBookmark: TButton; procedure FormCreate(Sender: TObject); procedure btnNextClick(Sender: TObject); procedure btnLastClick(Sender: TObject); procedure btnSetRecNoClick(Sender: TObject); procedure btnFirstClick(Sender: TObject); procedure btnPriorClick(Sender: TObject); procedure btnGetBookmarkClick(Sender: TObject); procedure btnGotoBookmarkClick(Sender: TObject);

Page 19 of 43Chapter 3: Client Dataset Basics

10/22/2001file://J:\Sams\chapters\WB850.html

Page 20: Client Dataset Basics

private { Private declarations } FBookmark: TBookmark; public { Public declarations } end; var frmMain: TfrmMain; implementation {$R *.xfm} procedure TfrmMain.FormCreate(Sender: TObject); begin ClientDataSet1.LoadFromFile(’C:\Employee.cds’); end; procedure TfrmMain.btnFirstClick(Sender: TObject); begin ClientDataSet1.First; end; procedure TfrmMain.btnPriorClick(Sender: TObject); begin ClientDataSet1.Prior; end; procedure TfrmMain.btnNextClick(Sender: TObject); begin ClientDataSet1.Next; end; procedure TfrmMain.btnLastClick(Sender: TObject); begin ClientDataSet1.Last; end; procedure TfrmMain.btnSetRecNoClick(Sender: TObject); var Value: string; begin Value := ’1’; if InputQuery(’RecNo’, ’Enter Record Number’, Value) then ClientDataSet1.RecNo := StrToInt(Value); end; procedure TfrmMain.btnGetBookmarkClick(Sender: TObject); begin if Assigned(FBookmark) then ClientDataSet1.FreeBookmark(FBookmark); FBookmark := ClientDataSet1.GetBookmark; end; procedure TfrmMain.btnGotoBookmarkClick(Sender: TObject); begin if Assigned(FBookmark) then ClientDataSet1.GotoBookmark(FBookmark)

Page 20 of 43Chapter 3: Client Dataset Basics

10/22/2001file://J:\Sams\chapters\WB850.html

Page 21: Client Dataset Basics

else ShowMessage(’No bookmark set!’); end; end.

Figure 3.6 shows this program at runtime.

Client Dataset Indexes

So far, we haven’t created any indexes on the client dataset and you might be wondering if (and why) they’re even necessary when sequential searches through the dataset (using Locate) are so fast.

Indexes are used on client datasets for at least three reasons:

z To provide faster access to data. A single Locate operation executes very quickly, but if you need to perform thousands of Locate operations, there is a noticeable performance gain when using indexes.

z To enable the client dataset to be sorted on-the-fly. This is useful when you want to order the data in a data-aware grid, for example.

z To implement maintained aggregates.

Figure 3.6 The Navigate application demonstrates various navigational techniques.

Creating Indexes

Like field definitions, indexes can be created at design-time or at runtime. Unlike field definitions, which are usually created at design-time, you might want to create and destroy indexes at runtime. For example, some indexes are only used for a short time—say, to create a report in a certain order. In this case, you might want to create the index, use it, and then destroy it. If you constantly need an index, it's better to create it at design-time (or to create it the first time you need it and not destroy it afterward).

Creating Indexes at Design-Time

To create an index at design-time, click the TClientDataSet component located on the form or data module. In the Object Inspector, double-click the IndexDefs property. The index editor appears.

To add an index to the client dataset, right-click the index editor and select Add from the pop-up menu. Alternately, you can click the Add icon on the toolbar, or simply press Ins.

Next, go back to the Object Inspector and set the appropriate properties for the index. Table 3.2 shows the index properties.

Table 3.2 Index Properties

Property Description

Page 21 of 43Chapter 3: Client Dataset Basics

10/22/2001file://J:\Sams\chapters\WB850.html

Page 22: Client Dataset Basics

Table 3.3 shows the various index options that can be set using the Options property.

Table 3.3 Index Options

You can create multiple indexes on a single dataset. So, you can easily have both an ascending and a descending index on EmployeeName, for example.

Creating and Deleting Indexes at Runtime

In contrast to field definitions (which you usually create at design-time), index definitions are something that you frequently create at runtime. There are a couple of very good reasons for this:

z Indexes can be quickly and easily created and destroyed. So, if you only need an index for a short

Name The name of the index. I recommend prefixing index names with the letters by (as in byName, byState, and so on).

Fields Semicolon-delimited list of fields that make up the index. Example: ’ID’ or ’Name;Salary’.

DescFields A list of the fields contained in the Fields property that should be indexed in descending order. For example, to sort ascending by name, and then descending by salary, set Fields to ’Name;Salary’ and DescFields to ’Salary’.

CaseInsFields A list of the fields contained in the Fields property that should be indexed in a manner which is not case sensitive. For example, if the index is on the last and first name, and neither is case sensitive, set Fields to ’Last;First’ and CaseInsFields to ’Last;First’.

GroupingLevel Used for aggregation. Options Sets additional options on the index. The options are

discussed in Table 3.3. Expression Not applicable to client datasets. Source Not applicable to client datasets.

Option Description IxPrimary The index is the primary index on the dataset. IxUnique The index is unique. IxDescending The index is in descending order. IxCaseInsensitive The index is not case sensitive. IxExpression Not applicable to client datasets. IxNonMaintained Not applicable to client datasets.

Page 22 of 43Chapter 3: Client Dataset Basics

10/22/2001file://J:\Sams\chapters\WB850.html

Page 23: Client Dataset Basics

period of time (to print a report in a certain order, for example), creating and destroying the index on an as-needed basis helps conserve memory.

z Index information is not saved to a file or a stream when you persist a client dataset. When you load a client database from a file or a stream, you must re-create any indexes in your code.

To create an index, you use the client dataset’s AddIndex method. AddIndex takes three mandatory parameters, as well as three optional parameters, and is defined like this:

procedure AddIndex(const Name, Fields: string; Options: TIndexOptions; const DescFields: string = ’’; const CaseInsFields: string = ’’; const GroupingLevel: Integer = 0);

The parameters correspond to the TIndexDef properties listed in Table 3.2. The following code snippet shows how to create a unique index by last and first names:

ClientDataSet1.AddIndex(’byName’, ’Last;First’, [ixUnique]);

When you decide that you no longer need an index (remember, you can always re-create it if you need it later), you can delete it using DeleteIndex. DeleteIndex takes a single parameter: the name of the index being deleted. The following line of code shows how to delete the index created in the preceding code snippet:

ClientDataSet1.DeleteIndex(’byName’);

Using Indexes

Creating an index doesn’t perform any actual sorting of the dataset. It simply creates an available index to the data. After you create an index, you make it active by setting the dataset’s IndexName property, like this:

ClientDataSet1.IndexName := ’byName’;

If you have two or more indexes defined on a dataset, you can quickly switch back and forth by changing the value of the IndexName property. If you want to discontinue the use of an index and revert to the default record order, you can set the IndexName property to an empty string, as the following code snippet illustrates:

// Do something in name order ClientDataSet1.IndexName := ’byName’; // Do something in salary order ClientDataSet1.IndexName := ’bySalary’; // Switch back to the default ordering ClientDataSet1.IndexName := ’’;

There is a second way to specify indexes on-the-fly at runtime. Instead of creating an index and setting the IndexName property, you can simply set the IndexFieldNames property. IndexFieldNames accepts a semicolon-delimited list of fields to index on. The following code shows how to use it:

ClientDataSet1.IndexFieldNames := ’Last;First’;

Page 23 of 43Chapter 3: Client Dataset Basics

10/22/2001file://J:\Sams\chapters\WB850.html

Page 24: Client Dataset Basics

Though IndexFieldNames is quicker and easier to use than AddIndex/IndexName, its simplicity does not come without a price. Specifically,

z You cannot set any index options, such as unique or descending indexes.

z You cannot specify a grouping level or create maintained aggregates.

z When you switch from one index to another (by changing the value of IndexFieldNames), the old index is automatically dropped. If you switch back at a later time, the index is re-created. This happens so fast that it’s not likely to be noticeable, but you should be aware that it’s happening, nonetheless. When you create indexes using AddIndex, the index is maintained until you specifically delete it using DeleteIndex.

Note

Though you can switch back and forth between IndexName and IndexFieldNames in the same application, you can’t set both properties at the same time. Setting IndexName clears IndexFieldNames, and setting IndexFieldNames clears IndexName.

Retrieving Index Information

Delphi provides a couple of different methods for retrieving index information from a dataset. These methods are discussed in the following sections.

GetIndexNames

The simplest method for retrieving index information is GetIndexNames. GetIndexNames takes a single parameter, a TStrings object, in which to store the resultant index names. The following code snippet shows how to load a list box with the names of all indexes defined for a dataset.

ClientDataSet1.GetIndexNames(ListBox1.Items);

Caution

If you execute this code on a dataset for which you haven’t defined any indexes, you’ll notice that there are two indexes already defined for you: DEFAULT_ORDER and CHANGEINDEX. DEFAULT_ORDER is used internally to provide records in nonindexed order. CHANGEINDEX is used internally to provide undo support, which is discussed later in this chapter. You should not attempt to delete either of these indexes.

TIndexDefs

Page 24 of 43Chapter 3: Client Dataset Basics

10/22/2001file://J:\Sams\chapters\WB850.html

Page 25: Client Dataset Basics

If you want to obtain more detailed information about an index, you can go directly to the source: TIndexDefs. TIndexDefs contains a list of all indexes, along with the information associated with each one (such as the fields that make up the index, which fields are descending, and so on).

The following code snippet shows how to access index information directly through TIndexDefs.

var Index: Integer; IndexDef: TIndexDef; begin ClientDataSet1.IndexDefs.Update; for Index := 0 to ClientDataSet1.IndexDefs.Count - 1 do begin IndexDef := ClientDataSet1.IndexDefs[Index]; ListBox1.Items.Add(IndexDef.Name); end; end;

Notice the call to IndexDefs.Update before the code that loops through the index definitions. This call is required to ensure that the internal IndexDefs list is up-to-date. Without it, it’s possible that IndexDefs might not contain any information about recently added indexes.

The following application demonstrates how to provide on-the-fly indexing in a TDBGrid. It also contains code for retrieving detailed information about all the indexes defined on a dataset.

Figure 3.7 shows the CDSIndex application at runtime, as it displays index information for the employee client dataset.

Listing 3.3 contains the complete source code for the CDSIndex application.

Figure 3.7 CDSIndex shows how to create indexes on-the-fly.

Listing 3.3 CDSIndex—MainForm.pas

unit MainForm; interface uses SysUtils, Classes, QGraphics, QControls, QForms, QDialogs, QStdCtrls, DB, DBClient, QExtCtrls, QActnList, QGrids, QDBGrids; type TfrmMain = class(TForm) DataSource1: TDataSource; pnlClient: TPanel; DBGrid1: TDBGrid; ClientDataSet1: TClientDataSet; pnlBottom: TPanel; btnDefaultOrder: TButton; btnIndexList: TButton; ListBox1: TListBox; procedure FormCreate(Sender: TObject); procedure DBGrid1TitleClick(Column: TColumn);

Page 25 of 43Chapter 3: Client Dataset Basics

10/22/2001file://J:\Sams\chapters\WB850.html

Page 26: Client Dataset Basics

procedure btnDefaultOrderClick(Sender: TObject); procedure btnIndexListClick(Sender: TObject); private { Private declarations } public { Public declarations } end; var frmMain: TfrmMain; implementation {$R *.xfm} procedure TfrmMain.FormCreate(Sender: TObject); begin ClientDataSet1.LoadFromFile(’C:\Employee.cds’); end; procedure TfrmMain.DBGrid1TitleClick(Column: TColumn); begin try ClientDataSet1.DeleteIndex(’byUser’); except end; ClientDataSet1.AddIndex(’byUser’, Column.FieldName, []); ClientDataSet1.IndexName := ’byUser’; end; procedure TfrmMain.btnDefaultOrderClick(Sender: TObject); begin // Deleting the current index will revert to the default order try ClientDataSet1.DeleteIndex(’byUser’); except end; ClientDataSet1.IndexFieldNames := ’’; end; procedure TfrmMain.btnIndexListClick(Sender: TObject); var Index: Integer; IndexDef: TIndexDef; begin ClientDataSet1.IndexDefs.Update; ListBox1.Items.BeginUpdate; try ListBox1.Items.Clear; for Index := 0 to ClientDataSet1.IndexDefs.Count - 1 do begin IndexDef := ClientDataSet1.IndexDefs[Index]; ListBox1.Items.Add(IndexDef.Name); end; finally ListBox1.Items.EndUpdate; end; end;

Page 26 of 43Chapter 3: Client Dataset Basics

10/22/2001file://J:\Sams\chapters\WB850.html

Page 27: Client Dataset Basics

end.

The code to dynamically sort the grid at runtime is contained in the method DBGrid1TitleClick. First, it attempts to delete the temporary index named byUser, if it exists. If it doesn’t exist, an exception is raised, which the code simply eats. A real application should not mask exceptions willy-nilly. Instead, it should trap for the specific exceptions that might be thrown by the call to DeleteIndex, and let the others be reported to the user.

The method then creates a new index named byUser, and sets it to be the current index.

Note

Though this code works, it is rudimentary at best. There is no support for sorting on multiple grid columns, and no visual indication of what column(s) the grid is sorted by. For an elegant solution to these issues, I urge you to take a look at John Kaster’s TCDSDBGrid (available as ID 15099 on Code Central at http://codecentral.borland.com).

Filters and Ranges

Filters and ranges provide a means of limiting the amount of data that is visible in the dataset, similar to a WHERE clause in a SQL statement. The main difference between filters, ranges, and the WHERE clause is that when you apply a filter or a range, it does not physically change which data is contained in the dataset. It only limits the amount of data that you can see at any given time.

Ranges

Ranges are useful when the data that you want to limit yourself to is stored in a consecutive sequence of records. For example, say a dataset contains the data shown in Table 3.4.

Table 3.4 Sample Data for Ranges and Filters

The data in this much-abbreviated table is indexed by birthday. Ranges can only be used when there is an active index on the dataset.

ID Name Birthday Salary

4 Bill Peterson 3/28/1957 $60,000.00

2 Frank Smith 8/25/1963 $48,000.00

3 Sarah Johnson 7/5/1968 $52,000.00

1 John Doe 5/15/1970 $39,000.00

5 Paula Wallace 1/15/1971 $36,500.00

Page 27 of 43Chapter 3: Client Dataset Basics

10/22/2001file://J:\Sams\chapters\WB850.html

Page 28: Client Dataset Basics

Assume that you want to see all employees who were born between 1960 and 1970. Because the data is indexed by birthday, you could apply a range to the dataset, like this:

ClientDataSet1.SetRange([’1/1/1960’], [’12/31/1970’]);

Ranges are inclusive, meaning that the endpoints of the range are included within the range. In the preceding example, employees who were born on either January 1, 1960 or December 31, 1970 are included in the range.

To remove the range, simply call CancelRange, like this:

ClientDataSet1.CancelRange;

Filters

Unlike ranges, filters do not require an index to be set before applying them. Client dataset filters are powerful, offering many SQL-like capabilities, and a few options that are not even supported by SQL. Tables 3.5–3.10 list the various functions and operators available for use in a filter.

Table 3.5 Filter Comparison Operators

Table 3.6 Filter Logical Operators

Function Description Example = Equality test Name = ’John

Smith’ <> Inequality test ID <> 100

< Less than Birthday < ’1/1/1980’

> Greater than Birthday > ’12/31/1960’

<= Less than or equal to

Salary <= 80000

>= Greater than or equal to

Salary >= 40000

BLANK Empty string field (not used to test for NULL values)

Name = BLANK

IS NULL Test for NULL value

Birthday IS NULL

IS NOT NULL Test for non-NULL value

Birthday IS NOT NULL

Function Example

Page 28 of 43Chapter 3: Client Dataset Basics

10/22/2001file://J:\Sams\chapters\WB850.html

Page 29: Client Dataset Basics

Table 3.7 Filter Arithmetic Operators

Table 3.8 Filter String Functions

Table 3.9 Filter Date/Time Functions

And (Name = ’John Smith’) and (Birthday = ’5/16/1964’)

Or (Name = ’John Smith’) or (Name = ’Julie Mason’)

Not Not (Name = ’John Smith’)

Function Description Example + Addition. Can be used with

numbers, strings, or dates/times.

Birthday + 30 <

’1/1/1960’ Name + ’X’ = ’SmithX’ Salary + 10000 = 100000

– Subtraction. Can be used with numbers or dates/times.

Birthday - 30 > '1/1/1960' Salary - 10000 > 40000

* Multiplication. Can be used with numbers only.

Salary * 0.10 > 5000

/ Division. Can be used with numbers only.

Salary / 10 > 5000

Function Description Example Upper Uppercase Upper(Name) = 'JOHN

SMITH'

Lower Lowercase Lower(Name) = 'john smith'

SubString Return a portion of a string SubString(Name,6) =

'Smith' SubString(Name,1,4) = 'John'

Trim Trim leading and trailing characters from a string

Trim(Name) Trim(Name, '.')

TrimLeft Trim leading characters from a string

TrimLeft(Name) TrimLeft(Name, '.')

TrimRight Trim trailing characters from a string

TrimRight(Name) TrimRight(Name, '.')

Function Description Example Year Returns the year portion of a date

value. Year(Birthday) = 1970

Month Month(Birthday) =

Page 29 of 43Chapter 3: Client Dataset Basics

10/22/2001file://J:\Sams\chapters\WB850.html

Page 30: Client Dataset Basics

Table 3.10 Other Filter Functions and Operators

To filter a dataset, set its Filter property to the string used for filtering, and then set the Filtered property to True. For example, the following code snippet filters out all employees whose names begin with the letter M.

ClientDataSet1.Filter := ’Name LIKE ’ + QuotedStr(’M%’); ClientDataSet1.Filtered := True;

To later display only those employees whose names begin with the letter P, simply change the filter, like this:

ClientDataSet1.Filter := ’Name LIKE ’ + QuotedStr(’P%’);

To remove the filter, set the Filtered property to False. You don’t have to set the Filter property to an empty string to remove the filter (which means that you can toggle the most recent filter on and off by switching the value of Filtered from True to False).

You can apply more advanced filter criteria by handling the dataset’s OnFilterRecord event (instead of setting the Filter property). For example, say that you want to filter out all employees whose last

Returns the month portion of a date value.

1

Day Returns the day portion of a date value.

Day(Birthday) = 15

Hour Returns the hour portion of a time value in 24-hour format.

Hour(Appointment) = 18

Minute Returns the minute portion of a time value.

Minute(Appointment) = 30

Second Returns the second portion of a time value.

Second(Appointment) = 0

GetDate Returns the current date and time. Appointment < GetDate

Date Returns the date portion of a date/time value.

Date(Appointment)

Time Returns the time portion of a date/time value.

Time(Appointment)

Function Description Example LIKE Partial string

comparison. Name LIKE ’%Smith%’

IN Tests for multiple values.

-Year(Birthday) IN (1960, 1970, 1980)

* Partial string comparison.

Name = ’John*’

Page 30 of 43Chapter 3: Client Dataset Basics

10/22/2001file://J:\Sams\chapters\WB850.html

Page 31: Client Dataset Basics

names sound like Smith. This would include Smith, Smythe, and possibly others. Assuming that you have a Soundex function available, you could write a filter method like the following:

procedure TForm1.ClientDataSet1FilterRecord(DataSet: TDataSet; var Accept: Boolean); begin Accept := Soundex(DataSet.FieldByName(’LastName’).AsString) = Soundex(’Smith’); end;

If you set the Accept parameter to True, the record is included in the filter. If you set Accept to False, the record is hidden.

After you set up an OnFilterRecord event handler, you can simply set TClientDataSet.Filtered to True. You don’t need to set the Filter property at all.

The following example demonstrates different filter and range techniques.

Listing 3.4 contains the source code for the main form.

Listing 3.4 RangeFilter—MainForm.pas

unit MainForm; interface uses SysUtils, Classes, QGraphics, QControls, QForms, QDialogs, QStdCtrls, DB, DBClient, QExtCtrls, QGrids, QDBGrids; type TfrmMain = class(TForm) DataSource1: TDataSource; pnlClient: TPanel; pnlBottom: TPanel; btnFilter: TButton; btnRange: TButton; DBGrid1: TDBGrid; ClientDataSet1: TClientDataSet; btnClearRange: TButton; btnClearFilter: TButton; procedure FormCreate(Sender: TObject); procedure btnFilterClick(Sender: TObject); procedure btnRangeClick(Sender: TObject); procedure btnClearRangeClick(Sender: TObject); procedure btnClearFilterClick(Sender: TObject); private { Private declarations } public { Public declarations } end; var frmMain: TfrmMain; implementation

Page 31 of 43Chapter 3: Client Dataset Basics

10/22/2001file://J:\Sams\chapters\WB850.html

Page 32: Client Dataset Basics

uses FilterForm, RangeForm; {$R *.xfm} procedure TfrmMain.FormCreate(Sender: TObject); begin ClientDataSet1.LoadFromFile(’C:\Employee.CDS’); ClientDataSet1.AddIndex(’bySalary’, ’Salary’, []); ClientDataSet1.IndexName := ’bySalary’; end; procedure TfrmMain.btnFilterClick(Sender: TObject); var frmFilter: TfrmFilter; begin frmFilter := TfrmFilter.Create(nil); try if frmFilter.ShowModal = mrOk then begin ClientDataSet1.Filter := frmFilter.Filter; ClientDataSet1.Filtered := True; end; finally frmFilter.Free; end; end; procedure TfrmMain.btnClearFilterClick(Sender: TObject); begin ClientDataSet1.Filtered := False; end; procedure TfrmMain.btnRangeClick(Sender: TObject); var frmRange: TfrmRange; begin frmRange := TfrmRange.Create(nil); try if frmRange.ShowModal = mrOk then ClientDataSet1.SetRange([frmRange.LowValue], [frmRange.HighValue]); finally frmRange.Free; end; end; procedure TfrmMain.btnClearRangeClick(Sender: TObject); begin ClientDataSet1.CancelRange; end; end.

As you can see, the main form loads the employee dataset from a disk, creates an index on the Salary field, and makes the index active. It then enables the user to apply a range, a filter, or both to the dataset.

Listing 3.5 contains the source code for the filter form. The filter form is a simple form that enables the user to select the field on which to filter, and to enter a value on which to filter.

Page 32 of 43Chapter 3: Client Dataset Basics

10/22/2001file://J:\Sams\chapters\WB850.html

Page 33: Client Dataset Basics

Listing 3.5 RangeFilter—FilterForm.pas

unit FilterForm; interface uses SysUtils, Classes, QGraphics, QControls, QForms, QDialogs, QStdCtrls, QExtCtrls; type TfrmFilter = class(TForm) pnlClient: TPanel; pnlBottom: TPanel; Label1: TLabel; cbField: TComboBox; Label2: TLabel; cbRelationship: TComboBox; Label3: TLabel; ecValue: TEdit; btnOk: TButton; btnCancel: TButton; private function GetFilter: string; { Private declarations } public { Public declarations } property Filter: string read GetFilter; end; implementation {$R *.xfm} { TfrmFilter } function TfrmFilter.GetFilter: string; begin Result := Format(’%s %s ’’%s’’’, [cbField.Text, cbRelationship.Text, ecValue.Text]); end; end.

The only interesting code in this form is the GetFilter function, which simply bundles the values of the three input controls into a filter string and returns it to the main application.

Listing 3.6 contains the source code for the range form. The range form prompts the user for a lower and an upper salary limit.

Listing 3.6 RangeFilter—RangeForm.pas

unit RangeForm; interface

Page 33 of 43Chapter 3: Client Dataset Basics

10/22/2001file://J:\Sams\chapters\WB850.html

Page 34: Client Dataset Basics

uses SysUtils, Classes, QGraphics, QControls, QForms, QDialogs, QExtCtrls, QStdCtrls; type TfrmRange = class(TForm) pnlClient: TPanel; pnlBottom: TPanel; Label1: TLabel; Label2: TLabel; ecLower: TEdit; ecUpper: TEdit; btnOk: TButton; btnCancel: TButton; procedure btnOkClick(Sender: TObject); private function GetHighValue: Double; function GetLowValue: Double; { Private declarations } public { Public declarations } property LowValue: Double read GetLowValue; property HighValue: Double read GetHighValue; end; implementation {$R *.xfm} { TfrmRange } function TfrmRange.GetHighValue: Double; begin Result := StrToFloat(ecUpper.Text); end; function TfrmRange.GetLowValue: Double; begin Result := StrToFloat(ecLower.Text); end; procedure TfrmRange.btnOkClick(Sender: TObject); var LowValue: Double; HighValue: Double; begin try LowValue := StrToFloat(ecLower.Text); HighValue := StrToFloat(ecUpper.Text); if LowValue > HighValue then begin ModalResult := mrNone; ShowMessage(’The upper salary must be >= the lower salary’); end; except ModalResult := mrNone; ShowMessage(’Both values must be a valid number’); end; end;

Page 34 of 43Chapter 3: Client Dataset Basics

10/22/2001file://J:\Sams\chapters\WB850.html

Page 35: Client Dataset Basics

end.

Figure 3.8 shows the RangeFilter application in operation.

Figure 3.8 RangeFilter applies both ranges and filters to a dataset.

Searching

In addition to filtering out uninteresting records from a client dataset, TClientDataSet provides a number of methods for quickly locating a specific record. Some of these methods require an index to be active on the dataset, and others do not. The search methods are described in detail in the following sections.

Nonindexed Search Techniques

In this section, I’ll discuss the search techniques that don’t require an active index on the client dataset. Rather than using an index, these methods perform a sequential search through the dataset to find the first matching record.

Locate

Locate is perhaps the most general purpose of the TClientDataSet search methods. You can use Locate to search for a record based on any given field or combination of fields. Locate can also search for records based on a partial match, and can find a match without respect to case.

TClientDataSet.Locate is defined like this:

function Locate(const KeyFields: string; const KeyValues: Variant; Options: TLocateOptions): Boolean; override;

The first parameter, KeyFields, designates the field (or fields) to search. When searching multiple fields, separate them by semicolons (for example, ’Name;Birthday’).

The second parameter, KeyValues, represents the values to search for. The number of values must match the number of key fields exactly. If there is only one search field, you can simply pass the value to search for here. To search for multiple values, you must pass the values as a variant array. One way to do this is by calling VarArrayOf, like this:

VarArrayOf([’John Smith’, ’4/15/1965’])

The final parameter, Options, is a set that determines how the search is to be executed. Table 3.11 lists the available options.

Table 3.11 Locate Options

Value Description loPartialKey

Page 35 of 43Chapter 3: Client Dataset Basics

10/22/2001file://J:\Sams\chapters\WB850.html

Page 36: Client Dataset Basics

Both options pertain to string fields only. They are ignored if you specify them for a nonstring search.

Locate returns True if a matching record is found, and False if no match is found. In case of a match, the record is made current.

The following examples help illustrate the options:

ClientDataSet1.Locate(’Name’, ’John Smith’, []);

This searches for a record where the name is ’John Smith’.

ClientDataSet1.Locate(’Name’, ’JOHN’, [loPartialKey, loCaseInsensitive]);

This searches for a record where the name begins with ’JOHN’. This finds ’John Smith’, ’Johnny Jones’, and ’JOHN ADAMS’, but not ’Bill Johnson’.

ClientDataSet1.Locate(’Name;Birthday’, VarArrayOf([’John’, ’4/15/1965’]), [loPartialKey]);

This searches for a record where the name begins with ’John’ and the birthday is April 15, 1965. In this case, the loPartialKey option applies to the name only. Even though the birthday is passed as a string, the underlying field is a date field, so the loPartialKey option is ignored for that field only.

Lookup

Lookup is similar in concept to Locate, except that it doesn’t change the current record pointer. Instead, Lookup returns the values of one or more fields in the record. Also, Lookup does not accept an Options parameter, so you can’t perform a lookup that is based on a partial key or that is not case sensitive.

Lookup is defined like this:

function Lookup(const KeyFields: string; const KeyValues: Variant; const ResultFields: string): Variant; override;

KeyFields and KeyValues specify the fields to search and the values to search for, just as with the Locate method. ResultFields specifies the fields for which you want to return data. For example, to return the birthday of the employee named John Doe, you could write the following code:

var V: Variant; begin V := ClientDataSet1.Lookup(’Name’, ’John Doe’, ’Birthday’); end;

KeyValues do not necessarily represent an exact match. Locate finds the first record whose field value starts with the value specified in KeyValues.

loCaseInsensitive Locate ignores case when searching for string fields.

Page 36 of 43Chapter 3: Client Dataset Basics

10/22/2001file://J:\Sams\chapters\WB850.html

Page 37: Client Dataset Basics

The following code returns the name and birthday of the employee with ID number 100.

var V: Variant; begin V := ClientDataSet1.Lookup(’ID’, 100, ’Name;Birthday’); end;

If the requested record is not found, V is set to NULL. If ResultFields contains a single field name, then on return from Lookup, V is a variant containing the value of the field listed in ResultFields. If ResultFields contains multiple single-field names, then on return from Lookup, V is a variant array containing the values of the fields listed in ResultFields.

Note

For a comprehensive discussion of variant arrays, see my book, Delphi COM Programming, published by Macmillan Technical Publishing.

The following code snippet shows how you can access the results that are returned from Lookup.

var V: Variant; begin V := ClientDataSet1.Lookup(’ID’, 100, ’Name’); if not VarIsNull(V) then ShowMessage(’ID 100 refers to ’ + V); V := ClientDataSet1.Lookup(’ID’, 200, ’Name;Birthday’); if not VarIsNull(V) then ShowMessage(’ID 200 refers to ’ + V[0] + ’, born on ’ + DateToStr(V[1])); end;

Indexed Search Techniques

The search techniques mentioned earlier do not require an index to be active (in fact, they don’t require the dataset to be indexed at all), but TDataSet also supports several indexed search operations. These include FindKey, FindNearest, and GotoKey, which are discussed in the following sections.

FindKey

FindKey searches for an exact match on the key fields of the current index. For example, if the dataset is currently indexed by ID, FindKey searches for an exact match on the ID field. If the dataset is indexed by last and first name, FindKey searches for an exact match on both the last and the first name.

FindKey takes a single parameter, which specifies the value(s) to search for. It returns a Boolean value that indicates whether a matching record was found. If no match was found, the current record pointer is unchanged. If a matching record is found, it is made current.

Page 37 of 43Chapter 3: Client Dataset Basics

10/22/2001file://J:\Sams\chapters\WB850.html

Page 38: Client Dataset Basics

The parameter to FindKey is actually an array of values, so you need to put the values in brackets, as the following examples show:

if ClientDataSet.FindKey([25]) then ShowMessage(’Found ID 25’); ... if ClientDataSet.FindKey([’Doe’, ’John’]) then ShowMessage(’Found John Doe’);

You need to ensure that the values you search for match the current index. For that reason, you might want to set the index before making the call to FindKey. The following code snippet illustrates this:

ClientDataSet1.IndexName := ’byID’; if ClientDataSet.FindKey([25]) then ShowMessage(’Found ID 25’); ... ClientDataSet1.IndexName := ’byName’; if ClientDataSet.FindKey([’Doe’, ’John’]) then ShowMessage(’Found John Doe’);

FindNearest

FindNearest works similarly to FindKey, except that it finds the first record that is greater than or equal to the value(s) passed to it. This depends on the current value of the KeyExclusive property.

If KeyExclusive is False (the default), FindNearest finds the first record that is greater than or equal to the passed-in values. If KeyExclusive is True, FindNearest finds the first record that is greater than the passed-in values.

If FindNearest doesn’t find a matching record, it moves the current record pointer to the end of the dataset.

GotoKey

GotoKey performs the same function as FindKey, except that you set the values of the search field(s) before calling GotoKey. The following code snippet shows how to do this:

ClientDataSet1.IndexName := ’byID’; ClientDataSet1.SetKey; ClientDataSet1.FieldByName(’ID’).AsInteger := 25; ClientDataSet1.GotoKey;

If the index is made up of multiple fields, you simply set each field after the call to SetKey, like this:

ClientDataSet1.IndexName := ’byName’; ClientDataSet1.SetKey; ClientDataSet1.FieldByName(’First’).AsString := ’John’; ClientDataSet1.FieldByName(’Last’).AsString := ’Doe’; ClientDataSet1.GotoKey;

After calling GotoKey, you can use the EditKey method to edit the key values used for the search. For example, the following code snippet shows how to search for John Doe, and then later search for John Smith. Both records have the same first name, so only the last name portion of the key needs to be

Page 38 of 43Chapter 3: Client Dataset Basics

10/22/2001file://J:\Sams\chapters\WB850.html

Page 39: Client Dataset Basics

specified during the second search.

ClientDataSet1.IndexName := ’byName’; ClientDataSet1.SetKey; ClientDataSet1.FieldByName(’First’).AsString := ’John’; ClientDataSet1.FieldByName(’Last’).AsString := ’Doe’; ClientDataSet1.GotoKey; // Do something with the record // EditKey preserves the values set during the last SetKey ClientDataSet1.EditKey; ClientDataSet1.FieldByName(’Last’).AsString := ’Smith’; ClientDataSet1.GotoKey;

GotoNearest

GotoNearest works similarly to GotoKey, except that it finds the first record that is greater than or equal to the value(s) passed to it. This depends on the current value of the KeyExclusive property.

If KeyExclusive is False (the default), GotoNearest finds the first record that is greater than or equal to the field values set after a call to either SetKey or EditKey. If KeyExclusive is True, GotoNearest finds the first record that is greater than the field values set after calling SetKey or EditKey.

If GotoNearest doesn’t find a matching record, it moves the current record pointer to the end of the dataset.

The following example shows how to perform indexed and nonindexed searches on a dataset.

Listing 3.7 shows the source code for the Search application, a sample program that illustrates the various indexed and nonindexed searching techniques supported by TClientDataSet.

unit MainForm; interface uses SysUtils, Classes, Variants, QGraphics, QControls, QForms, QDialogs, QStdCtrls, DB, DBClient, QExtCtrls, QActnList, QGrids, QDBGrids; type TfrmMain = class(TForm) DataSource1: TDataSource; pnlClient: TPanel; pnlBottom: TPanel; btnSearch: TButton; btnGotoBookmark: TButton; btnGetBookmark: TButton; btnLookup: TButton; DBGrid1: TDBGrid; ClientDataSet1: TClientDataSet; btnSetRecNo: TButton; procedure FormCreate(Sender: TObject); procedure btnGetBookmarkClick(Sender: TObject); procedure btnGotoBookmarkClick(Sender: TObject); procedure btnSetRecNoClick(Sender: TObject); procedure btnSearchClick(Sender: TObject);

Page 39 of 43Chapter 3: Client Dataset Basics

10/22/2001file://J:\Sams\chapters\WB850.html

Page 40: Client Dataset Basics

procedure btnLookupClick(Sender: TObject); private { Private declarations } FBookmark: TBookmark; public { Public declarations } end; var frmMain: TfrmMain; implementation uses SearchForm; {$R *.xfm} procedure TfrmMain.FormCreate(Sender: TObject); begin ClientDataSet1.LoadFromFile(’C:\Employee.cds’); ClientDataSet1.AddIndex(’byName’, ’Name’, []); ClientDataSet1.IndexName := ’byName’; end; procedure TfrmMain.btnGetBookmarkClick(Sender: TObject); begin if Assigned(FBookmark) then ClientDataSet1.FreeBookmark(FBookmark); FBookmark := ClientDataSet1.GetBookmark; end; procedure TfrmMain.btnGotoBookmarkClick(Sender: TObject); begin if Assigned(FBookmark) then ClientDataSet1.GotoBookmark(FBookmark) else ShowMessage(’No bookmark assigned’); end; procedure TfrmMain.btnSetRecNoClick(Sender: TObject); var Value: string; begin Value := ’1’; if InputQuery(’RecNo’, ’Enter Record Number’, Value) then ClientDataSet1.RecNo := StrToInt(Value); end; procedure TfrmMain.btnSearchClick(Sender: TObject); var frmSearch: TfrmSearch; begin frmSearch := TfrmSearch.Create(nil); try if frmSearch.ShowModal = mrOk then begin case TSearchMethod(frmSearch.grpMethod.ItemIndex) of smLocate: ClientDataSet1.Locate(’Name’, frmSearch.ecName.Text,

Page 40 of 43Chapter 3: Client Dataset Basics

10/22/2001file://J:\Sams\chapters\WB850.html

Page 41: Client Dataset Basics

[loPartialKey, loCaseInsensitive]); smFindKey: ClientDataSet1.FindKey([frmSearch.ecName.Text]); smFindNearest: ClientDataSet1.FindNearest([frmSearch.ecName.Text]); smGotoKey: begin ClientDataSet1.SetKey; ClientDataSet1.FieldByName(’Name’).AsString := frmSearch.ecName.Text; ClientDataSet1.GotoKey; end; smGotoNearest: begin ClientDataSet1.SetKey; ClientDataSet1.FieldByName(’Name’).AsString := frmSearch.ecName.Text; ClientDataSet1.GotoNearest; end; end; end; finally frmSearch.Free; end; end; procedure TfrmMain.btnLookupClick(Sender: TObject); var Value: string; V: Variant; begin Value := ’1’; if InputQuery(’ID’, ’Enter ID to Lookup’, Value) then begin V := ClientDataSet1.Lookup(’ID’, StrToInt(Value), ’Name;Salary’); if not VarIsNull(V) then ShowMessage(Format(’ID %s refers to %s, who makes %s’, [Value, V[0], FloatToStrF(V[1], ffCurrency, 10, 2)])); end; end; end.

Listing 3.8 contains the source code for the search form. The only interesting bit of code in this listing is the TSearchMethod, defined near the top of the unit, which is used to determine what method to call for the search.

Listing 3.8 Search—SearchForm.pas

unit SearchForm; interface uses SysUtils, Classes, QGraphics, QControls, QForms, QDialogs, QExtCtrls, QStdCtrls;

Page 41 of 43Chapter 3: Client Dataset Basics

10/22/2001file://J:\Sams\chapters\WB850.html

Page 42: Client Dataset Basics

type TSearchMethod = (smLocate, smFindKey, smFindNearest, smGotoKey, smGotoNearest); TfrmSearch = class(TForm) pnlClient: TPanel; pnlBottom: TPanel; Label1: TLabel; ecName: TEdit; grpMethod: TRadioGroup; btnOk: TButton; btnCancel: TButton; private { Private declarations } public { Public declarations } end; implementation {$R *.xfm} end.

Figure 3.9 shows the Search application at runtime.

Figure 3.9 Search demonstrates indexed and nonindexed searches.

Summary

TClientDataSet is an extremely powerful in-memory dataset that supports a number of high-performance sorting and searching operations. Following are several key points to take away from this chapter:

z You can create client datasets both at design-time and at runtime. This chapter showed how to save them to a disk for use in single-tier database applications.

z The three basic ways of populating a client dataset are

{ Manually with Append or Insert

{ From another dataset

{ From a file or stream (that is, via persisting client datasets)

z Datasets in Delphi can be navigated in a variety of ways: sequentially, via bookmarks, and via record numbers.

z You can create indexes on a dataset enabling you to quickly and easily sort the records in a given order, and to locate records that match a certain criteria.

Page 42 of 43Chapter 3: Client Dataset Basics

10/22/2001file://J:\Sams\chapters\WB850.html

Page 43: Client Dataset Basics

z Filters and ranges can be used to limit the amount of data that is visible in the dataset. Ranges are useful when the relevant data is stored in a consecutive sequence of records. Unlike ranges, filters do not require an index to be set before applying them.

z Locate and Lookup are nonindexed search techniques for locating a specific record in a client dataset. FindKey, FindNearest, GotoKey, and GotoNearest are indexed search techniques.

In the following chapter, I’ll discuss more advanced client dataset functionality.

© Copyright Pearson Education. All rights reserved.

Page 43 of 43Chapter 3: Client Dataset Basics

10/22/2001file://J:\Sams\chapters\WB850.html