VFI in Practice - Generic
Table Maintenance
In a previous
article I discussed the merits and mechanics of Visual Form Inheritance
(VFI). I mentioned that one of the best places to use VFI was in the
production of a generic table maintenance form. Regardless of the
table you are maintaining, your users will need to perform the standard
Add, Edit, and Delete operations. VFI allows you to write most of
this code, and perform most of this form layout, once, in a generic
super class. You can then create forms inheriting from this form,
designing these new forms with data and components specific to the
actual table you are editing.
This article will
describe a basic framework you can use as the basis for your data
editing forms. I’ll show two different layouts for the main form -
if you choose to use this code you could start with one of these,
or design your own. Either way, you should be able to use most of
the code and ideas in this article. And of course, because you’re
using VFI, if you subsequently change your mind about the layout,
you make only have to make the change in one place.
Generic
Table Maintenance Features and Layout
The generic table
maintenance form needs to allow users to:
Add new records
Edit
existing records
Delete
records
Browse
records
There are other
functions you could implement here as well - but in the interests
of restricting the length of this article we’ll leave those as an
exercise for the reader.
Now, since the
form does not know with which table it will be working, it cannot
layout editing controls specific to a table - that will be done in
the table specific sub classes which inherit from this generic form.
It is our goal, though, to place as much code, and perform as much
of the layout as possible, in this generic form. Let’s start with
a basic form layout which should work with most tables. Figure one
shows this first layout.
Figure1

Figure 1:
First layout of generic database maintenance form
The PageControl
contains two tabsheets labeled browse view and form view. The browse
tabsheet (shown) simply contains a database grid, which will list
records from the table being edited. The form tabsheet (not shown)
is empty; sub classes will layout this tabsheet with data aware controls
specific to the actual table being edited.
Note that the
generic form includes a TDataSource component, but does not include
any dataset (TQuery, TTable, or TStoredProc) components. There are
two problems with placing a dataset component directly on the generic
form:
- You are then
tied to using that style of access - i.e. TTable, TQuery, or TStoredProc.
The way we have the form it will work with either of these three
TDataSet components
- You
cannot use existing datasets stored in a datamodule. Our approach
allows dataset components to be located anywhere. They could be
in a datamodule, or in a subclass of this generic form.
The grid is linked
to the datasource, as is the navigator. As you’ll see, it’s the sub
forms’ responsibility to attach a dataset to the datasource - and
that is the only link between the form and the actual table being
maintained.
Figure 2 shows
an alternative layout - still using the PageControl and two tabsheets
- but using a more modern Coolbar as the container for the action
buttons. You probably have your own ideas for layout as well, but
whatever style you use you should still be able to use the code in
this article. Again, the only link between this form and the table
being edited is through the datasource component.
Figure2

Figure 2:
Second layout of generic database maintenance form
The generic form
is responsible for enabling / disabling the buttons and other components
when appropriate. The design does not allow editing, adding, or deleting
records when the browse tab is selected. All these actions must be
performed with the form tabsheet selected. Furthermore, it will not
support auto editing. Auto editing, the default behavior of datasource,
allows users to initiate editing just by clicking in a data aware
control. I prefer to require the users to explicitly request editing
by pressing the edit button. The edit button can ensure the form tab
is selected, and enable the data aware controls (they should be disabled
until the dataset is in edit mode), and move the dataset into edit
mode.
Sub
Class Responsibilities
Obviously the
generic form requires the sub form to perform certain functions. Most
importantly the sub class must associate the super class’s datasource
component with an actual dataset. It must also layout the sub class’s
form view tabsheet with controls linked to fields in the actual table.
Figure 3 shows a table specific form, inherited from the generic form,
used to edit the Customer.db table from Delphi’s DBDemos database.
Figure3

Figure 3:
Table specific form to edit DBDemos / Customer
The sub form includes
a TTable component configured to access the Customer table in the
DBDemos database. The sub form also sets the DataSet property of the
DataSource component to reference this Dataset. Note that the DataSource
component was introduced in the super class, but the sub class is
setting one of its properties. If you looked at the DFM file for the
sub form, you would not see the declaration of the DataSource component,
but you would see its DataSet property being set.
The sub form,
of course, is responsible for laying out the form tabSheet with data
aware controls specific to the table being edited. You can see this
is Figure 3.
In summary, then,
the sub classs’ responsibilities are:
- Attach the
DataSource component to a dataset
- Layout the
edit tab with controls specific to the table being edited
You could eliminate
the second responsibility by performing a default layout if the user
doesn’t supply one. The super class could implement a method to dynamically
create data aware controls from the fields in the table. The actual
coding of this is not too difficult, but formatting issues arise when
there are more fields when there is room on the tabsheet - so I’ll
leave that for another article.
The most common
mistakes programmers make when working with VFI is to forget to implement
the sub class responsibilities. In this case it’s not too difficult
to implement the sub classes - there are only two things they must
do - but in the real world the interface between the super class and
the sub class can be more involved. In my designs I always try and
implement some default behavior in a super class - allowing the sub
classes to override this behavior if they need. When the sub class
absolutely must implement something - as in this case where it must
associate the DataSource with a DataSet - I write code in the superclass
to verify the sub class actually implemented it. I’ll show this in
the next section.
Note that the
layout inherited from the superclass is often only the starting point
for the sub forms. As well as adding controls to the form view tabsheet,
the sub forms can also add additional action buttons and tabsheets.
Figure 4, taken from a course registration system, shows an example
of this - the child form has added an extra tabsheet to show the registrations
for each student. It also contains a menu - this is an MDI child form
so its menu will merge with its parent.
Figure
4

Figure4:
Screen shot showing additional TabSheet added to child form
Generic
Table Maintenance Implementation
Now we’ll take
a look at the generic super class. Note that we’ll present the class
as a finished design - but that’s rarely the way it works in practice.
In the real world it’s doubtful you’ll get the design perfect the
first time; rather, it will evolve as you start working with actual
sub classes. Class design is a very iterative process - you are continually
finding redundancy and duplication in sub classes and moving that
up the class hierarchy. Also note that the superclass is relatively
simple - you’ll probably want to extend it to allow orderand filtering
of records, and searhcing for records.
One of the requirements
of the generic superclass is to disable the data aware controls when
the user is not editing or adding records. The easiest way to implement
that is to disable the TabSheet itself, but that does not grey out
the edit controls. The only way to have the edit controls displayed
greyed out is to explicitly disable each one in turn. The super class,
of course, does not know the names of the controls on the form tab.
They are introduced in the table specific sub classes, and they are
different in each sub form. There are two ways to implement this disabling:
- Have each sub
form implement the disabling / enabling of its controls
- Write the code
once, in a generic manner, in the superclass
Naturally, we’ll
opt for the second approach. We need to implement a generic routine
which will enable / disable all the child controls of the form view
TabSheet. Controls which can have other controls as children are based
on the TWinControl class. TWinControl defines two properties you can
use to access its children:
ControlCount -
the number of children
Controls
- and array of references to those controls
To disable
all the children of a TWinControl called WinControl it’s tempting
to write:
0For
i := 1 To WinControl.ControlCount - 1
WinControl.Controls[i].Enabled
:= False;
While this works,
it will not grey out the children of any other windowed controls.
For example, imagine you used this code to disable the controls on
the Form View tabsheet, and one of those controls was a GroupBox,
which had its own children. This code would not grey out the controls
inside the groupbox. To implement this nested disabling you need a
recursive routine, as show in figure 5.
Figure 5
// Generic
routine to enable / disable the children of winParent
//
Pass the WinControl whose children you want to set,
//
and true to enable the controls and false to disable
Procedure TMaintainTemplateFrm.SetChildControls(winParent: TWinControl;State
: Boolean);
Var
i : Integer;
Begin
With
WinParent Do

For
i := 0 To WinParent.ControlCount - 1 Do


Begin



Controls[i].Enabled
:= State;


//
If this control can have children (it's a 





TWinControl


//
which has a Controls property), enable / 





disable
those

If
Controls[i] IS TWinControl Then

SetChildControls( TWinControl(Controls[i]), State)
End;
End;
Figure 5:
Generic recursive code to enable / disable a windowed control’s children
Here’s how you
would call SetChildControls to disable the Form View tabsheet’s children:
SetChildControls(FormTab,
False);
The generic form
form operates in one of two modes. In browse mode, the user is browsing
records. In edit mode, they are either editing an existing record
or adding a new record. The super class defines two routines which
take care of enabling / disabling controls when moving between the
modes. When switching to edit mode, the super class needs to set focus
to a control on the form view tabSheet. Since these controls are defined
in the table specific sub classes, the superclass does not know which
controls exist - so it locates the first TWinControl on the TabSheet.
The GetFirstEditcontrol method, shown in figure 6 shows this. The
super class declares GetFirstEditControl as virtual and protected
so that the sub classes can override it if necessary. This is a good
example of the super class providing default behavior, but allowing
sub classes to override it.
Figure 6
// Utility
routines to enable / disable buttons and to set focus to first edit
control
Procedure
TMaintainTemplateFrm.EditMode;
Var
FirstEditControl
: TWinControl;
Begin
//
Enable all the controls on the form tab
//
Simply enabling / disabling the tab does not grey
//
out the controls
SetChildControls(FormTab,
True);
DataPage.ActivePage
:= FormTab;
NewBtn.Enabled
:= False;
EditBtn.Enabled
:= False;
DeleteBtn.Enabled
:= False;
SaveBtn.Enabled
:= True;
CancelBtn.Enabled
:= True;
DbNav.Enabled
:= False;
//
Set focus to first control, on the Edit tab, 



which
can
//
receive focus
FirstEditControl
:= GetFirstEditControl;
FirstEditControl.SetFocus;
End;
Procedure TMaintainTemplateFrm.BrowseMode;
Begin
//
Disable all the controls on the form tab
//
Simply enabling / disabling the tab does not grey
//
out the controls
SetChildControls(FormTab,
False);
NewBtn.Enabled
:= True;
EditBtn.Enabled
:= not DataSource1.DataSet.Eof;
DeleteBtn.Enabled
:= not DataSource1.DataSet.Eof;
SaveBtn.Enabled
:= False;
CancelBtn.Enabled
:= False;
dbNav.Enabled
:= True;
End;
//
Return the first TWinControl on the form view tab
Function
TMaintainTemplateFrm.GetFirstEditControl : TWinControl;
Var
lFound
: Boolean;
i
: Integer;
Begin
lFound
:= False;
i
:= 0;
While
(not lFound) and (i <= FormTab.ControlCount -
1)
Do

Begin


lFound
:= (FormTab.Controls[i] IS TWinControl);


If
Not lFound Then



i
:= i + 1;

End;
Assert(
lFound, 'Template: No windowed controls 


found');
Result
:= TWinControl(FormTab.Controls[i]);
End;
Figure 6:
Utility code to switch between edit and browse modes
The last thing
to implement is the code for the Edit, Delete, Add, Save and Cancel
buttons. They all follow a similar form - they move the form into
the appropriate mode, access the dataset using DataSource.DataSet,
and call the appropriate DataSet methods. Figure 7 shows the code
in detail. It couldn’t be simpler.
Figure 7
Procedure TMaintainTemplateFrm.DeleteBtnClick(Sender:
TObject);
begin
If
MessageDlg('Delete this record', mtConfirmation,



[MBYes,
MBNo], 0) = mrYes Then

DataSource1.DataSet.Delete;
End;
procedure
TMaintainTemplateFrm.NewBtnClick(Sender: TObject);
begin
DataSource1.DataSet.Append;
EditMode;
end;
procedure
TMaintainTemplateFrm.SaveBtnClick(Sender: TObject);
begin
DataSource1.DataSet.Post
BrowseMode;
end;
procedure TMaintainTemplateFrm.CancelBtnClick(Sender:
TObject);
begin
DataSource1.DataSet.Cancel;
BrowseMode;
end;
procedure TMaintainTemplateFrm.EditBtnClick(Sender:
TObject);
begin
EditMode;
DataSource1.DataSet.Edit;
end;
Figure 7:
Generic code implementing Add, Edit, Delete, Save and Cancel
There is other
code in the template we won’t look at here. The onCloseQuery event,
for example, asks the user whether to save or cancel changes before
closing the form. There’s also code which prevents the user from moving
between tabSheets when edits are pending. The entire Pascal file is
available. Please contact
Web Tech to request this file.
Note the implementation
of the Delete push button. It simply asks the user to confirm that
he /she wants to delete that record. It’s very likely that sub classes
will override this method and issue a message pertinent to the record
being deleted. Note, however, that when you write code for this delete
button in a sub class, Delphi generates a call to the super class
method:
procedure TfrmCust.DeleteBtnClick(Sender:
TObject);
begin
inherited;
end;
The inherited
keyword means call a method with the same name in the super class.
You don’t want this in this case, so simply remove the call.
Defensive
Coding’
Whenever you write
generic code - or code which will be used by another programmer, it’s
best to write that code as defensively as possible. Your code defines
an interface to the user of the code - it may require the user to
use your interface in a certain way. Or as in this case, for a subclass
to implement something. Your code cannot assume the users are using
it correctly - in fact it should assume the worst.
Take the example
of this generic maintenance form. Imagine another programmer attempts
to use this form but neglects associate the datasource with a dataset.
When the user presses any of the buttons which operate on the table
Delphi raises an exception because your code is executing something
like:
DataSource.DataSet.SomeMethod
Yes, the other
programmer will be able to find his / her error eventually, but it
be easier if the superclass code could detect the fact that the sub
class had not implemented what it was supposed to implement, and report
the error in a more friendly manner.
In this example,
the super class’s FormCreate event can detect the omission and display
a message stating just that. The awkward bit is deciding what to do
when you detect the error. In this case there’s no point in having
the form display as the user can’t do anything, but that’s not as
easy as it sounds. The Form’s FormCreate event cannot execute the
Close method - and raising an exception doesn’t help either - the
exception is intercepted by the VCL and the form proceeds to display
and execute.
The best solution
I’ve been able to come up with is to use the Windows API to post a
WM_CLOSE message to the window. By posting the message, it’s put into
the queue for this window - when the window is finished with its creation
process it continues to process messages - and then receives the close
message. Figure 8 shows the FormCreate event for the super class which
checks to ensure the sub class has fulfilled its responsibilities
- closing the form if not.
Figure 8
procedure TMaintainTemplateFrm.FormCreate(Sender:
TObject);
begin
//
Check to ensure child form is a good boy...
//
1. DataSource.Dataset must be set
//
2. Form tab must be layed out
If
DataSource1.DataSet = Nil Then
Begin

ShowMessage('Template:
Forgot to set 











DataSource.DataSet');

PostMessage(Self.Handle,
WM_Close, 0, 0);

Exit;
End;
If
FormTab.ControlCount = 0 Then
Begin

ShowMessage('Template:
Forgot to layout form tab');

PostMessage(Self.Handle,
WM_Close, 0, 0);

Exit;
End;
//
Always start on Browse tab
DataPage.ActivePage
:= BrowseTab;
//
And start in Browse Mode
BrowseMode;
end;
Figure 8:
Super class code ensuring sub classes conform to the interface requirements
Summary
This article showed
how to use Visual Form Inheritance to implement a generic table maintenance
form. The super class contains all the code and layout common to all
forms - sub classes can implement table specific layout and code.
The goal is to place as much code and perform as much of the layout
as possible in the super class. This leads to more consistent user
interfaces, faster development time and less errors. In the interests
of brevity we only implemented a small number of features in the super
class. You may want to extend it to allow users to selected in which
order they want to browse records (populate a combo box with index
names for example), to search for records and filter records. Have
fun.
Author
Rick Spence is
technical director of Web
Tech Training & Development (formerly Database Programmers
Retreat) - a training and development company with offices in Florida
and the UK. You can reach Rick directly at RSpence@WebTechCorp.com.
General inquiries should be directed to training_us@webtechcorp.com.
