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
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.
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.
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.
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.
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.
// 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;
With WinParent Do
For i := 0 To WinParent.ControlCount - 1 Do
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)
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:
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.
// Utility routines to enable / disable buttons and to set focus to first edit control
FirstEditControl : TWinControl;
// Enable all the controls on the form tab
// Simply enabling / disabling the tab does not grey
// out the controls
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;
// Disable all the controls on the form tab
// Simply enabling / disabling the tab does not grey
// out the controls
NewBtn.Enabled := True;
EditBtn.Enabled := not DataSource1.DataSet.Eof;
DeleteBtn.Enabled := not DataSource1.DataSet.Eof;
SaveBtn.Enabled := False;
CancelBtn.Enabled := False;
dbNav.Enabled := True;
// Return the first TWinControl on the form view tab
Function TMaintainTemplateFrm.GetFirstEditControl : TWinControl;
lFound : Boolean;
i : Integer;
lFound := False;
i := 0;
While (not lFound) and (i <= FormTab.ControlCount - 1) Do
lFound := (FormTab.Controls[i] IS TWinControl);
If Not lFound Then
i := i + 1;
Assert( lFound, 'Template: No windowed controls found');
Result := TWinControl(FormTab.Controls[i]);
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.
Procedure TMaintainTemplateFrm.DeleteBtnClick(Sender: TObject);
If MessageDlg('Delete this record', mtConfirmation,
[MBYes, MBNo], 0) = mrYes Then
procedure TMaintainTemplateFrm.NewBtnClick(Sender: TObject);
procedure TMaintainTemplateFrm.SaveBtnClick(Sender: TObject);
procedure TMaintainTemplateFrm.CancelBtnClick(Sender: TObject);
procedure TMaintainTemplateFrm.EditBtnClick(Sender: TObject);
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 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);
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.
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:
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.
procedure TMaintainTemplateFrm.FormCreate(Sender: TObject);
// 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
ShowMessage('Template: Forgot to set DataSource.DataSet');
PostMessage(Self.Handle, WM_Close, 0, 0);
If FormTab.ControlCount = 0 Then
ShowMessage('Template: Forgot to layout form tab');
PostMessage(Self.Handle, WM_Close, 0, 0);
// Always start on Browse tab
DataPage.ActivePage := BrowseTab;
// And start in Browse Mode
Figure 8: Super class code ensuring sub classes conform to the interface requirements
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.
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 . General inquiries should be directed to .