Sunday, April 25, 2010

In-Memory DataSets: ClientDataSets and .NET DataTables Part 5: Managing the Change Cache

The change cache is the source of much of an in-memory dataset's power. It is through the change cache that you can permit a user to review their changes before committing them, as well as permit you to programmatically work with changes that have been made to the dataset.

Note: In the first article in this series, I called the change cache the change log. A reader responded that what I described was not really a log, since it doesn’t keep track of individual changes, only information about the state of individual records. The term change log originates from the TClientDataSet interface, which includes the LogChanges property and the MergeChangeLog method. I agree that the term change cache more accurately reflects this feature of in-memory datasets, and will use that term from now on.

The following are operations that can be performed on the change cache for any in-memory dataset:

  • Detecting changes
  • Cancel all changes
  • Filter on changes
  • Detect field-level changes
  • Cancel a single change
  • Erase the change cache
  • Commit changes in the change cache to the underlying database

The first six of these operations are described in the following sections. Committing changes to an underlying database is a much larger topic, and I will cover it in the next installment of this series.

Detecting Changes

You use the ClientDataSet's ChangeCount property to determine if there have been any changes to the data in the ClientDataSet since if was originally loaded. There are no changes if ChangeCount returns 0 (zero). If there have been one or more changes, ChangeCount returns the total number of records that have been inserted, deleted, or modified (including changes to nested datasets).

Note that ChangeCount only counts changed records. For example, a record that is inserted and then modified counts as only one change.

The following code segment demonstrates the use of ChangeCount.

if ClientDataSet1.State in [dsEdit, dsInsert] then
ClientDataSet1.Post;
if ClientDataSet1.ChangeCount > 0 then
ClientDataSet1.ApplyUpdates(0);

Note that there is a bug in ClientDataSets in the original release with Delphi 6 and 7 with respect to ChangeCount. If you save a ClientDataSet to a file or stream, and then later restore the ClientDataSet, ChangeCount will be zero immediately after you reload the ClientDataSet, even if there are changes in the change cache.

In .NET, you call the DataTable's GetChanges method. If GetChanges returns a nil reference, there are no changes. If there are changes, the DataTable returned by GetChanges contains the inserted, deleted, and modified records.

if DataTable1.GetChanges <> nil then
begin
//Work with the changes here
end;

Canceling All Changes

When you cancel all changes, the in-memory dataset reverts to the values it contained when it was originally loaded. You cancel all changes in a ClientDataSet by calling the CancelUpdates method. With a .NET DataTable, you call RejectChanges.

Canceling all changes is an irreversible action.

Filtering On Changes

Both ClientDataSets and .NET DataTables permit you to filter the dataset to display only inserted, deleted, and modified records. With a ClientDataSet, you invoke this operation using the StatusFilter property, which can be set to include the following four flags: usModified, usInserted, usDeleted, and usUnModified.

The following code segment demonstrates the use of StatusFilter:

if ClientDataSet1.State in [dsEdit, dsInsert] then
ClientDataSet1.Post;
if ClientDataSet1.ChangeCount > 0 then
begin
ClientDataSet1.StatusFilter := [usModified, usInserted, usDeleted];
//Do something with the changed records
end;

Just as you must use a DataView to obtain a filtered view of a DataTable, you must use a DataView to filter on Changed records. In this case, you use the RowStateFilter and set it to one of the following eight values of the DataViewRowState enumeration: OriginalRows, CurrentRows, Added, Deleted, ModifiedOriginal, ModifiedCurrent, UnChanged, and None.

The following code demonstrates how to obtain a DataView that contains only the records deleted from a DataTable:

DataView1 := DataView.Create(DataTable1);
DataView1.RowStateFilter := DataViewRowState.Deleted;
if DataView1.Count > 0 then
begin
// Do something with the deleted records
end;

Detecting Field-Level Changes

With ClientDataSets, you detect field-level changes by examining the OldValue and NewValue variant properties of your ClientDataSet's TFields. The following code demonstrates how to do this:

if ClientDataSet1.UpdateStatus = usModified then
begin
if ClientDataSet1.Fields[0].OldValue <> ClientDataSet1.Fields[0].NewValue then
begin
//The first field in the current record has been changed
end;
end;

With .NET DataTables, you must obtain two DataViews for the table, setting the RowStateFilter on one of them to ModifiedOriginal, and the RowStateFilter of the second to ModifiedCurrent. You can them compare the fields of the two DataViews to determine what has changed. This is demonstrated in the following code sample:

DataView1 := DataView.Create(DataTable1);
DataView2 := DataView.Create(DataTable2);
DataView1.RowStateFilter := DataViewRowState.ModifiedOriginal;
DataView2.RowStateFilter := DataViewRowState.ModifiedCurrent;
if DataView1.Item[0].Row[0].ToString <> DataView0.Item[0].Row[0].ToString then
begin
//Field 1 of record 1 has been changed.
//Assumes a string-compatible field
end;

Canceling a Single Change

ClientDataSets provides you with two mechanisms for canceling a change to a record. You call the UndoLastChange method to revert the record that was last inserted, deleted, or modified to its original state. By comparison, if the ClientDataSet is pointing to a record that was inserted, deleted, or modified, you can call RevertRecord.

The following code demonstrates the use of RevertRecord:

if ClientDataSet1.UpdateStatus in [usInserted, usDeleted, usModified] then
ClientDataSet1.RevertRecord;

For a .NET DataRow, you call the RejectChanges method to restore an inserted, deleted, or modified DataRow to its original state. The following code demonstrates how to restore all deleted records to a DataTable:


var
DataView2: DataView;
i: Integer;
begin
DataView2 := DataView.Create(DataSet1.Tables[0]);
DataView2.RowStateFilter := DataViewRowState.Deleted;
for i := (DataView2.Count -1) downto 0 do
DataView2.item[i].Row.RejectChanges;

Erasing the Change Cache

Erasing the change cache leaves any changes to the dataset intact, but deletes the record of those changes. Most developers do not want to erase the change cache since it makes it impossible to undo changes and also erases the information necessary to write the changes to the underlying database. About the only time that erasing the change cache makes sense is when you want to make the changes permanent before storing the dataset in a file or stream, and never have to resolve those changes back to some underlying database.

With ClientDataSets, you erase the change cache by calling MergeChangeLog. (Note that you can simply turn off the change cache with a ClientDataSet by setting the ClientDataSet's LogChanges property to a Boolean False before making any changes.)

With .NET DataTables, call AcceptChanges to erase the change cache.

Copyright © 2010 Cary Jensen. All Rights Reserved.

3 comments:

  1. "if the ClientDataSet is pointing to a record that was inserted, deleted, or modified, you can call RevertRecord."

    How can the datset be pointing to a record that has been deleted ?
    i.e. it will not be showing in the current records, so you can't call RevertRecord to undo the Delete action.

    ReplyDelete
  2. You use StatusFilter to enable the display of deleted records. If you wanted to display only the deleted records, you would use something like this:

    ClientDataSet1.StatusFilter := [usDeleted];

    ReplyDelete
  3. The easiest way to throw away the change cache is to call CancelUpdates.

    ReplyDelete