Sunteți pe pagina 1din 133

1 A Guide to Using the TClientDataSet in

Delphi applications
By Zarko Gaiic, About.com Guide
2 See More About:
O delphi database programming
O dbexpress
O tclientdataset
O using db-aware controls
Looking Ior a single-Iile, single-user database Ior your next Delphi application? Need to store
some application speciIic data but you do not want to user the Registry / INI / or something
else?
Why look outside the box? Delphi already has an answer Ior you: the TClientDataSet
component (located on the "Data Access" tab oI the component palette) represents an in-
memory database-independent dataset. Whether you use client datasets Ior Iile-based data,
caching updates, data Irom an external provider (such as working with an XML document or
in a multi-tiered application), or a combination oI these approaches such as a "brieIcase
model" application, you can take advantage oI broad range oI Ieatures client datasets support.
Time to learn about TClientDataSet:
A ClientDataSet in Every Database Application
The basic behavior oI the ClientDataSet is described, and an argument is made Ior the
extensive use oI ClientDataSets in most all database applications.
Defining a ClientDataSet's Structure Using FieldDefs
When creating a ClientDataSet's memory store on-the-Ily, you must explicitly deIine the
structure oI your table. This article shows you how to do it at both runtime and design-time
using FieldDeIs.
Defining a ClientDataSet's Structure Using TFields
This article demonstrates how to deIine a ClientDataSet's structure at both design-time and
runtime using TFields. How to create virtual and nested dataset Iields is also demonstrated.
Understanding ClientDataSet Indexes
A ClientDataSet does not obtain its indexes Irom the data it loads. Indexes, iI you want them,
must be explicitly deIined. This article shows you how to do this at design-time or runtime.
Navigating and Editing a ClientDataSet
You navigate and edit a ClientDataSet in a manner similar to how you navigate and edit
almost another other dataset. This article provides an introductory look at basic ClientDataSet
navigation and editing.
Searching a ClientDataSet
ClientDataSets provide a number oI diIIerent mechanisms Ior searching Ior and location data
in its columns. These techniques are covered in this continuation oI the discussion oI basic
ClientDataSet manipulation.
Filtering ClientDataSets
When applied to a dataset, a Iilter limits the records that are accessible. This article explores
the ins and outs oI Iiltering ClientDataSets.
ClientDataSet Aggregates and GroupState
This article describes how to use aggregates to calculate simple statistics, as well as how to
use group state to improve your user interIaces.
Nesting DataSets in ClientDataSets
Like the name suggests, a nested dataset is a dataset within a dataset. By nesting one dataset
inside another, you can reduce your overall storage needs, increase the eIIiciency oI network
communications, and simpliIy data operations.
Cloning ClientDatSet Cursors
When you clone a ClientDataSet's cursor, you create not only an additional pointer to a shared
memory store, but also an independent view oI the data. This article shows you how to use
this important capability
Deploying Applications that use ClientDataSets
Depending on what you do within your application, iI you use one or more ClientDataSets
you may need to deploy one or more libraries, in addition to your application's executable.
This article describes when and how
Creative Solutions Using ClientDataSets
ClientDataSets can be used Ior much more than displaying rows and columns Irom a
database. See how they solve applications issues Irom selecting options to process, progress
messages, creating audit trails Ior data changes and more.

3 A ClientDataSet in Every Database
Application
By: Cary Jensen
Abstract: This article is the Iirst in an extended series designed to explore the ClientDataSet.
The basic behavior oI the ClientDataSet is described, and an argument is made Ior the
extensive use oI ClientDataSets in most all database applications.
The ClientDataSet is a component that holds data in an in-memory table. Until recently, it was
only available in the Enterprise editions oI Delphi and C Builder. Now, however, it is
available in the proIessional editions oI these products, as well as Kylix. This article is the
Iirst in an extended series designed to explore the capabilities and Ieatures oI the
ClientDataSet.
I have been playing with an idea Ior a while, and I wanted the title oI this article to reIlect this
(with my apologies to Herbert Hoover Ior the pathetic turn oI his political promise oI "two
chickens in every pot and a car in every garage"). In short, I believe that a very strong
argument can be made Ior including one ClientDataSet and a corresponding DataSetProvider
Ior each TDataSet used in an application. Doing so provides your user interIace and runtime
code with a consistent set oI Ieatures (Iilters, ranges, searches, and so Iorth) regardless oI the
data access technology being employed.
Actually I have two goals in this Iirst oI many articles detailing the ClientDataSet. The Iirst is
to set Iorth the reasons why I believe that ClientDataSets should play a primary role in most
database applications. The second goal, and the one that I hope you Iind useIul whether or not
you accept my arguments, is to provide a general introduction to the nature and Ieatures oI the
ClientDataSet.
It's this second goal that I will address Iirst. SpeciIically, in order Ior my arguments to make
sense, it is essential to Iirst provide an overview oI the ClientDataSet, and how it interacts
with a DataSetProvider. This discussion will also serve as a primer Ior many oI the technique-
speciIic articles that will Iollow in this series. AIter this introduction I will return to my Iirst
premise, explaining in detail how you can improve your applications through the thoughtIul
use oI ClientDataSets.
4 Introduction to the ClientDataSet
The ClientDataSet has been around Ior a while: Since Delphi 3 to be precise. But up until
recently it has only been available in the Client/Server or Enterprise editions oI Delphi and
C Builder. In these editions the ClientDataSet was intended to hold data in a DataSnap
(Iormerly called MIDAS) client application. While many Enterprise edition developers did
make extensive use oI the ClientDataSet's Ieatures in non-DataSnap application, that this
component did not exist in the ProIession edition products made recommending its
widespread employment unrealistic.
With Borland's introduction oI dbExpress, which Iirst appeared in Kylix 1.0, the
ClientDataSet, and its companion, the DataSetProvider, are now part oI the Borland's
ProIessional Edition RAD (rapid application development) products, including Delphi 6,
Kylix 2, and C Builder 6. Now all Borland RAD developers have access to this powerIul
and Ilexible component (I'm not counting the Personal or Open edition developers in this
group, since those versions do not have the database-related components in the Iirst place).
With this in mind, let's now take a closer look at how the ClientDataSet works.
The ClientDataSet is a TDataSet descendant that holds data in memory in a table-like
structure consisting oI rows (records) and columns (Iields). Using the methods oI the
TDataSet class, a developer can navigate, sort, search, Iilter, and edit the data held in memory.
Because these operations are perIormed on data stored in memory, they are very Iast. For
example, on a test machine with 512 MB oI RAM running an 850 MHz Pentium 3, an index
was build on an integer Iield containing random numbers oI a 100,000 record table in iust
under one-halI second. Once built, this index can be used to perIorm near instantaneous
searches and set ranges on this indexed Iield.
The ClientDataSet actually contains two data stores. The Iirst, named Data, contains the
current view oI the data in memory, including all changes to that data since it was loaded. For
example, iI a record was deleted Irom the dataset, that record is absent Irom Data. Likewise,
records added to the ClientDataSet are visible in Data.
The second store, named Delta, represents the change log, and contains a record oI those
changes that have been made to Data. SpeciIically, Ior each record that was inserted or
deleted Irom Data, there resides a corresponding record in Delta. For modiIied records it is
slightly diIIerent. The change log contains two records Ior each record modiIied in Data. One
oI these is a duplicate oI the record that was originally modiIied. The second contains the
Iield-by-Iield changes made to the original record.
The change log serves two purposes. First, the inIormation in the change log can be used to
restore edits made to Data, so long as those changes have not yet been resolved to the
underlying data source. By deIault, this change log is always maintained, meaning that in
most applications the ClientDataSet is always caching updates.
The second role that the change log plays only applies to a ClientDataSet that is used in
coniunction with a DataSetProvider. In this role, the change log provides suIIicient detail to
permit the mechanisms supported by the DataSetProvider to apply the logged changes to the
dataset Irom which the data was loaded. This process begins when you explicitly call the
ClientDataSets ApplyUpdates method.
When a ClientDataSet is used to read and write data directly Irom a Iile, a DataSetProvider is
not used. In those cases, the change log is stored in this Iile each time you invoke the
ClientDataSets SaveToFile method, and restored each time you call LoadFromFile (or iI you
open and close the ClientDataSet when the FileName contains the name oI the Iile). The
change log is only cleared in this scenario when you invoke MergeChangeLog or
ClearChanges (this second method causes the changes to be lost).
There are quite a Iew diIIerences between how you use a ClientDataSet depending on whether
or not a DataSetProvider is employed. The Iollowing discussion Iocuses exclusively on the
situation where a ClientDataSet points to a DataSetProvider with its ProviderName property.
Using a ClientDataSet directly with Iiles will be discussed in detail in a Iuture article.
5 How a ClientDataSet and a DataSetProvider Interact
In order to use a ClientDataSet eIIectively you must understand how a ClientDataSet interacts
with a DataSetProvider. To illustrate this interaction I have created a Delphi proiect named
CDSLoadBehaviorDemo. The main Iorm Ior this proiect is shown in the Iollowing Iigure.
While I will describe what this proiect does, it is best iI you download this proiect Irom Code
Central and run it. That way you can observe Iirst-hand the interaction.

Here is the basic setup. The ClientDataSet points to a DataSetProvider through its
ProviderName property, and the DataSetProvider reIers to a TDataSet descendant through its
DataSet property. When you set the ClientDataSets Active property to True or invoke its
Open method, the ClientDataSet makes a data packet request Irom the DataSetProvider. This
provider then opens the dataset to which it points, goes to the Iirst record, and then scans
through the records until it reaches the end oI the Iile. With each record it encounters the
DataSetProvider encodes the data into a variant array. This variant array is sometimes reIerred
to as the data packet. When the DataSetProvider is done scanning the records, it closes the
dataset to which it points, and then passes the data packet to the ClientDataSet.
You can see this behavior in the CDSLoadBehaviorDemo proiect. The DBGrid on the right-
hand side oI the main Iorm is connected to a data source that points to a TTable Irom which
the DataSetProvider gets its data. When you select ClientDataSet ' Load Irom this proiect's
main menu, you will literally see the TTable's data being scanned in this DBGrid. Once the
DataSetProvider gets to the last record oI the TTable, the TTable is closed and this DBGrid
appears empty again, as shown in the Iollowing Iigure.

Whether or not the scanning oI the TTable is visible in the CDSLoadBehaviorDemo proiect is
conIigurable. Visible scanning is the deIault in this proiect, but because this visible scanning
requires so many screen repaints, the ClientDataSet takes quite a bit oI time to load the not
quite 1000 records oI the Items.db table (the table pointed to by the TTable). II you select
View ' View Table Loading to uncheck this menu option, and select ClientDataSet ' Load (iI
data is already loaded, you must Iirst select ClientDataSet ' Unload), you will notice that these
records load almost instantly. The actual load time oI a ClientDataSet depends on how much
data is loaded.
Returning to a description oI the ClientDataSet/DataSetProvider interaction, upon receiving
the variant array, the ClientDataSet unpacks this data into memory. The structure oI this
dataset is based on metadata that the DataSetProvider encodes in the variant array. Even
though the dataset to which the DataSetProvider pointed may contain one or more indexes,
the data packet contains no index inIormation. II you want indexes on the ClientDataSet, you
must deIine or create them. ClientDataSet indexes can be deIined at runtime using the
IndexDeIs property, and this topic will be discussed at length in a Iuture article.
The ClientDataSet now behaves iust like most any other opened TDataSet descendant. Its data
can be navigated, Iiltered, edited, indexed, and so Iorth. As pointed out earlier, any edits made
to the ClientDataSet will aIIect the contents oI both the Data and Delta properties. In essence,
these changes are cached, and are lost iI the ClientDataSet is closed without speciIically
telling it save the changes. Changes are saved by invoking the ClientDataSet's ApplyChanges
method.
6 Applying Changes to the Underlying Data Source
When you invoke ApplyChanges, the ClientDataSet passes Delta to the DataSetProvider.
How the DataSetProvider applies the changes depends on how you have conIigured it. By
deIault, the DataSetProvider will create an instance oI the TSQLResolver class, and this class
will generate SQL statements that will be executed against the underlying data source.
SpeciIically, the SQLResolver will generate one SQL statement Ior each deleted, inserted, and
modiIied record in the change log. Both the UpdateMode property oI the DataSetProvider, as
well as the ProviderFlags property oI the TFields Ior the provider's dataset, dictate exactly
how this SQL statement is Iormed. ConIiguring these properties will be discussed in a Iuture
article.
II the dataset to which the DataSetProvider points is an editable dataset, you can alternatively
set the provider's ResolveToDataSet property to True. With this conIiguration, a
SQLResolver is not used. Instead, the DataSetProvider will edit the dataset to which it points
directly. For example, the DataSetProvider will locate and delete each record marked Ior
deletion in the change log, and locate and change each record marked modiIied in the change
log.
II you download the CDSLoadBehaviorDemo proiect, you can see this Ior yourselI. From
your designer, select DataSetProvider1 and set its ResolveToDataSet property to True. Next,
run the proiect and load the ClientDataSet. AIter making several changes to the data, select
File ' ApplyUpdates. Depending on the speed oI your computer, you may or may not actually
see the DBGrid become active as the TTable is edited. However, on most systems you will
notice the DBNavigator buttons become active brieIly as a result oI the editing process. (II
your computer is too Iast, and you cannot see the DBGrid or the DBNavigator become active,
you can assign an event handler to the AIterPost or AIterDelete event handlers oI Table1, and
issue a MessageBeep or ShowMessage call. That way you will prove to yourselI that Table1
is being edited directly.)
There is a third option, which involves assigning an event handler to the DataSetProvider's
BeIoreUpdateRecord event handler. This event handler will then be invoked once Ior each
record in the change log. You use this event handler to apply the changes in the change log
programmatically, providing you with complete control over the resolution process. Writing
BeIoreUpdateRecord event handlers can be an involved process, and will be discussed in a
Iuture article.
When you invoke ApplyUpdates, you pass a single integer parameter. You use this parameter
to identiIy your level or tolerance Ior resolution Iailures. II you cannot tolerate any Iailures to
resolve changes to the underlying data source, pass the value 0 (zero). In this situation the
DataSetProvider starts a transaction prior to applying updates. II even a single error is
encountered, the transaction is rolled back, the change log remains unchanged, and the
oIIending record is identiIied to the ClientDataSet (by triggering its OnReconcileError event
handler, iI one has been assigned).
II you pass a positive integer when calling ApplyChanges, the transaction will be rolled back
only iI the speciIied number oI errors is exceeded. II Iewer than the speciIied number oI errors
is encountered, the transaction is committed and the Iailed records are returned to the
ClientDataSet. Furthermore, the applied records are removed Irom the change log, leaving
only the changes that could not be applied.
II the number oI Iailures exceeds the speciIied number, the transaction is rolled back, the
change log is unchanged, and the records that could not be resolved are identiIied to the
ClientDataSet as described earlier.
You can also pass a value oI 1 when invoking ApplyUpdates. In this situation no transaction
is started. Any records that can be applied are removed Irom the change log. Those whose
resolution Iail will remain in the change log, and are identiIied to the ClientDataSet through
its OnReconcileError event handler.
That's basically how it works, although there are a number oI variations that I have not
considered. For example, it is possible to limit how many records the ClientDataSet gets Irom
the DataSetProvider using the ClientDataSet's PacketRecords and FetchOnDemand
properties. Similarly, you can pass additional inIormation back and Iorth between the
ClientDataSet and the DataSetProvider using a number oI provided event handlers. Future
articles in this series will describe how and when to use these properties.
7 Using ClientDataSets Nearly Everywhere
Now that we've overviewed the basic workings oI the ClientDataSet and DataSetProvider
components, let's return to the premise that I laid out at the beginning oI this article. As I
mentioned in the introduction, a strong argument can be made Ior using a
ClientDataSet/DataSetProvider combination anytime data needs to be modiIied
programmatically or displayed using data-aware controls.
There are three basic beneIits to using ClientDataSet and DataSetProvider components Ior all
data access.
1. The combination provides a consistent set oI data access Ieatures, regardless oI which
data access mechanism you are using.
2. Their use provides a layer oI abstraction in the data access layer, making Iuture
changes to the data access mechanism easier to implement.
3. For local Iile-base systems (Paradox or dBase tables, Ior example), the ClientDataSet
can greatly reduce table and index corruption.
Let's consider each oI these points separately.
8 A Consistent. Rich Feature Set
The ClientDataSet provides your applications with a consistent and powerIul set oI Ieatures
independent oI the data access mechanism you are using. Among these Ieatures are an
editable result set, on-the-Ily indexes, nested dataset, ranges, Iilters, cloneable cursors,
aggregate Iields, group state inIormation, and much, much more. SpeciIically, even iI the data
access mechanism that you are using does not support a particular Ieature, such as aggregate
Iields or cloneable cursors, you have access to them through the ClientDataSet.
9 A Layer of Abstraction
In addition to the Ieatures supported by ClientDataSet, the ClientDataSet/DataSetProvider
combination serves as a layer oI abstraction between your application and the data access
mechanism. II at a later time you Iind that you must change the data access mechanism you
are using, such as switching Irom using the Borland Database Engine (BDE) to dbExpress, or
Irom ADO to InterBase Express, your user interIace Ieatures and programmatic control oI
data can remain largely unchanged. You simply need to hook the DataSetProvider to the new
data access components, and provide any necessary adiustment to your DataSetProvider
properties and event handlers.
Some people don't like the Iact that a ClientDataSet holds changes in cache until you call
ApplyUpdates. Fortunately, Ior those applications that need changes to be applied
immediately you can make a call to ApplyUpdates Irom the AIterPost and AIterDelete event
handlers oI the ClientDataSet.
10 Reduced Corruption
For developers who are still using local Iile-based databases, such as Paradox or dBase, there
is yet another very powerIul argument. Hooking a ClientDataSet/DataSetProvider pair to a
TTable can reduce the likelihood oI table or index corruption to near zero.
Table and index corruption occurs when something goes wrong while accessing the
underlying table. Since a TTable component has an open Iile handle on the underlying table
so long as the TTable is active, this corruption happens all too oIten in many applications.
When the data is extracted Irom a TTable to a ClientDataSet, however, the TTable is active
Ior only very short periods oI time; during loading and resolution, to be precise (assuming that
you set the TTable's Active property to False, leaving the activation entirely up to the
DataSetProvider). As a result, in most applications, accessing a TTable's data using a
ClientDataSet/DataSetProvider combination reduces the amount oI time that a Iile handle is
opened on the table to less than a Iraction oI one percent compared to what happens when a
TTable is used alone.
11 But It's Not for Every Application
While these arguments are compelling, I must also admit that this approach is not appropriate
Ior every application. That a ClientDataSet loads all oI its data into memory makes its use
much more diIIicult when you are working with large amounts oI data. There are work-
arounds that you can use iI you point a ClientDataSet to, say, a multi-million record data
source, but doing so sometimes requires a Iair amount oI coding, thereby complicating the
application.
For most applications, however, the combination oI Ieatures provided by the ClientDataSet
outweigh the disadvantages. But even iI you do not accept this argument, I think that you will
Iind many situations where the use oI a ClientDataSet enhances your application's Ieatures,
and simpliIies your eIIorts.

12 Defining a ClientDataSet's Structure
Using FieldDefs
By: Cary Jensen
Abstract: When creating a ClientDataSet's memory store on-the-Ily, you must explicitly
deIine the structure oI your table. This article shows you how to do it at both runtime and
design-time using FieldDeIs.
The ClientDataSet is an in-memory data store that lets you to view, edit, and navigate data.
Because these operations are perIormed on data held in memory, they tend to be perIormed
very quickly.
This is the second article in a series designed to detail the use oI the ClientDatSet. In the last
installment, I provided you with a basic overview oI ClientDataSet, with particular attention
paid to how a ClientDataSet gets its data Irom 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 oI using a DataSetProvider, it is possible to load and save the data held by a
ClientDataSet Irom the local Iile 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 deIines the Iields oI the dataset (commonly
reIerred to as a table's 8tructure), through the DataSetProvider. This metadata is produced by
the DataSetProvider, based on the DataSet to which it points.
When a ClientDataSet gets its data Irom a local Iile using MyBase, the metadata is read Irom
this Iile. However, neither mechanism is available when you create the in-memory dataset on-
the-Ily, at runtime. In these situations, it is necessary Ior you to explicitly deIine the structure
oI the ClientDataSet. DeIining this metadata can be done either at design-time or at runtime.
Once the metadata is deIined, 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 deIine the metadata oI a ClientDataset. You can use the FieldDeIs
property oI the ClientDataSet, or you can create TFields and associate them with the
ClientDataSet. Creating the metadata deIinitions using FieldDeIs is the most common.
However, FieldDeIs does not permit you to create virtual Iields, such as calculated or
aggregate Iields. Similarly, using FieldDeIs 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 FieldDeIs. The next
article in this series will discuss the use oI TFields to deIine the structure oI a ClientDataSet.
13 Defining a Table's Structure Using FieldDefs
You can conIigure FieldDeIs either at design time or at runtime. To deIine the structure oI a
client dataset at design time, you use the FieldDeIs collection editor to create individual
FieldDeI instances. You then use the Obiect Inspector to conIigure each FieldDeI, deIining
the Iield name, data type, size, or precision, among other options. At runtime, you deIine your
FieldDeI obiects by calling the FieldDeIs AddFieldDeI or Add methods. This section begins
by demonstrating how to create your ClientDataSet's structure at design-time. DeIining the
table structure at runtime is shown later in this article.
14 Creating FieldDefs at Design-time
You create FieldDeIs at design-time using the FieldDeIs collection editor. To display this
collection editor, select the FieldDeIs property oI a ClientDataSet in the Obiect Inspector and
click the displayed ellipsis button. The FieldDeIs collection editor is shown in the Iollowing
Iigure.

Using the FieldDeIs collection editor, click the Add New button (or press Ins) once Ior each
Iield that you want to include in your ClientDataSet. Each click oI the Add New button (or
press oI Ins) will create a new FieldDeI instance, which will be displayed in the collection
editor. For example, iI you add Iive new FieldDeIs to the FieldDeIs collection editor, it will
look something like that shown in the Iollowing Iigure.

You must conIigure each FieldDeI that is added to the FieldDeIs collection editor beIore the
dataset can be created. To conIigure a FieldDeI, select the FieldDeI you want to conIigure in
the collection editor or the Obiect TreeView, and then use the Obiect Inspector to set its
properties. The Iollowing is how the Obiect Inspector looks when a FieldDeI is selected.
(Notice that the Attributes property has been expanded to display its subproperties.)

At a minimum, you must set the DataType property oI each FieldDeI. You will also want to
set the Name property. The Name property deIines the name oI the corresponding Iield that
will be created.
Other properties you will oIten set include the Size property, which you deIine Ior String,
BCD (binary coded decimal), byte, and VarByte Iields, and the precision property Ior BCD
Iields. Similarly, iI a particular Iield requires a value beIore the record to which it is
associated can be posted, set the IaRequired subproperty oI the Attributes property to True.
For inIormation on the other properties oI the TFieldDeI class, see the online help.
AIter setting the necessary properties oI each FieldDeI, 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 Iollowing Iigure.

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

II you create your physical Iile at design-time, you will then likely need to deploy that Iile,
along with any other required Iiles. As a result, many ClientDataSet users create the
ClientDataSet at runtime. As mentioned earlier in this article, this task is perIormed by calling
the ClientDataSet's CreateDataSet method. For example, consider the Iollowing event
handler, which might be associated with the OnCreate event handler oI the Iorm to which it is
associated.
procedure TForm1.FormCreate(Sender: TJbject);
const
DataFile = 'mydata.xml';
begin
ClientDataSet1.FileName := ExtractFilePath(Application.ExeName) + DataFile;
if FileExists(ClientDataSet1.FileName) then
ClientDataSet1.Jpen
else
ClientDataSet1.CreateDataSet;
end;
This code begins by deIining the FileName property oI the ClientDataSet, pointing to a Iile
named mydata.xml in the application directory. Next, it tests to see iI this Iile already exists.
II it does, it opens the ClientDataSet, loading the speciIied Iile's metadata and data into
memory. II the Iile does not exist, it is created through a call to CreateDataSet. When
CreateDataSet is called, the in-memory structure is created based on the FieldDeIs property oI
the ClientDataSet.
15 Creating FieldDefs at Runtime
Being able to create FieldDeIs at design-time is an important Ieature, in that the Obiect
Inspector provides you with assistance in deIining the various properties oI each FieldDeI you
add. However, there may be times when you do not know the structure oI the dataset that you
need until runtime. In those cases, you must deIine the FieldDeIs property at runtime.
As mentioned earlier in this article, there are two methods that you can use to conIigure the
FieldDeIs property at runtime. The easiest technique is to use the Add method oI the
TFieldDeIs class. The Iollowing is the syntax oI 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 Iirst parameter is
the name oI the FieldDeI and the second is its type. II you need to set the Size property, as is
the case with Iields oI type ItString and ItBCD, set the Size property to the size oI the Iield.
For required Iields, set the Iourth property to a Boolean True.
The Iollowing code sample creates an in-memory table with Iive Iields.
procedure TForm1.FormCreate(Sender: TJbject);
const
DataFile = 'mydata.xml';
begin
ClientDataSet2.FileName :=
ExtractFilePath(Application.ExeName) + DataFile;
if FileExists(ClientDataSet2.FileName) then
ClientDataSet2.Jpen
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 deIining the name oI the data Iile, and then
testing whether or not it already exists. When it does not exist, the Add method oI the
FieldDeIs property is used to deIine the table structure, aIter which the in-memory dataset is
created using the CreateDataSet method.
II you consider how the Obiect Inspector looks when an individual FieldDeI is selected in the
FieldDeIs collection editor, you will notice that the Add method is rather limited. SpeciIically,
using the Add method you cannot create hidden Iields, readonly Iields, or BCD Iields where
you deIine precision. For these more complicated types oI FieldDeI deIinitions, you will need
to use the AddFieldDeI method oI the FieldDeIs property. The Iollowing is the syntax oI
AddFieldDeI:
function AddFieldDef: TFieldDef;
As you can see Irom this syntax, this method returns a TFieldDeI instance. Set the properties
oI this instance to conIigure the FieldDeI. The Iollowing code sample shows you how to do
this.
procedure TForm1.FormCreate(Sender: TJbject);
const
DataFile = 'mydata.xml';
begin
ClientDataSet2.FileName :=
ExtractFilePath(Application.ExeName) + DataFile;
if FileExists(ClientDataSet2.FileName) then
ClientDataSet2.Jpen
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;
16 Saving Data
II you have assigned a Iile name to the FileName property oI a ClientDataSet whose in-
memory table you create using CreateDataSet, and post at least one new record to the dataset,
a physical Iile will be written to disk when you close or destroy the ClientDataSet. This
happens automatically. Alternative, you can call the SaveToFile method oI the ClientDataSet
to explicitly save your data to a physical Iile. The Iollowing is the syntax oI SaveToFile
procedure SaveToFile(const FileName: string = '';
Format TDataPacketFormat=dfBinary);
As you can see, both oI the parameters oI this method are optional. II you omit the Iirst
parameter, the ClientDataSet saves to a Iile whose name is assigned to the FileName property.
II you omit the second parameter, the type oI Iile that is written to disk will depend on the Iile
extension oI the Iile to which you are saving the data. II the extension is XML, an XML
MyBase Iile is created. Otherwise, a binary MyBase Iile is written. You can override this
behavior by speciIying the type oI Iile you want to write. II you pass dIBinary as the second
parameter, a binary MyBase Iile is created. To create an XML MyBase Iile when the Iile
extension oI the Iile name is not XML, use dIXML.
On more than one occasion I have noticed that the XML MyBase Iile is not written to disk
correctly iI you do not explicitly call SaveToFile. ThereIore, even though a ClientDataSet can
save its data automatically, I make a habit oI explicitly calling SaveToFile beIore closing or
destroying a ClientDataSet.
17 An Example
An example application that demonstrates the use oI the FieldDeIs methods AddFieldDeIs
and Add can be downloaded Irom Code Central. The Iollowing is how the main Iorm oI this
application looks aIter File ' Create or Load is selected Irom the main menu.


18 Defining a ClientDataSet's Structure
Using TFields
By: Cary Jensen
Abstract: This article demonstrates how to deIine a ClientDataSet's structure at both design-
time and runtime using TFields. How to create virtual and nested dataset Iields is also
demonstrated.
In the last installment oI The ProIessional Developer, I described how to deIine the structure
oI a ClientDataSet using the ClientDataSet's FieldDeIs property. This structure is used to
create the in-memory data store when you call the ClientDataSet's CreateDataSet method. The
metadata describing this structure, and any data subsequently entered into the ClientDataSet,
will be saved to disk when the ClientDataSet's SaveToFile method is invoked.
While the FieldDeIs property provides you with a convenient and valuable mechanism Ior
deIining a ClientDataSet's structure, it has several short-comings. SpeciIically, you cannot use
FieldDeIs to create virtual Iields, which include calculated Iields, lookup Iields, and aggregate
Iields. In addition, creating nested datasets (one-to-many relationships) through FieldDeIs is
problematic. SpeciIically, while I have Iound it possible to create nested datasets using
FieldDeIs, I have not been able to successIully save and then later reload these nested datasets
into a ClientDataSets. Only the TFields method appears to create nested datasets that can be
reliably saved to the ClientDataSet's native local Iile Iormats and later re-loaded into memory.
Like the FieldDeIs method oI deIining the structure oI a ClientDataSet, you can deIine a
ClientDataSet's structure using TFields either at design-time or at runtime. Since the design-
time technique is the easiest to demonstrate, this article with start with it. DeIining a
ClientDataSet's structure using TFields at runtime is shown later in this article.
19 Defining a ClientDataSet's Structure at Design-Time
You deIine the TFields that represent the structure oI a ClientDataSet at design-time using the
Fields Editor. UnIortunately, this process is a bit more tedious than that using FieldDeIs.
SpeciIically, using the FieldDeIs collection editor you can quickly add one or more FieldDeI
deIinitions, each oI which deIines the characteristic oI a corresponding Iield in the
ClientDataSets's structure. Using the TFields method, you must add one Iield at a time. All
this really means is that it takes a little longer to deIine a ClientDataSet's structure using
TFields than it does using FieldDeIs.
Although using the TFields method oI deIining a ClientDataSet's structure is more time
consuming, it has the advantage oI permitting you to deIine both the Iields oI a table's
structure Ior the purpose oI storing data, as well as to deIine virtual Iields. Virtual Iields are
used deIine dataset Iields whose values are calculated at runtime -- the values are not
physically stored.
The Iollowing steps demonstrate how to deIine a ClientDataSet's structure using TFields as
design-time:
1. Place a ClientDataSet Irom the Data Access page oI the Component Palette onto a
Iorm.
2. Right-click the ClientDataSet and select Fields Editor. The empty Fields Editor is
shown in the Iollowing Iigure


3. Right-click the Fields Editor and select New Field (or simply press the INS key). The
New Field dialog box is displayed, as shown in the Iollowing Iigure.


4. Enter PartNo in the Name Iield, and Integer in the Type Iield. Leave the Field Type
radio button set to the deIault, which is Data. Your New Field dialog box should now
look something like the Iollowing.


5. Click OK to accept this new Iield. The newly added Iield should now appear in the
Fields Editor.
6. Repeat steps 3 through 5 to add three more Iields to the table structure. For the Iirst
Iield, set Name to Description, Type to String, and Size to 80. For the second Iield,
set Name to Price and Type to Currency. For the third Iield, set Name to Quantity
and Type to Integer. When you are done, the Fields Editor should look something like
the Iollowing.


20 Adding a Calculated Virtual Field
Adding a virtual Iield to a ClientDataSet's structure at design-time is only slightly more
complicated than adding a data Iield. This added complexity involves setting additional
properties and/or adding additional event handlers.
Let's begin by adding a calculated Iield. Calculated Iields require both a new Iield whose type
is Calculated, and an OnCalcFields event handler, which is associated with the ClientDataSet
itselI. This event handler is used to calculate the value that will be displayed in this virtual
Iield.
Note: This example demonstrates the addition oI a calculated virtual Iield, which is available
Ior most TDataSet descendents. Alternatively, these same basic steps can be used to add an
InternalCalc Iield, which is a special calculated Iield associated with ClientDataSets.
InternalCalc virtual Iields can be more eIIicient than Calculated virtual Iields, since they need
to be re-calculated less oIten than calculated Iields.
1. Begin by right-clicking the Fields Editor and selecting New Field (or press INS).
2. Using the New Fields dialog box, set Name to Total Price, Type to Currency, and
Field Type to Calculated. Click OK to add the new Iield.


3. Now select the ClientDataSet in the Obiect Inspector or the Obiect TreeView, and
display the Events page oI the Obiect Inspector.
4. Double-click the OnCalcFields event handler to add this event handler. In Delphi or
Kylix, complete this event handler as shown here
code:
procedure TDataModule2.ClientDataSet1CalcFields(DataSet: TDataSet);
begin
if (not ClientDataSet1.FieldbyName('Price').IsNull) and
(not ClientDataSet1.FieldbyName('Quantity').IsNull) then
ClientDataSet1.FieldByName('Total Price').Value :=
ClientDataSet1.FieldbyName('Price').Value
ClientDataSet1.FieldByName('Quantity').Value;
end;
21 Adding a Virtual Aggregate Field
Aggregate Iields, which can be used to perIorm a number oI automatic calculations across one
or more records oI your data, do not require event handlers, but do require that the
ClientDataSet have at least one index. The Iollowing steps will walk you through adding an
index, as well as an aggregate Iield that will use the index. A more complete discussion oI
ClientDataSet indexes will appear in a later article in this series.
1. With the ClientDataSet selected in the Obiect Inspector, choose the IndexDeIs
property and double-click the ellipsis button that appears. Using the IndexDeIs
collection editor, click the Add New button once.
2. With this newly adding IndexDeI selected in the IndexDeIs collection editor, use the
Obiect Inspector to set its Name property to PNIndex, and its Fields property to
PartNo.
3. Select the ClientDataSet in the Obiect Inspector once again. Set its IndexName
property to PNIndex and its AggregatesActive property to True.
4. We are now ready to add the aggregate Iield. Double-click the ClientDataSet to
display the Fields Editor (alternatively, you can right-click the ClientDataSet and
select Fields Editor Irom the displayed context menu).
5. Right-click the Fields Editor and select New Field.
6. Set Name to Total Parts and Data Type to Aggregate. Select OK to close the New
Field dialog box. The aggregate virtual Iield is displayed in its own section oI the
Fields Editor, as shown in the Iollowing Iigure.


7. Select the Total Parts aggregate Iield in the Fields Editor. Then, using the Obiect
Inspector, set the Expression property to Sum(Quantity), the IndexName property to
PXIndex, and Active to True.
That's all it takes. All you need to do now is call the CreateDataSet method at runtime (or
alternatively, right-click the ClientDataSet at design-time and select Create DataSet). OI
course, iI you want to actually see the resulting ClientDataSet, you will also have to hook it
up to one or more data-aware controls.
The use oI the TField deIinitions described here are demonstrated in the FieldDemo proiect,
which you can download Irom Code Central. The Iollowing is the main Iorm oI this proiect.

Notice that iust below the main menu there is a Label and a DBLabel. The DBLabel is
associated with the Total Parts aggregate Iield, and it is used to display the sum oI the values
entered in the Quantity Iield oI the ClientDataSet. The DBNavigator and the DBGrid that
appear on this main Iorm are associated with the ClientDataSet through a DataSource. This
ClientDataSet is created at runtime, iI it does not already exist. This is done Irom code
executed Irom the main Iorm's OnCreate event handler, shown here:
procedure TForm1.FormCreate(Sender: TJbject);
begin
DataModule2.ClientDataSet1.FileName :=
ExtractFilePath(Application.ExeName) + 'parts.xml';
if not FileExists(DataModule2.ClientDataSet1.FileName) then
DataModule2.ClientDataSet1.CreateDataSet
else
DataModule2.ClientDataSet1.Jpen;
end;
As you can see Irom this code, the ClientDataSet in this example resides on a data module.
Upon startup, this Iorm calculates the name oI the Iile in which the ClientDataSet's data can
be stored. It then tests to see iI this Iile already exists. II it does not, CreateDataSet is called,
otherwise the ClientDataSet is opened.
The Iollowing Iigure shows this Iorm at runtime, aIter some records have been added.

22 Creating Nested DataSet
Nested datasets represent one-to-many relationships. Imagine, Ior instance, that you have a
ClientDataSet designed to hold inIormation about your customers. Imagine Iurther that Ior
each customer you want to be able to store one or more phone numbers. There are three
techniques that developers oIten use to provide this Ieature. The Iirst, and least Ilexible
technique, is to add a Iixed number oI Iields to the ClientDataSet to hold the possible phone
numbers. For example, one Ior a business number, another Ior the a home number, and a third
Ior a mobile phone number. The problem with this approach is that you have to decide, in
advance, the maximum number oI phone numbers that you can store Ior any given customer.
The second technique is to create a separate Iile to hold customer phone numbers. This Iile
would have to include one or more Iields that deIine a link between a given customer and
their phone numbers (such as a unique customer identiIication number), as well as Iields Ior
holding the type oI phone number and the phone number itselI. Using this approach, you can
store any number oI phone numbers Ior each customer.
The third technique is to create a nested dataset. A nested dataset is created by adding a Field
oI DataSet type to a ClientDataSet's structure. This dataset Iield is then assigned to the
DataSetField property oI a second client dataset. Using this second ClientDataSet, you can
deIine Iields to store the one or more records oI related data. In this example it might make
sense to add two Iields, one to hold the type oI phone number (such as, home, cell, Iax, and so
Iorth), and a second to hold the phone number itselI. Similar to the second technique, nested
datasets permit a customer to have any number oI phone numbers. On the other hand, unlike
the second technique, in which phone numbers are stored in a separate Iile, there is no need
Ior any Iields to link phone numbers to customers, since the phone numbers are actually
"nested" within each customer's record.
Here is how you create a nested dataset at design-time.
1. Using the technique outlined earlier in this article (using the Fields Editor), create one
Iield oI data type Data Ior each regular Iield in the dataset (such as Customer Name,
Title, Address1, Address2, and so Iorth).
2. For each nested dataset, add a new Iield, using the same technique that you use Ior the
other data Iields, but set its Data Type to DataSet.
3. For each DataSet Iield that you add to your Iirst ClientDataSet, add an additional
ClientDataSet. Associate each oI these secondary ClientDataSets with one oI the
primary ClientDataSet's DataSet Iields using the secondary ClientDataSet's
DataSetField property.
4. To deIine the Iields oI each nested dataset, add Iields to each secondary ClientDataSet
using its Fields Editor, iust as you added Iields to the primary ClientDataSet. For
example, Iollowing the customer/phone numbers example discussed here, the nested
dataset Iields would include phone type and phone number.
For an example oI a proiect that demonstrates how to create nested datasets at design-time,
download the NestedDataSetFields proiect Irom Code Central. This proiect provides an
example oI how the customer/phone numbers application might be implemented. This proiect
contains a data module that includes two ClientDataSets. One is used to hold the customer
inIormation, and it includes a DataSet Iield called PhoneNumbers. This DataSet Iield is
associated with a second ClientDataSet through the second ClientDataSet's DataSetField
property. The Fields Editor Ior this second ClientDataSet, shown in the Iollowing Iigure,
displays its two String Iields, one Ior Phone Type and the other Ior Phone Number.

23 Creating a ClientDataSet's Structure at Runtime
using TFields
In the previous article in this series, where a ClientDataSet's structure was deIined using
FieldDeIs, you learned that you can deIine the structure oI a ClientDataSet both at design-
time as well as at runtime. As explained in that article, the advantage oI using design-time
conIiguration is that you can use the Ieatures oI the Obiect Inspector to assist in the deIinition
oI the ClientDataSet's structure. This approach, however, is only useIul iI you know the
structure oI your ClientDataSet in advance. II you do not, your only option is to deIine your
structure at runtime.
You deIine your TFields at runtime using the methods and properties oI the appropriate
TField or TDataSetField class. SpeciIically, you call the constructor oI the appropriate TField
or TDataSetField obiect, setting the properties oI the created obiect to deIine its nature.
Among the properties oI the constructed obiect, one oI the most important is the DataSet
property. This property deIines to which TDataSet descendant you want the obiect associated
(which will be a ClientDataSet in this case, since we are discussing this type oI TDataSet).
AIter creating all oI the TFields or TDataSetFields, you call the ClientDataSet's
CreateDataSet method. Doing so creates the ClientDataSet's structure based on the TFields to
which it is associated.
The Iollowing is a simple example oI deIining a ClientDataSet's structure using TFields.
procedure TForm1.FormCreate(Sender: TJbject);
begin
with ClientDataSet1 do
begin
with TStringField.Create(Self) do
begin
Name := 'ClientDataSet1FirstName';
FieldKind := fkData;
FieldName := 'FieldName';
Size := 72;
DataSet := ClientDataSet1;
end; //FieldName
with TMemoField.Create(Self) do
begin
Name := 'ClientDataSet1LastName';
FieldKind := fkData;
FieldName := 'Last Name';
DataSet := ClientDataSet1;
end; //Last Name
ClientDataSet1.CreateDataSet
end;
end;
You can test this code Ior yourselI easy enough. Simply create a proiect and place on the main
Iorm a ClientDataSet, a DataSource, a DBGrid, and a DBNavigator. Assign the DataSet
property oI the DBGrid and the DBNavigator to the DataSource, assign the DataSet property
oI the DataSource to ClientDataSet, and ensure that the ClientDataSet is named
ClientDataSet1. Finally, add the preceding code to the OnCreate event handler oI the Iorm to
which these components appear, and run the proiect.
24 TFields and FieldDefs are Different
When your structure is deIined using TFields, there is an important behavior that might not be
immediately obvious. SpeciIically, the TFields speciIied at design-time using the Fields
Editor deIine obiects that are created automatically when the Iorm, data module, or Irame to
which they are associated is created. These obiects deIine the ClientDataSet's structure, which
in turn deIines the value oI the ClientDataSet's FieldDeIs property.
This same behavior does not apply when a ClientDataSet's structure is deIined using
FieldDeIs at design-time. SpeciIically, the TFields oI a ClientDataSet whose structure is
deIined using FieldDeIs is deIined when the ClientDataSet's CreateDataSet method is
invoked. But they are also created when metadata is read Irom a previously saved
ClientDataSet Iile. II a ClientDataSet is loaded Irom a saved Iile, the structure deIined in the
metadata oI the saved Iile takes precedence. In other words, the FieldDeIs property created at
design-time is replaced by FieldDeIs deIined by the saved metadata, and this is used to create
the TFields.
When your ClientDataSet's structure is deIined using TFields at design-time, metadata in a
previously saved ClientDataSet is not used to deIine the TFields, since they already exist. As
a result, when a ClientDataSet's structure is deIined using TFields, and you attempt to load
previously save data, it is essential that the metadata in the Iile being loaded be consistent
with the deIined TFields.
25 Creating a ClientDataSet's Structure Using TFields at
Runtime
As mentioned in the preceding section, TFields deIined at design-time cause the automatic
creation oI the corresponding TField instances at runtime (as well as FieldDeIs). II you deIine
your ClientDataSet's structure at runtime, by calling the constructor oI the various TField and
TDataSetField obiects that you need, you must Iollow the call to these constructors with a call
to the ClientDataSet's CreateDataSet method beIore the ClientDataSet can be used. This is
true even when you intend to load the ClientDataSet Irom previously saved data.
The reason Ior this is that, as pointed out in the previous section, ClientDataSet structures
deIined using TFields do not rely on the metadata oI previously saved ClientDataSets.
Instead, the structure relies on the TFields and TDataSetFields that have been created Ior the
ClientDataSet. This becomes particularly obvious when you consider that virtual Iields are not
stored in the Iiles saved by a ClientDataSet. The only way that you can have virtual Iields in a
ClientDataSet whose structure is deIined at runtime is to create these Iields using the
appropriate constructors, and then call CreateDataSet to build the ClientDataSet's in-memory
data store. Only then can a compatible, previously saved data Iile be loaded into the
ClientDataSet.
Here is another way to put it. When you deIine your ClientDataSet's structure using
FieldDeIs, you call CreateDataSet only iI there is no previously saved data Iile. II there is a
previously saved data Iile, you simply load it into the ClientDataSet - CreateDataSet does not
need to be invoked. The ClientDataSet's structure is based on the saved metadata.
By comparison, when you deIine your ClientDataSet's structure using TFields at runtime, you
always call CreateDataSet (but only aIter creating and conIiguring the TField and
TDataSetField instances that deIine the ClientDataSet's structure). This must be done whether
or not you want to load previously saved data.
26 An Example
The VideoLibrary proiect, which can be downloaded Irom Code Central, includes code that
demonstrates how to create data, aggregate, lookup, and nested dataset Iields at runtime using
TFields. This proiect, whose running main Iorm is shown in the Iollowing Iigure, includes
two primary ClientDataSets. One is used to hold a list oI videos and another holds a list oI
Talent (actors). The ClientDataSet that holds the video inIormation contains two nested
datasets: one to hold the list oI talent Ior that particular video and another to hold a list oI the
video's special Ieatures (Ior instance, a music video Iound on a DVD).

This proiect is too complicated to describe adaquately in this limited space (I'll save that
discussion Ior a Iuture article). Instead, I;ll leave it up to you to download the proiect. In
particular, you will want to examine the OnCreate event handler Ior this proiect's data
module. There you will see how the various data Iields, virtual Iields, dataset Iields, and
indexes are created and conIigured.

27 Understanding ClientDataSet Indexes
By: Cary Jensen
Abstract: A ClientDataSet does not obtain its indexes Irom the data it loads. Indexes, iI you
want them, must be explicitly deIined. This article shows you how to do this at design-time or
runtime.
In many respects, an index on a ClientDataSet is like that on any other TDataSet descendant.
SpeciIically, an index controls the order oI records in the DataSet, as well as enables or
enhances a variety oI other operations, such as searches, ranges, and dataset linking.
In earlier articles in this series I described how the structure oI a ClientDataSet is deIined.
There you learned that, iI a ClientDataSet is loaded through a DataSetProvider, the structure
is based on the columns that the DataSetProvider obtains Irom its DataSet. When a
DataSetProvider is not involved, the structure is either based on metadata loaded Irom a Iile
previously saved by a ClientDataSet, or is deIined by the ClientDataSet's FieldDeIs property
or by TFields associated with the ClientDataSet.
Unlike a ClientDataSet's structure, which is normally obtained Irom existing data, a
ClientDataSet's indexes are not. SpeciIically, when a ClientDataSet is loaded with data
obtained Irom a DataSetProvider, or is loaded Irom a previously saved ClientDataSet Iile, the
ClientDataSet's structure is largely (and usually entirely) deIined by the DataSetProvider, or
loaded Irom the saved Iile. Indexes, with the exception oI two deIault indexes, are solely the
responsibility oI the ClientDataSet itselI. In other words, even iI the DataSet Irom which a
DataSetProvider obtains its data possesses indexes, those are unrelated to any indexes on the
ClientDataSet loaded Irom that DataSetProvider.
Consider the CUSTOMER table Iound in the example EMPLOYEE.GDB InterBase database
that ships with Delphi. There are Iour customer table-related indexes present in the database,
including indexes based on the CUSTNO, COMPANY, and COUNTRY Iields. Regardless
oI how you load the data Irom that table into a ClientDataSet, those indexes will be all but
ignored by the DataSetProvider, and will be absent in the ClientDataSet. With the exception
oI the two deIault indexes that a ClientDataSet creates Ior its own use, iI you want additional
indexes in a ClientDataSet, you must deIine them explicitly.
In general, the indexes oI a ClientDataSet can be divided into three categories: deIault
indexes, temporary indexes, and persistent indexes. Each oI these indexes is discussed in the
Iollowing sections.
28 Default Indexes
Most ClientDataSets have two deIault indexes, as shown in the Iollowing image oI the Obiect
Inspector. One oI these is named DEFAULTORDER, and the other is named
CHANGEINDEX. DEFAULTORDER represents the original order that the records where
loaded into the ClientDataSet. II the ClientDataSet is loaded through a DataSetProvider, this
order matches that oI the DataSet Irom which the DataSetProvider obtains its data. For
example, iI the DataSetProvider points to a SQLDataSet that includes a SQL query with an
ORDER BY clause, DEFAULTORDER will order the records in the same order as that
deIined by the ORDER BY clause. II the DataSetProvider doesn't speciIy an order, the deIault
order will match the natural order oI the records in the corresponding DataSet.

While DEFAULTORDER is associated with the records held in the Data property oI the
ClientDataSet, CHANGEINDEX is associated with the order oI records held in the Delta
property, also known as the change log. This index is maintained as changes are posted to a
ClientDataSet, and it controls the order in which the changed records will be processed by the
DataSetProvider when ApplyUpdates is called.
These deIault indexes have limited utility in most database applications. For example,
DEFAULTORDER can be used to return data held in a ClientDataSet to the originally
loaded order aIter having switched to some other index. In most cases, however, a
ClientDataSet's natural order is oI little interest. Most developers want to based indexes on
speciIic Iields, depending on the needs oI the application.
CHANGEINDEX, by comparison, can be used to display only those records that appear in the
change log, and in the order in which those changes will be applied iI ApplyUpdates is called.
Again, this order might be interesting, most developers are not concerned with the order in
which changes are applied. One reason is that there is another mechanism that a ClientDataSet
provides Ior this purpose: the StatusFilter property. StatusFilter permits you to display
speciIic kinds oI changes contained in the change log. These changes can be displayed using
any ClientDataSet index, not iust the order in which the changes where applied.
CHANGEINDEX is really only useIul when the order that the records where placed in the
change log is oI interest.
29 Creating Indexes
There are two types oI indexes that you explicitly create: temporary indexes and persistent
indexes. Each oI these index types play an important role in applications, permitting you to
control the order that records appear in the ClientDataSet, as well as to enable index-based
operations, including searches, ranges, and dataset linking. Each oI these index types is
discussed in the Iollowing sections.
30 Temporary Indexes
Temporary indexes are created with the IndexFieldNames property. To create a temporary
index, set the IndexFieldNames property to the name oI the Iield or Iields you want to base
the index on. When you need a multi-Iield index, separate the Iield names with semicolons.
For example, imagine that you have a ClientDataSet that contains customer records, including
account number, Iirst name, last name, city, state, and so on. II you want to sort this data by
last name and Iirst name (and assuming that these Iields are named FirstName and LastName,
respectively), you can create a temporary index by setting the client dataset's
IndexFieldNames property to the Iollowing string:
LastName;FirstName
As with all published properties, this can be done at design time, or it can be done in code at
runtime using a statement similar to the Iollowing:
ClientDataSet1.IndexFieldNames := 'LastName;FirstName';
When you assign a value to the ClientDataSet's IndexFieldNames property, the ClientDataSet
immediately generates the index. II the contents oI the ClientDataSet are being displayed,
those records will appear sorted in ascending order by the Iields oI the index, with the Iirst
Iield in the index sorted Iirst, Iollowed the second (iI present), and so on.
Indexes create this way are temporary in that when you change the value oI the
IndexFieldNames property, the previous index is discarded, and a new one is created. For
example, imagine that iI aIter you created the last name/Iirst name index, you then execute the
Iollowing statement:
ClientDataSet1.IndexFieldNames := 'FirstName'
This statement will cause the existing temporary index to be discarded and a new index to be
generated. II the new index deIines a sort order diIIerent Irom the previous index, the record
display order is also updated. II you later set the IndexFieldNames property back to
'LastName;FirstName', the Iirst name index will be discarded, and a new last name/Iirst name
index will be created.
Temporary indexes are extremely useIul under a number oI situations, such as when you want
to permit your users to sort the data based on any Iield or Iield combination. There are,
however, some drawbacks to temporary indexes. One oI these is that indexes take some time
to build, and temporary indexes must be re-built more oIten than persistent indexes. The time
it takes a ClientDataSet to build an index is based on the number oI records being indexed, the
Iield types being indexes, and number oI Iields in the index. Since these indexes are built in
memory, even a complicated temporary index can be built in a Iraction oI a second, so long as
there are less than 10,000 records or so in the ClientDataSet. Even with more than 100,000,
most indexes can be built in less than 10 seconds on a typical workstation.
A more important concern when deciding between temporary and persistent indexes involves
index Ieatures. SpeciIically, you can only build ascending temporary indexes. In addition,
temporary indexes do not support more advanced index options, such as unique indexes. II
you need a more complicated index, you will need to create persistent indexes.
31 Persistent Indexes
Persistent indexes are index deIinitions that can be used to build indexes at runtime. Once a
persistent index has been built, it remains available to the ClientDataSet so long as the
ClientDataSet remains open. For example, iI there is a persistent index based on a Iield named
FirstName, setting the ClientDataSet to use this index causes the index to be built. II you then
set the ClientDataSet to use another persistent index based on the last name/Iirst name Iield
combination, that index is built, but the Iirst name-based index is not discarded. II you then
set the ClientDataSet to use the Iirst name index once again, it immediately switches to that
previously created index. Unlike temporary indexes, persistent indexes are not discarded until
the ClientDataSet against which they were built is closed.
You create IndexDeIs at design-time using the IndexDeIs collection property editor, shown in
the Iollowing Iigure. To display this collection editor, select the IndexDeIs property oI a
ClientDataSet in the Obiect Inspector and click the ellipsis button that appears.

Note that the IndexDeIs collection property editor may not include deIault indexes. Whether
or not deIault indexes appear depends on whether or not you have loaded data into the
ClientDataSet at design-time, and where you loaded that data Irom.
Click the Add New button on the IndexDeIs collection editor toolbar (or press the INS key)
once Ior each persistent index that you want to deIine Ior a ClientDataSet. Each time you
click the Add New button (or press INS), a new IndexDeI is created. Complete the index
deIinitions by selecting each IndexDeI in the IndexDeIs collection editor, one at a time, and
conIiguring it using the Obiect Inspector. The Obiect Inspector, with an IndexDeI selected, is
shown in the Iollowing Iigure. Note that the Options property has been expanded to show its
various Ilags.

At a minimum, you must set the Fields property oI an IndexDeI to the name oI the Iield or
Iields to be indexed. II you are building a multi-Iield index, separate the Iield names with
semicolons. You cannot include virtual Iields, such as calculated or aggregate Iields, in an
index.
By deIault, indexes created using IndexDeIs are ascending indexes. II you want the index to
be a descending index, set the ixDesccending Ilag in the Options property. Alternatively, you
can set the DescFields property to a semicolon-separated list oI the Iields that you want sorted
in descending order. Using DescFields, you can deIine an index in which one or more, but not
necessarily all Iields, are sorted in descending order.
Indexed string Iields normally are case sensitive. II you want string Iields to be indexes
without regards to the case oI the strings, you can set the ixCaseInsensitive Ilag in the Options
property. Or, you can include a semicolon-separated list oI Iields whose contents you want
sorted case insensitive in the CaseInsFields property. Use the CaseInsFields property when
you want to sort some, but not all, string Iields without regards to case.
II you want the ClientDataSet to maintain inIormation about groups, set the GroupingLevel
property. Groups reIer to the unique values on one or more Iields oI an index. Setting
GroupingLevel to 0 maintains no grouping inIormation, treating all records in a ClientDataSet
as belonging to a single group. A GroupingLevel oI 1 treats all records that contains the same
value in the Iirst Iield oI the index as a group. Setting GroupingLevel to 2 treats all records
with the combination oI vlaues on the Iirst two Iields oI the index as a group, and so on.
GroupingLevel is typically only useIul iI you are using aggregate Iields, or want to call the
GetGroupState method. Grouping will be discussed in greater detail in a Iuture article in this
series.
In addition to sorting records, indexes can ensure the uniqueness oI records. II you want to
ensure that no two records contain the same data in the Iield or Iields oI an index, set the
ixUnique Ilag in the IndexDeI's Option property.
The remaining properties oI the TIndexDeI class do not apply to ClientDataSets. For example,
ClientDataSets do not support expression, primary, or non-maintained indexes. As a result, do
not set the Expression property or add the ixNonMaintained or ixPrimary Ilags to the Options
property when deIining an IndexDeI Ior a ClientDataSet. Likewise, Source only applies to
DataSets that reIer to dBASE tables. Do not set the Source property when deIining an index
Ior ClientDataSets.
32 Using Persistent Indexes
A persistent index is created when a ClientDataSet's IndexName property is set to the name oI
an IndexDeI. II IndexName is set at design-time, or is set prior to opening a ClientDataSet,
that index is built immediately aIter the ClientDataSet is opened. Note that a ClientDataSet
does not build an index until it needs it. SpeciIically, even iI you have IiIty diIIerent persistent
indexes deIined Ior a ClientDataSet, no index is actually built until the ClientDataSet is
opened, and then the only index that will be built will be the one whose name is assigned to
the IndexName property. II IndexName is not set to the name oI an index, the
DEFAULTORDER index is used.
33 Creating Persistent Indexes at Runtime
To create IndexDeIs at runtime, you use either the Add or AddIndexDeI methods oI the
obiect assigned to the ClientDataSet's IndexDeIs property, or you can call the ClientDataSet's
AddIndex method. Like the related AddFieldDeI, AddIndexDeI is more Ilexible than
AddIndex, which makes it the recommended method Ior adding a persistent index at runtime.
AddIndexDeI returns an IndexDeI instance, which you use to set the properties oI the index.
For example, the Iollowing statement creates an IndexDeI Ior the data in the ClientDataSet,
and then makes this the active index:
with ClientDataSet1.IndexDefs.AddIndexDef do
begin
Name := 'LastFirstIdx';
Fields := 'LastName;FirstName';
Jptions := ixDescending, ixCaseInsensitive`;
end;
ClientDataSet1.IndexName := 'LastFirstIdx';
Unlike AddFieldDeIs, the AddIndex method is a method oI the TCustomClientDataSet class.
The Iollowing is the syntax oI AddIndex:
procedure AddIndex(const Name, Fields: string; Jptions: TIndexJptions;
const DescFields: string = ''; const CaseInsFields: string = '';
const GroupingLevel: Integer = 0);
As you can see Irom this syntax, this method requires at least three parameters. The Iirst
parameter is the name oI the index you are creating, the second is the semicolon-separated list
oI the index Iields, and the third is the index options. Note, however, that only the
ixCaseInsensitive, ixDescending, and ixUnique TIndexOptions are valid when you invoke
AddIndex. Using any oI the other TIndexOptions Ilags raises an exception.
The Iourth parameter, DescFields, is an optional parameter that you can use to list the Iields
oI the index that you want to sort in descending order. You use this parameter when you want
some oI the index Iields to be sorted in ascending order and others in descending order. When
you use DescFields, do not include the ixDescending Ilag in Options.
Like DescFields, CaseInsFields is an optional String property that you can use to select which
Iields oI the index should be sorted without respect to uppercase or lowercase characters.
When you use CaseInsFields, do not include the ixCaseInsensitive Ilag in Options.
The Iinal parameter, GroupingLevel, is an optional parameter that you use to deIine the
deIault grouping level to use when the index is selected.
34 An Example: Creating Indexes On-the-fly
One oI the most requested Ieatures in a database application is the ability to sort the data
displayed in a DBGrid by clicking on the column title. The CDSSort proiect demonstrates
how you can add this Ieature to any DBGrid that displays data Irom a ClientDataSet. (Click
here to download this proiect.) This proiect makes use oI a generic procedure named
SortCustomClientDataSet. This procedure is designed to work with any
TCustomClientDataSet descendant, including ClientDataSet, SQLClientDataSet,
BDEClientDataSet, and IBClientDataSet. However, some oI the properties used in this code
are not visible to the TCustomClientDataSet class. SpeciIically, the IndexDeIs and
IndexName properties are declared protected in TCustomClientDataSet. As a result, this code
relies on runtime type inIormation (RTTI) to work with these properties. This means that any
unit implementing this procedure must use the TypInIo unit.
The Iollowing is the SortCustomClientDataSet procedure:
uses TypInfo; //TypInfo needed for RTTI GetObjectProp
//IsPublishedProp, and SetStrProp methods

function SortCustomClientDataSet(DataSet: TCustomClientDataSet;
const FieldName: String): Boolean;
var
i: Integer;
IndexDefs: TIndexDefs;
IndexName: String;
IndexJptions: TIndexJptions;
Field: TField;
begin
Result := False;
Field := DataSet.Fields.FindField(FieldName);
//If invalid field name, exit.
if Field = nil then Exit;
//if invalid field type, exit.
if (Field is TJbjectField) or (Field is TBlobField) or
(Field is TAggregateField) or (Field is TVariantField)
or (Field is TBinaryField) then Exit;
//Get IndexDefs and IndexName using RTTI
if IsPublishedProp(DataSet, 'IndexDefs') then
IndexDefs := GetJbjectProp(DataSet, 'IndexDefs') as TIndexDefs
else
Exit;
if IsPublishedProp(DataSet, 'IndexName') then
IndexName := GetStrProp(DataSet, 'IndexName')
else
Exit;
//Ensure IndexDefs is up-to-date
IndexDefs.Update;
//If an ascending index is already in use,
//switch to a descending index
if IndexName = FieldName + '__IdxA'
then
begin
IndexName := FieldName + '__IdxD';
IndexJptions := ixDescending`;
end
else
begin
IndexName := FieldName + '__IdxA';
IndexJptions := `;
end;
//Look for existing index
for i := 0 to Pred(IndexDefs.Count) do
begin
if IndexDefsi`.Name = IndexName then
begin
Result := True;
Break
end; //if
end; // for
//If existing index not found, create one
if not Result then
begin
DataSet.AddIndex(IndexName, FieldName, IndexJptions);
Result := True;
end; // if not
//Set the index
SetStrProp(DataSet, 'IndexName', IndexName);
end;
This code begins by veriIying that the Iield passed in the second parameter exists, and that it
is oI the correct type. Next, the code veriIies that the client dataset passed in the Iirst Iormal
parameter has an IndexDeIs property. II so, it assigns the value oI this property to a local
variable. It then calculates an index name by appending the characters "IdxA" or "IdxD"
to the name oI the Iield to index on, with IdxA being used Ior an ascending index, and
IdxD Ior a descending index.
Next, the IndexDeIs property is scanned Ior an existing index with the calculated name. II one
is Iound (because it was already created in response to a previous header click), that index is
set to the IndexName property. II the index name is not Iound, a new index with that name is
created, and then the dataset is instructed to use it.
In the CDSSort proiect, this code is called Irom within the DBGrid's OnTitleClick event
handler. The Iollowing is how this event handler is implemented in the CDSSortGrid proiect:
procedure TForm1.DBGrid1TitleClick(Column: TColumn);
begin
SortCustomClientDataSet(ClientDataSet1, Column.FieldName);
end;
As pointed out above, this code has the drawback oI requiring RTTI, which is necessary since
the IndexDeIs and IndexName properties oI the TCustomClientDataSet class are protected
properties. The CDSSort proiect also includes a Iunction named SortClientDataSet. This
Iunction, shown in the Iollowing code segment, is signiIicantly simpler, in that it does not
require RTTI. However, it can only be passed an instance oI the TClientDataSet class,
meaning that it cannot be used with other TCustomerClientDataSet provided by Delphi, such
as BDEClientDataSets and SQLClientDataSets.
function SortClientDataSet(ClientDataSet: TClientDataSet;
const FieldName: String): Boolean;
var
i: Integer;
NewIndexName: String;
IndexJptions: TIndexJptions;
Field: TField;
begin
Result := False;
Field := ClientDataSet.Fields.FindField(FieldName);
//If invalid field name, exit.
if Field = nil then Exit;
//if invalid field type, exit.
if (Field is TJbjectField) or (Field is TBlobField) or
(Field is TAggregateField) or (Field is TVariantField)
or (Field is TBinaryField) then Exit;
//Get IndexDefs and IndexName using RTTI
//Ensure IndexDefs is up-to-date
ClientDataSet.IndexDefs.Update;
//If an ascending index is already in use,
//switch to a descending index
if ClientDataSet.IndexName = FieldName + '__IdxA'
then
begin
NewIndexName := FieldName + '__IdxD';
IndexJptions := ixDescending`;
end
else
begin
NewIndexName := FieldName + '__IdxA';
IndexJptions := `;
end;
//Look for existing index
for i := 0 to Pred(ClientDataSet.IndexDefs.Count) do
begin
if ClientDataSet.IndexDefsi`.Name = NewIndexName then
begin
Result := True;
Break
end; //if
end; // for
//If existing index not found, create one
if not Result then
begin
ClientDataSet.AddIndex(NewIndexName,
FieldName, IndexJptions);
Result := True;
end; // if not
//Set the index
ClientDataSet.IndexName := NewIndexName;
end;

35 Navigating and Editing a ClientDataSet
By: Cary Jensen
Abstract: You navigate and edit a ClientDataSet in a manner similar to how you navigate and
edit almost another other dataset. This article provides an introductory look at basic
ClientDataSet navigation and editing.
I usually try to start Irom the beginning, covering the more basic techniques beIore continuing
to the more advanced, and that has been my plan with this series. In the articles that precede
this one I have provided a general introduction to the use and behaviors oI a ClientDataSet, as
well as how to create its structure and indexes. In this installment I will take an introductory
look at the manipulation oI data stored in a ClientDataSet. Topics to be covered include basic
programmatic navigation oI the data in a ClientDataSet, as well as simple editing operations.
The next two articles in this series will demonstrate record searching and ranges and Iilters.
Only aIter these Ioundation topics are covered will I continue to the more interesting things
that you can do with a ClientDataSet, such as creating nested datasets, cloning cursors,
deIining aggregate Iields, and more.
For those oI you who are already well versed in working with datasets, you will only need to
quickly skim through this article to see iI there is something that you Iind interesting. II you
are Iairly new to dataset programming, however, this article will provide you with essential
inIormation on the use oI ClientDataSets. As an added beneIit, most oI these techniques are
appropriate Ior any other datasets that you may have a chance to use.
While this article Iocuses primarily on the use oI code to navigate and edit data in a
ClientDataSet, a natural place to begin this discussion is with Delphi data-aware controls and
the navigation and editing Ieatures they provide.
36 Navigating with Data-Aware Controls
There are two classes oI controls that provide data navigation. The Iirst class is navigation-
speciIic controls. Delphi provides you with one control in this category, the DBNavigator.
The DBNavigator, shown in the Iollowing image, provides a VCR-like interIace Ior
navigating data and managing records. Record navigation is provided by the First, Next, Prior,
and Last buttons. Record management is provided by the Edit, Post, Cancel, Delete, Insert,
and ReIresh buttons. You can control which buttons are displayed by a DBNavigator through
its VisibleButtons property. For example, iI you are using the DBNavigator in coniunction
with a ClientDataSet that reads and writes its data Irom a local Iile (Borland calls this
technology MyBase), you will want to remove the nbReIresh Ilag Irom the VisibleButtons
property, since attempting to ReIresh a ClientDataSet that uses MyBase raises an exception.

Another DBNavigator property whose deIault value you may want to change is ShowHint.
Some users have diIIiculty interpreting the glyphs on the DBNavigator's buttons. For those
users, setting ShowHint to True supplements the glyphs with popup help hints. You can
control the text oI these hints by editing the Hints property.
The second category oI controls that provide navigation is the multi-record controls. Delphi
includes two: the DBGrid and DBCtrlGrid. A DBGrid displays data in a row/column Iormat.
By deIault, all Iields oI the ClientDataSet are displayed in the DBGrid. You can control which
Iields are displayed, as well as speciIic column characteristics, such as color, by editing the
DBGrid's Columns collection property. The Iollowing is an example oI a DBGrid.

A DBCtrlGrid, by comparison, is a limited, multi-record container. It is limited in that it can
only hold certain Delphi components, including Labels, DBEdits, DBLabels, DBMemos,
DBImages, DBComboBoxes, DBCheckBoxes, DBLookupComboBoxes, and DBCharts. It is
also limited in that it is not available in Kylix. As a result, the DBCtrlGrid is little used. An
example oI a two-row, one-column DBCtrlGrid is shown in the Iollowing Iigure.

Depending on which multi-record control you are using, you can navigate between records
using UpArrow, DownArrow, Tab, Ctrl-End, Ctrl-Home, PgDn, PgUp, among others. These
key presses may produce the same eIIect as clicking the Next, Prior, Last, First, and so on,
buttons in a DBNavigator. It is also possible to navigate the records oI a dataset using the
vertical scrollbar oI these controls.
How you edit a record using these controls also depends on which type oI control you are
using, as well as their properties. Using the deIault properties oI these controls, you can
typically press F2 or click twice on a Iield in one oI these controls to begin editing. Posting a
record occurs when you navigate oII an edited record. Inserting and deleting records,
depending on the control's property settings, can also be achieved using Ins and Ctrl-Del,
respectively. Other operations, such as ReIresh, are not directly supported. Consequently, in
most cases, multi-record controls are combined with a DBNavigator to provide a complete set
oI record management options.
37 Detecting Changes to Record State
Changes that occur when a user navigates or manages a record using a data-aware control is
something that you may want to get involved with, programmatically. For those situations,
there are a variety oI event handlers that you can use to evaluate what a user is doing, and
provide a customized response. ClientDataSets, as well as all other TDataSet descendents,
posses the Iollowing event handlers: AIterCancel, AIterClose, AIterDelete, AIterEdit,
AIterInsert, AIterOpen, AIterPost, AIterReIresh, AIterScroll, BeIoreCancel, BeIoreClose,
BeIoreDelete, BeIoreEdit, BeIoreInsert, BeIoreOpen, BeIorePost, BeIoreReIresh,
BeIoreScroll, OnCalcFields, OnDeleteError, OnEditError, OnFilterRecord, OnNewRecord,
and OnPostError.
There are additional event handlers that are available in most situations where a ClientDataSet
is being navigated and edited, and which are always available when data-aware controls are
concerned. These are the event handlers associated with a DataSource. Since all data-aware
controls must be connected to at least one DataSource, the event handlers oI a DataSource
provide you with another source oI customization when a user navigates and edits records.
These event handlers are OnDataChange, OnStateChange, and OnUpdateData.
OnDataChange triggers whenever a ClientDataSet arrives at a new records, as well as when a
ClientDataSet arrives at the Iirst record when it is initially opened. OnStateChange triggers
when a ClientDataSet changes between state, such as when it changes Irom dsBrowse to
dsEdit (when a user enters the edit mode), or when it changes Irom dsEdit to dsBrowse
(Iollowing the posting or cancellation oI a change). Finally, OnUpdateData triggers when the
dataset to which the DataSource points is posting its data.
38 Navigating Programmatically
Whether data-aware controls are involved or not, it is sometimes necessary to use code to
navigate and edit data in a ClientDataSet, or any DataSet descendent Ior that matter. For a
ClientDataSet, these core navigation methods include First, Next, Prior, Last, MoveBy, and
RecNo. The use oI First, Next, Prior, and Last are pretty much selI-explanatory. Each one
produces an eIIect similar to the corresponding buttons on a DBNavigator.
MoveBy permits you to move Iorward and backward in a ClientDataSet, relative to the
current record. For example, the Iollowing statement moves the current cursor 5 records
Iorward in the dataset (iI possible):
ClientDataSet1.MoveBy(5);
To move backwards in a dataset, pass MoveBy a negative number. For example, the
Iollowing statement will move the cursor to the record that is 100 records prior to the current
records (again, iI possible):
ClientDataSet1.MoveBy(-100);
The use oI RecNo to navigate might come as a surprise. This property, which is always
returns -1 in the TDataSet class, can be used Ior two purposes. You can read this property to
learn the position oI the current record in the current record order (based on which index is
currently selected). In the ClientDataSet you can also write to this property. Doing so moves
the cursor to the record in the position deIined by the value you assign to this property. For
example, the Iollowing statement will move the cursor to the record in the 5th position oI the
current index order (iI possible):
ClientDataSet1.RecNo := 5;
Each oI the preceding examples has been qualiIied by the statement that the operation will
succeed iI possible. This qualiIication has two aspects to it. First, the cursor movement will
not take place iI the current record has been edited, but cannot be posted. For example, iI data
that cannot pass at least one the ClientDataSet's Contraints has been added to a record. When
you attempt to navigate oII a record that cannot be posted, an exception is raised.
The second situation where the record navigation might not be possible is related to the
current record position and the number oI records in the dataset. For example, iI the current
record is the last in the dataset, it makes no sense to move 5 records Iorward. Similarly, iI the
current record is the 99th in the dataset, an attempt to move backwards by 100 records will
Iail. You can determine whether an attempt to navigate succeeded or Iailed by reading the EoI
and BoI properties oI the ClientDataSet. EoI (end-oI-Iile) will return True iI a navigation
method attempted to move beyond the end oI the table. When EoI returns True, the current
record is the last record in the dataset.
Similarly, BoI will return True iI a backwards navigation attempted to move beIore the
beginning oI the dataset. In that situation the current record is the Iirst record in the dataset.
RecNo behaves diIIerently. Attempting to set RecNo to a record beyond the end oI the table,
or prior to the beginning oI the table, raises an exception.
39 Scanning a ClientDataSet
Combining several oI the methods and properties described so Iar provides you with a
mechanism Ior scanning a ClientDataSet. Scanning simply means the systematic navigation
Irom one record to the next, until all records in the dataset have been visited. The Iollowing
code segment demonstrates how to scan a ClientDataSet.
procedure TForm1.Button1Click(Sender:

TJbject);
begin
if not ClientDataSet1.Active then ClientDataSet1.Jpen;
ClientDataSet1.First;
while not ClientDataSet1.EJF do
begin
//perform some operation based on one or
//more fields of the ClientDataSet
ClientDataSet1.Next;
end;
end;
40 Editing a ClientDataSet
You edit a current record in a ClientDataSet by calling its Edit method, aIter which you
change the values oI one or more oI its Iields. Once your changes have been made, you can
either move oII the record to attempt to post the new values, or you can explicitly call the
ClientDataSet's Post method. In most cases, navigating oII the record and calling Post
produce the same eIIect. But there are two instances where they do not, and it is due to these
situations that an explicit call to Post should be considered essential. In the Iirst instance, iI
you are editing the last record in a dataset and then call Next or Last, the edited record is not
posted. The second situation is similar, and involves editing the Iirst record in a dataset
Iollowed by a call to either Prior to First. So long as you always call Post prior to attempting
to navigate, you can be assured that your edited record will be posted (or raise an exception
due to a posting Iailure).
II you modiIy a record, and then decide not to post the change, or discover that you cannot
post the change, you can cancel all changes to the record by calling the ClientDataSet's
Cancel method. For example, iI you change a record, and then Iind that calling Post raises an
exception, you can call Cancel to cancel the changes and return the dataset to the dsBrowse
state.
To insert and post a record you have several options. You can call Insert or Append, aIter
which your cursor will be on a newly inserted record (assuming that you started Irom the
dsBrowse state. II you were editing a record prior to calling Insert or Append, a new record
will not be inserted iI the record being edited can not be posted). Once it is inserted, assign
data to the Iields or that record and call Post to post those changes.
The alternative to calling Insert or Append is to call InsertRecord or AppendRecord. These
methods insert a new record, assign data to one or more Iields, and attempt to post, all in a
single call. The Iollowing is the syntax oI the InsertRecord method. The syntax oI
AppendRecord is identical.
procedure InsertRecord(const Values: array of const);
You include in the constant array the data values you want to assign to each Iield in the
dataset. II you want to leave particular Iield unassigned, include the value null in the variant
array. Fields you want to leave unassigned at the end oI the record can be omitted Irom the
constant array. For example, II you are inserting and posting a new record into a Iour-Iield
ClientDataSet, and you want to assign the Iirst Iield the value 1000 (a Iield associated with a
unique index), leave the second and Iourth Iields unassigned, but assign a value oI 'new' to the
third record, your InsertRecord invocation may look something like this:
ClientDataSet1.InsertRecord(1001, null,

'new'`);
The Iollowing code segment demonstrates another instance oI record scanning, this time with
edits that need to be posted to each record. In this example, Edit and Post are perIormed
within try blocks. II the record was placed in the edit mode (which corresponds to the dsEdit
state), and cannot be posted, the change is canceled. II the record cannot even be placed into
edit state (which Ior a ClientDataSet should only happen iI the dataset has its ReadOnly
property set to True), the attempt to post changes is skipped.
procedure TForm1.Button1Click(Sender:

TJbject);
begin
if not ClientDataSet1.Active then ClientDataSet1.Jpen;
ClientDataSet1.First;
while not ClientDataSet1.EJF do
begin
try
ClientDataSet1.Edit;
try
ClientDataSet1.Fields0`.Value :=
UpperCase(ClientDataSet1.Fields0`.Value);
ClientDataSet1.Post;
except
//record cannot be posted. Cancel;
ClientDataSet1.Cancel;
end;
except
//Record cannot be edited. Skip
end;
ClientDataSet1.Next;
end; //while
end;
Note: Rather than simply canceling changes that cannot be posted, an alternative except
clause would identiIy why the record could not post, and produce a log which can be used to
apply the change at a later date. Also note that iI these changes are being cached, Ior update in
a subsequent call to ApplyUpdates, the ClientDataSet provides an OnReconcileError event
handler that can be used to process Iailed postings.
41 Disabling Controls While Navigating
II the ClientDataSet that you are navigating programmatically is attached to data-aware
controls through a DataSource, and you take no other precautions, the data-aware controls
will be aIIected by the navigation. In the simplest case, where you move directly to another
record, the update is welcome, causing the controls to repaint with the data oI the newly
arrived at record. However, when your navigation involves moving to two or more records in
rapid succession, such as is the case when you scan a ClientDataSet, the updates can have
severe results.
There are two reasons Ior this. First, the Ilicker caused by the data-aware controls repainting
as the ClientDataSet arrives at each record is distracting. More importantly, however, is the
overhead associated with a repaint. Repainting visual controls is one oI the slowest processes
in most GUI (graphic user interIace) applications. II your navigation involves visiting many
records, as oIten the case when you are scanning, the repaints oI your data-aware controls
represents a massive amount oI unnecessary overhead.
To prevent your data-aware controls Irom repainting when you need to programmatically
change the current record more than once you need to call the ClientDataSet's
DisableControls method (this is generally try oI any dataset, as DisableControls is
implemented in the TDataSet class). When DisableControls is called, the ClientDataSet stops
communicating with any DataSources that point to it. As a result, the data-aware controls that
point to those DataSources are never made aware oI the navigation. Once you are done
navigating, call the ClientDataSet's EnableControls. This will resume the communication
between the ClientDataSets and any DataSources that point to it. It will also result in the data-
aware controls being instructed to repaint themselves. However, this repaint occurs only once,
in response to the call to EnableControls, and not due to any oI the individual navigations that
occurred since DisableControls was called.
Is it important to recognized that between the time you call DisableControls and
EnableControls, the ClientDataSet is in an abnormal state. In Iact, iI you call DisableControls
and never call a corresponding EnableControls, the ClientDataSet will appear to the user to
have stopped Iunctioning, based on the lack oI activity in the data-aware controls. As a result,
it is essential that iI you call DisableControls, you structure your code in such a way that a call
to EnableControls is guaranteed. One way to do this it to enter a try-Iinally aIter a call to
DisableControls, invoking the corresponding EnableControls in the Iinally block.
The Iollowing is an example oI a scan where the user interIace is not updated until all record
navigation has completed.
procedure TForm1.Button1Click(Sender:

TJbject);
begin
if not ClientDataSet1.Active then ClientDataSet1.Jpen;
ClientDataSet1.DisableControls;
try
ClientDataSet1.First;
while not ClientDataSet1.EJF do
begin
try
ClientDataSet1.Edit;
try
ClientDataSet1.Fields0`.Value :=
UpperCase(ClientDataSet1.Fields0`.Value);
ClientDataSet1.Post;
except
//record cannot be posted. Cancel;
ClientDataSet1.Cancel;
end;
except
//Record cannot be edit. Skip
end;
ClientDataSet1.Next;
end; //while
finally
ClientDataSet1.EnableControls;
end; //try-finally
end;
42 Navigation Demonstration
The Navigation proiect, which you can download Irom Code Central by clicking this link
Navi gation Proiect, demonstrates the various methods and properties described in this article.
The Iollowing Iigure shows this proiect when it is running.

Each oI the Buttons on this Iorm is associated with an event handler that perIorms the
indicated type oI navigation. In addition, this proiect includes OnDataChange and
OnStateChange DataSource event handlers that are used to update the panels in the StatusBar
at the bottom oI the Iorm. These event handlers are shown in the Iollowing code listing.

procedure TForm1.SelectDataFile;
begin
if JpenDialog1.Execute then
begin
if ClientDataSet1.Active then ClientDataSet1.Close;
ClientDataSet1.FileName := JpenDialog1.FileName;
ClientDataSet1.Jpen;
end
else
Halt;
end;

procedure TForm1.FormCreate(Sender: TJbject);
begin
SelectDataFile;
end;

procedure TForm1.DataSource1DataChange(Sender: TJbject; Field: TField);
begin
StatusBar1.Panels0`.Text := 'Record ' +
IntToStr(ClientDataSet1.RecNo) + ' of ' +
IntToStr(ClientDataSet1.RecordCount);
StatusBar1.Panels2`.Text :=
'BJF = ' + BoolToStr(ClientDataSet1.Bof, True) +

'. ' +
'EJF = ' + BoolToStr(ClientDataSet1.Eof, True) +

'. ';
end;

procedure TForm1.DataSource1StateChange(Sender: TJbject);
begin
StatusBar1.Panels1`.Text :=
'State = ' + GetEnumName(TypeInfo(TDataSetState),
Jrd(ClientDataSet1.State));
end;

procedure TForm1.FirstBtnClick(Sender: TJbject);
begin
ClientDataSet1.First;
end;

procedure TForm1.NextBtnClick(Sender: TJbject);
begin
ClientDataSet1.Next;
end;

procedure TForm1.PriorBtnClick(Sender: TJbject);
begin
ClientDataSet1.Prior;
end;

procedure TForm1.LastBtnClick(Sender: TJbject);
begin
ClientDataSet1.Last;
end;

procedure TForm1.ScanForwardBtnClick(Sender: TJbject);
begin
if ControlsStateBtnGrp.ItemIndex = 1 then
ClientDataSet1.DisableControls;
try
ClientDataSet1.First;
while not ClientDataSet1.Eof do
begin
//do something with a record
ClientDataSet1.Next;
end;
finally
if ControlsStateBtnGrp.ItemIndex = 1 then
ClientDataSet1.EnableControls;
end;
end;

procedure TForm1.ScanBackwardBtnClick(Sender: TJbject);
begin
if ControlsStateBtnGrp.ItemIndex = 1 then
ClientDataSet1.DisableControls;
try
ClientDataSet1.Last;
while not ClientDataSet1.Bof do
begin
//do something with a record
ClientDataSet1.Prior;
end;
finally
if ControlsStateBtnGrp.ItemIndex = 1 then
ClientDataSet1.EnableControls;
end;
end;

procedure TForm1.MoveByBtnClick(Sender: TJbject);
begin
ClientDataSet1.MoveBy(UpDown1.Position);
end;

procedure TForm1.RecNoBtnClick(Sender: TJbject);
begin
ClientDataSet1.RecNo := UpDown2.Position;
end;

procedure TForm1.Jpen1Click(Sender: TJbject);
begin
SelectDataFile;
end;

procedure TForm1.Close1Click(Sender: TJbject);
begin
ClientDataSet1.Close;
end;

43 Searching a ClientDataSet
By: Cary Jensen
Abstract: ClientDataSets provide a number oI diIIerent mechanisms Ior searching Ior and
location data in its columns. These techniques are covered in this continuation oI the
discussion oI basic ClientDataSet manipulation.
In this article I am continuing the coverage oI basic ClientDataSet usage. In the last
installment oI this series I discussed how to navigate and edit a ClientDataSet. In this article I
show you how to Iind a record based on the data it contains.
In the context oI this article, 8earching means to either move the current record pointer to a
particular record based on the data held in the record, or to read data Irom a record based on
its data. Filtering, which shares some similarities with searching, involves restricting the
accessible records in a ClientDataSet to those that contain certain data. This article does not
demonstrate how to Iilter a ClientDataSet. That topic is be discussed in the next article in this
series.
44 Scanning for Data
The simplest, and typically slowest mechanism Ior searching is perIormed by scanning. As
you learned in the preceding article in this series (click here to read it), you can scan a table
by moving to either the Iirst or last record in the current index order, and then navigating
record-by-record until every record in the view has been visited. II used Ior searching, your
code reads each record's data as you scan. When a record containing the desired data is Iound,
the scanning process can be terminated.
The Iollowing code segment provides a simple example oI how a search operation like this
might look.
procedure TForm1.ScanBtnClick(Sender: TJbject);
var
Found: Boolean;
begin
Found := False;
ClientDataSet1.DisableControls;
Start;
try
ClientDataSet1.First;
while not ClientDataSet1.Eof do
begin
if ClientDataSet1.FieldsFieldListComboBox.ItemIndex`.Value =
ScanForEdit.Text then
begin
Found := True;
Break;
end;
ClientDataSet1.Next;
end;
Done;
finally
ClientDataSet1.EnableControls;
end;
if Found then ShowMessage(ScanForEdit.Text +
' found at record ' + IntToStr(ClientDataSet1.RecNo))
else
ShowMessage(ScanForEdit.Text + ' not found');
end;
As you learned Irom the preceding article in this series, scanning involves Iirst moving to one
end oI the dataset (the Iirst record in this example), and then navigating sequentially to each
record in the view. When searching using this technique, upon arriving at a record you read
one or more Iields to determine whether or not the current record is the one Ior which you are
looking. II the record contains the data you need, do something, such as terminate the search
and display the located record to the user. In this particular case, the code is searching Ior a
value entered into an Edit named ScanForEdit. The Iield being searched is the Iield name
currently selected in the IndexOnComboBox combobox.
This code is taken Irom the CDSSearch proiect, available Ior download Irom Code Central
(click here to download). The main Iorm oI this proiect is shown in the Iollowing Iigure.

Note that the data used in this example is Iound in the items.cds example Iile that ships with
Delphi.
The only method calls within this code that are not part oI the runtime library (RTL) or visual
component library (VCL), are the Start and Done methods. These methods are designed to
initiate and complete a perIormance monitor, which is used by all search-initiating event
handlers in this proiect to provide a relative measure oI perIormance. The perIormance
inIormation is displayed in the StatusBar oI this proiect, as can be seen in the preceding
Iigure. The implementation oI Start and Done is shown in the Iollowing code segment.
procedure TForm1.Start;
begin
StartTick := TimeGetTime;
end;

procedure TForm1.Done;
begin
EndTick := TimeGetTime;
StatusBar1.Panels0`.Text := 'Starting tick: ' +
IntToStr(StartTick);
StatusBar1.Panels1`.Text := 'Ending tick: ' +
IntToStr(EndTick);
StatusBar1.Panels2`.Text := 'Duration (in milliseconds): ' +
IntToStr(EndTick - StartTick);
end;
Both Start and Done make use oI the TimeGetTime Iunction, which is imported in the
MMSystem unit. This Iunction returns a tick count, which represents the number oI
milliseconds that have past since Windows was started. TimeGetTime is signiIicantly more
accurate than GetTickCount, a commonly-used timing Iunction. Normally, TimeGetTime is
accurate within Iive milliseconds under NT, and within one millisecond under Windows 98.
45 Finding Data
One oI the oldest mechanisms Ior searching a dataset was introduced in Delphi 1. This
method, FindKey, permits you to search one or more Iields oI the current index Ior a
particular value. FindKey, and its close associate, FindNearest, both make use oI the current
index to perIorm the search. As a result, the search is always index-based, and always very
Iast.
Both FindKey and FindNearest take a single constant array parameter. You include in this
array the values Ior which you want to search on the Iields on the index, with the Iirst element
in the array being searched Ior in the Iirst Iield oI the index, the second Iield in the array (iI
provided) searched Ior in the second Iield oI the index, and so Iorth. Since the search is
indexed-based, the number oI Iields searched obviously cannot exceed the number oI Iields in
the index (though there is no problem iI you want to search on Iewer Iields than are contained
in the index).
In the CDSSearch proiect, the only indexes available are temporary indexes associated with
single Iields in the dataset. (The current temporary index is based on the Iield listed in the
IndexOnComboBox, shown in the preceding Iigure.) Consequently, the demonstrations oI the
FindKey and FindNearest methods in this proiect are limited to single Iields, speciIically the
value entered into the ScanForEdit Edit component. The Iollowing are the event handlers
associated with the FindKey and FindNearest buttons in this proiect, respectively.
procedure TForm1.FindKeyBtnClick(Sender:

TJbject);
begin
Start;
if ClientDataSet1.FindKey(ScanForEdit.Text`) then
begin
Done;
StatusBar1.Panels3`.Text := ScanForEdit.Text +
' found at record ' +
IntToStr(ClientDataSet1.RecNo);
end
else
begin
Done;
StatusBar1.Panels3`.Text :=
ScanForEdit.Text + ' not found';
end;
end;

procedure TForm1.FindNearestBtnClick(Sender: TJbject);
begin
Start;
ClientDataSet1.FindNearest(ScanForEdit.Text`);
Done;
StatusBar1.Panels3`.Text := 'The nearest match to ' +
ScanForEdit.Text + ' found at record ' +
IntToStr(ClientDataSet1.RecNo);
end;
The Iollowing Iigure shows the result oI a search perIormed on an index based on the
OrderNo Iield. In this case, as in the preceding Iigure, the value being searched Ior is OrderNo
1278. Notice that in the StatusBar this FindKey search took signiIicantly less time than the
search using scanning.

While FindKey and FindNearest are identical in syntax, there is a subtle diIIerence in how
they operate. FindKey is a Boolean Iunction method that returns True iI a matching record is
located. In that case, the cursor is repositioned in the ClientDataSet to the Iound record. II
FindKey Iails it return False, and the current record pointer does not change.
Unlike FindKey, which is a Iunction, FindNearest is a procedure method. Technically
speaking, FindNearest always succeeds, moving the cursor to the record that most closely
matches the search criteria. For example, in Iollowing Iigure FindNearest is used to locate the
record whose OrderNo most closely matches the value 99999. As you can see in this Iigure,
the located record contains OrderNo 1860, the highest OrderNo in the table, and the last
record in the current index order.

46 Going to Data
GotoKey and GotoNearest provide the same searching Ieatures as FindKey and FindNearest,
respectively. The only diIIerence between these two sets oI methods is how you deIine your
search criteria. As you have already learned, FindKey and FindNearest are passed a constant
array as a parameter, and the search criteria are contained in this array.
Both GotoKey and GotoNearest take no parameters. Instead, their search criteria is deIined
using the search key buIIer. The search key buIIer contains one Iield Ior each Iield in the
current index. For example, iI the current index is based on the Iield OrderNo, the search key
buIIer contains one Iield: OrderNo. By comparison, iI the current index contains three Iields
the search key buIIer also contains three Iields.
Fields in the search key buIIer can only be modiIied when the ClientDataSet is in a special
state called the dsSetKey state. To clear the search key buIIer and enter the dsSetKey state,
call the ClientDataSet's SetKey method. II you have previously assigned one or more values
to the search key buIIer, you can enter the dsSetKey state without clearing the search key
buIIer's contents by calling the ClientDataSet's EditKey method. From within the dsSetKey
state, you assign data to Iields in the search key buIIer as iI you were assigning data to the
ClientDataSet's Iields. For example, assuming that the current index is based on the OrderNo
Iield, the Iollowing lines oI code assign the value 1278 to the OrderNo Iield oI the search key
buIIer:
ClientDataSet1.SetKey;
ClientDataSet1.FieldByName('JrderNo').Value := 1278;
As should be apparent, using GotoKey or GotoNearest requires more lines oI code than
FindKey and FindNearest. For example, once again assuming that the current index is based
on the OrderNo Iield, consider the Iollowing statement:
ClientDataSet1.FindKey(ScanForEdit.Text`);
Achieving the same result using GotoKey requires three lines oI code, since you must Iirst
enter the dsSetKey state and edit the search key buIIer. The Iollowing lines oI code, which
use GotoKey, perIorm precisely the same search as the preceding line oI code:
ClientDataSet1.SetKey;
ClientDataSet1.FieldByName('JrderNo').Value := ScanForEdit.Text;
ClientDataSet1.GotoKey;
The Iollowing event handlers are associated with the buttons labeled Goto Key and Goto
Nearest in the CDSSearch proiect.
procedure TForm1.GotoKeyBtnClick(Sender:

TJbject);
begin
Start;
ClientDataSet1.SetKey;
ClientDataSet1.FieldsIndexJnComboBox.ItemIndex`.AsString :=
Trim(ScanForEdit.Text);
if ClientDataSet1.GotoKey then
begin
Done;
StatusBar1.Panels3`.Text := ScanForEdit.Text +
' found at record ' +
IntToStr(ClientDataSet1.RecNo);
end
else
begin
Done;
StatusBar1.Panels3`.Text :=
ScanForEdit.Text + ' not found';
end;
end;

procedure TForm1.GotoNearestBtnClick(Sender: TJbject);
begin
Start;
ClientDataSet1.SetKey;
ClientDataSet1.FieldsIndexJnComboBox.ItemIndex`.AsString :=
ScanForEdit.Text;
ClientDataSet1.GotoNearest;
Done;
StatusBar1.Panels3`.Text := 'The nearest match to ' +
ScanForEdit.Text + ' found at record ' +
IntToStr(ClientDataSet1.RecNo);
end;
47 Locating Data
One oI the drawbacks to the Find and Goto methods is that the search is based on the current
index. Depending no the data you are searching Ior, you might have to change the current
index beIore perIorming the search. Fortunately, ClientDataSets support two generally high-
perIormance searching mechanisms that do not require you to change the current index. These
are Locate and Lookup.
Locate, like FindKey and GotoKey, makes the located record the current record iI a match is
Iound. In addition, Locate is a Iunction method, returning a Boolean True iI the search results
in a match. Lookup is somewhat diIIerent, returning speciIic Iields Irom a located record, but
never moving the current record pointer. Lookup is described separately in the Iollowing
section.
What makes Locate and Lookup so special is that they do not require you to create or switch
indexes, but still provide much Iaster perIormance than scanning. In a number oI tests that I
have conducted, Locate Iound a record Iour times Iaster than did scanning. For example,
when searching Ior data in a record at position 90,000 oI a 100,000 record table, Locate
located the record in about 500 milliseconds, while scanning Ior that record took longer than 2
seconds. Admittedly, FindKey took only 10 milliseconds to Iind that record. But the index
that FindKey required Ior the search took almost 1 second to build.
The Iollowing is the syntax oI Locate:
function Locate(const KeyFields: string;
const KeyValues: Variant; Jptions: TLocateJptions): Boolean;
II you are locating a record based on a single Iield, the Iirst argument is the name oI that Iield
and the second argument is the value you are searching Ior. To search on more than one Iield,
pass a semicolon-separated string oI Iield names in the Iirst argument, and a variant array
containing the search values corresponding to the Iield list in the second argument.
The third argument oI Locate is a TLocateOptions set. This set can contain up to two Ilags,
loCaseInsensitive and loPartialKey. Include loCaseInsensitive to ignore case in your search
and loPartialKey to match any value that begins with the values you pass in the second
argument.
II the search is successIul, Locate makes the located record the current record and returns a
value oI True. II the search is not successIul, Locate returns False, and the cursor does not
move.
Imagine that you are searching the Customer.xml Iile that ships with Delphi. The Iollowing
statement will locate the Iirst record in the ClientDataSet whose Company name is Ocean
Paradise.
ClientDataSet1.Locate('Company', 'Jcean Paradise',`);
The next example demonstrates a partial match, searching Ior the Iirst company whose name
starts with the letter u or U.
ClientDataSet1.Locate('Company','u',loCaseInsensitive, loPartialKey`);
Searching Ior two or more Iields is somewhat more involved, in that you must pass the search
values using a variant array. The Iollowing lines oI code demonstrate how you can search Ior
the record where the Company Iield contains Unisco and the City Iield contains Freeport.
var
SearchList: Variant;
begin
SearchList := VarArrayCreate(0, 1`, VarVariant);
SearchList0` := 'Unisco';
SearchList1` := 'Freeport';
ClientDataSet1.Locate('Company;City', SearchList, loCaseInsensitive`);

Instead oI using VarArrayCreate, you can use VarArrayOI. VarArrayOI takes a constant array
oI the values Irom which to create the variant array. This means that you must know at
design-time how many elements your variant array will have. By comparison, the dimensions
oI the variant array created using VarArrayOI can include variables, which permits you to
determine the array size at runtime. The Iollowing code perIorms the same search as the
preceding code, but makes use oI an array created using VarArrayOI.
var
SearchList: Variant;
begin
SearchList := VarArrayJf('Unisco','Freeport'`);
ClientDataSet1.Locate('Company;City',SearchList,loCaseInsensitive`);
p~II you reIer back to the CDSSearch proiect main Iorm shown in the earlier Iigures oI this
article, you will notice a StringGrid in the upper-right corner. Data entered into the Iirst two
columns oI this grid are used to create the KeyFields and KeyValues arguments oI Locate,
respectively. The Iollowing methods, Iound in the CDSSearch proiect, generate these
parameters.
function TForm1.GetKeyFields(var

FieldStr: String): Integer;
const
FieldsColumn = 0;
var
i : Integer;
Count: Integer;
begin
Count := 0;
for i := 1 to 20 do
begin
if StringGrid1.CellsFieldsColumn,i` < '' then
begin
if FieldStr = '' then FieldStr :=
StringGrid1.CellsFieldsColumn,i`
else
FieldStr := FieldStr + ';' +
StringGrid1.CellsFieldsColumn,i`;
inc(Count);
end
else
Break;
end;
Result := Count;
end;

function TForm1.GetKeyValues(Size: Integer): Variant;
const
SearchColumn = 1;
var
i: Integer;
begin
Result := VarArrayCreate(0,Pred(Size)`, VarVariant);
for i := 0 to Pred(Size) do
Resulti` := StringGrid1.CellsSearchColumn, Succ(i)`;
end;
The Iollowing code is associated with the OnClick event handler oI the button labeled Locate
in the CDSSearch proiect. As you can see, in this code the Locate method is invoked based on
the values returned by calling GetKeyFields and GetKeyValues.
procedure TForm1.LocateBtnClick(Sender:

TJbject);
var
FieldList: String;
Count: Integer;
SearchArray: Variant;
begin
FieldList := '';
Count := GetKeyFields(FieldList);
SearchArray := GetKeyValues(Count);
Start;
if ClientDataSet1.Locate(FieldList, SearchArray, `) then
begin
Done;
StatusBar1.Panels3`.Text :=
'Match located at record ' +
IntToStr(ClientDataSet1.RecNo);
end
else
begin
Done;
StatusBar1.Panels3`.Text := 'No match located';
end;
end;
48 Using Lookup
Lookup is similar in many respects to Locate, with one very important diIIerence. Instead oI
moving the current record pointer to the located record, Lookup returns a variant containing
data Irom a located record without moving the current record pointer. The Iollowing is the
syntax oI Lookup.

function Lookup(const KeyFields: string;
const KeyValues: Variant; const ResultFields: string): Variant;
The KeyFields and KeyValues parameters oI Lookup are identical in purpose to those in the
Locate method. ResultFields is a semicolon separated string oI Iield names whose values you
want returned. II Lookup Iails to Iind the record you are searching Ior, it returns a null variant.
Otherwise, it returns a variant containing the Iield values requested in the ResultFields
parameter.
The event handler associated with the Lookup button in the CDSSearch proiect makes use oI
the GetKeyFields and GetKeyValues methods Ior deIining the KeyFields and KeyValues
parameters oI the call to Lookup, based again on the Iirst two columns oI the StringGrid. In
addition, this event handler makes use oI the GetResultFields method to construct the
ResultFields parameter Irom the third column oI the grid. The Iollowing is the code associated
with the GetResultFields method.
function TForm1.GetResultFields: String;
const
ReturnColumn = 2;
var
i: Integer;
begin
for i := 1 to Succ(StringGrid1.RowCount) do
if StringGrid1.CellsReturnColumn, i` < '' then
if Result = '' then
Result := StringGrid1.CellsReturnColumn, i`
else
Result := Result + ';' +
StringGrid1.CellsReturnColumn, i`
else
Break;
end;
The Iollowing is the code associated with the OnClick event handler oI the button labeled
Lookup.
procedure TForm1.LookupBtnClick(Sender:

TJbject);
var
ResultFields: Variant;
KeyFields: String;
KeyValues: Variant;
ReturnFields: String;
Count, i: Integer;
DisplayString: String;
begin
Count := GetKeyFields(KeyFields);
DisplayString := '';
KeyValues := GetKeyValues(Count);
ReturnFields := GetResultFields;
Start;
ResultFields := ClientDataSet1.Lookup(KeyFields,
KeyValues, ReturnFields);
Done;
if VarIsNull(ResultFields) then
DisplayString := 'Lookup record not found'
else
if VarIsArray(ResultFields) then
for i := 0 to VarArrayHighBound(ResultFields,1) do
if i = 0 then
DisplayString := 'Lookup result: ' +
VarToStr(ResultFieldsi`)
else
DisplayString := DisplayString +
';' + VarToStr(ResultFieldsi`)
else
DisplayString := VarToStr(ResultFields);
StatusBar1.Panels3`.Text := DisplayString
end;
The Iollowing Iigure shows the main Iorm oI the CDSSearch proiect Iollowing a call to
Locate. Notice that the current record is still the Iirst record in the ClientDataSet, even though
the data returned Irom the call to Locate was Iound much later in the current index order.

49 Summary
ClienDataSets provide a number oI mechanisms Ior searching their columns. The simplest,
those oIten slowest, is to scan the ClientDataSet Ior particular values. FindKey and
FindNearest (and their GotoKey and GotoNearest counterparts), are extremely Iast, since they
use an index. However, the Find and Goto methods might require you to Iirst select or build
an appropriate index. By comparison, Locate and Lookup provide relatively good
perIormance without requiring an index, making them the preIerred searching methods in
applications where speed is not critical.
50 Filtering ClientDataSets
By: Cary Jensen
Abstract: When applied to a dataset, a Iilter limits the records that are accessible. This article
explores the ins and outs oI Iiltering ClientDataSets.
This article is part oI an extended series exploring the ClientDataSet in detail. In case you are
new to this series, a ClientDataSet is a component that provides an in-memory table that can
be manipulated easily and eIIiciently. Previous articles in this series have provided a broad
overview oI ClientDataSet usage, but in the past two installments I have been covering the
essential, basic operations involving ClientDataSet. In this article I am completing the
discussion oI Ioundation issues with a look at dataset Iiltering.
When you Iilter a dataset, you restrict access to a subset oI records contained in the
ClientDataSet's in-memory store. For example, imagine that you have a ClientDataSet that
includes one record Ior every one oI your company's customers, world-wide. Without
Iiltering, all customer records are accessible in the dataset. That is, it is possible to navigate,
view, and edit any customer in the dataset. Through Iiltering you can make the ClientDataSet
appear to include only those customers who live in the United States, or in London, England,
or who live on a street named Enterprise Way. This example, oI course, assumes that there is
a Iield in the ClientDataSet that contains country names, or Iields containing City and Country
names, or a Iield holding street names. In other words, a Iilter limits the accessible records
based on data that is stored in the ClientDataSet, and is eIIective the the extent that the data in
the ClientDataSet can be used to limit which records are accessible.
A ClientDataSet supports two Iundamentally diIIerent mechanisms Ior creating Iilters. The
Iirst oI these involves a range, which is an index-based Iiltering mechanism. The second,
called a filter, is more Ilexible than ranges, but is slower to apply and cancel. Both oI these
approaches to Iiltering are covered in this article.
But beIore addressing Iiltering directly, there are a couple oI important points that need to be
made. The Iirst is that Iiltering is a client-side operation. SpeciIically, the Iilters discussed in
this article are applied to the data loaded into a ClientDataSet's in-memory store. For example,
you may load 10,000 records into a ClientDataSet (every customer record, Ior instance), and
then apply a Iilter that limits access to only those customers located in New York City. Once
applied, the Iilter may make the ClientDataSet to appear to contain only 300 records (given
that 300 oI your customer's are located in New York City). Although the Iiltered
ClientDataSet provides access only to these 300 records, all 10,000 records remain in
memory. In other words, a Iilter does not reduce the overhead oI your ClientDataSet, it simply
restricts access to a subset oI the ClientDataSet's records.
The second point is that instead oI using a Iilter, you may be better oII limiting how many
records you load into the ClientDataSet in the Iirst place. Consider the 10,000 customer
records once again. Instead oI loading all 10,000 records into memory, and then Iiltering on
the City Iield, it might be better to load only a subset oI the customer records into the
ClientDataSet. While partial loading not available when a ClientDataSet is loaded Irom the
local Iile system using MyBase, it is an option when loading a ClientDataSet through a
DataSetProvider.
For example, imagine that your DataSetProvider points to a SQLDataSet whose
CommandText contains the Iollowing SQL query:
SELECT FRJM CUSTJMER WHERE CITY = 'New York City'
When the ClientDataSet's Open method is called, this SQL select statement is executed, and
only those 300 or so records Irom your New York City-based customers are loaded into the
ClientDataSet. This approach greatly reduces the memory overhead oI the ClientDataSet,
since Iewer records need to be stored in memory.
Actually, there are a number oI techniques that permit you to load selected records Irom a
dataset through a DataSetProvider into a ClientDataSet, including the use oI parameterized
queries, nested datasets, dynamic SQL, among others. An thorough examination oI these
techniques will appear in a Iuture article in this series. Nonetheless, Irom the perspective oI
this article, these techniques are not technically Iiltering, since they do not limit access within
the ClientDataSet to a subset oI its loaded records..
So when do you use Iiltering as opposed to loading only selected records into a
ClientDataSet? The answer boils down to three basic issues: bandwidth, source oI data, and
client-side Ieatures.
When loading a ClientDataSet Irom DataSetProvider, and bandwidth is low, as is oIten the
case in distributed applications, it is normally best to load only selected records. In this
situation, loading records that are not going to be displayed consumes bandwidth
unnecessarily, aIIecting the perIormance oI your application as well as that oI others that
share the bandwidth. On the other hand, iI bandwidth is plentiIul and the entire dataset is
relatively small, it is oIten easier to load all data and Iilter on those records you want
displayed.
The second consideration is data location. II you are loading data Irom a previously saved
ClientDataSet (in either Borland's proprietary binary Iormat or in XML Iormat), you have no
choice. Filtering is the only option Ior showing iust a subset oI records. Only when you are
loading data through a DataSetProvider do you have a choice to use a Iilter or selective
loading oI data.
The Iinal consideration is related to client-side Ieatures, the most common oI which is speed.
Once data is loaded into a ClientDataSet, most Iilters are applied very quickly, even when a
large amount oI data needs to be Iiltered. As a result, Iiltering permits you to rapidly alter
which subset oI records are displayed. A simple click oI a button or menu selection can
almost instantly switch your ClientDataSet Irom displaying customers Irom New York City to
displaying customers Irom FrankIurt, Germany, without a network round-trip.
As mentioned earlier, there are two basic approaches to Iiltering: ranges and Iilters. Let's start
by looking at ranges.
51 Setting a Range
Ranges, while less Ilexible than Iilters, provide the Iastest option Ior displaying a subset oI
records Irom a ClientDataSet. In short, a range is an index-based mechanism Ior deIining the
low and high values oI records to be displayed in the ClientDataSet. For example, iI the
current index is based on customer's last name, a range can be used to display all customer's
whose last name is 'Jones.' Or, a range can be used to display only customer's whose last name
begins with the letter 'J'. Similarly, iI a ClientDataSet is indexed on an integer Iield called
Credit Limit, a range can be used to display only those customers whose credit limit is greater
than (US) $1,000, or between $0 and $1000.
There are two ways to set a range. The Iirst, and easiest, is to use the SetRange method.
SetRange deIines a range using a single method invocation. The second mechanism is to enter
the dsSetKey state, which requires a minimum oI three method calls, and oIten Iour.
In Delphi and Kylix, SetRange has the Iollowing syntax:
procedure SetRange(const StartValues, EndValues: array of const);
As you can see Irom this syntax, you pass two constant arrays when you call SetRange. The
Iirst array contains the low values oI the range values Ior the Iields oI the index, with the Iirst
element in the array being the low end oI the range Ior the Iirst Iield in the index, the second
element being the low end oI the range Ior the second Iield in the index, and so on. The
second array contains the high end values Ior the index Iields, with the Iirst element in the
second array being the high end oI the range on the Iirst Iield oI the index, the second element
being the high end on the second Iield oI the index, and so Iorth. These arrays can contain
Iewer elements than the number oI Iields in the current index, but cannot contain more.
Consider again our example oI a ClientDataSet that holds all customer records. Given that
there is a Iield in this dataset named 'City,' and you want to display only records Ior customers
who live in New York City, you can use the Iollowing statements:
ClientDataSet1.IndexFieldNames := 'City';
ClientDataSet1.SetRange('New York City'`, 'New York City'`);
The Iirst statement creates a temporary index on the City Iield, while the second sets the
range. OI course, iI the ClientDataSet was already using an index where the Iirst Iield oI the
index was the City Iield, you would omit the Iirst statement in the preceding code segment.
The preceding example set the range on a single Iield, but it is oIten possible to set a range on
two or more Iields oI the current index. For example, imagine that you want to display only
those customers whose last name is Walker and who live in San Antonio, Texas. The
Iollowing statements show you how:
ClientDataSet1.IndexFieldNames := 'LastName;City;State';
ClientDataSet1.SetRange('Walker', 'San Antonio', 'TX'`, 'Walker', 'San
Antonio', 'TX'`);
In both oI these preceding examples the beginning and ending ranges contained the same
values. But this is not always the case. For example, imagine that you want to set a range to
include only those customers whose credit limit is greater than (US) $1,000. This can be
accomplished using statements similar to the Iollowing:
ClientDataSet1.IndexFieldNames := 'CreditLimit';
ClientDataSet1.SetRange(1000`, MaxInt`);
52 Using ApplyRange
In a previous article in this series you learned that there are two index-based methods Ior
locating a record based on an exact match. One, FindKey, is a selI-contained statement Ior
locating a record based on Iields oI the current index. By comparison, GotoKey is more
involved, requiring you to Iirst call SetKey to enter the dsSetKey state, during which you
deIine your search criteria, and then complete the operation with a call to GotoKey. SetRange
is similar to FindKey, where a single statement deIines the range as well as sets it.
ApplyRange, by comparison, is similar to GotoKey.
To use ApplyRange you begin by calling SetRangeStart (or EditRangeStart). Doing so places
the ClientDataSet in the dsSetKey state. While in this state you assign values to one or more
oI the TFields involved in the current index to deIine the low values oI the range. As is the
case with SetRange, iI you deIine a single low value, it must be to the Iirst Iield oI the current
index. II you deIine a low range value Ior two Iields, they must necessarily be the Iirst two
Iields oI the index.
AIter setting the low range values, you call SetRangeEnd (or EditRangeEnd). You now assign
values to one or more Iields oI the current index to deIine the high values Ior the range. Once
both the start values and end values have been set, you call ApplyRange to Iilter the
ClientDataSet on the deIined range.
For example, the Iollowing statements use ApplyRange to display only customers who live in
New York City in the customer table.
ClientDataSet1.IndexFieldNames := 'City';
ClientDataSet1.SetRangeStart;
ClientDataSet1.FieldByName('City').Value := 'New York City';
ClientDataSet1.SetRangeEnd;
ClientDataSet1.FieldByName('City').Value := 'New York City';
ClientDataSet1.ApplyRange;
Just like SetRange, ApplyRange can be used to set a range on more than one Iield oI the
index, as shown in the Iollowing example.
ClientDataSet1.IndexFieldNames := 'LastName;City;State';
ClientDataSet1.SetRangeStart;
ClientDataSet1.FieldByName('LastName').Value := 'Walker';
ClientDataSet1.FieldByName('City').Value := 'San Antonio';
ClientDataSet1.FieldByName('State').Value := 'TX';
ClientDataSet1.SetRangeEnd;
ClientDataSet1.FieldByName('LastName').Value := 'Walker';
ClientDataSet1.FieldByName('City').Value := 'San Antonio';
ClientDataSet1.FieldByName('State').Value := 'TX';
ClientDataSet1.ApplyRange;
Both oI the preceding examples made use oI SetRangeStart and SetRangeEnd. In some cases,
you can use EditRangeStart and/or EditRangeEnd instead. In short, iI you have already set
low and high values Ior a range, and want to modiIy some, but not all, values, you can use
EditRangeStart and EditRangeEnd. Calling SetRangeStart clears any previous values in the
range. By comparison, iI you call EditRangeStart, the previously deIined low values remain in
the range Iields. II you want to change some, but not all, oI the low range values, call
EditRangeStart and modiIy only those Iields whose low values you want to change. Likewise,
iI you want to change some, but not all, oI the high range values, do so by calling
EditRangeEnd.
For example, the Iollowing code segment will display all records where the customer's credit
limit is between (US) $1,000 and (US) $5,000.
ClientDataSet1.IndexFieldNames := 'CreditLimit';
ClientDataSet1.SetRange(1000`,5000`);
II you then want to set a range between $1,000 and $10,000, you can do so using the
Iollowing statements:
ClientDataSet1.EditRangeEnd;
ClientDataSet1.FieldByName('CreditLimit').Value := 10000;
ClientDataSet1.ApplyRange;
53 Canceling a Range
Whether you have created a range using SetRange or ApplyRange, you cancel that range by
calling the ClientDataSet's CancelRange method. The Iollowing example demonstrates how a
call to CancelRange looks in code:
ClientDataSet1.CancelRange;
54 A Comment About Ranges
Earlier in this article I mentioned that it is 'sometimes' possible to set a range on two or more
Iields. The implication oI this statement is that sometimes it is not, which is true. When
setting a range on two or more Iields, only the last Iield oI the range can speciIy a range oI
values, all other Iields must have the same value Ior both the low and high ends oI the range.
For example, the Iollowing range will display all records where the credit limit is between
$1,000 and $5,000 Ior customers living in New York City.
ClientDataSet1.IndexFieldNames := 'City;CreditLimit';
ClientDataSet1.SetRange('New York City', 1000`, 'New York City', 5000`);
By comparison, the Iollowing statement will display all records Ior customers whose credit
limit is between $1,000 and $5,000, regardless oI which city they live in.
ClientDataSet1.IndexFieldNames := 'CreditLimit;City';
ClientDataSet1.SetRange(1000, 'New York City'`, 5000, 'New York City'`);
The diIIerence between these two ranges is that in the Iirst range, the low and high value in
the Iirst Iield oI the range was a constant value, New York City. In the second, a range
appears (1000-5000). In this case, the second Iield oI the range is ignored.
There is another aspect oI ranges that is rather odd when working with ClientDataSets. This is
related to the KeyExclusive property inherited by the ClientDataSet Irom TDataSet.
Normally, this property can be used to deIine how ranges are applied. When KeyExclusive iI
False (its deIault value), the range includes both the low and high values oI the range. For
example, iI you set a range on CreditLimit to 1000 and 5000, records where the credit limit is
1000 or 5000 will appear in the range. II KeyExclusive is set to True, only customer records
where the credit limit is greater than 1000 but less than 5000 would appear in the range.
Customers with credit limits oI exactly 1000 or 5000 will not.
Maybe its me, but when I try to programmatically set the KeyExclusive property on a
ClientDataSet it raises an exception. I have concluded Irom this that KeyExclusive does not
apply to ClientDataSets. II you can get KeyExclusive to work with ClientDataSets in Delphi 6
or Delphi 7, I'd like to know.
55 Using Filters
Because ranges rely on indexes, they are applied very quickly. For example, on a 100,000
record table, with an index on the FirstName Iield, setting a range to show only records Ior
customers where the Iirst name is Scarlett was applied in less than 10 milliseconds on a 850
MHz Pentium III with 512 MB RAM (the resulting view contained only 133 records).
Filters, by comparison, do not use indexes. Instead, they operate by evaluating the records oI
the ClientDataSet, displaying only those records that pass the Iilter. Since Iilters do not use
indexes, they are not as Iast (Iiltering on the Iirst name Scarlett took iust under 500
milliseconds on the same database). However, they are much more Ilexible.
A ClientDataSet has Iour properties that apply to Iilters. These are Filter, Filtered,
FilterOptions, and OnFilterRecord (an event property). In its simplest case, a Iilter requires
the use oI two oI these properties: Filter and Filtered. Filtered is a Boolean property that you
use to turn on and oII the Iilter. II you want to Iilter records, set Filtered to True, otherwise set
Filtered to False (the deIault value).
Hide image

When Filtered is set to True, the ClientDataSet uses the value oI the Filter property to identiIy
which records will be displayed. You assign to this property a Boolean expression containing
at least one comparison operation involving at least one Iield in the dataset. You can use any
comparison operators, include , ~, , ~, , and ~. As long as the Iield name does not
include any spaces, you include the Iield name directly in the comparison without delimiters.
For example, iI your ClientDataSet includes a Iield named City, you can set the Filter
property to the Iollowing expression to display only customers living in New York City:
City = 'New York City'
Note that the single quotes are required here, since New York City is a string. II you want to
assign a value to the Filter property at runtime, you must include the single quotes in the
string that you assign to the property. The Iollowing is one example oI how to do this:
ClientDataSet1.Filter := 'City = ' + QuotedStr('New York City');
The preceding code segment used the QuotedStr Iunction, which is located in the SysUtils
unit. The alternative is to use something like the Iollowing. Personally, I preIer using
QuotedStr, as it is much easier to debug and maintain.
ClientDataSet1.Filter := 'City = ''Freeport''';
In the preceding examples the Iield name oI the Iield in the Iilter did not include spaces. II one
or more Iields that you want to use in a Iilter include spaces in their Iield names, enclose those
Iield names in square braces. (Square braces can also be used around Iield names that do not
include spaces.) For example, iI your ClientDataSet contains a Iield named 'Last Name,' you
can use a statement similar to the Iollowing to create a Iilter.
ClientDataSet1.Filter := 'Last Name` = ' + QuotedStr('Williams');
These examples have demonstrated only simple expressions. However, complex expressions
can be used. SpeciIically, you can combine two or more comparisons using the AND, OR,
and NOT logical operators. Furthermore, more than one Iield can be involved in the
comparison. For example, you can use the Iollowing Filter to limit records to those where the
City Iield is San Francisco, and the last name is Martinez:
ClientDataSet1.Filter := 'City` = '+ QuotedStr('San Francisco') +
'and Last Name` = ' + QuotedStr('Martinez');
Assigning a value to the Filter property does not automatically mean that records will be
Iiltered. Only when the Filtered property is set to True does the Filter property actually
produce a Iiltered dataset. Furthermore, iI the Filter property contains an empty string, setting
Filtered to True has no eIIect.
By deIault, Iilters are case sensitive and perIorm a partial match to the Iilter criteria. You can
inIluence this behavior using the FilterOptions property. This property is a set property that
can contain zero or more oI the Iollowing two Ilags: IoCaseInsensitive and
IoNoPartialMatch. When IoCaseInsensitive is included in the set, the Iilter is not case
sensitive.
When IoNoPartialMatch is included in the set, partial matches are excluded Irom the Iiltered
DataSet. When IoNoPartialCompare is absent Irom the FilterOptions property, partial
matches are identiIied by an asterisk ('*') in the last character oI your Iilter criteria. All Iields
whose contents match the characters to the leIt oI the asterisk are included in the Iilter. For
example, consider the Iollowing Iilter:
ClientDataSet1.Filter := 'City = '+ QuotedStr('San ');
This so long as IoNoPartialCompare is absent Irom the FilterOptions property, this Iilter will
include any city whose name begins 'San ,' such as San Francisco or San Antonio.
Partial matches can also be used with compound Boolean expressions. For example, the
Iollowing Iilter will display all customer's whose names begin with the letter M, and who live
in a city whose name begins with 'New,' such as Newcastle or New York City.
ClientDataSet1.Filter := 'City = '+ QuotedStr('New') +
'and Last Name` = ' + QuotedStr('M');
56 Using the OnFilterRecord Event Handler
There is a second, somewhat more Ilexible way to deIine a Iilter. Instead oI using the Filter
property, you can attach code to the OnFilterRecord event handler. When Filtered is set to
True, this event handler triggers Ior every record in the dataset. When called, this event
handler is passed a Boolean parameter by reIerence, named Accept, that you use to indicate
whether or not the current record should be included in the Iiltered view. From within this
event handler, you can perIorm almost any test you can imagine. For example, you can veriIy
that the current record is associated with a record in another table. II, based on this test, you
wish to exclude the current record Irom the view, you set the value oI the Accept Iormal
parameter to False. This parameter is True by deIault.
The Filter property normally consists oI one or more comparisons involving values in Iields
oI the ClientDataSet. OnFilterRecord event handlers, however, can include any comparison
you want. And there lies the danger. SpeciIically, iI the comparison that you perIorm in the
OnFilterRecord event handler is time consuming, the Iilter will be slow. In other words, you
should try to optimize any code that you place in an OnFilterRecord event handler, especially
iI you need to Iilter a lot oI records.
The Iollowing is a simple example oI an OnFilterRecord event handler.
procedure TForm1.ClientDataSet1FilterRecord(DataSet: TDataSet;
var Accept: Boolean);
begin
Accept := ClientDataSet1.Fields1`.AsString = 'Scarlett';
end;
57 Navigating Using a Filter
Whether you have set Filtered to True or not, you can still use a Filter Ior the purpose oI
navigating selected records. For example, although you may want to view all records in a
database, you may want to quickly move between records that meet speciIic criteria. For
example, you may want to be able to quickly navigate between those records where the
customer has a credit limit in excess oI (US) $5,000.
A ClientDataSet exposes Iour methods Ior navigating using a Iilter. These methods are
FindFirst, FindLast, FindNext, and FindPrior. When you execute one oI these methods, the
ClientDataSet will locate the requested record based on the current Filter property, or
OnFilterRecord event handler. This navigation, however, does not require that the Filtered
property be set to True. In other words, while all records oI the ClientDataSet may be visible,
the Iilter can be used to quickly navigate between those records that match the Iilter.
When you execute the methods FindNext or FindPrior, the ClientDataSet sets a property
named Found. II Found is True, a next record or a prior record was located, and is now the
current record. II Found returns False, the attempt to navigate Iailed.
58 Using Ranges and Filter Together
Ranges make use oI indexes, and are very Iast. Filters are slower, but are more Ilexible.
Fortunately, both ranges and Iilters can be used together. Using ranges with Iilters is
especially helpIul when a you cannot use a range alone, and your Iilter is a complicated one
that would otherwise take a long time to apply. In those situations, it is best to Iirst set a
range, limiting the number oI records that need to be Iiltered to the smallest possible set that
includes all records oI the Iilter. The Iilter can be applied on the resulting range. Since Iewer
records need to be evaluated Ior the Iilter, the combined operations will be Iaster than using a
Filter alone.
59 An Example
The various Iilter-related techniques discussed in this article, with the exception oI the
OnFilterRecord event handler, are demonstrated in the CDSFilter proiect, which can be
downloaded Irom Code Central by clicking here. The main Iorm oI this proiect is shown in
the Iollowing Iigure.
Hide image

In addition to demonstrating the use oI SetRange, ApplyRange, Filter, Filtered, and
FilterOptions, this proiect also provides you with Ieedback concerning Iilter perIormance. The
Iollowing Iigure shows a large dataset that has not yet had its range set.
Hide image

In the Iollowing Iigure, a range has been applied, and only one record appears (out oI 100,000
records). In this case, the range was applied in less than 10 milliseconds.

Hide image

60 ClientDataSet Aggregates and
GroupState
By: Cary Jensen
Abstract: This article describes how to use aggregates to calculate simple statistics, as well as
how to use group state to improve your user interIaces.
One oI the advantages to using a ClientDataSet in your applications is the large number oI
Ieatures it enables. In this article I continue my series on using ClientDataSets with a look at
both aggregates and group state. Aggregates are obiects that can perIorm the automatic
calculation oI basic descriptive statistics based on the data stored in a ClientDataSet. Group
state, by comparison, is inIormation that identiIies the relative position oI a record within a
group oI records, based on an index. Together, these two Ieatures permit you to add easy-to-
maintain capabilities to your applications.
II you are unIamiliar with either aggregates or group state, you might be wondering why I am
covering these two Ieatures together in this article. The answer is simple. Both are associated
with grouping level, which is an index-related Ieature. Because the discussion oI aggregates
necessarily involves grouping level, the coverage oI group state is a natural addition. This
article begins with a look at aggregates. Group state is covered in a later section.
61 Understanding Aggregates
An aggregate is an obiect that can automatically perIorm simple descriptive statistical
calculations across one or more records in a ClientDataSet. For example, imagine that you
have a ClientDataSet that contains a list oI all purchases by your customers. II each record
contains a Iield that identiIies the customer, the number oI items purchased, and the total
value oI the purchase, an aggregate can calculate the sum oI all purchases across all records in
the table. Yet another aggregate can calculate the average number oI items purchased by each
customer. In all, a total oI Iive statistics are supported by aggregates. These are count,
minimum, maximum, sum, and average.
There are two types oI obiects that you can use to create aggregates: TAggregate obiects and
TAggregateField obiects. A TAggregate is a TCollectionItem descendant, and a
TAggregateField is a descendent oI the TField class. While these two aggregate types are
similar in how you conIigure them, they diIIer in their use. SpeciIically, a TAggregateField,
because it is a TField descendent, can be associated with a data-aware control, permitting the
value oI the aggregate to be displayed automatically. By comparison, a TAggregate is an
obiect whose value you must explicitly read at runtime.
One characteristic shared by both types oI aggregates is that they require quite a Iew speciIic
steps to conIigure. II you have never used aggregates in the past, be patient. II your aggregates
do not appear to work at Iirst, you probably missed one or more steps. However, aIter you get
comIortable conIiguring aggregates, you will Iind that they are relatively simple to use.
Because TAggregateField instances are somewhat easier to use, they will be discussed in the
Iollowing section. Using TAggregates is discussed later in this article.
62 Creating Aggregate Fields
Aggregate Iields are virtual, persistent Iields. While they are similar to other virtual, persistent
Iields, such as calculated and lookup Iields, there is one very important diIIerence.
SpeciIically, introducing one or more aggregate Iields does not preclude the automatic,
runtime creation oI dynamic Iields. By comparison, creating at least one other type oI
persistent Iield, such as a data Iield, lookup Iield, or calculated Iield, prevents the
ClientDataSet Irom creating other TFields Ior that ClientDataSet at runtime. As a result, it is
always saIe to create aggregate Iields at design-time, whether or not you intend to instantiate
any other TField instances at design-time.
As mentioned earlier, adding an aggregate Iield requires a number oI speciIic steps in order to
conIigure it correctly. These are:
O Add the aggregate Iield to a ClientDataSet. This can be done at design-time using the
Fields Editor, or at runtime using the TAggregateField's constructor.
O Set the aggregate Iield's Expression property to deIine the calculation that the
aggregate will perIorm
O Set the aggregate Iield's IndexName property to identiIy the index to base grouping
level on
O Set the aggregate Iield's GroupingLevel property to identiIy which records to perIorm
the aggregation across
O Set the aggregate Iield's Active property to True to activate it
O Set the aggregate Iield's Visible property to True
O Set the AggregatesActive property oI the ClientDataSet to which the aggregate is
associated to True
Because there are so many steps here, it is best to discuss creating aggregate Iields using an
example. Use the Iollowing steps in Delphi or Kylix to create a simple proiect to which an
aggregate Iield will be added.
1. Create a new proiect.
2. Add to your main Iorm a DBNavigator, a DBGrid, a ClientDataSet, and a DataSource.
3. Set the Align property oI the DBNavigator to alTop, and the Align property oI the
DBGrid to Client. Next, set the DataSource property oI both the DBNavigator and the
DBGrid to DataSource1. Now set the DataSet property oI the DataSource to
ClientDataSet1.
4. Set the Filename property oI the ClientDataSet to the Orders.cds table (or Orders.xml),
located in Borland's shared data directory. II you installed your soItware using the
deIault installation paths in Windows, you will Iind this Iile in c:Program
FilesCommon FilesBorland SharedData.
Your main Iorm should now look something like the Iollowing:

63 Adding the Aggregate Field
At design-time, you add aggregate Iields using the ClientDataSet's Fields Editor. Use the
Iollowing steps to add an aggregate Iield
1. Right-click the ClientDataSet and select Fields Editor.


2. Right-click the Fields Editor and select New Field (or press Ctrl-N). Delphi displays
the New Field dialog box.


3. Set Name to CustomerTotal and select the Aggregate radio button in the Field type
area. Your New Field dialog box should now look like the Iollowing


4. Click OK to close the New Field dialog box. You will now see the newly added
aggregate Iield in the Fields Editor, as shown here.


64 Defining the Aggregate Expression
The Expression property oI an aggregate deIines the calculation the aggregate will perIorm.
This expression can consist oI constants, Iield values, and aggregate Iunctions. The aggregate
Iunctions are AVG, MIN, MAX, SUM, and COUNT. For example, to deIine a calculation
that will total the AmountPaid Iield in the Orders.cds table, you use the Iollowing expression:
SUM(AmountPaid)
The argument oI the aggregate Iunction can include two or more Iields in an expression, iI
you like. For example, iI you have two Iields in your table, one named Quantity and the other
named Price, you can use the Iollowing expression:
SUM(Quantity Price)
The expression can also include constants. For example, iI the tax rate is 8.25, you can
create an aggregate that calculates total plus tax, using something similar to this:
SUM(Total 1.0825)
You can also set the Expression property to perIorm an operation on two aggregate Iunctions,
as shown here
MIN(SaleDate) - MIN(ShipDate)
as well as perIorm an operation between an expression Iunction and a constant, as in the
Iollowing
MAX(ShipDate) + 30
You cannot, however, include an aggregate Iunction as the expression oI another aggregate
Iunction. For example, the Iollowing is illegal:
SUM(AVG(AmountPaid)) //illegal
Nor can you use a calculation between an aggregate Iunction and a Iield. For example, iI
Quantity is the name oI a Iield, the Iollowing expression is illegal:
SUM(Price) Quantity //illegal
In this particular case, we want to calculate the total oI the AmountPaid Iield. To do this, use
the Iollowing steps:
1. Select the aggregate Iield in the Fields Editor.
2. Using the Obiect Inspector, set the Expression property to SUM(AmountPaid) and its
Currency property to True.
65 Setting Aggregate Index and Grouping Level
An aggregate needs to know across which records it will perIorm the calculation. This is done
using the IndexName and GroupingLevel properties oI the aggregate. Actually, iI you want to
perIorm a calculation across all records in a ClientDataSet, you can leave IndexName blank,
and GroupingLevel set to 0.
II you want the aggregate to perIorm its calculation across groups oI records, you must have a
persistent index whose initial Iields deIine the group. For example, iI you want to calculate
the sum oI the AmountPaid Iield separately Ior each customer, and a customer is identiIied by
a Iield name CustNo, you must set IndexName to the name oI a persistent index whose Iirst
Iield is CustNo. II you want to perIorm the calculation Ior each customer Ior each purchase
date, and you have Iields named CustNo and SaleDate, you must set IndexName to the name
oI a persistent index that has CustNo and SaleDate as its Iirst two Iields.
The persistent index whose name you assign to the IndexName property can have more Iields
than the number oI Iields you want to group on. This is where GroupingLevel comes in. You
set GroupingLevel to the number oI Iields oI the index that you want to treat as a group. For
example, imagine that you set IndexName to an index based on the CustNo, SaleDate, and
PurchaseType Iields. II you set GroupingLevel to 0, the aggregate calculation will be
perIormed across all records in the ClientDataSet. Setting GroupingLevel to 1 perIorms the
calculation Ior each customer (since CustNo is the Iirst Iield in the index). Setting
GroupingLevel to 2 will perIorm the calculation Ior each customer Ior each sale date (since
these are the Iirst two Iields in the index).
It is interesting to note that the TIndexDeI class, the class used to deIine a persistent index,
also has a GroupingLevel property. II you set this property Ior the index, the index will
contain additional inIormation about record grouping. So long as you are setting an
aggregate's GroupingLevel to a value greater than 0, you can improve the perIormance oI the
aggregate by setting the persistent index's GroupingLevel to a value at least as high as the
aggregate's GroupingLevel. Note, however, that a persistent index whose GroupingLevel
property is set to a value greater than 0 takes a little longer to generate and update, since it
must also produce the grouping inIormation. This overhead is minimal, but should be
considered iI the speed oI index generation and maintenance is a concern.
The Iollowing steps walk you through the process oI creating a persistent index on the CustNo
Iield, and then setting the aggregate Iield to use this index with a grouping level oI 1.
1. Select the ClientDataSet in the Obiect Inspector and select its IndexDeIs property.
Click the ellipsis button oI the IndexDeIs property to display the IndexDeIs collection
editor.


2. Click the Add New button in the IndexDeIs collection editor toolbar to add a new
persistent index.
3. Select the newly added index in the IndexDeIs collection editor. Using the Obiect
Inspector, set the Name property oI this IndexDeI to CustIdx, its Fields property to
CustNo, and its GroupingLevel property to 1. Close the IndexDeIs collection editor.
4. With the ClientDataSet still selected, set its IndexName property to CustIdx.
5. Next, using the Fields Editor, once again select the aggregate Iield. Set its IndexName
property to CustIdx, and its GroupingLevel property to 1. The Obiect Inspector should
look something like the Iollowing


66 Making the Aggregate Field Available
The aggregate Iield is almost ready. In order Ior it to work, you must set the aggregate Iield's
Active property and its Visible property to True. In addition, you must set the ClientDataSet's
AggregatesActive property to True. AIter doing this, the aggregate will be automatically
calculated when the ClientDataSet is made active.
With aggregate Iields, there is one more step, which is associating the aggregate with a data-
aware control (iI this is what you want to do). The Iollowing steps demonstrate how to
activate the aggregate, as well as make it visible in the DBGrid.
1. With the aggregate Iield selected in the Obiect Inspector, set its Active property to
True and its Visible property to True.
2. Next, select the ClientDataSet and set its AggregatesActive property to True and its
Active property to True.
3. Now, right-click the DBGrid and select Columns. This causes the Columns collection
editor to be displayed.
4. Click the Add All button on the Columns collection editor toolbar to add persistent
columns Ior each dynamic Iield in the ClientDataSet.


5. Now click the Add New button on the Columns collection editor toolbar to add one
more TColumn.


6. With this new TColumn selected, set its FieldName property to CustomerTotal. In
order to see this calculated Iield easily, drag the new column to a higher position in the
Columns collection editor. For example, move this new column to the third position
within the Columns collection editor.


7. That's it. II you have Iollowed all oI these steps, your newly added aggregate Iield
should be visible in the third column oI your DBGrid, as shown in the Iollowing
Iigure.


A couple oI additional comments about active aggregates are in order here. First, the
ClientDataSet's AggregatesActive property is one that you might Iind yourselI turning on and
oII at runtime. Setting AggregatesActive to False is extremely useIul when you must add,
remove, or change a number oI records at runtime. II you make changes to a ClientDataSet's
data, and these changes aIIect the aggregate calculation, these changes will be much slower iI
AggregatesActive is True, since the aggregate calculations will be updated with each and
every change. AIter making your changes, setting AggregatesActive to True will cause the
aggregates to be recalculated.
Rather than turning all aggregates oII or on, the Active property oI individual aggregates can
be manipulated at runtime. This can be useIul iI you have many aggregates, but only need one
or two to be updated during changes to the ClientDataSet. Subsequently turning other
aggregates back on will immediately trigger their recalculation. At runtime you can read the
ClientDataSet's ActiveAggs TList property to see which aggregates are currently active Ior a
given grouping level.
67 Creating Aggregate Collection Items
Aggregate collection items, like aggregate Iields, perIorm the automatic calculation oI simple
descriptive statistics. However, unlike aggregate Iields, they must be read at runtime in order
to use their value. Aggregate collection items cannot be hooked up to data-aware controls. But
with that exception in mind, nearly all other aspects oI the conIiguration oI aggregate
collection items is the same as Ior aggregate Iields.
The Iollowing steps demonstrate how to add and use aggregate collection items in a proiect.
These steps assume that you have been Iollowing along with the steps provided earlier in this
article to deIine the aggregate Iield.
1. Select the ClientDataSet in the Obiect Inspector and select its Aggregates property.
Click the ellipsis button Ior the Aggregates property to display the Aggregates
collection editor.


2. Click the Add New button on the Aggregates collection editor's toolbar to add two
aggregates to your ClientDataSet.
3. Select the Iirst aggregate in the Aggregates collection editor. Using the Obiect
Inspector, set the aggregate's Expression property to AVG(AmountPaid), its
AggregateName property to CustAvg, its IndexName property to CustIdx, its
GroupingLevel property to 1, its Active property to True, and its Visible property to
True.
4. Select the second aggregate in the Aggregates collection editor. Using the Obiect
Inspector, set its Expression property to MIN(SaleDate), its AggregateName property
to FirstSale, its IndexName property to CustIdx, its GroupingLevel property to 1, its
Active property to True, and its Visible property to True.
5. Add a PopupMenu Irom the Standard page oI the component palette to your proiect.
Using the Menu Designer (double-click the PopupMenu to display this editor), add a
single MenuItem, setting its caption to About this customer.
6. Set the PopupMenu property oI the DBGrid to PopUpMenu1.
7. Finally, add the Iollowing event handler to the Add this customer MenuItem:

procedure TForm1.Aboutthiscustomer1Click(Sender: TJbject);
begin
ShowMessage('The average sale to this customer is ' +
Format('%.2m', StrToFloat(ClientDataSet1.Aggregates0`.Value)`) +
'. The first sale to this customer was on '+
DateToStr(ClientDataSet1.Aggregates1`.Value));
end;
8. II you now run this proiect, your main Iorm should look something like that shown in
the Iollow Iigure.


9. To see the values calculated by the aggregate collection items, right-click a record and
select About this customer. The displayed dialog box should look something like the
Iollowing Iigure:


68 Understanding Group State
Group state reIers to the relative position oI a given record within its group. Using group state
you can learn whether a given record is the Iirst record in its group (given the current index),
the last record in a group, neither the last nor the Iirst record in the group, or the only record
in the group. You determine group state Ior a particular record by calling the ClientDataSet's
GetGroupState method. This method has the Iollowing syntax:
function GetGroupState(Level: Integer): TGroupPosInds;
When you call GetGroupState, you pass an integer indicating grouping level. Passing a value
oI 0 (zero) to GetGroupState will return the current record's relative position within the entire
dataset. Passing a value oI 1 will return the current record's group state with respect to the Iirst
Iield oI the current index, passing a value oI 2 will return the current record's group state with
respect to the Iirst two Iields oI the current index, and so on.
GetGroupState returns a set oI TGroupPosInd Ilags. TGroupPosInd is declared as Iollows:
TGroupPosInd = (gbFirst, gbMiddle, gbLast);
As should be obvious, iI the current record is the Iirst record in the group, GetGroupState will
return a set containing the gbFirst Ilag. II the record is the last record in the group, this set will
contain gbLast. When GetGroupState is called Ior a record somewhere in the middle oI a
group, the gbMiddle Ilag is returned. Finally, iI the current record is the only record in the
group, GetGroupState returns a set containing both the gbFirst and gbLast Ilags.
GetGroupState can be particularly useIul Ior suppressing redundant inIormation when
displaying a ClientDataSet's data in a multi-record view, like that provided by the DBGrid
component. For example, consider the preceding Iigure oI the main Iorm. Notice that the
CustomerTotal aggregate Iield value is displayed Ior each and every record, even though it is
being calculated on a customer-by-customer basis. Not only is the redundant aggregate data
unnecessary, it makes reading the data more diIIicult.
Using GetGroupState you can test whether or not a particular record is the Iirst record Ior the
group, and iI so, display the value Ior CustomerTotal Iield. For records that are not the Iirst
record in their group (based on the CustIdx index), you simply skip printing. Determining
group state and suppressing or displaying the data can be achieved by adding an OnGetText
event handler to the CustomerTotal aggregate Iield. The Iollowing is an example oI how this
event handler might look:
procedure TForm1.ClientDataSet1CustomerTotalGetText(Sender: TField;
var Text: String; DisplayText: Boolean);
begin
if gbFirst in ClientDataSet1.GetGroupState(1) then
Text := Format('%.2m',StrToFloat(Sender.Value)`)
else
Text := '';
end;
II you want to also suppress the CustNo Iield, you must add persistent Iields Ior all oI the
Iields in the ClientDataSet that you want to appear in the grid, and then add the Iollowing
event handler to the CustNo Iield's OnGetText event handler:
procedure TForm1.ClientDataSet1CustNoGetText(Sender: TField;
var Text: String; DisplayText: Boolean);
begin
if gbFirst in ClientDataSet1.GetGroupState(1) then
Text := Sender.Value
else
Text := '';
end;
The Iollowing Iigure shows the main Iorm Irom the running CDSAggs proiect, which
demonstrates the techniques described is the article. Notice that the CustNo and
CustomerTotals Iields are displayed only Ior the Iirst record in each group (when grouped on
CustNo). You can download this sample proiect Irom Code Central by clicking here.


69 Nesting DataSets in ClientDataSets
By: Cary Jensen
Abstract: Like the name suggests, a nested dataset is a dataset within a dataset. By nesting one
dataset inside another, you can reduce your overall storage needs, increase the eIIiciency oI
network communications, and simpliIy data operations.
To put it simply, nested datasets are one-to-many relationships embodied in a single
ClientDataSet memory store. When the ClientDataSet is associated with a local Iile, saved as
either a CDS binary Iile or XML, this related data is stored in a single CDS or XML Iile.
When associated with a ClientDataSet that obtains its data through a DataSetProvider, the
data packet is assembled Irom data retrieved through two or more related datasets.
Nested datasets provide you with a number oI important advantages. For one, they typically
reduce the amount oI memory required to hold your data, whether it is the in-memory data
store itselI, or the data Iile saved to disk through calls to the ClientDataSet's SaveToFile
method.
Second, when used with DataSnap, Borland's multitier development Iramework, nested
datasets reduce network traIIic. SpeciIically, nested datasets permit data Irom two or more
related datasets to be packaged in a single data packet, which can then be transmitted between
the DataSnap server and client more eIIiciently. For the same reason, nested datasets reduce
the overall size oI data stored by ClientDataSets in local Iiles.
While these are important characteristics, a third characteristic oI nested datasets is the one
that is commonly considered their most valuable. Nested datasets permit the acquisition oI
data Irom, and resolution oI changes to, two or more underlying tables using a single
ClientDataSet.
While developers who use nested datasets value this capability, it is more diIIicult to
appreciate iI you have never worked with them beIore. Consider this: using nested datasets
you can access data in two or more tables by calling the Open method oI a single
ClientDataSet. Furthermore, when you are through making changes, all updates are saved or
applied with iust one call to SaveToFile (Ior local Iiles) or ApplyUpdates (Ior data obtained
Irom a database server). In addition, iI you are saving changes by calling ApplyUpdates, the
changes to the two or more involved tables can be applied in an all-or-none Iashion in a single
transaction.
But there is more. When nested datasets are involved, data being applied as a result oI a call
to ApplyUpdates is resolved to the underlying datasets in the appropriate order, respecting the
needs oI master-detail relationships. For example, a newly created master table record is
inserted beIore any related detail table records are inserted. Deleted records, by comparison,
are removed Irom detail tables prior to the deletion oI any master table records.
A single ClientDataSet can have up to 15 nested datasets. Each oI these nested datasets can, in
turn, contain up to 15 nested datasets, which in turn can have nested datasets as well. In other
words, nested datasets permit you to represent a complex hierarchical relationship using one
ClientDataSet. In practice, however, nested datasets greater than two levels deep are
uncommon.
70 Creating Nested DataSets
There are two distinct ways to create nested datasets, depending on how the structure oI the
ClientDataSet is obtained. II you are creating your ClientDataSet's structure at runtime by
invoking CreateDataSet, nested datasets are deIined using TFields oI the type TDataSetField.
These TDataSetField instances can be instantiated either at design-time or at runtime,
depending on the needs oI your application.
The second way to create nested datasets is to load a data into a ClientDataSet Irom a
DataSetProvider. II the DataSetProvider is pointing to the master dataset oI a master-detail
relationship, and that relationship is deIined using properties, the data packet returned to the
ClientDataSet contains one nested dataset Ior each detail table linked to the master table. Each
oI these two approaches are covered in the Iollowing sections.
71 Defining TDataSetFields
When a ClientDataSet's structure needs to be created at runtime by calling CreateDataSet, you
deIine its structure using TFields, where each oI the nested datasets are represented by
TDataSetField instances. II you are deIining your structure at design time, you add the
TFields that deIine the table's structure using the Fields Editor. II you need to deIine your
structure at runtime, you call the constructor Ior each TField you want added to your table,
setting the necessary properties to associate the newly created Iield with the ClientDataSet. In
this second case, each nested dataset Iield is created by a call to the TDataSetField
constructor.
DeIining a ClientDataSet's structure using TFields was discussed at length in a previous
article in this series titled "DeIining a ClientDataSet's Structure Using TFields." While that
article, which you can view by clicking here, mentioned the general steps used to deIine
nested datasets using TFields, this section goes Iurther, providing you with a step-by-step
demonstration oI the technique.
First, let's review the steps required to deIine the structure oI a ClientDataSet to include
nested datasets.
1. Using the Fields Editor, create one Iield oI data type Data Ior each regular Iield in the
dataset (such as Customer Name, Title, Address1, Address2, and so Iorth).
2. For each nested dataset, add a new Iield, using the same technique that you use Ior the
other data Iields, but set its Data Type to DataSet.
3. For each DataSet Iield that you add to your Iirst ClientDataSet, add an additional
ClientDataSet. Associate each oI these secondary ClientDataSets with one oI the
primary ClientDataSet's DataSet Iields using the secondary ClientDataSet's
DataSetField property.
4. DeIine the Iields oI each nested dataset by adding individual TFields to each
secondary ClientDataSets you added in step 3. Just as you did with the initial
ClientDataSet, you add these Iields using the Fields Editor.
The Iollowing steps walk you through creating a proiect that includes a ClientDataSet whose
structure includes nested datasets.
1. Begin by creating a new proiect.
2. On the main Iorm, add two Panels Irom the Standard page oI the Component Palette.
In each panel place a DBNavigator and a DBGrid. These components are located on
the Data Controls page. Align both DBNavigators to alTop, and both DBGrids to
alClient. Set the Align property oI the Iirst Panel to alTop, and the second to alClient.
(II you want to be really Iancy, aIter you align the Iirst panel to alTop, but beIore
aligning the second panel, place a TSplitter Irom the Additional page oI the
Component Palette on your Iorm, and align it to top as well. The splitter permits the
user to customize the percentage oI the Iorm occupied by each panel at runtime.) Your
Iorm should look something like the Iollowing.


3. Add to this Iorm two DataSources and two ClientDataSets Irom the Data Access page.
Set the DataSource property oI the DBNavigator and DBGrid in the top panel to
DataSource1, and the DataSource property oI the DBNavigator and DBGrid appearing
in the bottom panel to DataSource2. In addition, set the DataSet property oI
DataSource2 to ClientDataSet1, and the DataSet property oI DataSource2 to
ClientDataSet2.
4. Right-click ClientDataSet1 and select Fields Editor. Add Iive Iields to the Field
Editor, one at a time, by either pressing Ctrl-N or right-clicking the Fields Editor and
selecting Add Iield. Use the Iollowing table to set the Name and Type Iields on the
New Field dialog that each oI these TFields require. Make sure that the Field Type
radio group is set to Data. Accept the deIault values Ior all oI the remaining Iields
Name Type
Invoice No Integer
Invoice Date Date
Customer No Integer
Employee No Integer
Details DataSet
5.
6. Close the Fields Editor Ior ClientDataSet1.
7. Select ClientDataSet2 and set its DataSetField property to ClientDataSet1Details.
8. Right-click ClientDataSet2 and select Fields Editor. Add three Iields to the Field
Editor, one at a time, by either pressing Ctrl-N or right-clicking the Fields Editor and
selecting Add Iield. Use the Iollowing table to set the Name and Type Iield properties
on the New Field dialog that each oI these TFields require. Make sure that the Field
Type radio group is set to Data. Accept the deIault values Ior all oI the remaining
Iields. Close the Fields Editor Ior ClientDataSet2.
Name Type
Part No Integer
Quantity Integer
Price Currency
9.
10.With ClientDataSet2 still selected, set its DataSetField property to
ClientDataSet1Details.
11.Add the Iollowing event handler to the OnCreate property oI the main Iorm.
procedure TForm1.FormCreate(Sender: TJbject);
begin
ClientDataSet1.FileName := ExtractFilePath(Application.ExeName) +
'data.xml';
if FileExists(ClientDataSet1.FileName) then
ClientDataSet1.Jpen
else
ClientDataSet1.CreateDataSet;
ClientDataSet1.LogChanges := False;
end;
12.Run the Iorm. II you have not yet added any data, this Iorm should look something
like the Iollowing.


13.Now, add some data to both tables. Once some data is added, it might look something
like the Iollowing.


14.Notice that iI you click the dataset Iield named Details associated with ClientDataSet1
twice, you will get a small grid that you can use to enter, edit, and view the nested
dataset, as shown in the Iollowing Iigure.


II you inspected the OnCreate event handler Ior the main Iorm oI this proiect, you will have
noticed that any data that you enter is stored in a Iile named data.xml. The Iollowing image
shows how the data in this XML Iile looks like, once it has been Iormatted by the
FormatXMLData Iunction, which is Iound in the XMLDoc unit (this unit only ships with
Enterprise and Architect versions oI Delphi 6 and later).

As you can see, the FIELDS~ element oI this XML Iile contains one empty FIELD~
element Ior each Iield in the primary dataset. In addition, the FIELD~ with the attrname
attribute value oI Details itselI contains a FIELDS~ element, which in turn contains the
empty FIELD~ elements that describe this nested dataset's structure.
Likewise, the ROWDATA~ element, which contains the actual data, contains one empty
ROW~ element Ior each Iield in the primary dataset. Here we Iind the Details~ element,
which holds the data Ior the Detail nested dataset.
The source code Ior this proiect can be downloaded Irom Code Central by clicking here.
72 Created TDataSetFields at Runtime
One oI the Iacts that you learn pretty early in your Delphi development is that iI you can
perIorm a task at design time you probably can perIorm that same task at runtime. This is
certainly true with respect to nested datasets. In short, you create a nested dataset by
perIorming the Iollowing tasks in code.
1. Call the constructor oI the TDataSetField class, assigning the necessary values to the
properties oI the resulting obiect. As is the case with all TField that you create
dynamically, one oI the more important properties is the DataSet property, a property
that identiIies which TDataSet instance this Iield is to be associated with.
2. Assign the resulting TDataSetField to the DataSetField oI the ClientDataSet that you
will use to display and edit the data stored in the nested dataset.
In the article that I published previously concerning deIining the structure oI a ClientDataSet
using TFields, I described a complicated proiect named VideoLibrary. This proiect includes
two examples oI a nested dataset's runtime construction. You can download this proiect Irom
Code Central by clicking on this link.
All oI the essential code Ior the process oI creating a nested dataset and associating it with a
ClientDataSet can be Iound in the OnCreate event handler oI the data module Ior this proiect.
The Iollowing segment, taken Irom this event handler, demonstrates the construction oI a
TDataSetField instance, including the setting oI its properties.
//Note: For TDataSetFields, FieldKind is fkDataSet by default
with TDataSetField.Create(Self) do
begin
Name := 'VideosCDSTalentByVideo';
FieldName := 'TalentByVideo';
DataSet := VideosCDS;
end;
Associating this Iield with a ClientDataSet is even simpler. This process is demonstrated in
the DataSetField property assignment appearing at the top oI the Iollowing code segment. The
remaining lines demonstrate the creation oI the actual Iields oI the nested dataset.
//TalentByVideosCDS
TalentByVideosCDS.DataSetField :=
TDataSetField(FindComponent('VideosCDSTalentByVideo'));
with TStringField.Create(Self) do
begin
Name := 'TalentByVideosID';
FieldKind := fkData;
FieldName := 'TalentID';
Size := 42;
DataSet := TalentByVideosCDS;
Required := True;
end; //ID
with TStringField.Create(Self) do
begin
Name := 'TalentByVideosName';
FieldKind := fklookup;
FieldName := 'Name';
Size := 50;
DataSet := TalentByVideosCDS;
KeyFields := 'TalentID';
LookupDataSet := TalentCDS;
LookupKeyFields := 'ID';
LookupResultField := 'Name';
end; //ID
with TMemoField.Create(Self) do
begin
Name := 'TalentByVideosComment';
FieldKind := fkData;
FieldName := 'Comment';
DataSet := TalentByVideosCDS;
end; //ID
73 Creating Nested DataSets Using Dynamically Linked
DataSets
Nested datasets are automatically created when a dataset provider's DataSet property points to
the master dataset oI a master-detail relationship. A master-detail relationship, as the term is
being used here, exists when one dataset, the detail dataset, is linked to another, the master
dataset, through properties oI the detail dataset.
For example, a master-detail relationship exists when a BDE Table component is linked to
another via the MasterSource and MasterFields properties. Likewise, a master-detail
relationship exists when a SQLQuery component is linked to another dataset using the
DataSource property in coniunction with a parameterized query (where one or more
parameter names in the detail table query match Iield names in the master dataset).
When a DataSetProvider points to the master table one oI these mater-detail relationships, the
data packet that it provides to a ClientDataSet includes one DataSetField Ior each detail
dataset.
Creating nested datasets through dynamically linked datasets is not limited to BDE and
dbExpress datasets. Nested datasets can also be created using IBExpress, ADO, and MyBase
datasets, as well as many third-party TDataSet descendants. For example, the ADSTable and
ADSQuery components provided by Extended System to connect to the Advantage Database
Server can by linked dynamically, which will then produce nested datasets when their data is
provided through a DataSetProvider.
The Iollowing steps demonstrate how to create nested datasets using dynamically linked
datasets.
1. Create a new proiect.
2. Design the main Iorm to look similar to the one created earlier in this article.
SpeciIically, add two Panels Irom the Standard page oI the Component Palette. In each
panel place a DBNavigator and a DBGrid. These components are located on the Data
Controls page. Align both DBNavigators to alTop, and both DBGrids to alClient. Set
the Align property oI the Iirst Panel to alTop, and the second to alClient. (Again, iI
you want a better interIace, aIter you align the Iirst panel to alTop, but beIore aligning
the second panel, place a TSplitter Irom the Additional page oI the Component Palette
on your Iorm, and align it to top as well. The splitter permits the user to customize the
percentage oI the Iorm occupied by each panel at runtime.) Also add two DataSources
Irom the Data Access page oI the Component Palette onto this Iorm. Set the
DataSource property oI the top DBNavigator and DBGrid to DataSource1, and the
DataSource property oI the bottom DBNavigator and DBGrid to DataSource 2. Your
Iorm should look something like the Iollowing.


3. Select File ' New ' Data Module to add a data module to your proiect. From the BDE
page oI the Component Palette add two Tables, and Irom the Data Access page add
one DataSetProvider, one DataSource, and two ClientDataSets. Your data module
should look something like the Iollowing.


4. Set the DatabaseName property oI Table1 and Table2 to DBDEMOS. Set the
TableName property oI Table1 to customer.db, and the TableName property oI Table2
to orders.db. Also, set the IndexName property oI Table2 to CustNo. Next, set the
DataSet property oI DataSource1 to Table1.
5. Now it is time to create the dynamic link. Select the MasterFields property oI Table2
and click the ellipsis to display the Field Link Designer. Select CustNo in both the
Detail Fields and Master Fields lists, and then click the Add button. AIter you click the
Add button, the string CustNo -~ CustNo will appear in the Joined Fields list. Click
OK to close the Field Link Designer.


6. Set the DataSet property oI the DataSetProvider to Table1, and set the ProviderName
property oI ClientDataSet1 to DataSetProvider1.
7. At this point, iI you make ClientDataSet1 active it will contain one TField Ior each
Iield in Table1, as well as an additional DataSetField Ior the associated records oI
Table2. These detail records can be associated with ClientDataSet2 at runtime by
assigning the DataSetField property oI ClientDataSet2 to this nested dataset. To do
this at design time, however, you must create persistent Iields Ior ClientDataSet1.
8. To create persistent Iield Ior ClientDataSet1, right-click ClientDataSet1 and select
Fields Editor. Right-click in the Fields Editor and select Add all Iields. AIter you add
all Iields to the Fields Editor, you will see the DataSetField at the end oI the list, as
shown in the Iollowing Iigure.


9. Select ClientDataSet2 and set its DataSetField property to ClientDataSet1Table2.
10.Return to the main Iorm. Add a main menu to this Iorm. Right-click the main menu
and select Menu Designer. Set the Caption property oI the top-level menu item to File.
Add two menu items under File, with the Iirst one having the caption Open, and the
second having the Caption Apply Updates. The menu designer should look something
like the Iollowing.


11.Close the menu designer.
12.Select File ' Use unit, and add the data module's unit to the main Iorm's uses clause.
13.From the main Iorm, select File ' Open to add an OnClick event handler to this menu
item. Edit the event handler to look like the Iollowing:
procedure TForm1.Jpen1Click(Sender: TJbject);
begin
DataModule2.ClientDataSet1.Jpen;
end;
14.Now select File ' Apply Updates Irom the main Iorm's menu to create an OnClick
event handler Ior it. Edit this event handler to look like the Iollowing.
procedure TForm1.ApplyUpdates1Click(Sender: TJbject);
begin
DataModule2.ClientDataSet1.ApplyUpdates(0);
end;
15.Finally, add an OnClose event handler to the main Iorm. Edit this event handler to
look like this:
procedure TForm1.FormClose(Sender: TJbject; var Action:
TCloseAction);
begin
with DataModule2 do
begin
if ClientDataSet1.State in dsEdit, dsInsert` then
ClientDataSet1.Post;
if ClientDataSet1.ChangeCount 0 then
if MessageDlg('Save changes.', mtConfirmation,
mbJKCancel, 0) = mrJK then
ClientDataSet1.ApplyUpdates(0);
end;
end;
16.Set the DataSet property oI DataSource1 to DataModule2.ClientDataSet1, and the
DataSet property oI DataSource to DataModule2.ClientDataSet2.
17.Finally, since you cannot call ReIresh on a ClientDataSet that does not have a
DataSetProvider (and ClientDataSet2 does not have a DataSetProvider, remove the
nbReIresh Ilag Irom the VisibleButtons property oI DBNavigator2.
18.Save your proiect, and then run it. AIter you select File ' Open Irom the main Iorm's
main menu, your Iorm should look something like the Iollowing.


Just as you do when using TFields to create nested datasets, the master ClientDataSet is used
to control both the master and all detail tables. SpeciIically, when you open ClientDataSet1,
both ClientDataSet1 and ClientDataSet2 are populated with data. Similarly, you call
ApplyUpdates only on ClientDataSet1. Doing so saves all changes, even those made through
ClientDataSet2.
You can even use the State and ChangeCount properties oI ClientDataSet1 to determine the
condition oI all datasets. You will notice this iI you run the proiect, select File ' Open, and
then make a single change to one oI the records in the detail table. BeIore posting this change,
try to close the application. This causes the OnClose event handler to trigger, where the code
will determine that an unposted record needs to be posted, and then ask you whether you want
your changes saved or not. In other words, the State property oI ClientDataSet1 is in dsEdit
state when a unposted change appears in a nested dataset, and calling ApplyUpdates applies
all changes, even those posted to the nested datasets.
You can download the source code Ior this proiect Irom Code Central by clicking here.
74 Nested DataSets and Referential Integrity
ReIerential integrity reIers to the relationships oI master-detail records. You control
reIerential integrity oI nested datasets using the Ilags oI the Options property oI the
DataSetProvider. The Iollowing Iigure shows the expanded Options property oI a
DataSetProvider displayed in the Obiect Inspector.

Add poCascadeDeletes to cause a master record deletion to delete the corresponding detail
records. When poCascadeDeletes is not set, master records cannot be deleted iI there are
associated detail records.
Add poCascadeUpdates to Options to propagate changes to the master table key Iields to
associated detail records. II poCascadeUpdates is not set, you cannot change master table
Iields involved in the master-detail link.
Note also that the Options property also contains a poFetchDetailsOnDemand property. II this
Ilag is set, detail records are not automatically loaded when you open the master
ClientDataSet. In this case, you must explicitly call the master ClientDataSet's FetchDetails
method in order to load the nested datasets.

75 Cloning ClientDatSet Cursors
By: Cary Jensen
Abstract: When you clone a ClientDataSet's cursor, you create not only an additional pointer
to a shared memory store, but also an independent view oI the data. This article shows you
how to use this important capability.
II you have been Iollowing this series, you are no doubt aware that I am a huge Ian oI
ClientDataSets. Indeed, I Ieel like I haven't stopped smiling since Borland added them to the
ProIessional edition oI their RAD (rapid application development) products, including Delphi,
Kylix, and C Builder. The capabilities that ClientDataSets can add to an application are
many. But oI all the Ieatures made available in ClientDataSets, I like cloned cursors the most.
This is the tenth article in this series, and you might be wondering why I've waited until now
to discuss cloning cursors. The answer is rather simple. The Iull power oI cloned cursors is
not obvious until you know how to navigate, edit, Iilter, search, and sort a ClientDataSet's
data. In some respects, the preceding articles in this series have been leading up to this one.
Normally, the data that you load into a client dataset is retrieved Irom another dataset or Irom
a Iile. But what do you do when you need two diIIerent views oI essentially the same data?
One alternative is to load a second copy oI the data into a second ClientDataSet. This
approach, however, results in an unnecessary increase in network traIIic (or disk access) and
places redundant data in memory. In some cases, a better option is to clone the cursor oI an
already populated ClientDataSet. When you clone a cursor, you create a second, independent
pointer to an existing ClientDataSet's memory store, including Delta (the change log).
Importantly, the cloned ClientDataSet has an independent current record, Iilter, index,
provider, and range.
It is diIIicult to appreciate the power oI cloned cursors without actually using them, but some
examples can help. Previously you learned that the data held by a ClientDataSet is stored
entirely in memory. Imagine that you have loaded 10,000 records into a ClientDataSet, and
that you want to compare two separate records in the ClientDataSet programmatically. One
approach is locate the Iirst record and save some oI its data into local variables. You can then
locate the second record and compare the saved data to that in the second record. Yet another
approach is to load a second copy oI the data in memory. You can then locate the Iirst record
in one ClientDataSet, the second record in the other ClientDataSet, and then directly compare
the two record.
A third approach, and one that has advantages over the Iirst two, is to utilize the one copy oI
data in memory, and clone a second cursor onto this memory store. The cloned cursor acts is
iI it were a second copy oI the data in memory, in that you now have two cursors (the original
and the clone), and each can point to a diIIerent record. Importantly, only one copy oI the data
is stored in memory, and the cloned cursor provides a second, independent pointer into it. You
can then point the original cursor to one record, the cloned cursor to the other, and directly
compare the two records.
Here's another example. Imagine that you have a list oI customer invoices stored in memory
using a ClientDataSet. Suppose Iurther that you need to display this data to the end user using
two diIIerent sort orders, simultaneously. For example, imagine that you want to use one
DBGrid to display this data sorted by customer account number, and another DBGrid to
display this data by invoice date. While your Iirst inclination might be to load the data twice,
using two ClientDataSets, a cloned cursor perIorms the task much more eIIiciently. AIter
loading the data into a single ClientDataSet, a second ClientDataSet is used to clone the Iirst.
The Iirst ClientDataSet can be sorted by customer account number, and the second can be
sorted by invoice date. Even though the data appears in memory only once, each oI the
ClientDataSets contain a diIIerent view.
76 How to Clone a ClientDataSet
You clone a ClientDataSet's cursor by invoking the CloneCursor method. This method has the
Iollowing syntax:
procedure CloneCursor(Source :TCustomClientDataSet;
Reset: Boolean; KeepSettings: Boolean = False);
When you invoke CloneCursor, the Iirst argument is an already active ClientDataSet that
points to the memory store you want to work with. The second parameter, Reset, is used to
either keep or discard the original ClientDataSet's view. II you pass a value oI False, the
values oI the IndexName (or IndexFieldNames), Filter, Filtered, MasterSource, MasterFields,
OnFilterRecord, and ProviderName properties are set to match that oI the source client
dataset. Passing True in the second parameter resets these properties to their deIault
values. (A special case with respect to Iilters is discussed in the Iollowing section.)
For example, iI you invoke CloneCursor, passing a value oI True in the second parameter, the
cloned ClientDataSet's IndexFieldNames property will contain an empty string, regardless oI
the value oI the IndexFieldNames property oI the original ClientDataSet. To put this another
way, the cloned cursor may or may not start out with similar properties to the ClientDataSet it
was cloned Irom, depending on the second parameter.
You include the third, optional parameter, passing a value oI True, typically in coniunction
with a Reset value oI False. In this situation, the properties oI the cloned cursor match that oI
the original dataset, but may not actually be valid, depending on the situation. In most cases, a
call to CloneCursor only includes the Iirst two parameters.
Although the cloned cursor may not share many view-related properties with the
ClientDataSet it was cloned Irom, it may present a view that nearly matches the original. For
example, the current record oI a cloned cursor is typically the same record that was current in
the original. Similarly, iI a ClientDataSet uses an index to display records in a particular
order, the clone's natural order will match the indexed view oI the original, even though the
IndexName or IndexFieldNames properties oI the clone may be empty.
This view duplication also applies to ranges. SpeciIically, iI you clone a ClientDataSet that
has a range set, the clone will employ that range, regardless oI the values you pass in the
second and third parameters. However, you can easily change that range, either by setting a
new range, or dropping the range by calling the ClientDataSet's CancelRange method. These
ranges are independent, however, in that each ClientDataSet pointing to a common memory
store can have a diIIerent range, or one can have a range and the other can employ no range.
In general, I think it is a good idea to make Iew assumptions about the view oI a cloned
cursor. In other words, your saIest bet is to clone a cursor passing a value oI True in the Reset
Iormal parameter. II you pass a value oI False in the Reset parameter, I suggest that you insert
a comment or Todo item into your code, and document how you expect the view oI the clone
to appear. Doing so will help you Iix problems that could potentially be introduced iI Iuture
implementations oI the CloneCursor method change the view oI the clone.
A single ClientDataSet can be cloned any number oI times, creating many diIIerent views Ior
the same data store. Furthermore, you can clone a clone to create yet another pointer to the
original data store. You can even clone the clone oI a clone. It really does not matter. There is
a single data store, and a ClientDataSet associated with it, whether it was created by cloning
or not, points to that data store.
Here is another way to think oI this. Once a ClientDataSet is cloned, the clone and the original
have equal status, as Iar as the memory store is concerned. For example, you can load one
ClientDataSet with data, and then clone a second ClientDataSet Irom it. You can then close
the original ClientDataSet, either by calling its Close method or setting its Active property to
False. Importantly, the clone will remain open. To put this another way, so long as one oI the
ClientDataSets remain open, whether it was the original ClientDataSet used to load the data or
a clone, the data and change log remain in memory.
77 Cloning a Filtered ClientDataSet: A Special Case
Similar to cloning a ClientDataSet that uses a range, there is an issue with cloning
ClientDataSets that are Iiltered. SpeciIically, iI you clone a ClientDataSet that is currently
being Iiltered (its Filter property is set to a Iilter string, and Filtered is True), and you pass a
value oI False (no reset) in the second parameter oI the CloneCursor invocation, the cloned
ClientDataSet will employ the Iilter also. However, unlike when a cloned cursor has a range,
which can be canceled, the cloned cursor will necessarily be Iiltered. In other words, in this
situation, the clone can never cancel the Iilter it gets Irom the ClientDataSet it was cloned
Irom. (Actually, you can apply a new Iilter to the Iiltered view, but that does not cancel the
original Iilter. It merely adds an additional Iilter on top oI the original Iilter.)
This eIIect does not occur when Filter is set to a Iilter expression, and Filtered is set to False.
SpeciIically, iI Filter is set to a Iilter expression, and Filtered is False, cloning the cursor with
a Reset value oI False will cause the cloned view to have a Iilter expression, but it will not be
Iiltered. Furthermore, you can set an alternative Filter expression, and set or drop the Iilter, in
which case the clone may include more records than the Iiltered ClientDataSet Irom which it
was cloned.
The discrepancy between the way a non-reset clone works with respect to the Filtered
property is something that can potentially cause maior problems in your application.
Consequently, I suggest that you pass a value oI True in the Reset Iormal parameter oI
CloneCursor iI you are cloning an actively Iiltered ClientDataSet. You can then set the Iilter
on the clone, in which case the Iilters will be completely independent.
78 The Shared Data Store
The Iact that all clones share a common data store has some important implications.
SpeciIically, any changes made to the common data store directly aIIects all ClientDataSets
that use it. For example, iI you delete a record using a cloned cursor, the record instantly
appears to be deleted Irom all ClientDataSets pointing to that same store and an associated
record appears in the change log (assuming that the public property LogChanges is set to
True, its deIault value). Similarly, calling ApplyUpdates Irom any oI the ClientDataSets
attempts to apply the changes in the change log. In addition, setting one oI the ClientDataSet's
Readonly property to True prevents any changes to the data Irom any oI its associated
ClientDataSets.
Note that the documentation states that passing a value oI True in the second parameter oI
CloneCursor will cause the clone to employ the deIault ReadOnly property, rather than the
ReadOnly value oI the ClientDataSet that is being cloned. Nonetheless, setting ReadOnly on a
ClientDataSet to True makes the data store readonly, which aIIects all ClientDataSets
pointing to that data store.
Likewise, iI you have at least one change in the change log, calling RevertRecord, or even
CancelChanges, aIIects the single change log (represented by the Delta property). For
example, iI you deleted a record Irom a cloned ClientDataSet, it will appear to be instantly
deleted Irom the views oI all the ClientDataSets associated with the single data store. II you
then call UndoLastChange on one oI the ClientDataSets, that deleted record will be removed
Irom the change log, and will instantly re-appear in all oI the associated ClientDataSets.
79 Cloning Examples
Cloning a cursor is easy, but until you see it in action it is hard to really appreciate the power
that cloned cursors provide. The Iollowing examples are designed to give you a Ieel Ior how
you might use cloned cursors in your applications.
80 Creating Multiple Views of a Data Store
This Iirst example, CloneAndFilter, shows how you can display many diIIerent views oI a
common data store using cloned cursors. You can download this proiect Irom Code Central
by clicking here.. The Iollowing is the main Iorm oI this application, which permits you to
select the Iile to load into a ClientDataSet and then clone.

Once you click the button labeled Load and Display, an instance oI the TViewForm class is
created, and the selected Iile is loaded into a ClientDataSet associated with this class. The
Iollowing is how the instance oI the TViewForm looks when the customer.cds Iile is loaded
into the ClientDataSet.

II you have been Iollowing this series, you will recognize this Iorm as being similar to the one
I used to demonstrate Iilters and ranges. I used this Iorm here, oI course, so that you can apply
Iilters, set ranges, change indexes, and edit and navigate records, and then see what inIluence
these setting have on a clone oI this view.
As you can see Irom the preceding Iorm, there are two buttons associated with cloning a
cursor. The Iirst, labeled CloneCursor: Reset, will call CloneCursor with a value oI True
passed in the second parameter. The Iollowing is the OnClick event handler associated with
this button.
procedure TViewForm.CloneResetBtnClick(Sender: TJbject);
var
ViewForm: TViewForm;
begin
ViewForm := TViewForm.Create(Application);
ViewForm.ClientDataSet1.CloneCursor(Self.ClientDataSet1, True);
ViewForm.CancelRangeBtn.Enabled := Self.CancelRangeBtn.Enabled;
ViewForm.Caption := 'Cloned ClientDataSet';
ViewForm.Show;
end;
When you click CloneCursor: Reset, an instance oI the TViewForm class is created, and the
ClientDataSet that appears on this instance is cloned Irom the one that appears on SelI. The
Iollowing shows an example oI what this Iorm might look like aIter a cursor is cloned.

Besides this Iorm's caption, it appears to be a separate view oI the originally loaded Iile. You
can now set Iilters, ranges, and change the current record. Importantly, both oI the Iiles use
the single copy oI the data originally loaded into memory.
The second button associated with cloning is labeled CloneCursor: No Reset. The Iollowing
code is associated with the OnClick event handler oI this button.
procedure TViewForm.CloneNoResetBtnClick(Sender: TJbject);
begin
ViewForm := TViewForm.Create(Application);
ViewForm.ClientDataSet1.CloneCursor(Self.ClientDataSet1, False);
ViewForm.CancelRangeBtn.Enabled := Self.CancelRangeBtn.Enabled;
ViewForm.Caption := 'Cloned ClientDataSet';
ViewForm.Show;
end;
Obviously, the only diIIerence between this method and the preceding one is the value passed
in the second parameter oI the call to CloneCursor.
UnIortunately, the static nature oI the screenshots in this article do little to demonstrate what
is going on here. I urge you to either create your own demonstration proiect, or to download
this proiect, and play with cloned cursors Ior a while. For example, create three clones oI the
same cursor, and then close the original TViewForm instance. Then, with several oI the
TViewForm instances displayed, post a change to one oI the records. You will notice that all
instances oI the displayed Iorm will instantly display the updated data. Next, undo that change
by clicking the button labeled Undo Last Change (try doing this on a Iorm other than the one
you posted the change on).
Here's another thing to try. Clone several cursors and then delete a record Irom one oI the
visible Iorms. This record will immediately disappear Irom all Iorms. Then, click the button
labeled Empty Change Log (again, it does not matter on which Iorm you click this button).
The deleted record will instantly reappear on all visible Iorms.
81 Self-Referencing Master-Details
Most database developers have some experience create master-detail views oI data. This type
oI view, sometimes also called a one-to-many view or a parent-child view, involves
displaying the zero or more records Irom a detail table that are associated with the currently
selected record in a master table. You can easily create this kind oI view using the
MasterSource and MasterFields properties oI a ClientDataSet, given that you have two tables
with the appropriate relationship (such as Borland's sample customer.cds and orders.cds Iiles).
While most master-detail views involve two tables, what do you do iI you want to create a
similar eIIect using a single table. In other words, what iI you want to select a record Irom a
table and display other related records oI that same table in a separate view.
Sound weird? Well, not really. Consider Borland's sample Iile items.cds. Each record in this
Iile contains an order number, a part number, the quantity ordered, and so Iorth. Imagine that
when you select a particular part associated with an given order you want to also see, in a
separate view, all orders Irom this table in which that same part was ordered. In this example,
all oI the data resides in a single table (items.cds).
Fortunately, cloned cursors give you a powerIul way oI displaying master-detail relationships
within a single table. This technique is demonstrated in the MasterDetailClone proiect, which
can be downloaded Iorm Code Central by clicking here..
The main Iorm oI this running proiect can be seen in the Iollowing Iigure. Notice that when a
record associated with part number 12306 is selected (in this case, Ior order number 1009),
the detail view, which appears in the lower grid on this Iorm, displays all orders that include
part number 12306 (including order number 1009).

This Iorm also contains a checkbox, which permits you to either include or exclude the
current order number Irom the detail list. When this checkbox is not checked, the current
order in the master table does not appear in the detail, as shown in the Iollowing Iigure.

In this proiect, the detail view is created using a cloned cursor oI the master table
(ClientDataSet1) Irom the OnCreate event handler oI the main Iorm. The Iollowing is the
code associated with this event handler.
procedure TForm1.FormCreate(Sender: TJbject);
begin
if not FileExists(ClientDataSet1.FileName) then
begin
ShowMessage('Cannot find ' + ClientDataSet1.FileName +
'. Please assign the items.cds table ' +
'to the FileName property of ClientDataSet1 ' +
'before attempting to run the application again');
Halt;
end;
ClientDataSet1.Jpen;
//Assign the OnDataChange event handler _after_
//opening the ClientDataSet
DataSource1.JnDataChange := DataSource1DataChange;
//Clone the detail cursor.
ClientDataSet2.CloneCursor(ClientDataSet1, True);
//Create and assign an index to the cloned cursor
ClientDataSet2.AddIndex('PartIndex','PartNo',`);
ClientDataSet2.IndexName := 'PartIndex';
ClientDataSet2.Filtered := True;
//Invoke the OnDataChange event handler to
//create the detail view
DataSource1DataChange(Self, PartFld);
end;
Once this event handler conIirms that the Iile name associated with ClientDataSet1 is valid,
there are Iour steps that are taken that contribute to the master-detail view. The Iirst step is
that ClientDataSet1 is opened, which must occur prior to cloning the cursor.
The second step is that an OnDataChange event handler, which creates the detail view, is
assigned to DataSource1. This is the DataSource that points to ClientDataSet1. Once this
assignment is made, the detail table view is updated each time OnDataChange is invoked
(which occurs each time a change is made to a Iield in ClientDataSet1, as well as each time
ClientDataSet1 arrives at a new current record).
The third operation perIormed by this event handler is the cloning oI the detail table cursor,
assigning an appropriate index, and setting the cloned cursor's Filtered property to True. In
this proiect, the order oI steps two and three are interchangeable.
The Iorth step is to invoke the OnDataChange event handler oI DataSoure1. This invocation
causes the cloned cursor to display its initial detail view.
As must be obvious Irom this discussion, the OnDataChange event handler actually creates
the detail view. The Iollowing is the code associated with this event handler.
procedure TForm1.DataSource1DataChange(Sender: TJbject; Field: TField);
begin
PartFld := ClientDataSet1.FieldByName('PartNo');
ClientDataSet2.SetRange(PartFld.AsString`, PartFld.AsString`);
if not IncludeCurrentJrderCbx.Checked then
ClientDataSet2.Filter := 'JrderNo < ' +
QuotedStr(ClientDataSet1.FieldByName('JrderNo').AsString)
else
ClientDataSet2.Filter := '';
end;
The Iirst line oI code in this event handler obtains a reIerence to the part number Iield oI
ClientDataSet1. The value oI this Iield is then used to create a range on the cloned cursor.
This produces a detail view that includes all records in the clone whose part number matches
the part number oI the current master table record. The remainder oI this event handler is
associated with the inclusion or exclusion oI the order Ior the master table's current record
Irom the detail table. II the Include Current OrderNo checkbox is not checked, a Iilter that
removes the master order number is assigned to the cloned cursors Filter property (remember
that Filtered is set to True). This serves to suppress the display oI the master table's order
number Irom the detail table. II Include Current OrderNo is checked, an empty string is
assigned to the clone's Filter property.
That last piece oI interesting code in this proiect is associated with the OnClick event handler
oI the Include Current OrderNo checkbox. This code, shown in the Iollowing method, simply
invokes the OnDataChange event handler oI DataSource1 to update the detail view.
procedure TForm1.IncludeCurrentJrderCbxClick(Sender: TJbject);
begin
DataSource1DataChange(Self, ClientDataSet1.Fields0`);
end;
Although this proiect is really quite simple, I think the results are nothing short oI Iantastic.
82 Deleting a Range of Records
This third, and Iinal example Iurther demonstrates how creative use oI a cloned cursor can
provide you with an alternative mechanism Ior perIorming a task. In this case, the task is to
delete a range oI records Irom a ClientDataSet.
Without using a cloned cursor, you might delete a range oI records Irom a ClientDataSet by
searching Ior records in the range and deleting them, one by one. Alternatively, you might set
an index and use the SetRange method to Iilter the ClientDataSet to include only those
records you want to delete, which you then delete, one by one.
Whether you use one oI these approaches, or some similar technique, your code might also
need to be responsible Ior restoring the pre-deletion view oI the ClientDataSet, in particular iI
the ClientDataSet was being displayed in the user interIace. For example, you would probably
what to note the current record beIore you begin the range deletion, and restore that record as
the current record when done (so long as the previous current record was not one oI those that
was deleted). Similarly, iI you had to switch indexes in order to perIorm the deletion, you
would likely want to restore the previous index.
Using a cloned cursor to delete the range provides you with an important beneIit. SpeciIically,
you can perIorm the deletion using the cloned cursor without having to worry about the view
oI the original ClientDataSet. SpeciIically, once you clone the cursor, you perIorm all changes
to the ClientDataSet's view on the clone, leaving the original view undisturbed.
The Iollowing is the CDSDeleteRange Iunction Iound in the CDSDeleteRange proiect, which
you can download Irom Code Central by clicking here.
function CDSDeleteRange(SourceCDS: TClientDataSet; const IndexFieldNames:
String;
const StartValues, EndValues: array of const): Integer;
var
Clone: TClientDataSet;
begin
//initialize number of deleted records
Result := 0;
Clone := TClientDataSet.Create(nil);
try
Clone.CloneCursor(SourceCDS, True);
Clone.IndexFieldNames := IndexFieldNames;
Clone.SetRange(StartValues, EndValues);
while Clone.RecordCount 0 do
begin
Clone.Delete;
Inc(Result);
end;
finally
Clone.Free;
end;
end;
This Iunction begins by creating a temporary ClientDataSet, which is cloned Irom the
ClientDataSet passed to the Iunction in the Iirst parameter. The clone is then indexed and
Iiltered using a range, aIter which all records in the range are deleted.
The Iollowing Iigure shows the running CDSDeleteRange proiect. This Iigure depicts the
application iust prior to clicking the button labeled Delete Range. As you can see in this
Iigure, the range to be deleted includes all records where the State Iield contains the value HI.

While this example proiect includes only one Iield in the range, in practice you can have up to
as many Iields in the range as there are Iields in the current index. For more inIormation on
SetRange, see "Searching a ClientDataSet," or reIer to the SetRange entry in the online
documentation.
The Iollowing is the code associated with the OnClick event handler oI the button labeled
Delete Range. As you can see, the deletion is perIormed simply by calling the
CDSDeleteRange
procedure TForm1.Button1Click(Sender: TJbject);
begin
if (Edit1.Text = '') and (Edit2.Text = '') then
begin
ShowMessage('Enter a range before attempting to delete');
Exit;
end;
CDSDeleteRange(ClientDataSet1, IndexListBox.ItemsIndexListBox.ItemIndex`,
Edit1.Text`,Edit2.Text`);
end;
The Iollowing Iigure shows this same application immediately Iollowing the deletion oI the
range. Note that because the deletion was perIormed by the clone, the original view oI the
displayed ClientDataSet is undisturbed, with the exception, oI course, oI the removal oI the
records in the range. Also, because operations perIormed on the data store and change log are
immediately visible to all ClientDataSets using a shared in-memory dataset, the deleted
records immediately disappear Irom the displayed grid, without requiring any kind oI reIresh.

I have to admit, I really like this example. Keep in mind, however, that the point oI this
example is not about deleting records. It is that a cloned cursor provided an attractive
alternative mechanism Ior perIorming the task.

83 Deploying Applications that use
ClientDataSets
By: Cary Jensen
Abstract: Depending on what you do within your application, iI you use one or more
ClientDataSets you may need to deploy one or more libraries, in addition to your application's
executable. This article describeswhen and how.
I've discussed a number oI ClientDataSet topics in the articles that have appeared in this
series, but I have not said much about how to deploy applications that use ClientDataSets.
Now that I think about it, this is a topic that I should have covered earlier, but as the old
saying goes, better late than never.
The Iact is, iI you include even one ClientDataSet in your application, you need to take at
least one additional step in order to deploy that application to another machine. Fortunately,
the step is pretty simple. You either have to deploy an additional library with your application,
or you have to manually add the MidasLib unit to your proiect's uses clause.
To best understand this, let's create a simple application that includes a ClientDataSet, and
then look at the modules that get loaded when you run it. Use the Iollowing steps:
1. Create a new proiect, and add a ClientDataSet to your main Iorm.
2. Set the FileName property oI the ClientDataSet to a local ClientDataSet Iile. For
example, set it to the customer.cds Iile. II you installed Delphi using the deIault
directory locations, this Iile can be Iound in c:Program FilesCommon FilesBorland
SharedData. Under Kylix, this Iile is located in the demos/db/data directory under
where Kylix is installed.
3. Set the ClientDataSet's Active property to True.
4. Run your application.
5. Select View ' Debug Windows ' Modules to display the Modules window. II you are
running Windows, your Modules window will look similar to that shown in the
Iollowing Iigure.


II your Modules window is empty, your integrated debugger is probably disabled. Select
Tools ' Debugger Options, and then enable the Integrated Debugger checkbox to turn your
integrated debugger back on. You will then need to recompile and run your application.
Notice the last entry in this Iigure, midas.dll. Midas.dll is the DLL (dynamic link library)
under Windows that contains the routines that a ClientDataSet needs. These routines are
required anytime you activate a ClientDataSet, whether you are using it with local Iiles, as in
this case, or any other way. For example, iI you are simply using a ClientDataSet to store data
temporarily in memory, it will also need access to these routines.
II you are using Kylix, the ClientDataSet relies on a shared obiect library named
libmidas.so.1. (Actually, libmidas.so.1 is a symbolic link. In Kylix 2, this Iile is symbolically
linked to libmidas.so.1.0.) This is shown in the Iollowing Iigure.

I must admit that I think the Modules window is one oI the more important in the IDE
(integrated development environment). This window displays all libraries that your
application has loaded, including ActiveX servers (under Windows). I make a habit oI
checking the Modules window beIore I deploy an application. This way I can veriIy that I will
deploy all libraries required by my application.
When you installed Delphi (or Kylix), the installer also installed the midas library. As a result,
iI you create an application that employs a ClientDataSet and run it only on your development
machine, that library is already available. II you need to distribute this application you may
also need to deploy this library to a location where the application can Iind it. Under
Windows, you will likely install this library in the Windows system directory (or system32).
With Kylix, you may need to install this Iile to the location pointed to by the
LDLIBRARYPATH environment variable.
84 Deploying Applications Without the Midas Library
You may have noticed that in the preceding paragraph I was equivocal about the need to
install the midas library. This is because there is a simple step that you can take that will make
deployment oI this library unnecessary. SpeciIically, iI you add the MidasLib unit to your
proiect's uses clause, your application will link all oI the routines required by the
ClientDataSet into your executable. As a result, the midas library will not be loaded at
runtime, and thereIore does not need to be deployed.
You can demonstrate this easily. Take the proiect you created by Iollowing the steps given
earlier in this article, and add MidasLib to your proiect's uses clause. When you are done,
your proiect source will look something like the Iollowing.
program Project1;

uses
MidasLib,
Forms,
Unit1 in 'Unit1.pas' Form1};

R *.res}

begin
Application.Initialize;
Application.CreateForm(TForm1, Form1);
Application.Run;
end.
II you now run your application again, and then display the Modules window, it will look
something like this.

As you can see, the DLL midas.dll is not listed in this window, which means that it is no
longer being loaded by the application.
85 Why Not Always Use MidasLib
I'll bet you're wondering why you don't simply included MidasLib in the uses clause oI all oI
your applications that use ClientDataSets. The reason is that using the MidasLib unit
increases the size oI your executable. Not by much, but it does increase its size.
How much? Well, that's easy to test. Once you compile your application, you can view the
InIormation dialog box to get some basic statistics about the compiled executable, including
its overall Iile size. To display this dialog box, select Proiect ' InIormation Ior Proiect Irom
Delphi's main menu. For example, the Iollowing Iigure shows this dialog box aIter compiling
the simple application created earlier in this article, prior to adding MidasLib to the proiect's
uses clause.

Displaying this dialog box again aIter adding MidasLib to the proiect's uses clause and
recompiling shows that the executable has grown in size, as shown here.

The diIIerence is a little over 200 K bytes.
There is another issue that applies when you are using Delphi. II you deploy two or more
applications that make use oI ClientDataSets, and you install midas.dll in a shared directory,
when those applications are running at the same time they will use only one copy oI the DLL
in memory, using less RAM overall. Again, it's not much oI a savings, but it is a savings.
Under Linux, shared libraries are not shared in memory. Each instance oI the library is loaded
into its own process. (They are called 8hared librarie8 because two or more applications can
share the same Iile on disk.) As a result, this same savings in RAM is not realized when two
or more Kylix applications that use ClientDataSets are running simultaneously.
My recommendation? Personally, I preIer to include MidasLib in my proiect's uses clause.
This way I avoid potential problems associated with external DLLs, such as their being
overwritten by other applications. It also makes deployment iust a little bit easier.

86 Creative Solutions Using ClientDataSet
By: Eric Whipple
Abstract: ClientDataSets can be used Ior much more than displaying rows and columns Irom
a database. See how they solve applications issues Irom selecting options to process, progress
messages, creating audit trails Ior data changes and more.
Eric Whipple is the director oI Internal Development Ior Barden Entertainment, makers oI the
Digital Video Jukebox(trade; and other distributed kiosk applications. He is responsible Ior
the planning, design, and implementation oI Web services and other distributed architectures.
He is also the author oI Kylix 2 Development (Wordware Publishing) and numerous articles
Ior Delphi InIormant and the Borland Developer Network.
ewhipple(bardenent.com
Creative Solutions
Using ClientDataSets
by
Martin Rudy
ClientDataSets can be used Ior much more than displaying rows and columns Irom a
database. They can be used to solve applications issues Irom selecting options to process,
progress messages, creating audit trails Ior data changes and more.
This session shows techniques where ClientDataSets can be used Ior a variety oI application
solutions where data is to be created, stored, displayed, and used Ior internal processing that
users never see. The intent oI the session is to expand developer usage oI ClientDataSets
beyond the standard row/column usage.
87 Contents
The maior topics covered are:
O File selection and progress messages
O Creating master lookup tables
O Creating a record-copy routine
O Custom audit trail using change log
O Using xml Iormat Ior CDS development
O Using ClientDataSet as an internal data structure
O Storing error codes and messages during development
88 File selection and progress messages
Contents
ClientDataSets (CDS) provide an easy-to-use data structure that is handy Ior many application
tasks. The Iirst example shown is how to use a CDS to display a list oI Iiles to selection Ior
processing and then display progress messages as each Iile is processed. Figure 1 shows an
example oI the sample Iorm Irom the Filelist proiect.

Figure 1: File list selection and progress messages
The concept here is to retrieve a list oI Iiles to be imported Irom a speciIied directory. The Get
File List button retrieves a list oI Iiles and initially selects each Iile name retrieved. The
checkmark in the Select column indicates the Iile is to be selected. Users can remove any oI
the Iiles Iorm the list by changing the select column to N beIore selecting the Import Files
button.
For each Iile imported, the Process Message column is updated indicating the progress oI the
import process and any problems with the import. In this example, the Iile Order1404.xml had
a problem with the Iile indicating there is an issue with the xml Iormatting. As each Iile is
processed, the grid is updated with the progress when the import starts and aIter Iinishing the
import the Iinal result.
NOTE: In this example the components that ship with Delphi were used. Third-party grids
provide a checkbox option Ior the Select column and a multi-line cell Ior the Process Message
column giving an improved display Ior messages.
The structure oI the CDS contains only three Iields: SelectRcd, FileName and ProcessMsg.
Records are initially inserted into the CDS by retrieving all Iiles in a speciIic directory and
setting the SelectRcd Iield to Y on Post. A hidden TFileListBox is used to easily get the Iiles
in the directory. The code to load the data is shown below.
procedure TfrmFileListExample.pbGetFileListClick(Sender: TJbject);
var
I: Integer;
CurCursor: TCursor;
begin
CurCursor := Screen.Cursor;
Screen.Cursor := crHourGlass;
try
cdsFileList.Close;
cdsFileList.CreateDataSet;
cdsFileList.Jpen;
cdsFileList.LogChanges := cbxLogChanges.Checked;
// put list of all available files in grid
for i:=0 to FileListBox.Items.Count - 1 do
begin
cdsFileList.Append;
cdsFileList.FieldByName('FileName').AsString :=
FileListBox.Items.Stringsi`;
Ensure the SelectRcd field is set last because there is an
OnChange event which posts the record. This event will ensure
at runtime the record is not left in edit when user changes the
record selection by clicking on the checkbox.

This also means the Post method below is not necessary.
}
cdsFileList.FieldByName('SelectRcd').AsString := 'Y';
// cdsFileList.Post; // THIS IS NOT REQUIRED, DONE IN OnChange
// EVENT FOR SelectRcd Field
// See AfterOpen event below
end;
cdsFileList.First;
finally
Screen.Cursor := CurCursor;
end;
end;
In this example, the Select column is the only Iield that the user can modiIy. When the Select
column value is changed, the record in automatically posted. In the demo there is no real
processing oI the Iile, there is only a simulation oI processing and displaying the progress.
Normally a checkbox control would be used but the DBGrid does not support that Ieature.
The demo proiect has modiIied the grid to include an OnDblClick event which toggles the
value between Y and N.
The concept oI this example is basic but using a CDS makes the creation oI the selection and
UI very easy to implement. AIter the import processing is complete, saving the CDS in an
XML Iormat creates a Iile with the contents shown in Figure 2:
Figure 2: CDS contents as an XML file after getting file list import simulation
There are two main sections oI the xml Iile: data structure and actual data. The METADATA
section deIines the Iields that are in the Iile (also called data packet). The Iield name, data
type and width are included. The second section, ROWDATA, contains the values Ior each
Iield and the RowState value. The RowState shows that status oI the data row. Table 1 shows
the values and their meaning. Any row can have a combination oI RowState values to indicate
multiple changes made to a row.
RowState
Value

Description
1 Original record
2 Deleted record
4 Inserted record
8 Updated record
64 Detail updates
Table 1: RowState values and Description
By deIault, a CDS will create a change log Ior every modiIication made to the data. Each
insert, delete and update is logged. II logging oI the changes is not necessary, you can set the
CDS LogChanges property to False. In the Filelist proiect, iI the Log Changes checkbox is
unchecked, the contents oI the XML Iile aIter getting the Iile list and simulating the import is
shown in Figure 3.

Figure 3: CDS contents with no logging
You can also achieve both the ability to log changes and beIore saving merge all changes to
each row into a single record. This is done using the MergeChangeLog method. Executing the
MergeChangeLog method beIore saving the Iile creates the same output iI no logging was
perIormed.
89 Creating master lookup tables
Contents
Lookup tables provide an excellent way to ensure valid values are entered into Iields and UI
support Ior combo and list boxes to select Irom. Using the built-in data-aware components
that support this Ieature requires separate datasets Ior each component and sometimes
DataSource components are required.
For some application requirements, creating a table that is essentially a grouping oI tables can
eliminate this need Ior multiple tables in the database. This next example, named
MstrLookup, demonstrates how you can have a master lookup table and how to use CDS
components and the cloning Ieature to support multiple lookup datasets without individual
tables Ior each type oI lookup.
The Iirst step is to create a table in the database that contains the lookup values Ior the various
types oI groupings. The design used here is to have a Iield that groups the data, a Iield Ior the
lookup code, and a Iield Ior a description. The Iollowing is an example oI the create statement
Ior a table named MasterLookup.
CREATE TABLE MasterLookup` (
LookupGroup` char` (10) NJT NULL ,
LookupCode` char` (10) NJT NULL ,
LookupDesc` varchar` (65) NJT NULL )
There are three Iields in the table. LookupGroup is used as the grouping or table name. The
lookup code is in the LookupCode Iield. LookupDesc Iield contains the description Ior the
code value. II the lookup table has codes that are selI-explanatory, the code and description
Iields will be the same.
In this example, the ORDERS table is used Irom. There are two Iields that can use this lookup
Ieature: ShipVia and PaymentMethod.
The general technique is to retrieve the values Irom the MasterLookup table into a CDS. A
separate CDS is placed in the client data module Ior each lookup table. The rows Ior each
lookup CDS are set in the OnCreate oI the data module. The code to create the two tables Ior
ShipVia and PaymentMethod is shown below.
procedure TForm1.GetLkupData;
begin
Get lookup data into CDS }
with cdsMasterLookup do
begin
Ensure MasterLookup is open }
Jpen;
Filter := 'LookupGroup = ''ShipVia''';
Filtered := True;
cdsLkupShipVia.CloneCursor(cdsMasterLookup,False,True);
Filter := 'LookupGroup = ''PayType''';
cdsLkupPayType.CloneCursor(cdsMasterLookup,False,True);
Filtered := False;
Filter := '';
cdsLkupShipVia.Jpen;
cdsLkupPayType.Jpen;
end;
end;
A CDS named cdsMasterLookup is used Ior all rows in the MasterLookup table. Its source oI
data is a DataSetProvider (DSP) using a Table component as the DataSet. Since the number oI
rows is very small in MasterLookup, Iiltering is used to restrict the values beIore the CDS is
cloned. Each group requires a new Iilter on the master table. The CloneCursor method is
called to make a copy oI the Iiltered rows. CloneCursor allows a separate lookup CDS to
share the data belonging to the CDS Ior MasterLookup. Each oI the cloned CDS does not
have a DataSetProvider.
In this example the DBLookupComboBox component is uses to provide the list oI lookup
values. Attempting the assignment oI the KeyField property generates an error indicating the
CDS does not have the ProviderName assigned. ThereIore, the lookup properties cannot be
set. The solution to this problem is to initially use the CDS Ior the MasterLookup table, assign
the KeyField and ListField properties then reassign the ListSource property back to the
DataSource Ior the lookup CDS. When this is complete, the combo boxes work properly.
90 Creating a record-copy routine
Contents
Some applications have a requirement to provide a copy record Ieature or at least copy all but
a Iew oI the Iields. ClientDataSets provide an easy solution Ior this type oI requirement. We
will look at two solutions: the Iirst uses an existing CDS, the second is a generic Iunction
where the CDS is instantiated, used and destroyed. The code below contains the Iirst example.
procedure TfrmCopyRcd.pbCopyRcdClick(Sender: TJbject);
var
I: Integer;
begin
with ClientDataSet1 do
begin
Jpen;
Insert;
for I:=0 to fieldcount-1 do
FieldsI`.Assign(ADJDataSet1.FindField(FieldsI`.FieldName));
end;

with ADJDataSet1 do
begin
Insert;
for I:=0 to fieldcount-1 do
FieldsI`.Assign(ClientDataSet1.FindField(FieldsI`.FieldName));
end;
ClientDataSet1.Cancel;
end;
The basic concept here is to use the CDS as a temporary buIIer to hold the data. This Iirst
example uses an existing CDS that is the same structure oI the table used to copy to and Irom.
AIter putting the CDS into the Insert state, the FieldCount property is used to cycle through
all the Iields in the CDS and copy matching Iields Irom the existing dataset. In this example
ADO is used as the database connectivity option. AIter all Iields are copied, a new record is
inserted into the ADO dataset and the record is copiedIrom the CDS, leaving the ADO dataset
in Insert mode.
For a known structure this works but is not to helpIul Irom a generic standpoint. A general
purpose Iunction where the CDS is instantiated as needs is more useIul. The Iunction
CopyDataSetRcd shown below supports this requirement.
function TfrmCopyRcd.CopyDataSetRcd(DataSet: TDataSet): Boolean;
var
I: Integer;
cds: TClientDataSet;
begin
result := False;
cds := TClientDataSet.Create(nil);
try
cds.FieldDefs.Assign(DataSet.FieldDefs);
cds.CreateDataSet;
with cds do
begin
Jpen;
Insert;
for I:=0 to fieldcount-1 do
FieldsI`.Assign(DataSet.FindField(FieldsI`.FieldName));
end;

with DataSet do
begin
Insert;
for I:=0 to fieldcount-1 do
FieldsI`.Assign(cds.FindField(FieldsI`.FieldName));
end;
cds.Cancel;
result := True;
finally
cds.Free;
end;
end;
In the CopyDataSetRcd, the CDS is called using the dataset that needs the current record
copied. An instance oI a CDS is created at the beginning oI the Iunction. The Iields are
assigned based on the dataset passed to the Iunction. This allows the dynamic creation oI the
CDS to match any dataset passed. The same basic technique as the Iirst example is used Ior
the remainder oI the Iunction.
Most applications do not have a need to create exact duplicates oI existing records but this
basic concept can be useIul where there is a need to copy many oI the Iields Irom the previous
record. The basic technique can be used to create a copy oI the record and either delete or
prevent copying Iield(s) that are part oI the primary key. You can also add an additional
parameter which speciIies the Iields to copy or the Iields not to copy making the duplication
process generic but speciIic to the Iields to include or exclude.
91 Custom audit trail using change log
Contents
A change log is maintained by the CDS Ior each insert, update, and delete. The CDS Delta
property contains all records in the change log. A separate record is added to the log Ior each
insert and delete. When an existing record is modiIied, two records are entered in the log. The
Iirst record, with a status oI usUnmodified, contains all Iield values Ior the record beIore any
modiIication was made. The second record, with a status oI usModified, contains only the
Iield values that have changed. All non-modiIied Iields are null in the second record. The
CDS Delta property is what the provider receives as the DataSet property in the
OnUpdateData event.
In the demo proiect, the third tab displays the contents oI the change log. Figure 4 shows the
log aIter perIorming an edit on one row, an insert, and a delete. Note, the UpdateStatus Iield
does not exist in the data, it is a calculated Iield used to display the status oI each record.

Figure 4: Change log display
The last two records are Ior a deleted and inserted record. On an insert, any Iield where data is
entered is placed in the change log. For deleted records, all the original Iield values are placed
in the log.
The Iirst tow records are matching pair. The Iirst record oI the pair contains all the values oI
the record beIore any changes were made. The second record, with an UpdateStatus oI
ModiIied, contains the values Ior every Iield in the record that changed. In this example, the
values oI both Addr1 and Addr2 Iields have been modiIied.
Displaying the change log requires an extra CDS in the application and a small amount oI
code. The code that is used in the demo client is as Iollows:
procedure TfrmMain.PageControl1Change(Sender: TJbject);
begin
if PageControl1.ActivePage = tbsDelta then
try
cdsCustDelta.Close;
cdsCustDelta.Data := cdsCustomer.Delta;
cdsCustDelta.Jpen;
except
MessageDlg('No delta records exist.',mtWarning,mbJK`,0);
end;
end;
The CDS cdsCustomer contains the data Irom the provider. The CDS Ior showing the change
log is named cdsCustDelta. When the third tab is selected, the Delta property oI cdsCustomer
is assigned to the Data property oI cdsCustDelta. The try except block is used to display a
simple message when there are no modiIications to the data.
The value Ior the calculated UpdateStatus Iield is assigned using the CDS UpdateStatus
method. Table 2 lists the Iour return values Ior UpdateStatus and a description.
UpdateStatus
Value

Description
usModiIied ModiIications made to record
usInserted Record has been inserted
usDeleted Record has been deleted
usUnModiIied Original record
Table 2: RowState values and Description
The OnCalcFields Ior the CDS is shown below.
procedure TdmMain.cdsCustomerDeltaCalcFields(DataSet: TDataSet);
begin
with DataSet do
begin
case UpdateStatus of
usModified : FieldByName('UpdateStatus').AsString := 'M';
usInserted : FieldByName('UpdateStatus').AsString := 'I';
usDeleted : FieldByName('UpdateStatus').AsString := 'D';
usUnModified : FieldByName('UpdateStatus').AsString := 'U';
end;
end;
end;
Using the CopyDataSetRcd Iunction described in the previous section along with the CDS
logging Ieature, you can create a custom audit trail process in your applications. The proiect
CDSAudit is used Ior this example.
The basic concept here is to copy the change log beIore the ApplyUpdates is executed then
save the changes to an existing table. The Iollowing are two code snippets used in the
example.
procedure TForm4.cdsCustBeforeApplyUpdates(Sender: TJbject;
var JwnerData: JleVariant);
begin
Save Delta }
CDSLog.Data := cdsCust.Delta;
end;

procedure TForm4.pbShowChangeLogClick(Sender: TJbject);
begin
CDSLog.Data := cdsCust.Delta;

Screen.Cursor := crHourGlass;
try
tblCustAudit.Jpen;
CDSLog.First;
while not CDSLog.Eof do
begin
try
CopyDataSetRcd(CDSLog,tblCustAudit);
tblCustAudit.FieldByName('ModType').AsString :=
CDSLog.FieldByName('UpdateStatus').AsString;
tblCustAudit.FieldByName('ModDate').AsDateTime := Date;
tblCustAudit.Post;
except
if tblCustAudit.State = dsBrowse then
tblCustAudit.Cancel;
raise;
end;
CDSLog.Next;
end;
finally
Screen.Cursor := crDefault;
end;
end;

procedure TForm4.CDSLogCalcFields(DataSet: TDataSet);
begin
with DataSet do
begin
case UpdateStatus of
usModified : FieldByName('UpdateStatus').AsString := 'M';
usInserted : FieldByName('UpdateStatus').AsString := 'I';
usDeleted : FieldByName('UpdateStatus').AsString := 'D';
usUnModified : FieldByName('UpdateStatus').AsString := 'U';
end;
end;
end;
The Iirst procedure is used by a DSP to automatically copy the change log to an existing CDS
beIore the updates are applied. This keeps a copy oI the changes beIore they are applied to the
database.
The second procedure is used to actually save the change log to a table. Each record in the
change log is copied to the audit table. Additionally the type oI modiIication and date oI
modiIication is added to the audit table record. The UpdateStatus Iield is a calculated Iield
added to the log dataset. It is assigned using the UpdateStatus method oI a CDS. The third
procedure above show how the calculated UpdateStatus Iield is generated.
92 Using xml format for CDS development
Contents
Incremental design is part oI many application development cycles. ClientDataSets provide an
easy tool Ior structure modiIications during the prototyping and prooI-oI-concept phases Ior
dataset design. Using the XML Iormat, Iields can easily be added, deleted or modiIied. Data
can easily be added or changed without having to use a database backend.
You can start designing a dataset with a new CDS. One oI the options is to use is the Fields
Editor to add new Iields. AIter adding the Iields, you right-mouse click on the CDS and select
Create DataSet Irom the speed menu. This creates the in-memory dataset which is static in the
Iorm or data module.
To start getting data into the CDS, a small application is needed. This is basically a grid,
DataSource and a Button containing a SaveToFile call using the XML Iormat as Iollows:
CDS.SaveToFile('CDS.XML',dIXML);
The Iirst parameter speciIies the Iile name with optional Iull path. The second parameter
indicates the saved Iormat is to be XML. AIter entering a Iew records, the output is as shown
in Figure 5.

Figure 5: Saved CDS data in XML
The change log is always generated by deIault. You can change this deIault by adding to the
OnCreate Ior the Iorm a line setting the LogChanges property oI the CDS to False or use the
MergeChangeLog method to combine the data and change log beIore saving. The latter is
used here to support undo oI changes during data entry.
Saving the structure now allows both Iields and data to be added. The XML Iile can but
updated to include new Iields and records, any existing data can be modiIied provided it
Iollows the metadata deIinition.
There are two keys to getting this technique to work Ior you: 1) need to know what is required
Ior the data type values; and 2) need to know iI there are any Iormatting issues Ior non-text
data types. Figure 6 shows lists some oI the commonly used data types, the value used in the
XML metadata and sample on how to Iormat the data.
Figure 6: Common data types as shown in XML metadata
This technique is demonstrated Iurther in the next section.
93 Using ClientDataSet as an internal data structure
Contents
This section picks up Irom the previous topics and expands on the usage oI ClientDataSets in
the development phase. The example used is an application that was a prototype Ior a Sunday
School game Ior the Books oI the Bible where users would test their knowledge oI putting the
books in the correct order and correct sections on a bookshelI.
Part oI the example is how to use ClientDataSets during the development oI the concept and
as internal data Ior drag/drop inIormation to ensure an order sequence as new UI items are
added to the display dynamically and randomly at runtime.
Below is the prototype Iorm Ior the example. Two grids are used: one to create the data used
in the random creation oI the books to display and second shows the internal data stored as the
user placed the new book on the shelI in the correct position. The two white rectangles
represent the separate shelves in a bookcase. When the Create Book button is clicked, a new
book is created and placed next to the top shelI. The width oI the component was based on the
BookWidth Iield in the Iirst CDS. The user then drags the book to the appropriate place and,
based on the data in the internal CDS, a comparison is made iI the drop oI the book was in the
correct sequential location.
Figure 7: Application using CDS for application prototype
NOTE: The original prototype was done with Delphi 7 and third-party components. These
components did not exist with Delphi 9 so modiIications were made to use what shipped with
Delphi. A TMemo control replaced the third-party component used to represent the book and
the book width had to be excluded.
Two ClientDataSets where used to create the prototype. The Iirst CDS was used to store the
books. It started with only the BookNo and BookName Iields. The data Ior the books was
loaded using the technique described in the previous section. To assist the drag and drop
processing and development, an internal data structure was required. The second CDS was
created Ior this purpose primarily to use a DBGrid Ior data display oI the assigned values.
This made debugging the process easier because values where displayed as they were
assigned. The Fields Editor was used to create the Iields Ior the second dataset and
CreateDataSet was added to the Iorms OnCreate.
During the prototype creation, Iields where added to both ClientDataSets as needed using the
technique described in the previous section. For example, the ObiectName Iield was added to
the second CDS. This value is the Name property Ior each book added. It consists oI the text
Book and the book BookNo numeric value converted to a two character text value. The
ObiectName value is used with a call to FindComponent during the insertion process oI a new
book. Another example is the BookWidth property in the Iirst CDS. This was used to provide
data to set the width oI the component representing the book when it was created. This gave a
visual representation oI the relative size oI the book when the third-party control was used.
In the prototype, all books are listed in the upper-right grid. The Iinal version is to randomly
generate which book to be placed which is not in the prototype version. Generating a book to
be placed is done by either clicking the Create Book button or a double-click on the book
grid. The currently selected record in the grid will be the book created to place in the shelI.
This is placed to the leIt oI the Iirst shelI. The Iollowing code is used to generate a book.
procedure TForm1.pbCreateBookClick(Sender: TJbject);
var
bk: TMemo;
begin
Generate a new book object for selected book }
bk := TMemo.Create(self);
bk.Parent := self;
bk.Tag := cdsBibleBooks.FieldByName('BookNo').AsInteger;
if bk.Tag < 10 then
bk.Name := 'Book0' + IntToStr(bk.Tag)
else
bk.Name := 'Book' + IntToStr(bk.Tag);
bk.Lines0` := (cdsBibleBooks.FieldByName('BookName').AsString);
bk.Left := 2;
bk.Top := 5;
bk.Height := 160;
bk.Width := 14;
bk.Alignment := taCenter;
bk.DragMode := dmAutomatic;
bk.Color := clBlue;
bk.Font.Color := clWhite;
bk.Font.Name := 'Courier New';
end;
The list oI books is stored in the CDS named cdsBibleBooks. The current record in the CDS
is used to set property values oI the TMemo instance created. The Tag property is assigned
the BookNo Iield which is a unique value. The Name property is assigned to a unique value
using the BookNo Iield. Some oI the TMemo properties assigned are to get the vertical text
display. Automatic drag mode is used to simpliIy the prototype creation Ior drag-and-drop.
When a user drags the new book to a location to place in on the selI, the Iollowing code is
executed:
procedure TForm1.Shape1DragDrop(Sender, Source: TJbject; X, Y: Integer);
var
L,T,S: Integer;
begin
with Sender as TShape do
begin
L := Left;
T := Top;
S := Tag;
end;
if Source is TMemo then
with Source as TMemo do
begin
Look to see if book dropped in correct location }
if BookLocJK(S,Tag,X+L) then
begin
if cdsBkShlf.RecordCount 0 then
TMemo(Source).Left := X + L
else
TMemo(Source).Left := 8;
TMemo(Source).Top := 4 + T;
Locate BookName, if found, update position.
If not found, insert into table.
}
if cdsBkShlf.Locate('BookName',TMemo(Source).Text,`) then
begin
cdsBkShlf.Edit;
cdsBkShlf.FieldByName('ShelfNo').AsInteger := S;
cdsBkShlf.FieldByName('ShelfLeftPos').AsInteger := X;
end
else
begin
cdsBkShlf.Append;
cdsBkShlf.FieldByName('ShelfNo').AsInteger := S;
cdsBkShlf.FieldByName('BookNo').AsInteger := Tag;
cdsBkShlf.FieldByName('ShelfLeftPos').AsInteger := X;
cdsBkShlf.FieldByName('BookName').AsString :=
TMemo(Source).Text;
cdsBkShlf.FieldByName('JbjectName').AsString := Name;
end;
cdsBkShlf.Post;
ResuffleBooks(S);
end
else
MessageDlg('Book location incorrect.',mtWarning,mbJK`,0);
end;
end;
AIter checking iI the Sender is a TMemo, there is a check to see iI the book is dropped in the
correct position. The call to BookLocOK indicates iI the position was. II the result is True, the
book is placed in the correct position otherwise a simple message is displayed indicating the
position is incorrect. For each new book added, there is a check iI the book already has been
placed. In this prototype, there is no check Ior the correct shelI the book is to be placed in.
ThereIore, a book that was originally placed in the Iirst shelI can be moved to the second shelI
and the ShelINo and ShelILeItPos Iields need to be updated. II the book is newly added, a
new record is appended to the CDS.
The data stored in cdsBkShlI is only used during the running oI the application. The grid
showing the contents is Ior development purposes only. It provides instant Ieedback on the
values assigned and is a tool that can be adiusted during the development process to Iind
Ilaws in the basic design. Other data structures that are more eIIicient can be used, but in a
prototype, Iast-paced development cycle, a CDS and DBGrid can expedite the process.
94 Storing error codes and messages during development
Contents
Some applications have a need to support error and message codes with associated text Ior the
messages. ClientDataSets can be used to create a repository Ior storing both the codes and text
Ior each message. The XML Iormat is used in this example which allows Ior easy insertion
and modiIication oI error codes and descriptions. This XML Iile is loaded at runtime and
Iunctions are shown which use this data. Figure 8 shows the basic structure oI the XML Iile.
This data can then be moved to the shipping database when appropriate. A technique is also
shown how to store this data statically in the application as a CDS thus allowing the same
Iunctions to be used when the XML was loaded at startup.
Figure 8: Sample structure for error codes and messages with sample data
A CDS is loaded with the error code and message data when the data module is created using
the Iollowing:
cdsErrMsg.LoadFromFile('ErrMsg.XML');
The Iunction in the Iollowing code retrieves the text Ior the speciIied error code.
function TdmMain.GetErrMsg(ErrCd: Integer): String;
begin
if cdsErrMsg.Locate('ErrorCode',ErrCd,`) then
result := cdsErrMsg.FieldByName('ErrorMessage').AsString
else
result := 'Invalid Error Code';
end;
This Iunction can be used in any validation process where the business rule maps to a speciIic
error code. The Iollowing is an example oI using the Iunction in the BeIorePost event oI the
CDS.
procedure TfrmValidation.ClientDataSet1BeforePost(DataSet: TDataSet);
begin
Ensure required fields are entered }
with DataSet do
begin
if FieldByName('Customer').AsString = '' then
raise Exception.Create(dmMain.GetErrMsg(101));
if FieldByName('Contact_First').AsString = '' then
raise Exception.Create(dmMain.GetErrMsg(102));
if FieldByName('Contact_Last').AsString = '' then
raise Exception.Create(dmMain.GetErrMsg(103));
if (FieldByName('Address_Line1').AsString = '') and
(FieldByName('Address_Line2').AsString < '') then
raise Exception.Create(dmMain.GetErrMsg(104));
end;
end;
As additional business rules are added, requiring the increase in error codes, new entries can
easily be inserted into the XML Iile. This can be done using any text editor or by creating a
small application that loads the existing XML Iile, supports modiIications to the data and
saves the error codes and messages back to the XML Iile. This process continues during the
development phase.
When time comes to ship the application, most likely you will not want to ship an XML Iile
that is loaded on startup. Two options can be used to replace the loading oI the XML data.
The data can be moved to a table in the database and retrieved as the application opens or the
data can be made static in the exe itselI.
Moving data to the applications database requires a little bit oI extra eIIort. You can write a
small application that inserts the data Irom the CDS XML Iile by either directly inserting into
the table or you can use a DataSetProvider, a second CDS and have the records inserted into
the database using ApplyUpdates. Another modiIication is required at startup to retrieve the
data into the CDS, replacing the LoadFromFile call that previously loaded the XML Iile.
The second option is to load the CDS at design-time and make the data part oI the exe. You
right-click on the CDS and select the Load Irom MyBase table menu option and choose the
saved XML Iile. When the proiect is saved, the data becomes stored in the Iorm/data module
containing the CDS and becomes static Ior the released exe. Provided you dont change the
Active property oI the CDS, the error codes and messages exist Ior each exe created. When a
change is required, the XML Iile is updated and you re-load at design time the new data.
Using the technique oI an XML Iile provides another option during development. The
developers can use the Iile Ior coding and the documentation department can review the error
messages and update per their standards. Adding another Iield to the CDS structure Ior notes
allows both developers and documenters the ability to provide Iurther inIormation on the error
and Iurther detail about potential cause and correction iI needed. This inIormation can be
added into the Iinal documentation or provide online inIormation Ior the Iinal product.
95 Summary
ClientDataSets, they are not iust Ior multi-tier applications any more. They can be used in
applications based on Paradox or Access data, InterBase, Oracle, SQL Server, and any other
database, Ior XML based applications and any combination oI local storage to multi-tier. Any
time an application needs some type oI data structure to store data where the data needs to be
viewed and manipulated, ClientDataSets provide an excellent solution.

S-ar putea să vă placă și