If you have been following this series, you are no doubt aware that I am a
huge fan of ClientDataSets. Indeed, I feel like I haven't stopped smiling since
Borland added them to the Professional edition of their RAD (rapid application
development) products, including Delphi, Kylix, and C++ Builder. The
capabilities that ClientDataSets can add to an application are many. But of all
the features 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
full power of cloned cursors is not obvious until you know how to navigate,
edit, filter, 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 from another dataset or
from a file. But
what do you do when you need two different views of essentially the same data?
One alternative is to load a second copy of the data into a second ClientDataSet.
This approach, however, results in an unnecessary increase in network traffic
(or disk access) and places redundant data in memory. In some cases, a better
option is to clone the cursor of 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, filter, index, provider, and
range.
It is difficult to appreciate the power of 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 first record and save some of 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 of the data in
memory. You can then locate the first 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 first two, is to
utilize the one
copy of data in memory, and clone a second cursor onto this memory store.
The cloned cursor acts is if it were a second copy of the data in memory, in
that you now have two cursors (the original and the clone), and each can point
to a different record. Importantly, only one copy of 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 of customer invoices stored in
memory using a ClientDataSet. Suppose
further that you need to display this data to the end user using two different
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 first inclination might
be to load the data twice, using two ClientDataSets, a cloned cursor performs
the task much more efficiently. After loading the data into a single
ClientDataSet, a second ClientDataSet is used to clone the first. The
first 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 of the ClientDataSets contain a different view.
How to Clone a ClientDataSet
You clone a ClientDataSet's cursor by invoking the CloneCursor method. This method has the following syntax:
procedure CloneCursor(Source :TCustomClientDataSet;
Reset: Boolean; KeepSettings: Boolean = False);
When you invoke CloneCursor, the first 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. If you pass a value of False, the
values of the IndexName (or IndexFieldNames), Filter, Filtered, MasterSource,
MasterFields, OnFilterRecord, and ProviderName properties are set to
match that of the source client dataset. Passing True in the second parameter resets these properties to
their default values. (A special case with respect to filters is discussed
in the following section.)
For example, if you invoke CloneCursor, passing a value of True in the
second parameter, the cloned ClientDataSet's IndexFieldNames property
will contain an empty string, regardless of the value of the IndexFieldNames property of
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
from, depending
on the second parameter.
You include the third, optional parameter, passing a value
of True, typically in conjunction with a Reset value of False. In this
situation, the properties of the cloned cursor match that of the original
dataset, but may not actually be valid, depending on the situation. In most
cases, a call to CloneCursor only includes the first two parameters.
Although the cloned cursor may not share many view-related
properties with the ClientDataSet it was cloned from, it may present a view that
nearly matches the original. For example, the current record of a cloned cursor
is typically the same record that was current in the original. Similarly, if a
ClientDataSet uses an index to display records in a particular order, the
clone's natural order will match the indexed view of the original, even though
the IndexName or IndexFieldNames properties of the clone may be empty.
This view duplication also applies to ranges. Specifically,
if you clone a ClientDataSet that has a range set, the clone will
employ that range, regardless of 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 different 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 few
assumptions about the view of a cloned cursor. In other words, your safest bet
is to clone a cursor passing a value of True in the Reset formal parameter. If
you pass a value of 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 of the
clone to appear. Doing so will help you fix problems that could potentially be
introduced if future implementations of the CloneCursor method change the view
of the clone.
A single ClientDataSet can be cloned any number of times, creating many
different views for 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 of 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 of this. Once a ClientDataSet is cloned, the clone and the original have
equal status, as far as the memory store is concerned. For example, you can load one ClientDataSet with data, and
then clone a second ClientDataSet from 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 of 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.
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 filtered. Specifically, if you
clone a ClientDataSet that is currently being filtered (its Filter property is
set to a filter string, and Filtered is True), and you pass a value of False (no
reset) in the second parameter of the CloneCursor invocation, the cloned
ClientDataSet will employ the filter also. However, unlike when a cloned cursor
has a range, which can be canceled, the cloned cursor will necessarily be
filtered. In other words, in this situation, the clone can never cancel the
filter it gets from the ClientDataSet it was cloned from. (Actually, you can
apply a new filter to the filtered view, but that does not cancel the original
filter. It merely adds an additional filter on top of the original filter.)
This effect does not occur when Filter is set to a filter
expression, and Filtered is set to False. Specifically, if Filter is set to a
filter expression, and Filtered is False, cloning the cursor with a Reset value
of False will cause the cloned view to have a filter expression, but it will not
be filtered. Furthermore, you can set an alternative Filter expression, and set
or drop the filter, in which case the clone may include more records than the
filtered ClientDataSet from 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 major
problems in your application. Consequently, I
suggest that you pass a value of True in the Reset formal parameter of
CloneCursor if you are cloning an actively filtered ClientDataSet. You can then
set the filter on the clone, in which case the filters will be completely independent.
The Shared Data Store
The fact that all clones share a common data store has some important
implications. Specifically, any changes made to the common data store directly
affects all ClientDataSets that use it. For example, if you delete a record using a cloned cursor,
the record instantly appears to be deleted from 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 default value). Similarly,
calling ApplyUpdates from any of the ClientDataSets attempts to apply the
changes in the change log. In addition, setting one of the ClientDataSet's Readonly
property to True prevents any changes to the data from any of its associated
ClientDataSets.
Note that the documentation states that passing a value of True in the second
parameter of CloneCursor will cause the clone to employ the default ReadOnly
property, rather than the ReadOnly value of the ClientDataSet that is being
cloned. Nonetheless, setting ReadOnly on a ClientDataSet to True makes the data
store readonly, which affects all ClientDataSets pointing to that data store.
Likewise, if you have at least one change in the change log, calling
RevertRecord, or even CancelChanges, affects the single change log (represented
by the Delta property). For example, if you deleted a record from a cloned
ClientDataSet, it will appear to be instantly deleted from the views of all the
ClientDataSets associated with the single data store. If you then call
UndoLastChange on one of the ClientDataSets, that deleted record will be removed
from the change log, and will instantly re-appear in all of the associated
ClientDataSets.
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 following examples are
designed to give you a feel for how you might use cloned cursors in your
applications.
Creating Multiple Views of a Data Store
This first example, CloneAndFilter, shows how you can display many different views of a common
data store using cloned cursors. You can download this project from Code Central
by clicking here.. The following is the main form of this application, which
permits you to select the file to load into a ClientDataSet and then clone.
Once you click the button labeled Load and Display, an instance of the
TViewForm class is created, and the selected file is loaded into a ClientDataSet
associated with this class. The following is how the instance of the TViewForm
looks when the customer.cds file is loaded into the ClientDataSet.
If you have been following this series, you will recognize this form as being
similar to the one I used to demonstrate filters and ranges. I used this form
here, of course, so that you can apply filters, set ranges, change indexes, and
edit and navigate records, and then see what influence these setting have on a
clone of this view.
As you can see from the preceding form, there are two buttons associated with
cloning a cursor. The first, labeled CloneCursor: Reset, will call CloneCursor
with a value of True passed in the second parameter. The following is the
OnClick event handler associated with this button.
procedure TViewForm.CloneResetBtnClick(Sender: TObject);
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 of
the TViewForm class is created, and the ClientDataSet that appears on this
instance is cloned from the one that appears on Self. The following shows an
example of what this form might look like after a cursor is cloned.
Besides this form's caption, it appears to be a separate view of the
originally loaded file. You can now set filters, ranges, and change the current
record. Importantly, both of the files use the single copy of the data
originally loaded into memory.
The second button associated with cloning is labeled CloneCursor: No Reset.
The following code is associated with the OnClick event handler of this button.
procedure TViewForm.CloneNoResetBtnClick(Sender: TObject);
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 difference between this method
and the preceding one is the
value passed in the second parameter of the call to CloneCursor.
Unfortunately, the static nature of the screenshots in this article do little
to demonstrate what is going on here. I urge you to either create your own
demonstration project, or to download this project, and play with cloned
cursors for a while. For example, create three clones of the same cursor, and
then close the original TViewForm instance. Then, with several of the TViewForm
instances displayed, post a change to one of the records. You will notice that
all instances of the displayed form will instantly display the updated data.
Next, undo that change by clicking the button labeled Undo Last Change (try
doing this on a form other than the one you posted the change on).
Here's another thing to try. Clone several cursors and then delete a record
from one of the visible forms. This record will immediately disappear from all
forms. Then, click the button labeled Empty Change Log (again, it does not
matter on which form you click this button). The deleted record will instantly
reappear on all visible forms.
Self-Referencing Master-Details
Most database developers have some experience create master-detail views of
data. This type of view, sometimes also called a one-to-many view or a
parent-child view, involves displaying the zero or more records from a detail
table that are associated with the currently selected record in a master table. You
can easily create this kind of view using the MasterSource and MasterFields
properties of a ClientDataSet, given that you have two tables with the
appropriate relationship (such as Borland's sample customer.cds and orders.cds
files).
While most master-detail views involve two tables, what do you do if you want to create
a similar effect using a single table. In other words, what if you want to
select a record from a table and display other related records of that same
table in a separate view.
Sound weird? Well, not really. Consider Borland's
sample file items.cds. Each record in this file contains an order number, a part
number, the quantity ordered, and so forth. Imagine that when you select a
particular part associated with an given order you want to also see, in a
separate view, all orders from this table in which that same part was ordered. In this example,
all of the data resides in a single table (items.cds).
Fortunately, cloned cursors give you a powerful way of displaying
master-detail relationships within a single table. This technique is
demonstrated in the MasterDetailClone project, which can be downloaded form Code
Central by clicking here..
The main form of this running project can be seen in the following figure.
Notice that when a record associated with part number 12306 is selected (in this
case, for order number 1009), the detail view, which appears in the lower grid
on this form, displays all orders that include part number 12306 (including
order number 1009).
This form also contains a checkbox, which permits you to either include or
exclude the current order number from 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 following figure.
In this project, the detail view is created using a cloned cursor of the
master table (ClientDataSet1) from the OnCreate event handler of the main form.
The following is the code associated with this event handler.
procedure TForm1.FormCreate(Sender: TObject);
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.Open;
//Assign the OnDataChange event handler _after_
//opening the ClientDataSet
DataSource1.OnDataChange := 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 confirms that the file name
associated with ClientDataSet1 is valid, there are four steps that are taken
that contribute to the master-detail view. The first 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
field in ClientDataSet1, as well as each time ClientDataSet1 arrives at a new
current record).
The third operation performed by this event handler is the cloning of the
detail table cursor, assigning an appropriate index, and setting the cloned
cursor's Filtered property to True. In this project, the order of steps two and
three are interchangeable.
The forth step is to invoke the OnDataChange event handler of DataSoure1.
This invocation causes the cloned cursor to display its initial detail view.
As must be obvious from this discussion, the OnDataChange event handler
actually creates the detail view. The following is the code associated with this
event handler.
procedure TForm1.DataSource1DataChange(Sender: TObject; Field: TField);
begin
PartFld := ClientDataSet1.FieldByName('PartNo');
ClientDataSet2.SetRange([PartFld.AsString], [PartFld.AsString]);
if not IncludeCurrentOrderCbx.Checked then
ClientDataSet2.Filter := 'OrderNo <> ' +
QuotedStr(ClientDataSet1.FieldByName('OrderNo').AsString)
else
ClientDataSet2.Filter := '';
end;
The first line of code in this event handler obtains
a reference to the part number field of ClientDataSet1. The value of this field
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 of the current
master table record. The remainder of this event handler is associated with the
inclusion or exclusion of the order for the master table's current record from
the detail table. If the Include Current OrderNo checkbox is not checked, a
filter 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 of the master table's order number from the detail table.
If Include Current OrderNo is checked, an empty string is assigned to the
clone's Filter property.
That last piece of interesting code in this project is associated with the
OnClick event handler of the Include Current OrderNo checkbox. This code, shown
in the following method, simply invokes the OnDataChange event handler of DataSource1 to
update the detail view.
procedure TForm1.IncludeCurrentOrderCbxClick(Sender: TObject);
begin
DataSource1DataChange(Self, ClientDataSet1.Fields[0]);
end;
Although this project is really quite simple, I think
the results are nothing short of fantastic.
Deleting a Range of Records
This third, and final example further demonstrates how creative use of a
cloned cursor can provide you with an alternative mechanism for performing a
task. In this case, the task is to delete a range of records from a ClientDataSet.
Without using a cloned cursor, you might delete a range of records from a
ClientDataSet by searching for records in the range and deleting them, one by one.
Alternatively, you might set an index and use the SetRange method to filter the
ClientDataSet to include only those records you want to delete, which you then
delete, one by one.
Whether you use one of these approaches, or some similar technique,
your code might also need to be responsible for restoring the pre-deletion view
of the ClientDataSet, in particular if the ClientDataSet was being displayed in
the user interface. For example, you would probably what to note the current
record before 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 of those that was deleted).
Similarly, if you had to switch indexes in order to perform the deletion, you
would likely want to restore the previous index.
Using a cloned cursor to delete the range provides you with an important
benefit. Specifically, you can perform the deletion using the cloned cursor
without having to worry about the view of the original ClientDataSet. Specifically, once
you clone the cursor, you perform all changes to the ClientDataSet's view on the
clone, leaving the original view undisturbed.
The
following is the CDSDeleteRange function found in the CDSDeleteRange project,
which you can download from 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 function begins by creating a temporary ClientDataSet, which is cloned
from the ClientDataSet passed to the function in the first parameter. The clone
is then indexed and filtered using a range, after which all records in the range
are deleted.
The following figure shows the running CDSDeleteRange project. This figure
depicts the
application just prior to clicking the button labeled Delete Range. As you can
see in this figure, the range to be deleted includes all records where the State
field contains the value HI.
While this example project includes only one field in the range, in practice
you can have up to as many fields in the range as there are fields in the
current index. For more information on SetRange, see "Searching a
ClientDataSet," or refer to the SetRange entry in the online documentation.
The following is the code associated with the OnClick event handler of the
button labeled Delete Range. As you can see, the deletion is performed simply by
calling the CDSDeleteRange
procedure TForm1.Button1Click(Sender: TObject);
begin
if (Edit1.Text = '') and (Edit2.Text = '') then
begin
ShowMessage('Enter a range before attempting to delete');
Exit;
end;
CDSDeleteRange(ClientDataSet1, IndexListBox.Items[IndexListBox.ItemIndex],
[Edit1.Text],[Edit2.Text]);
end;
The following figure shows this same application immediately following the
deletion of the range. Note that because the deletion was performed by the
clone, the original view of the displayed ClientDataSet is undisturbed, with the
exception, of course, of the removal of the records in the range. Also, because
operations performed on the data store and change log are immediately visible to
all ClientDataSets using a shared in-memory dataset, the deleted records
immediately disappear from the displayed grid, without requiring any kind of
refresh.
I have to admit, I really like this example. Keep in mind, however, that the
point of this example is not about deleting records. It is that a cloned cursor provided an
attractive alternative mechanism for performing the task.
About the Author
Cary Jensen is President of Jensen Data Systems, Inc., a Texas-based training
and consulting company that won the 2002 Delphi Informant Magazine Readers
Choice award for Best Training. He is the author and presenter for Delphi
Developer Days (www.DelphiDeveloperDays.com), an information-packed Delphi
(TM) seminar series that tours North America and Europe, and Delphi Developer
Days Power Workshops, focused Delphi (TM) training. Cary is also an
award-winning, best-selling co-author of eighteen books, including Building
Kylix Applications (2001, Osborne/McGraw-Hill), Oracle JDeveloper (1999, Oracle
Press), JBuilder Essentials (1998, Osborne/McGraw-Hill), and Delphi In Depth
(1996, Osborne/McGraw-Hill). For information about onsite training and
consulting you can contact Cary at cjensen@jensendatasystems.com, or visit his
Web site at www.JensenDataSystems.com.
Click here for a
listing of upcoming seminars, workshops, and conferences where Cary Jensen is
presenting.
Breaking News: Get hands-on training with Cary Jensen. Jensen Data
Systems, Inc. is proud to announce Delphi
Developer Days Power Workshops, focused Delphi (TM) training. These intense,
two-day workshops give you the opportunity to explore and implement a variety of
Delphi techniques with Cary Jensen, one of the world's leading Delphi experts.
Workshop topics include ClientDataSet, IntraWeb, and more. Due to the
hands-on nature of these workshops, class size is very limited. Reserve your
seat now. Click here
for more information about Delphi Developer Days Power Workshops, or visit http://www.DelphiDeveloperDays.com.


Copyright
) 2002 Cary Jensen, Jensen Data Systems, Inc.
ALL RIGHTS RESERVED. NO PART OF THIS DOCUMENT CAN BE COPIED IN ANY FORM WITHOUT
THE EXPRESS, WRITTEN CONSENT OF THE AUTHOR.
Connect with Us