Navigating Client Datasets
A dataset is worthless without a means of moving forward and/or backward through it. Delphi's datasets provide a large number of methods for traversing a dataset. The following sections discuss Delphi's support for dataset navigation.
Sequential Navigation
The most basic way to navigate through a dataset is sequentially in either forward or reverse order. For example, you might want to iterate through a dataset when printing a report, or for some other reason. Delphi provides four simple methods to accomplish this:
First moves to the first record in the dataset. First always succeeds, even if the dataset is empty. If it is empty, First sets the dataset's EOF (end of file) property to True.
Next moves to the next record in the dataset (if the EOF property is not already set). If EOF is True, Next will fail. If the call to Next reaches the end of the file, it sets the EOF property to True.
Last moves to the last record in the dataset. Last always succeeds, even if the dataset is empty. If it is empty, Last sets the dataset's BOF (beginning of file) property to True.
Prior moves to the preceding record in the dataset (if the BOF property is not already set). If BOF is True, Prior will fail. If the call to Prior reaches the beginning of the file, it sets the BOF property to True.
The following code snippet shows how you can use these methods to iterate through a dataset:
if not ClientDataSet1.IsEmpty then begin ClientDataSet1.First; while not ClientDataSet1.EOF do begin // Process the current record ClientDataSet1.Next; end; ClientDataSet1.Last; while not ClientDataSet1.BOF do begin // Process the current record ClientDataSet1.Prior; end; end;
Random-Access Navigation
In addition to First, Next, Prior, and Last (which provide for sequential movement through a dataset), TClientDataSet provides two ways of moving directly to a given record: bookmarks and record numbers.
Bookmarks
A bookmark used with a client dataset is very similar to a bookmark used with a paper-based book: It marks a location in a dataset so that you can quickly return to it later.
There are three operations that you can perform with bookmarks: set a bookmark, return to a bookmark, and free a bookmark. The following code snippet shows how to do all three:
var Bookmark: TBookmark; begin Bookmark := ClientDataSet1.GetBookmark; try // Do something with ClientDataSet1 here that changes the current record ... ClientDataSet1.GotoBookmark(Bookmark); finally ClientDataSet1.FreeBookmark(Bookmark); end; end;
You can create as many bookmarks as you want for a dataset. However, keep in mind that a bookmark allocates a small amount of memory, so you should be sure to free all bookmarks using FreeBookmark or your application will leak memory.
There is a second set of operations that you can use for bookmarks instead of GetBookmark/GotoBookmark/FreeBookmark. The following code shows this alternate method:
var BookmarkStr: string; begin BookmarkStr := ClientDataSet1.Bookmark; try // Do something with ClientDataSet1 here that changes the current record ... finally ClientDataSet1.Bookmark := BookmarkStr; end; end;
Because the bookmark returned by the property, Bookmark, is a string, you don't need to concern yourself with freeing the string when you're done. Like all strings, Delphi automatically frees the bookmark when it goes out of scope.
Record Numbers
Client datasets support a second way of moving directly to a given record in the dataset: setting the RecNo property of the dataset. RecNo is a one-based number indicating the sequential number of the current record relative to the beginning of the dataset.
You can read the RecNo property to determine the current absolute record number, and write the RecNo property to set the current record. There are two important things to keep in mind with respect to RecNo:
Attempting to set RecNo to a number less than one, or to a number greater than the number of records in the dataset results in an At beginning of table, or an At end of table exception, respectively.
The record number of any given record is not guaranteed to be constant. For instance, changing the active index on a dataset alters the record number of all records in the dataset.
NOTE
You can determine the number of records in the dataset by inspecting the dataset's RecordCount property. When setting RecNo, never attempt to set it to a number higher than RecordCount.
However, when used discriminately, RecNo has its uses. For example, let's say the user of your application wants to delete all records between the John Smith record and the Fred Jones record. The following code shows how you can accomplish this:
var RecNoJohn: Integer; RecNoFred: Integer; Index: Integer; begin if not ClientDataSet1.Locate('Name', 'John Smith', []) then raise Exception.Create('Cannot locate John Smith'); RecNoJohn := ClientDataSet1.RecNo; if not ClientDataSet1.Locate('Name', 'Fred Jones', []) then raise Exception.Create('Cannot locate Fred Jones'); RecNoFred := ClientDataSet1.RecNo; if RecNoJohn < RecNoFred then // Locate John again ClientDataSet1.RecNo := RecNoJohn; for Index := 1 to Abs(RecNoJohn - RecNoFred) + 1 do ClientDataSet1.Delete; end;
This code snippet first locates the two bounding records and remembers their absolute record numbers. Then, it positions the dataset to the lower record number. If Fred occurs before John, the dataset is already positioned at the lower record number.
Because records are sequentially numbered, we can subtract the two record numbers (and add one) to determine the number of records to delete. Deleting a record makes the next record current, so a simple for loop handles the deletion of the records.
Keep in mind that RecNo isn't usually going to be your first line of attack for moving around in a dataset, but it's handy to remember that it's available if you ever need it.
Listing 3.2 contains the complete source code for an application that demonstrates the different navigational methods of client datasets.
Listing 3.2 NavigateMainForm.pas
unit MainForm; interface uses SysUtils, Classes, QGraphics, QControls, QForms, QDialogs, QStdCtrls, DB, DBClient, QExtCtrls, QActnList, QGrids, QDBGrids, QDBCtrls; type TfrmMain = class(TForm) DataSource1: TDataSource; pnlClient: TPanel; pnlBottom: TPanel; btnFirst: TButton; btnLast: TButton; btnNext: TButton; btnPrior: TButton; DBGrid1: TDBGrid; ClientDataSet1: TClientDataSet; btnSetRecNo: TButton; DBNavigator1: TDBNavigator; btnGetBookmark: TButton; btnGotoBookmark: TButton; procedure FormCreate(Sender: TObject); procedure btnNextClick(Sender: TObject); procedure btnLastClick(Sender: TObject); procedure btnSetRecNoClick(Sender: TObject); procedure btnFirstClick(Sender: TObject); procedure btnPriorClick(Sender: TObject); procedure btnGetBookmarkClick(Sender: TObject); procedure btnGotoBookmarkClick(Sender: TObject); private { Private declarations } FBookmark: TBookmark; public { Public declarations } end; var frmMain: TfrmMain; implementation {$R *.xfm} procedure TfrmMain.FormCreate(Sender: TObject); begin ClientDataSet1.LoadFromFile('C:\Employee.cds'); end; procedure TfrmMain.btnFirstClick(Sender: TObject); begin ClientDataSet1.First; end; procedure TfrmMain.btnPriorClick(Sender: TObject); begin ClientDataSet1.Prior; end; procedure TfrmMain.btnNextClick(Sender: TObject); begin ClientDataSet1.Next; end; procedure TfrmMain.btnLastClick(Sender: TObject); begin ClientDataSet1.Last; end; procedure TfrmMain.btnSetRecNoClick(Sender: TObject); var Value: string; begin Value := '1'; if InputQuery('RecNo', 'Enter Record Number', Value) then ClientDataSet1.RecNo := StrToInt(Value); end; procedure TfrmMain.btnGetBookmarkClick(Sender: TObject); begin if Assigned(FBookmark) then ClientDataSet1.FreeBookmark(FBookmark); FBookmark := ClientDataSet1.GetBookmark; end; procedure TfrmMain.btnGotoBookmarkClick(Sender: TObject); begin if Assigned(FBookmark) then ClientDataSet1.GotoBookmark(FBookmark) else ShowMessage('No bookmark set!'); end; end.
Figure 3.6 shows this program at runtime.