I usually try to start from the beginning, covering the more basic
techniques before 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 of a ClientDataSet, as well as how
to create its structure and indexes. In this installment I will take an
introductory look at the manipulation of data stored in a ClientDataSet. Topics
to be covered include basic programmatic navigation of 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 filters. Only after
these foundation 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, defining aggregate fields, and more.
For those of you who are already well versed in working with datasets,
you will only need to quickly skim through this article to see if there is
something that you find interesting. If you are fairly new to dataset
programming, however, this article will provide you with essential information
on the use of ClientDataSets. As an added benefit, most of these techniques are
appropriate for any other datasets that you may have a chance to use.
While this article focuses primarily on the use of 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 features they
provide.
Navigating with Data-Aware Controls
There are two classes of controls that provide data navigation. The first
class
is navigation-specific controls. Delphi provides you with one control in this
category, the DBNavigator.
The DBNavigator, shown in the following image, provides a VCR-like
interface for 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 Refresh buttons. You can
control which buttons are displayed by a DBNavigator through its
VisibleButtons property. For example, if you are using the DBNavigator in
conjunction with a ClientDataSet that reads and writes its data from a local
file (Borland calls this technology MyBase), you will want to remove the
nbRefresh flag
from the VisibleButtons property, since attempting to Refresh a ClientDataSet
that uses MyBase raises an exception.
Another DBNavigator property whose default value you may want to change is
ShowHint. Some users have difficulty 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 of these hints by editing
the Hints property.
The
second category of controls that provide navigation is the multi-record controls.
Delphi includes
two: the DBGrid
and DBCtrlGrid. A DBGrid displays data in a row/column format. By default, all
fields of the ClientDataSet are displayed in the DBGrid. You can control which
fields are displayed, as well as specific column characteristics, such as color,
by editing the DBGrid's Columns collection property. The following is an example
of 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 of a two-row,
one-column DBCtrlGrid is shown in the following figure.
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 effect as
clicking the Next, Prior, Last, First, and so on, buttons in a DBNavigator. It
is also possible to navigate the records of a dataset using the vertical
scrollbar of these controls.
How you edit a record using these controls also depends on which type of
control you are using, as well as their properties. Using the default properties
of these controls, you can
typically press F2 or click twice on a field in one of these controls to begin
editing. Posting a record occurs when you navigate off 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 Refresh, are not directly supported. Consequently, in most cases,
multi-record controls are combined with a
DBNavigator to provide a complete set of record management options.
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 of 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 following event handlers: AfterCancel, AfterClose,
AfterDelete, AfterEdit, AfterInsert, AfterOpen, AfterPost, AfterRefresh,
AfterScroll, BeforeCancel, BeforeClose, BeforeDelete, BeforeEdit, BeforeInsert,
BeforeOpen, BeforePost, BeforeRefresh, BeforeScroll, 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 of a DataSource provide you with another source
of 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 first record when it is initially opened.
OnStateChange triggers when a ClientDataSet changes between state, such as when
it changes from dsBrowse to dsEdit (when a user enters the edit mode), or when
it changes from dsEdit to dsBrowse (following the posting or cancellation of a
change). Finally, OnUpdateData
triggers when the dataset to which the DataSource points is posting its data.
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 for that matter. For a ClientDataSet, these core navigation
methods include First, Next, Prior, Last, MoveBy, and RecNo. The use of First,
Next, Prior, and Last are pretty much self-explanatory. Each one produces an
effect similar to the corresponding buttons on a DBNavigator.
MoveBy permits you to move forward and backward in a ClientDataSet,
relative to the current record. For example, the following statement moves the
current cursor 5 records forward in the dataset (if possible):
ClientDataSet1.MoveBy(5);
To move backwards
in a dataset, pass
MoveBy a negative number. For example, the following statement will move the
cursor to the record that is 100 records prior to the current records (again, if
possible):
ClientDataSet1.MoveBy(-100);
The use of RecNo
to navigate might
come as a surprise. This property, which is always returns -1 in the TDataSet
class, can be used for two purposes. You can read this property to learn the
position of 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 defined by the
value you assign to this property. For example, the following statement will
move the cursor to the record in the 5th position of the current index order (if
possible):
ClientDataSet1.RecNo := 5;
Each of the
preceding examples has
been qualified by the statement that the operation will succeed if
possible. This qualification has two aspects to it. First, the cursor movement
will not take place if the current record has been edited, but cannot be posted.
For example, if data that cannot pass at least one the ClientDataSet's
Contraints has been added to a record. When you attempt to navigate off 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 of records in the dataset.
For example, if the current record is the last in the dataset, it makes no sense
to move 5 records forward. Similarly, if the current record is the 99th in the
dataset, an attempt to move backwards by 100 records will fail. You can
determine whether an attempt to navigate succeeded or failed by reading the Eof
and Bof properties of the ClientDataSet. Eof (end-of-file) will return True if a
navigation method attempted to move beyond the end of the table. When Eof
returns True, the current record is the last record in the dataset.
Similarly, Bof will return True if a backwards navigation attempted to
move before the beginning of the dataset. In that situation the current record
is the first record in the dataset.
RecNo behaves differently. Attempting to set RecNo to a record beyond the
end of the table, or prior to the beginning of the table, raises an exception.
Scanning a ClientDataSet
Combining several of the methods and properties described so far
provides you with a mechanism for scanning a ClientDataSet. Scanning simply
means the systematic navigation from one record to the next, until all records
in the dataset have been visited. The following code
segment demonstrates how to scan a ClientDataSet.
procedure TForm1.Button1Click(Sender:
TObject);
begin
if not ClientDataSet1.Active then ClientDataSet1.Open;
ClientDataSet1.First;
while not ClientDataSet1.EOF do
begin
//perform some operation based on one or
//more fields of the ClientDataSet
ClientDataSet1.Next;
end;
end;
Editing a ClientDataSet
You edit a current record in a ClientDataSet by calling its Edit method,
after which you change the values of one or more of its fields. Once your
changes have been made, you can either move off the record to attempt to post
the new values, or you can explicitly call the ClientDataSet's Post method. In
most cases, navigating off the record and calling Post produce the same effect.
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 first
instance, if 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 first record in a dataset followed 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 failure).
If you modify 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, if you change
a record, and then find 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, after which your cursor will be on a newly inserted record (assuming
that you started from the dsBrowse state. If you were editing a record prior to
calling Insert or Append, a new record will not be inserted if the record being
edited can not be posted). Once it is inserted, assign data to the fields 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
fields, and attempt to post, all in a single call. The following is the syntax
of the InsertRecord method. The syntax of 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 field
in the dataset. If you want to leave particular field unassigned, include the
value null in the variant array. Fields you want to leave unassigned at the end
of the record can be omitted from the constant array. For example, If you are
inserting and posting a new record into a four-field ClientDataSet, and you want
to assign the first field the value 1000 (a field associated with a unique
index), leave the second and fourth fields unassigned, but assign a value of
'new' to the third record, your InsertRecord invocation may look something like
this:
ClientDataSet1.InsertRecord([1001, null,
'new']);
The following code segment demonstrates another instance of
record
scanning, this time with edits that need to be posted to each record. In this
example, Edit and Post are performed within try blocks. If the record was placed
in the edit mode (which corresponds to the dsEdit state), and cannot be posted,
the change is canceled. If the record cannot even be placed into edit state
(which for a ClientDataSet should only happen if the dataset has its ReadOnly
property set to True), the attempt to post changes is skipped.
procedure TForm1.Button1Click(Sender:
TObject);
begin
if not ClientDataSet1.Active then ClientDataSet1.Open;
ClientDataSet1.First;
while not ClientDataSet1.EOF do
begin
try
ClientDataSet1.Edit;
try
ClientDataSet1.Fields[0].Value :=
UpperCase(ClientDataSet1.Fields[0].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 identify 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 if these changes are being cached, for update in a subsequent call
to ApplyUpdates, the ClientDataSet provides an OnReconcileError event handler
that can be used to process failed postings.
Disabling Controls While
Navigating
If 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 affected 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 of 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 for this. First, the flicker 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 of the slowest processes in most GUI (graphic user
interface) applications. If your navigation involves visiting many records, as
often the case when you are scanning, the repaints of your data-aware controls
represents a massive amount of unnecessary overhead.
To prevent your data-aware controls from 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 of 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 of 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 of 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 fact, if you
call DisableControls and never call a corresponding EnableControls, the
ClientDataSet will appear to the user to have stopped functioning, based on the
lack of activity in the data-aware controls. As a result, it is essential that
if 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-finally
after a call to DisableControls, invoking the corresponding EnableControls in
the finally block.
The following is an example of a scan where the user interface is not updated
until all record navigation has completed.
procedure TForm1.Button1Click(Sender:
TObject);
begin
if not ClientDataSet1.Active then ClientDataSet1.Open;
ClientDataSet1.DisableControls;
try
ClientDataSet1.First;
while not ClientDataSet1.EOF do
begin
try
ClientDataSet1.Edit;
try
ClientDataSet1.Fields[0].Value :=
UpperCase(ClientDataSet1.Fields[0].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;
Navigation Demonstration
The Navigation project, which you can download from Code Central by clicking
this link Navi
gation Project, demonstrates the various methods and properties described in
this
article. The following figure shows this project when it is running.
Each of the Buttons on this form is associated with an event handler that
performs the indicated type of navigation. In addition, this project includes
OnDataChange and OnStateChange DataSource event handlers that are used to update
the panels in the StatusBar at the bottom of the form. These event handlers are
shown in the following code listing.
procedure TForm1.SelectDataFile;
begin
if OpenDialog1.Execute then
begin
if ClientDataSet1.Active then ClientDataSet1.Close;
ClientDataSet1.FileName := OpenDialog1.FileName;
ClientDataSet1.Open;
end
else
Halt;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
SelectDataFile;
end;
procedure TForm1.DataSource1DataChange(Sender: TObject; Field: TField);
begin
StatusBar1.Panels[0].Text := 'Record ' +
IntToStr(ClientDataSet1.RecNo) + ' of ' +
IntToStr(ClientDataSet1.RecordCount);
StatusBar1.Panels[2].Text :=
'BOF = ' + BoolToStr(ClientDataSet1.Bof, True) +
'. ' +
'EOF = ' + BoolToStr(ClientDataSet1.Eof, True) +
'. ';
end;
procedure TForm1.DataSource1StateChange(Sender: TObject);
begin
StatusBar1.Panels[1].Text :=
'State = ' + GetEnumName(TypeInfo(TDataSetState),
Ord(ClientDataSet1.State));
end;
procedure TForm1.FirstBtnClick(Sender: TObject);
begin
ClientDataSet1.First;
end;
procedure TForm1.NextBtnClick(Sender: TObject);
begin
ClientDataSet1.Next;
end;
procedure TForm1.PriorBtnClick(Sender: TObject);
begin
ClientDataSet1.Prior;
end;
procedure TForm1.LastBtnClick(Sender: TObject);
begin
ClientDataSet1.Last;
end;
procedure TForm1.ScanForwardBtnClick(Sender: TObject);
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: TObject);
begin
if ControlsStateBtnGrp.ItemIndex = 1 then
ClientDataSet1.DisableControls;
try
ClientDataSet1.Last;
while not ClientDataSet1.Bof do
begin
//do something with a
Connect with Us