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 of the data. This article shows you how to use this important capability.

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.



Server Response from: ETNASC04