Although it is generally faster to write Delphi applications based on data-aware controls, this approach is not required. When you need precise control over the user interface of a database application, you might want to customize the transfer of the data from the field objects to the visual controls. My view is that doing so is necessary only in specific cases, because you can customize the data-aware controls extensively by setting the properties and handling the events of the field objects. However, trying to work without the data-aware controls should help you better understand Delphi's default behavior.
The development of an application not based on data-aware controls can follow two different approaches: You can mimic the standard Delphi behavior in code, possibly departing from it in specific cases, or you can go for a more customized approach. I'll demonstrate the first technique in the NonAware example and the latter in the SendToDb example.
To build an application that doesn't use data-aware controls but behaves like a standard Delphi application, you can write event handlers for the operations that would be performed automatically by data-aware controls. Basically, you need to place the dataset in edit mode as the user changes the content of the visual controls and update the field objects of the dataset as the user exits from the controls, moving the focus to another element.
This approach can be handy for integrating a control that's not data aware into a standard application.
The other element of the NonAware example is a list of buttons corresponding to some of the buttons in the DBNavigator control; these buttons are connected to five custom actions. I could not use the standard dataset actions for this example because they automatically hook to the data source associated with the control having the focus—a mechanism that fails with the example's non-data-aware edit boxes. In general, you could also hook a data Source with each of the actions' DataSource property, but in this specific case we don't have a data source in the example.
The program has several event handlers I haven't used for past applications using data-aware controls. First, you have to show the current record's data in the visual controls (as in Figure 13.14) by handling the OnAfterScroll event of the dataset component:
procedure TForm1.cdsAfterScroll(DataSet: TDataSet); begin EditName.Text := cdsName.AsString; EditCapital.Text := cdsCapital.AsString; ComboContinent.Text := cdsContinent.AsString; EditArea.Text := cdsArea.AsString; EditPopulation.Text := cdsPopulation.AsString; end;
The control's OnStateChange event handler displays the table's status in a status bar control. As the user begins typing in one of the edit boxes or drops down the combo box list, the program sets the table to edit mode:
procedure TForm1.EditKeyPress(Sender: TObject; var Key: Char); begin if not (cds.State in [dsEdit, dsInsert]) then cds.Edit; end;
This method is connected to the OnKeyPress event of the five components and is similar to the OnDropDown event handler of the combo box. As the user leaves one of the visual controls, the OnExit event handler copies the data to the corresponding field, as in this case:
procedure TForm1.EditCapitalExit(Sender: TObject); begin if (cds.State in [dsEdit, dsInsert]) then cdsCapital.AsString := EditCapital.Text; end;
The operation takes place only if the table is in edit mode—that is, only if the user has typed in this or another control. This behavior is not ideal, because extra operations are done even if the edit box's text didn't change; however, the extra steps happen fast enough that they aren't a concern. For the first edit box, you check the text before copying it, raising an exception if the edit box is empty:
procedure TForm1.EditNameExit(Sender: TObject); begin if (cds.State in [dsEdit, dsInsert]) then if EditName.Text <> '' then cdsName.AsString := EditName.Text else begin EditName.SetFocus; raise Exception.Create ('Undefined Country'); end; end;
An alternative approach for testing the value of a field is to handle the dataset's BeforePost event. Keep in mind that in this example, the posting operation is not handled by a specific button but takes place as soon as a user moves to a new record or inserts a new one:
procedure TForm1.cdsBeforePost(DataSet: TDataSet); begin if cdsArea.Value < 100 then raise Exception.Create ('Area too small'); end;
In each case, an alternative to raising an exception is to set a default value. However, if a field has a default value, it is better to set it up front, so a user can see which value will be sent to the database. To accomplish this, you can handle the dataset's AfterInsert event, which is fired immediately after a new record has been created (I could have used the OnNewRecord event, as well):
procedure TForm1.cdsAfterInsert(DataSet: TDataSet); begin cdsContinent.Value := 'Asia'; end;
You can further customize your application's user interface if you decide not to handle the same sequence of editing operations as in standard Delphi data-aware controls. This approach allows you complete freedom, although it might cause some side effects (such as limited ability to handle concurrency, which I'll discuss in Chapter 14).
For this new example, I replaced the first edit box with another combo box and replaced all the buttons related to table operations (which corresponded to DBNavigator buttons) with two custom buttons that get the data from the database and send an update to it. Again, this example has no DataSource component.
The GetData method, connected to the corresponding button, gets the fields corresponding to the record indicated in the first combo box:
procedure TForm1.GetData; begin cds.Locate ('Name', ComboName.Text, [loCaseInsensitive]); ComboName.Text := cdsName.AsString; EditCapital.Text := cdsCapital.AsString; ComboContinent.Text := cdsContinent.AsString; EditArea.Text := cdsArea.AsString; EditPopulation.Text := cdsPopulation.AsString; end;
This method is called whenever the user clicks the button, selects an item in the combo box, or presses the Enter key while in the combo box:
procedure TForm1.ComboNameClick(Sender: TObject); begin GetData; end; procedure TForm1.ComboNameKeyPress(Sender: TObject; var Key: Char); begin if Key = #13 then GetData; end;
To make this example work smoothly, at startup the combo box is filled with the names of all the countries in the table:
procedure TForm1.FormCreate(Sender: TObject); begin // fill the list of names cds.Open; while not cds.Eof do begin ComboName.Items.Add (cdsName.AsString); cds.Next; end; end;
With this approach, the combo box becomes a sort of selector for the record, as you can see in Figure 13.15. Thanks to this selection, the program doesn't need navigational buttons.
The user can also change the values of the controls and click the Send button. The code to be executed depends on whether the operation is an update or an insert. You can determine this by looking at the name (although with this code, a wrong name can no longer be modified):
procedure TForm1.SendData; begin // raise an exception if there is no name if ComboName.Text = '' then raise Exception.Create ('Insert the name'); // check if the record is already in the table if cds.Locate ('Name', ComboName.Text, [loCaseInsensitive]) then begin // modify found record cds.Edit; cdsCapital.AsString := EditCapital.Text; cdsContinent.AsString := ComboContinent.Text; cdsArea.AsString := EditArea.Text; cdsPopulation.AsString := EditPopulation.Text; cds.Post; end else begin // insert new record cds.InsertRecord ([ComboName.Text, EditCapital.Text, ComboContinent.Text, EditArea.Text, EditPopulation.Text]); // add to list ComboName.Items.Add (ComboName.Text) end;
Before sending the data to the table, you can do any sort of validation test on the values. In this case, it doesn't make sense to handle the events of the database components, because you have full control over when the update or insert operation is performed.