The DVD Catalog is a simple database application that illustrates how to do the following:
Define the structure of the TClientDataSet component
Load data from and save TClientDataSet data to XML files
Filter items in the database
Find a specific item in the database
Keep track of changes made to the database
Iterate through all records
Optimize the size of the database
First, you need to drop the TDataSource and TClientDataSet components on the Designer Surface and link them by assigning the TClientDataSet component to the DataSet property of the TDataSource component. You can also rename the TClientDataSet component to CDS to reduce typing.
Now, select the TClientDataSet component on the Designer Surface, select the FieldDefs property in the Object Inspector, and click the (…) button to display the FieldDefs Collection Editor.
Figure 19-5: The FieldDefs Collection Editor
To add new fields to the table, click the Add New button or press Insert on the keyboard. Since we only need to store the movie's ID and name, click the Add New button twice to create two undefined fields.
Figure 19-6: New fields
To complete the process of adding new fields to the TClientDataSet component, you need to change several properties of both fields. First, you should rename the first field MovieID and set its DataType property to ftInteger. Then, change the name of the second field to MovieName, set its DataType property to ftString, and, since movie names are sometimes pretty long, assign 100 to its Size property. Finally, close the FieldDefs Collection Editor, right-click the TClientDataSet component on the Designer Surface, and select Create DataSet to create the new dataset. You need to do this if you want to assign the TClientDataSet's fields to data-aware controls at design time, which is what we'll do in a moment.
To create the user interface of the DVD Catalog application displayed in Figure 19-1, you simply need to drop several components on the Designer Surface.
First, drop the TActionManager, TActionMainMenuBar, and TActionToolBar components from the Additional category. The TActionMainMenuBar and TActionToolBar components will automatically align themselves to the top of the form.
Then, drop two TGroupBox components from the Standard category, add a TEdit and a TListBox to the left TGroupBox component, and add two data-aware TDBEdit components to the right TGroupBox component. Finally, add as many TLabel components as you need to describe the purpose of these text boxes (see Figure 19-7).
Figure 19-7: The application's user interface
Finally, drop a TStatusBar component from the Win32 category on the Designer Surface and set its SimplePanel property to True. Also, connect the data-aware components in the right TGroupBox with the TDataSource component, assign the MovieID field to the DataField property of the first TDBEdit, and assign the MovieName field to the DataField property of the second TDBEdit.
Before adding actions to the TActionManager component, we need to create three utility methods for loading and saving data and for displaying all movie names in a TListBox. First, here's the DisplayDataset method that displays all movie names in a TListBox component:
procedure TMainForm.DisplayDataset; begin MovieList.Clear; MovieList.Items.BeginUpdate; CDS.First; while not CDS.Eof do begin MovieList.Items.Add(CDS.FieldByName('MovieName').AsString); CDS.Next; end; MovieList.Items.EndUpdate; if MovieList.Items.Count = 0 then StatusBar.SimpleText := 'The disc catalog is empty.' else StatusBar.SimpleText := 'Discs in catalog: ' + IntToStr(MovieList.Items.Count); end;
As you can see, the DisplayDataset method is pretty simple because its only job is to loop through all records in the TClientDataSet and copy anything stored in the MovieName field to the TListBox component.
To begin copying the data from the MovieName field, the DisplayDataset method calls the TClientDataSet's First method to move to the first record. Then it enters the while not Eof loop, which loops through all records in the TClientDataSet. The FieldByName method is used to find the MovieName field in the active record and then, in order to add the value of the MovieName field to a TListBox, the field's AsString property is used to treat the field's value as a string. Finally, the while loop calls the TClientDataSet's Next method, which moves to the next record in the table and makes it the active record.
The OpenCatalog and SaveCatalog methods for loading and saving data are likewise simple:
procedure TMainForm.OpenCatalog(const AFileName: string); begin CDS.Close; CDS.FileName := AFileName; { call CreateDataSet to create an empty dataset } if not FileExists(AFileName) then CDS.CreateDataSet; CDS.Open; { call DisplayDataSet to automatically display the data when a file is opened or when a new file is created } DisplayDataset; end; procedure TMainForm.SaveCatalog(const AFileName: string); begin CDS.FileName := AFileName; CDS.SaveToFile(CDS.FileName, dfXML); end;
The TClientDataSet's SaveToFile method accepts two parameters: the destination file name and the format in which you want to save the TClientDataSet's data. You can save the data in any of the three formats provided by the TDataPacketFormat enumeration:
TDataPacketFormat = (dfBinary, dfXML, dfXMLUTF8);
The default dfBinary value is used to save data into binary CDS files, which are smaller than XML files but cannot be easily viewed and edited in a text editor. The two XML values allow you to save data in XML format.
Now it's time to add several actions to the TActionManager component. Double-click the TActionManager component on the Designer Surface and then click the New Action button five times to add five new actions for the New, Open, Save, Save As, and Exit commands of the File menu, as shown in Figure 19-8.
Figure 19-8: Adding actions to the TActionManager
After you've created the five actions, it's time to define their categories. Since we're going to use these actions to implement the File menu commands, we need to select all five actions and set their Category properties to "File" (see Figure 19-9).
Figure 19-9: Placing the actions in a category
To select these five actions, first click on Action1, then press and hold Shift on the keyboard, and finally click on Action5. The selected actions will be placed into the File category after you type File in the Category property and press Enter.
Categorizing actions in the TActionManager helps you to easily build the main menu. For instance, to create the entire File menu, you only have to select the File category in the TActionManager and drag and drop it to the TActionMain- MenuBar component on the Designer Surface. Note that Figure 19-10 shows a TMainMenuBar component on which the File category was already dropped from the TActionManager component.
Figure 19-10: Creating a menu by dropping a category from the TActionManager to the TActionMainMenuBar
To completely implement these actions, you have to change their Caption properties, add a TOpenDialog and a TSaveDialog component to the Designer Surface, and create an OnExecute event handler for each action. These OnExecute event handlers are displayed in Listing 19-1.
Figure 19-11: File menu actions
Since we're only going to work with XML files, set the DefaultExt property of both common dialogs to "xml" and the Filter property to "MyBase XML Table
(*.xml)|*.xml".
Listing 19-1: OnExecute event handlers
procedure TMainForm.NewActionExecute(Sender: TObject); begin if OpenDialog1.Execute then OpenCatalog(OpenDialog1.FileName); end; procedure TMainForm.OpenActionExecute(Sender: TObject); begin if OpenDialog1.Execute then OpenCatalog(OpenDialog1.FileName); end; procedure TMainForm.SaveActionExecute(Sender: TObject); begin if CDS.FileName <> '' then SaveCatalog(CDS.FileName) else SaveAsAction.Execute; end; procedure TMainForm.SaveAsActionExecute(Sender: TObject); begin if SaveDialog1.Execute then SaveCatalog(SaveDialog1.FileName); end; procedure TMainForm.ExitActionExecute(Sender: TObject); begin Close; end;
To add new records to the TClientDataSet you need to create a simple dialog box to enable the user to enter the ID and name values for the new movie (see Figure 19-12).
Figure 19-12: The Add New Movie dialog box
The OK button's ModalResult property is set to mrNone because it must first check whether both the movie's name and ID are entered and only then return mrOK in ModalResult to let us know everything is OK. The Cancel button's ModalResult property can automatically be set to mrCancel to notify us when the user decides not to add a new record to the TClientDataSet.
The OK button's OnClick event handler, which checks if everything is entered, as well as the OnClick event handlers of the + and – buttons are displayed in the following listing.
Listing 19-2: The application logic of the Add New Movie dialog box
procedure TAddMovieForm.MinusButtonClick(Sender: TObject); var Number: Integer; begin Number := StrToInt(MovieID.Text) - 1; if Number < 1 then begin Number := 1; MessageDlg('Disc ID cannot be less than 1!', mtInformation, [mbOK], 0); end; MovieID.Text := IntToStr(Number); end; procedure TAddMovieForm.PlusButtonClick(Sender: TObject); var Number: Integer; begin Number := StrToInt(MovieID.Text) + 1; MovieID.Text := IntToStr(Number); end; procedure TAddMovieForm.OKButtonClick(Sender: TObject); const ALLOW: array[Boolean] of TModalResult = (mrNone, mrOK); begin { allow adding the item to the database only if both ID and name are properly defined } ModalResult := ALLOW[(MovieID.Text <> '') and (MovieName.Text <> '')]; if ModalResult = mrNone then MessageDlg('Please enter both ID and disc name before clicking OK.', mtInformation, [mbOK], 0); end;
Now that you have the dialog box, you can create the Add action that will display the dialog box to enable the user to enter the required data, check if the user pressed OK on the dialog box, and call the TClientDataSet's AppendRecord method to add the user's data to the TClientDataSet. After you create the Add action, you can drop it on the TActionToolBar component to create the Add button.
Here's the OnExecute event handler of the Add action:
procedure TMainForm.AddActionExecute(Sender: TObject); begin with TAddMovieForm.Create(Self) do begin MovieID.Text := IntToStr(Succ(MovieList.Items.Count)); ShowModal; if ModalResult = mrOK then begin CDS.AppendRecord([StrToInt(MovieID.Text), MovieName.Text]); FindEdit.Clear; { remove the filter to see all items } { refresh the list box } DisplayDataset; end; // if ModalResult Free; end; // with end;
The following figure shows the Add New Movie dialog box and how the entire application looks and works at this stage.
Figure 19-13: The Add New Movie dialog box at run time
To search for records in a TClientDataSet, you can use the Locate method and pass the name of the field you want to search as the first parameter, the value you want to find as the second parameter, and search options as the last parameter:
function TCustomClientDataSet.Locate(const KeyFields: string; const KeyValues: Variant; Options: TLocateOptions): Boolean;
The TLocateOptions type is a set that enables you to perform a case-insensitive search and to search using only part of the value:
type TLocateOption = (loCaseInsensitive, loPartialKey); TLocateOptions = set of TLocateOption;
We need to call the Locate method in the OnClick event handler of the list box to activate the appropriate record in the TClientDataSet when the user selects an item in the list. Here's the OnClick event handler of the list box:
procedure TMainForm.MovieListClick(Sender: TObject); begin if MovieList.ItemIndex <> -1 then CDS.Locate('MovieName', MovieList.Items[MovieList.ItemIndex], []); end;
If you run the application now and click on an item in the list box, the Locate method will find and activate the appropriate record, and you'll be able to see the selected item's details in the data-aware components in the right group box, as shown in Figure 19-14.
Figure 19-14: Using the Locate method to activate the selected item
To delete the active record, we need to create the Delete action, which will call the TClientDataSet's Delete method to remove the active record only when a valid item is selected in the list.
Here are the OnExecute and OnUpdate event handlers of the Delete action:
procedure TMainForm.DeleteActionExecute(Sender: TObject); const DELETE_MOVIE = 'Do you really want to delete disc:'#13; begin if MessageDlg(DELETE_MOVIE + MovieList.Items[MovieList.ItemIndex] + '?', mtConfirmation, mbYesNo, 0) = mrYes then begin CDS.Delete; DisplayDataset; end; // if MessageDlg end;
The following figure shows the Delete action at run time.
Figure 19-15: Deleting records
The two TClientDataSet properties that enable us to filter its records are Filter and Filtered. If you want to display only records that meet a certain condition, you have to set the Filtered property to True and write a filter string in the Filter property. For instance, if you want to display only the movie that's called "A" (without quotes), write the following filter:
MovieName = 'A'
We are now going to implement an incremental filter, which will filter the TClientDataSet's records as we type it in the Find edit box. To do this, we need to use the Substring method in the Filter string. For instance, to check if the first two characters in the movie name are "ab", we have to write the following filter:
Substring(MovieName, 1, 2) = 'ab'
The following listing shows the OnChange event handler of the Find edit box.
Listing 19-3: Incremental filtering
procedure TMainForm.FindEditChange(Sender: TObject); const SUBSTRING = 'Substring(MovieName, 1, %d) = ''%s'''; var selectedMovie: string; begin CDS.Filter := Format(SUBSTRING, [Length(FindEdit.Text), FindEdit.Text]); { enable filtering only if there's text in the FindEdit edit box } CDS.Filtered := FindEdit.Text <> ''; { call DisplayDataSet to show only the filtered items } DisplayDataSet; selectedMovie := CDS.FieldByName('MovieName').AsString; MovieList.ItemIndex := MovieList.Items.IndexOf(selectedMovie); end;
To see how filtering works, take a look at the following figure.
Figure 19-16: Incremental filtering at run time
The TClientDataSet stores data in two separate packets called Data and Delta. The Data packet contains the current state of the data, and the Delta packet logs changes made to the TClientDataSet and holds inserted, updated, and deleted records. The Delta packet is very useful at run time since we can determine how many records were modified and we can select whether we want to accept or discard changes made to the data. However, the Delta packet, like normal data, is also written to the file, which increases the size of the file unnecessarily. The following figure shows a SampleDB.xml file that has both normal data and a change log (the PARAMS tag and the RowState attribute).
Figure 19-17: An XML file that contains both normal data and the change log
To have the application work properly and to reduce the size of the file on disk, we need to call the MergeChangeLog method before saving the data in the SaveCatalog method. Here's the updated SaveCatalog method:
procedure TMainForm.SaveCatalog(const AFileName: string); begin CDS.FileName := AFileName; { call MergeChangeLog to apply changes to the database to reduce its size and to have the OnCloseQuery event handler work properly } CDS.MergeChangeLog; CDS.SaveToFile(CDS.FileName, dfXML); end;
Finally, the last thing we have to do is write an OnCloseQuery event handler that will ask the user if he or she wants to save or discard changes made to the data. To see if the data was changed, check the TClientDataSet's ChangeCount property, which holds the number of changes in the change log. Note that in order to have this property report the correct number of changes, the file loaded from disk must not contain the change log (this is why we have to merge changes before saving the data to disk).
If the user wants to save changes made to the data, call the SaveAction's OnExecute handler. If the user wants to discard the changes, call the TClientDataSet's CancelUpdates method.
Here's the entire OnCloseQuery event handler:
procedure TMainForm.FormCloseQuery(Sender: TObject; var CanClose: Boolean); begin if CDS.ChangeCount > 0 then begin case MessageDlg('Save changes to the database?', mtConfirmation, mbYesNoCancel, 0) of mrYes: SaveAction.Execute; mrNo: CDS.CancelUpdates; mrCancel: CanClose := False; end; // case end; // if end;