Now that you understand the theory of how the data link classes work, let's begin building some data-aware controls. The first two examples are data-aware versions of the ProgressBar and TrackBar common controls. You can use the first to display a numeric value, such as a percentage, in a visual way. You can use the second to allow a user to change the numeric value.
The code for all of the components built in this chapter is in the MdDataPack folder, which also includes a similarly named package for installing them all. Other folders include sample programs that use these components.
A data-aware version of the ProgressBar control is a relatively simple case of a data-aware control, because it is read-only. This component is derived from the version that's not data-aware and adds a few properties of the data link object it encapsulates:
type TMdDbProgress = class(TProgressBar) private FDataLink: TFieldDataLink; function GetDataField: string; procedure SetDataField (Value: string); function GetDataSource: TDataSource; procedure SetDataSource (Value: TDataSource); function GetField: TField; protected // data link event handler procedure DataChange (Sender: TObject); public constructor Create (AOwner: TComponent); override; destructor Destroy; override; property Field: TField read GetField; published property DataField: string read GetDataField write SetDataField; property DataSource: TDataSource read GetDataSource write SetDataSource; end;
As with every data-aware component that connects to a single field, this control makes available the DataSource and DataField properties. There is little code to write; simply export the properties from the internal data link object, as follows:
function TMdDbProgress.GetDataField: string; begin Result := FDataLink.FieldName; end; procedure TMdDbProgress.SetDataField (Value: string); begin FDataLink.FieldName := Value; end; function TMdDbProgress.GetDataSource: TDataSource; begin Result := FDataLink.DataSource; end; procedure TMdDbProgress.SetDataSource (Value: TDataSource); begin FDataLink.DataSource := Value; end; function TMdDbProgress.GetField: TField; begin Result := FDataLink.Field; end;
Of course, to make this component work, you must create and destroy the data link when the component itself is created or destroyed:
constructor TMdDbProgress.Create (AOwner: TComponent); begin inherited Create (AOwner); FDataLink := TFieldDataLink.Create; FDataLink.Control := Self; FDataLink.OnDataChange := DataChange; end; destructor TMdDbProgress.Destroy; begin FDataLink.Free; FDataLink := nil; inherited Destroy; end;
In the preceding constructor, notice that the component installs one of its own methods as an event handler for the data link. This is where the component's most important code resides. Every time the data changes, you modify the output of the progress bar to reflect the value of the current field:
procedure TMdDbProgress.DataChange (Sender: TObject); begin if FDataLink.Field is TNumericField then Position := FDataLink.Field.AsInteger else Position := Min; end;
Following the convention of the VCL data-aware controls, if the field type is invalid, the component doesn't display an error message—it disables the output. Alternatively, you might want to check the field type when the SetDataField method assigns it to the control.
In Figure 17.1 you can see an example of the DbProgr application's output, which uses both a label and a progress bar to display an order's quantity information. Thanks to this visual clue, you can step through the records and easily spot orders for many items. One obvious benefit to this component is that the application contains almost no code, because all the important code is in the MdProgr unit that defines the component.
As you've seen, a read-only data-aware component is not difficult to write. However, it becomes extremely complex to use such a component inside a DBCtrlGrid container.
If you remember the discussion of the Notification method in Chapter 9, you might wonder what happens if the data source referenced by the data-aware control is destroyed. The good news is that the data source has a destructor that removes itself from its own data links. So, there is no need for a Notification method for data-aware controls, although you'll see books and articles suggesting it, and VCL includes plenty of this useless code.
The next step is to write a component that allows a user to modify the data in a database, not just browse it. The overall structure of this type of component isn't very different from the previous version, but there are a few extra elements. In particular, when the user begins interacting with the component, the code should put the dataset into edit mode and then notify the dataset that the data has changed. The dataset will then use a FieldDataLink event handler to ask for the updated value.
To demonstrate how you can create a data-aware component that modifies the data, I extended the TrackBar control. This isn't the simplest example, but it demonstrates several important techniques.
Here's the definition of the component's class (from the MdTrack unit of the MdDataPack package):
type TMdDbTrack = class(TTrackBar) private FDataLink: TFieldDataLink; function GetDataField: string; procedure SetDataField (Value: string); function GetDataSource: TDataSource; procedure SetDataSource (Value: TDataSource); function GetField: TField; procedure CNHScroll(var Message: TWMHScroll); message CN_HSCROLL; procedure CNVScroll(var Message: TWMVScroll); message CN_VSCROLL; procedure CMExit(var Message: TCMExit); message CM_EXIT; protected // data link event handlers procedure DataChange (Sender: TObject); procedure UpdateData (Sender: TObject); procedure ActiveChange (Sender: TObject); public constructor Create (AOwner: TComponent); override; destructor Destroy; override; property Field: TField read GetField; published property DataField: string read GetDataField write SetDataField; property DataSource: TDataSource read GetDataSource write SetDataSource; end;
Compared to the read-only data-aware control you built earlier, this class is more complex, because it has three message handlers, including component notification handlers, and two new event handlers for the data link. The component installs these event handlers in the constructor, which also disables the component:
constructor TMdDbTrack.Create (AOwner: TComponent); begin inherited Create (AOwner); FDataLink := TFieldDataLink.Create; FDataLink.Control := Self; FDataLink.OnDataChange := DataChange; FDataLink.OnUpdateData:= UpdateData; FDataLink.OnActiveChange := ActiveChange; Enabled := False; end;
The get and set methods and the DataChange event handler are similar to those in the TMdDbProgress component. The only difference is that whenever the data source or data field changes, the component checks the current status to see whether it should enable itself:
procedure TMdDbTrack.SetDataSource (Value: TDataSource); begin FDataLink.DataSource := Value; Enabled := FDataLink.Active and (FDataLink.Field <> nil) and not FDataLink.Field.ReadOnly; end;
This code tests three conditions: the data link should be active, the link should refer to an actual field, and the field shouldn't be read-only.
When the user changes the field, the component should consider that the field name might be invalid; to test for this condition, the component uses a try/finally block:
procedure TMdDbTrack.SetDataField (Value: string); begin try FDataLink.FieldName := Value; finally Enabled := FDataLink.Active and (FDataLink.Field <> nil) and not FDataLink.Field.ReadOnly; end; end;
The control executes the same test when the dataset is enabled or disabled:
procedure TMdDbTrack.ActiveChange (Sender: TObject); begin Enabled := FDataLink.Active and (FDataLink.Field <> nil) and not FDataLink.Field.ReadOnly; end;
The most interesting portion of this component's code is related to its user interface. When a user begins moving the scroll thumb, the component puts the dataset into edit mode, lets the base class update the thumb position, and alerts the data link (and therefore the data source) that the data has changed. Here's the code:
procedure TMdDbTrack.CNHScroll(var Message: TWMHScroll); begin // enter edit mode FDataLink.Edit; // update data inherited; // let the system know FDataLink.Modified; end; procedure TMdDbTrack.CNVScroll(var Message: TWMVScroll); begin // enter edit mode FDataLink.Edit; // update data inherited; // let the system know FDataLink.Modified; end;
When the dataset needs new data—for example, to perform a Post operation—it requests it from the component via the TFieldDataLink class's OnUpdateData event:
procedure TMdDbTrack.UpdateData (Sender: TObject); begin if FDataLink.Field is TNumericField then FDataLink.Field.AsInteger := Position; end;
If the proper conditions are met, the component updates the data in the proper table field. Finally, if the component loses the input focus, it should force a data update (if the data has changed) so that any other data-aware components showing the value of that field will display the correct value as soon as the user moves to a different field. If the data hasn't changed, the component doesn't bother updating the data in the table. This is the standard CMExit code for components used by VCL and borrowed for this component:
procedure TMdDbTrack.CMExit(var Message: TCMExit); begin try FDataLink.UpdateRecord; except SetFocus; raise; end; inherited; end;
A demo program is available for testing this component; you can see its output in Figure 17.2. The DbTrack program contains a check box to enable and disable the table, the visual components, and a couple of buttons you can use to detach the vertical TrackBar component from the field it relates to. I placed these on the form to test enabling and disabling the track bar.