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-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:
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.
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.
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.
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;
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.
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.
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:
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.
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:
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.
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.
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.
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:
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;
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.
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.
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:
type TDBCalendar = class(TSampleCalendar); ... protected procedure KeyDown(var Key: Word; Shift: TShiftState; X: Integer; Y: Integer); override; ... end;
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.
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.
type TDBCalendar = class(TSampleCalendar); private procedure UpdateData(Sender: TObject); ... end;
procedure UpdateData(Sender: TObject);
begin
FDataLink.Field.AsDateTime := CalendarDate; { set field link to calendar date }
end;
constructor TDBCalendar.Create(AOwner: TComponent); begin inherited Create(AOwner); FReadOnly := True; FDataLink := TFieldDataLink.Create; FDataLink.OnDataChange := DataChange; FDataLink.OnUpdateData := UpdateData; end;
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:
type TDBCalendar = class(TSampleCalendar); private procedure Change; override; ... end;
TDBCalendar.Change;
begin
FDataLink.Modified; { call the Modified method }
inherited Change; { call the inherited Change method }
end;
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:
type TDBCalendar = class(TSampleCalendar); private procedure CMExit(var Message: TWMNoParams); message CM_EXIT; ... end;
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;
pubsweb@inprise.com
Copyright © 1999, Inprise Corporation. All rights reserved.