Chapter 42
Making a control data aware

When working with database connections, it is often convenient to have controls that are data aware. That is, the application can establish a link between the control and some part of a database. Delphi includes data-aware labels, edit boxes, list boxes, combo boxes, lookup controls, and grids. You can also make your own controls data aware. For more information about using data-aware controls, see "Using data controls".

There are several degrees of data awareness. The simplest is read-only data awareness, or data browsing, the ability to reflect the current state of a database. More complicated is editable data awareness, or data editing, where the user can edit the values in the database by manipulating the control. Note also that the degree of involvement with the database can vary, from the simplest case, a link with a single field, to more complex cases, such as multiple-record controls.

This chapter first illustrates the simplest case, making a read-only control that links to a single field in a dataset. The specific control used will be the calendar created in "Customizing a grid", TSampleCalendar. You can also use the standard calendar control on the Samples page of the Component palette, TCalendar.

The chapter then continues with an explanation of how to make the new data-browsing control a data-editing control.

Creating a data-browsing control

Creating a data-aware calendar control, whether it is a read-only control or one in which the user can change the underlying data in the dataset, involves the following steps:

Creating and registering the component

Creation of every component begins the same way: create a unit, derive a component class, register it, compile it, and install it on the Component palette. This process is outlined in "Creating a new component".

For this example, follow the general procedure for creating a component, with these specifics:

The resulting unit should look like this:

unit DBCal;

interface

uses SysUtils, WinTypes, WinProcs, Messages, Classes, Graphics, Controls, 
  Forms, Grids, Calendar;

type
  TDBCalendar = class(TSampleCalendar)
  end;

procedure Register;

implementation

procedure Register;
begin
  RegisterComponents('Samples', [TDBCalendar]);
end;

end.

You can now proceed with making the new calendar a data browser.

Making the control read-only

Because this data calendar will be read-only with respect to the data, it makes sense to make the control itself read-only, so users will not make changes within the control and expect them to be reflected in the database.

Making the calendar read-only involves,

Note that if you started with the TCalendar component from Delphi's Samples page instead of TSampleCalendar, it already has a ReadOnly property, so you can skip these steps.

Adding the ReadOnly property

By adding a ReadOnly property, you will provide a way to make the control read-only at design time. When that property is set to True, you can make all cells in the control unselectable.

  1. Add the property declaration and a private field to hold the value:
    type
      TDBCalendar = class(TSampleCalendar)
      private
        FReadOnly: Boolean;                                     { field for internal storage }
      public
        constructor Create(AOwner: TComponent); override;     { must override to set default }
      published
        property ReadOnly: Boolean read FReadOnly write FReadOnly default True;
      end;
    ...
    constructor TDBCalendar.Create(AOwner: TComponent);
    begin
      inherited Create(AOwner);                     { always call the inherited constructor! }
      FReadOnly := True;                                             { set the default value }
    end;
    
  2. Override the SelectCell method to disallow selection if the control is read-only. Use of SelectCell is explained in "Excluding blank cells".
    function TDBCalendar.SelectCell(ACol, ARow: Longint): Boolean;
    begin
      if FReadOnly then Result := False                         { cannot select if read only }
      else Result := inherited SelectCell(ACol, ARow);     { otherwise, use inherited method }
    end;
    

Remember to add the declaration of SelectCell to the type declaration of TDBCalendar, and append the override directive.

If you now add the calendar to a form, you will find that the component ignores clicks and keystrokes. It also fails to update the selection position when you change the date.

Allowing needed updates

The read-only calendar uses the SelectCell method for all kinds of changes, including setting the Row and Col properties. The UpdateCalendar method sets Row and Col every time the date changes, but because SelectCell disallows changes, the selection remains in place, even though the date changes.

To get around this absolute prohibition on changes, you can add an internal Boolean flag to the calendar, and permit changes when that flag is set to True:

type
  TDBCalendar = class(TSampleCalendar)
  private
    FUpdating: Boolean;                                  { private flag for internal use }
  protected
    function SelectCell(ACol, ARow: Longint): Boolean; override;
  public
    procedure UpdateCalendar; override;                { remember the override directive }
  end;
...
function TDBCalendar.SelectCell(ACol, ARow: Longint): Boolean;
begin
  if (not FUpdating) and FReadOnly then Result := False       { allow select if updating }
  else Result := inherited SelectCell(ACol, ARow);     { otherwise, use inherited method }
end;

procedure TDBCalendar.UpdateCalendar;
begin
  FUpdating := True;                                         { set flag to allow updates }
  try
    inherited UpdateCalendar;                                          { update as usual }
  finally
    FUpdating := False;                                          { always clear the flag }
  end;
end;

The calendar still disallows user changes, but now correctly reflects changes made in the date by changing the date properties. Now that you have a true read-only calendar control, you are ready to add the data-browsing ability.

Adding the data link

The connection between a control and a database is handled by a class called a data link. The data-link class that connects a control with a single field in a database is TFieldDataLink. There are also data links for entire tables.

A data-aware control owns its data-link class. That is, the control has the responsibility for constructing and destroying the data link. For details on management of owned classes, see "Creating a graphic component".

Establishing a data link as an owned class requires these three steps:

  1. Declaring the class field
  2. Declaring the access properties
  3. Initializing the data link

Declaring the class field

A component needs a field for each of its owned classes, as explained in "Declaring the class fields". In this case, the calendar needs a field of type TFieldDataLink for its data link.

Declare a field for the data link in the calendar:

type
  TDBCalendar = class(TSampleCalendar)
  private
    FDataLink: TFieldDataLink;
  ...
  end;

Before you can compile the application, you need to add DB and DBCtrls to the unit's uses clause.

Declaring the access properties

Every data-aware control has a DataSource property that specifies which data-source class in the application provides the data to the control. In addition, a control that accesses a single field needs a DataField property to specify that field in the data source.

Unlike the access properties for the owned classes in the example in "Creating a graphic component", these access properties do not provide access to the owned classes themselves, but rather to corresponding properties in the owned class. That is, you will create properties that enable the control and its data link to share the same data source and field.

Declare the DataSource and DataField properties and their implementation methods, then write the methods as "pass-through" methods to the corresponding properties of the data-link class:

An example of declaring access properties

type
  TDBCalendar = class(TSampleCalendar)
  private                                            { implementation methods are private }
    ...
    function GetDataField: string;                   { returns the name of the data field }
    function GetDataSource: TDataSource;           { returns reference to the data source }
    procedure SetDataField(const Value: string);             { assigns name of data field }
    procedure SetDataSource(Value: TDataSource);                { assigns new data source }
  published                                    { make properties available at design time }
    property DataField: string read GetDataField write SetDataField;
    property DataSource: TDataSource read GetDataSource write SetDataSource;
  end;
...
function TDBCalendar.GetDataField: string;
begin
  Result := FDataLink.FieldName;
end;

function TDBCalendar.GetDataSource: TDataSource;
begin
  Result := FDataLink.DataSource;
end;

procedure TDBCalendar.SetDataField(const Value: string);
begin
  FDataLink.FieldName := Value;
end;

procedure TDBCalendar.SetDataSource(Value: TDataSource);
begin
  FDataLink.DataSource := Value;
end;

Now that you have established the links between the calendar and its data link, there is one more important step. You must construct the data link class when the calendar control is constructed, and destroy the data link before destroying the calendar.

Initializing the data link

A data-aware control needs access to its data link throughout its existence, so it must construct the data-link object as part of its own constructor, and destroy the data-link object before it is itself destroyed.

Override the Create and Destroy methods of the calendar to construct and destroy the data-link object, respectively:

type
  TDBCalendar = class(TSampleCalendar)
  public                                  { constructors and destructors are always public }
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    ...
  end;
...
constructor TDBCalendar.Create(AOwner: TComponent);
begin
  FDataLink := TFieldDataLink.Create;                     { construct the data-link 
object }
  inherited Create(AOwner);                  { always call the inherited constructor first }
  FReadOnly := True;                                                { this is already here }
end;

destructor TDBCalendar.Destroy;
begin
  FDataLink.Free;                                  { always destroy owned objects first... }
  inherited Destroy;                                   { ...then call inherited destructor }
end;

Now you have a complete data link, but you have not yet told the control what data it should read from the linked field. The next section explains how to do that.

Responding to data changes

Once a control has a data link and properties to specify the data source and data field, it needs to respond to changes in the data in that field, either because of a move to a different record or because of a change made to that field.

Data-link classes all have events named OnDataChange. When the data source indicates a change in its data, the data-link object calls any event handler attached to its OnDataChange event.

To update a control in response to data changes, attach a handler to the data link's OnDataChange event.

In this case, you will add a method to the calendar, then designate it as the handler for the data link's OnDataChange.

Declare and implement the DataChange method, then assign it to the data link's OnDataChange event in the constructor. In the destructor, detach the OnDataChange handler before destroying the object.

type
  TDBCalendar = class(TSampleCalendar)
  private { this is an internal detail, so make it private }
    procedure DataChange(Sender: TObject);         { must have proper parameters for event }
  end;
...

constructor TDBCalendar.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);                  { always call the inherited constructor first }
  FReadOnly := True;                                                { this is already here }
  FDataLink := TFieldDataLink.Create;                     { construct the data-link 
object }
  FDataLink.OnDataChange := DataChange;                          { attach handler to event }
end;

destructor TDBCalendar.Destroy;
begin
  FDataLink.OnDataChange := nil;                 { detach handler before destroying object }
  FDataLink.Free;                                  { always destroy owned objects first... }
  inherited Destroy;                                   { ...then call inherited destructor }
end;

procedure TDBCalendar.DataChange(Sender: TObject);
begin
  if FDataLink.Field = nil then                         { if there is no field assigned... }
    CalendarDate := 0                                             { ...set to invalid date }
  else CalendarDate := FDataLink.Field.AsDateTime;   { otherwise, set calendar to the date }
end;

You now have a data-browsing control.

Creating a data-editing control

When you create a data-editing control, you create and register the component and add the data link just as you do for a data-browsing control. You also respond to data changes in the underlying field in a similar manner, but you must handle a few more issues.

For example, you probably want your control to respond to both key and mouse events. Your control must respond when the user changes the contents of the control. When the user exits the control, you want the changes made in the control to be reflected in the dataset.

The data-editing control described here is the same calendar control described in the first part of the chapter. The control is modified so that it can edit as well as view the data in its linked field.

Modifying the existing control to make it a data-editing control involves:

Changing the default value of FReadOnly

Because this is a data-editing control, the ReadOnly property should be set to False by default. To make the ReadOnly property False, change the value of FReadOnly in the constructor:

constructor TDBCalendar.Create(AOwner: TComponent);
begin
  ...
  FReadOnly := False;  { set the default value }
  ...
end;

Handling mouse-down and key-down messages

When the user of the control begins interacting with it, the control receives either mouse-down messages (WM_LBUTTONDOWN, WM_MBUTTONDOWN, or WM_RBUTTONDOWN) or a key-down message (WM_KEYDOWN) from Windows. To enable a control to respond to these messages, you must write handlers that respond to these messages.

Responding to mouse-down messages

A MouseDown method is a protected method for a control's OnMouseDown event. The control itself calls MouseDown in response to a Windows mouse-down message. When you override the inherited MouseDown method, you can include code that provides other responses in addition to calling the OnMouseDown event.

To override MouseDown, add the MouseDown method to the TDBCalendar class:

type
  TDBCalendar = class(TSampleCalendar);
   ...
  protected
    procedure MouseDown(Button: TButton, Shift: TShiftState, X: Integer, Y: Integer);
      override;
   ...
  end;

procedure TDBCalendar.MouseDown(Button: TButton; Shift: TShiftState; X, Y: Integer);
var
  MyMouseDown: TMouseEvent;
begin
  if not ReadOnly and FDataLink.Edit then
    inherited MouseDown(Button, Shift, X, Y)
  else
  begin
    MyMouseDown := OnMouseDown;
    if Assigned(MyMouseDown then MyMouseDown(Self, Button, Shift, X, Y);
  end;
end;

When MouseDown responds to a mouse-down message, the inherited MouseDown method is called only if the control's ReadOnly property is False and the data-link object is in edit mode, which means the field can be edited. If the field cannot be edited, the code the programmer put in the OnMouseDown event handler, if one exists, is executed.

Responding to key-down messages

A KeyDown method is a protected method for a control's OnKeyDown event. The control itself calls KeyDown in response to a Windows key-down message. When overriding the inherited KeyDown method, you can include code that provides other responses in addition to calling the OnKeyDown event.

To override KeyDown, follow these steps:

  1. Add a KeyDown method to the TDBCalendar class:
    type
      TDBCalendar = class(TSampleCalendar);
       ...
      protected
        procedure KeyDown(var Key: Word; Shift: TShiftState; X: Integer; Y: Integer);
          override;
       ...
      end;
    
  2. Implement the KeyDown method:
    procedure KeyDown(var Key: Word; Shift: TShiftState);
    var
      MyKeyDown: TKeyEvent;
    begin
      if not ReadOnly and (Key in [VK_UP, VK_DOWN, VK_LEFT, VK_RIGHT, VK_END,
        VK_HOME, VK_PRIOR, VK_NEXT]) and FDataLink.Edit then
        inherited KeyDown(Key, Shift)
      else
      begin
        MyKeyDown := OnKeyDown;
        if Assigned(MyKeyDown) then MyKeyDown(Self, Key, Shift);
      end;
    end;
    

When KeyDown responds to a mouse-down message, the inherited KeyDown method is called only if the control's ReadOnly property is False, the key pressed is one of the cursor control keys, and the data-link object is in edit mode, which means the field can be edited. If the field cannot be edited or some other key is pressed, the code the programmer put in the OnKeyDown event handler, if one exists, is executed.

Updating the field data-link class

There are two types of data changes:

The TDBCalendar component already has a DataChange method that handles a change in the field's value in the dataset by assigning that value to the CalendarDate property. The DataChange method is the handler for the OnDataChange event. So the calendar component can handle the first type of data change.

Similarly, the field data-link class also has an OnUpdateData event that occurs as the user of the control modifies the contents of the data-aware control. The calendar control has a UpdateData method that becomes the event handler for the OnUpdateData event. UpdateData assigns the changed value in the data-aware control to the field data link.

  1. To reflect a change made to the value in the calendar in the field value, add an UpdateData method to the private section of the calendar component:
    type
      TDBCalendar = class(TSampleCalendar);
      private
        procedure UpdateData(Sender: TObject);
       ...
      end;
    
  2. Implement the UpdateData method:
    procedure UpdateData(Sender: TObject);
    begin
      FDataLink.Field.AsDateTime := CalendarDate;        { set field link to calendar date }
    end;
    
  3. Within the constructor for TDBCalendar, assign the UpdateData method to the OnUpdateData event:
    constructor TDBCalendar.Create(AOwner: TComponent);
    begin
      inherited Create(AOwner);
      FReadOnly := True;
      FDataLink := TFieldDataLink.Create;
      FDataLink.OnDataChange := DataChange;
      FDataLink.OnUpdateData := UpdateData;
    end;
    

Modifying the Change method

The Change method of the TDBCalendar is called whenever a new date value is set. Change calls the OnChange event handler, if one exists. The component user can write code in the OnChange event handler to respond to changes in the date.

When the calendar date changes, the underlying dataset should be notified that a change has occurred. You can do that by overriding the Change method and adding one more line of code. These are the steps to follow:

  1. Add a new Change method to the TDBCalendar component:
    type
      TDBCalendar = class(TSampleCalendar);
      private
        procedure Change; override;
       ...
      end;
    
  2. Write the Change method, calling the Modified method that informs the dataset the data has changed, then call the inherited Change method:
    TDBCalendar.Change;
    begin
      FDataLink.Modified;                                         { call the Modified method }
      inherited Change;                                   { call the inherited Change method }
    end;
    

Updating the dataset

So far, a change within the data-aware control has changed values in the field data-link class. The final step in creating a data-editing control is to update the dataset with the new value. This should happen after the person changing the value in the data-aware control exits the control by clicking outside the control or pressing the Tab key.

VCL has defined message control IDs for operations on controls. For example, the CM_EXIT message is sent to the control when the user exits the control. You can write message handlers that respond to the message. In this case, when the user exits the control, the CMExit method, the message handler for CM_EXIT, responds by updating the record in the dataset with the changed values in the field data-link class. For more information about message handlers, see "Handling messages."

To update the dataset within a message handler, follow these steps:

  1. Add the message handler to the TDBCalendar component:
    type
      TDBCalendar = class(TSampleCalendar);
      private
        procedure CMExit(var Message: TWMNoParams); message CM_EXIT;
       ...
      end;
    
  2. Implement the CMExit method so it looks something like this:
    procedure TDBCalendar.CMExit(var Message: TWMNoParams);
    begin
      try
        FDataLink.UpdateRecord;                          { tell data link to update database }
      except
        on Exception do SetFocus;                      { if it failed, don't let focus leave }
     end;
      inherited;
    end;