LIVING ON THE EDGE: RECORD ARRAYS, CONFIGURATION CONTROL, AND THE RAD ENVIRONMENT
OVERVIEWOne way of performing the modifications "on the edges" (i.e., not having to get into the body of the core implementation) is to use defined records in ordered arrays, in which the record fields contain configuration information and method pointers for specialized processing, and the association of a particular operation is determined by the ordinal position of a specific record within an array of such records.
This article describes how record arrays can be exploited, with several example Delphi projects that illuminate various aspects of their use. Along the way, some nomenclature conventions will be defined, a few coding "tricks" will be described, and the beginning of a reusable template will appear. This approach can be used with any presentation surface (e.g., GUIs) as well as invisible containers (e.g., datamodules), but the example projects will concentrate on Page Controls and their tabsheets. (All of the example projects were done with Delphi 5 Enterprise Edition; the database example uses ADO components.)
This article as an MS Word document and the Delphi sample projects can be downloaded as a ZIPed file, size 236.531KB.
RECORDS REVISITED
As a short refresher, records are arbitrary but logical collections of defined types, comparable to a table row in Access or SQL. The entries in a record are called fields and are comparable to the columns of a table's row.
A record is declared in a straightforward manner, as in the following (taken from the ufmMain_RA_Basics.pas unit, starting at line 35):
// Configuration record format
rTabSheetSectionConfiguration = record
oEditFocus : TWinControl; // Edit control to receive focus when in MODIFY/NEW mode
oEditParent : TPageControl; // The parent control containing the edit controls
bEntered : boolean; // TRUE if an entering process has already been done for this tabsheet
dbgMainGrid : TDBGrid; // For controlling the main grid's focus
sQueryDesc : string; // Describes the query
oRecordCount : TLabel; // Active query's record count label
oTabSheet : TTabSheet; // The tabsheet associated with this record
SectionEnter : TTabSheetSectionEnterProcedure; // Called when the tabsheet is entered
SectionExit : TTabSheetSectionExitProcedure; // Called when the tabsheet is exited
SectionModify : TTabSheetSectionModifyProcedure; // Called before a MODIFY operation is initiated
SectionNew : TTabSheetSectionNewProcedure; // Called before a NEW operation is initiated
SectionSave : TTabSheetSectionSaveProcedure; // Called before a SAVE operation is initiated
end;
By convention (as well as to satisfy the Compiler's requirements) records are defined as types before the form type declaration in the unit's interface section. Notice that the record type's name ends in 'Configuration', which refers to the records as a group. By convention, all elements that are related to record processing and manipulation will end with this word.
The field definitions can be any defined type. The first part of the field definitions in this example are generally described in their comments, but the fields beginning with the word "Section" need an explanation here. They are method pointers that can be invoked when the associated operation is activated, and they are defined as follows (ufmMain_RA_Basics.pas unit, starting at line 28):
// TabSheet Method Pointers TTabSheetSectionEnterProcedure = procedure of object; // Called when the tabsheet is entered TTabSheetSectionExitProcedure = procedure of object; // Called when the tabsheet is exited TTabSheetSectionModifyProcedure = procedure of object; // Called before a MODIFY operation TTabSheetSectionNewProcedure = procedure of object; // Called before a NEW operation is initiated TTabSheetSectionSaveProcedure = procedure of object; // Called before a SAVE operation is initiated
Also by convention, any element that has the word "Section" in it refers to a specific record with the "Configuration" group of records.
The array of records is in the form's private (ufmMain_RA_Basics.pas unit line 140):
FarTabSheetSectionConfiguration : array [tscAFIRST..tscZLAST] of rTabSheetSectionConfiguration;
This also requires an explanation. The array of records is an enumerated array, where each ordinal element is given a type name with a three-letter lower case prefix. (There are a lot of types declared in the Delphi environment, and using a three-letter prefix reduces the chance that the Compiler will use one of those.) The array's lower and upper bounds can be better understood by looking at the enumerated type array that will be used as an index into the Configuration array (ufmMain_RA_Basics.pas unit, starting at line 23):
TTabSheetSections = (tscAFIRST, //*** EMPTY PLACEHOLDER
tscOne, tscTwo, tscThree //+++ PUT TAB SHEET ENUMERATED TYPE NAMES HERE
,tscZLAST); //*** EMPTY PLACEHOLDER
This is a programming convenience, in that tscAFIRST and tscZLAST are empty placeholders, between which the named type elements (tscOne, tscTwo, and tscThree) are inserted, where each type element is associated with a tabsheet in the same order as the tabsheet is placed on the parent page control. The advantage of doing this is that the private variable FarTabSheetSectionConfiguration does not have to be changed every time a section is added to (or removed from) the record array.
(NOTE: If the declaration
TTabSheetSections = (tscOne, tscTwo, tscThree);is used, then this declaration
FarTabSheetSectionConfiguration : array [tscOne..tscThree] of rTabSheetSectionConfiguration;will also have to be used.)
THE BASICS: THE FIRST EXAMPLE PROJECT
The first example project (located at \RA_Basics\RA_Basics.dpr) is very simple, consisting of a main form, a common toolbar, menu items, an action list, and a page control that has three tabsheets; Figure 1 details this at design time.
Each tabsheet has a record entry as described above, with the record fields used to direct some action when the associated tabsheet becomes active. For example, if an EDIT mode is initiated, then the entry control (e.g., a DBEdit object) specified in the oEditFocus field will have its focus set, provided that it's defined (i.e., not nil). In this first basic example project, various pop-up messages will be displayed as tabsheets are selected.First, the Configuration records will have to be populated, which is done in the InitializeConfiguration private procedure (ufmMain_RA_Basics.pas unit, starting at line 168), which by convention is always placed at the start of the unit's implementation section. The template (commented out) serves as a guide for populating the field values for each record in the array, each associated with a tabsheet. At the beginning of this procedure all of the records in the array are cleared out, to become pointer nils, empty strings, etc., so that only those fields that are required need be populated. In this example, each record will have the associated tabsheet's reference (not really needed but included for convenience), and the Enter, Exit, Modify and New section method pointers are set to common handlers in the "APPLICATION-SPECIFIC PROCESSING" portion of the unit, which by convention is always at the end of the unit's core implementation code: Ideally, code changes are made on the edges, not in the core.The InitializeConfiguration private procedure is called in the form's OnCreate event handler, which also sets the page control's active page index to the first tabsheet, and sets the FtscCurrent private variable to the first active entry in the TTabSheetSections enumerated type array (i.e., to tscOne instead of tscAFIRST, which is just an empty placeholder).All of the action happens in the page control's OnChange and OnChanging event handlers.
The OnChanging code is simple enough:
procedure TfmMain_RA_Basics.pcMainChanging(Sender: TObject; var AllowChange: Boolean);
begin
if (actCancel.Enabled or actSave.Enabled) then
begin
MessageDlg('There is pending Post that must be cleared first!',
mtError, [mbOK], 0);
AllowChange := FALSE;
end
else
if Assigned(FarTabSheetSectionConfiguration[FtscCurrent].SectionExit) then
FarTabSheetSectionConfiguration[FtscCurrent].SectionExit;
end;
A tabsheet change will not be allowed if an edit or modify mode is in effect. Otherwise, if a SectionExit method is defined for this specific section (as determined by the FtscCurrent private variable as an enumerated section index) then it will be invoked. In this example, all it does is display a message that the particular tabsheet is being exited.The OnChange code is even simpler:
procedure TfmMain_RA_Basics.pcMainChange(Sender: TObject);
begin
FtscCurrent := TTabSheetSections(pcMain.ActivePageIndex + 1);
if Assigned(FarTabSheetSectionConfiguration[FtscCurrent].SectionEnter) then
FarTabSheetSectionConfiguration[FtscCurrent].SectionEnter;
end;
All this code does is set the enumerated section's type index to the page control's (adjusted) active
page index, and if a SectionEnter method is defined then it is invoked. (The index adjustment is required
because the first ordinal entry in the TTabSheetSections enumerated type array is tscAFIRST, a
dummy placeholder.)Although other things might also be done in these two event handlers, the important
thing is that this code does not have to be changed regardless of how many tabsheets the page control has,
and any specialized processing (e.g., applying business rules in the SectionSave procedure) can not only
be specific to, and independent of, other tabsheets, but can also be added in a localized section of code
isolated from the core implementation.The tabsheet entering procedure, EnteringATabSheet
(ufmMain_RA_Basics.pas line 351), will display an entering message only on the first time the entering
occurs. This can be used for some initial set up, such as opening or refreshing a query. The other
thing this code does is enable some of the toolbar buttons to emulate an editing session. (In practice,
the management of the toolbar and other common controls would be done by a security object of some kind.)
This first example project is very simple, but it doesn't really "do" anything other than show how code
changes can be done on the edges without requiring changes to the core implementation. Now to something
more interesting.
NESTED PAGE CONTROLS: THE SECOND EXAMPLE PROJECT
For applications that use a database, the presentations are more detailed. The second example project
(in \RA_DD2\RA_DD2.dpr) has the same main shell (e.g., toolbar, menu items, etc.) but with three page
controls, as shown in Figure 2. The pcMain page control has two tabsheets, one for Customers
and one for Suppliers. Each of these tabsheets in turn have their own page control
(pcCustomers and pcSuppliers), each with two tabsheets, one for contact information and
the other for details.There is a Configuration group for the Main, Customer and
Supplier sections, along with their associated enumerated type arrays, record arrays, and
configuration population, essentially identical in design to those in the basic project (see the code
in ufmMain_DD2.pas unit). The main action is still in the OnChange and OnChanging event
handlers for each of the three page controls. All of the record field population is done in the
InitializeConfiguration private procedure, which also includes initial settings so that changes
don't have to be done in the form's OnCreate event handler.Each of the page controls'
OnChanging event handlers need to test for an active edit mode, which has been combined in the
CheckEditMode private procedure. One other change is theSetGridFocus private procedure,
used to control the active grid when transitioning between edit and browse modes.
The main page control doesn't have any direct editable controls, so it's record definition (in the rMainTabSheetSectionConfiguration declaration) is fairly simple. The two nested page controls (pcCustomers and pcSuppliers) have more configuration information (in the rCustomersTabSheetSectionConfiguration and rSuppliersTabSheetSectionConfiguration declarations), and in this example the field definitions for the two are identical; sometimes they are different. Similarly, this example has two tabsheets for each main section, each having two sections (contact info and details), but there could be more or less depending on an application's requirements.
This example is starting to look like a template. It has a main page control, each tabsheet of which
has a data grid and, in turn, a child page control, each of these having their own tabsheet collection.
Regardless of how many tabsheets the main page control has, and regardless of how many tabsheets the
ancillary child page controls have, the core implementation isn't changed, and additions only need be
done by adding tabsheets as required, populating their associated configuration records, and adding any
specialized processing, all on the edges of the unit's code. Now it's time to apply these techniques to
a datamodule.
LAST EXAMPLE: A GUI-FORM/DATAMODULE PAIR
The same techniques of configuration record arrays can also be applied to a datamodule, but to fully
exploit the advantages the datamodule will have to be organized in a certain way; as a significant
side benefit, this will also be the start of an outline for a reusable datamodule template.For various
and specific reasons, it's highly convenient to pair a GUI-form with a datamodule. We'll focus first
on the paired datamodule, and then examine how the GUI-form is integrated with it. This example Delphi
project, \RA_DM\RA_DM.dpr, includes a simple Access database (RA_DM.mdb) that will be accessed using
ADO objects.The Datamodule FirstThere are two main queries used in this example,
qryCustomers and qrySuppliers, and their SQL statements, simple as they are, have been
applied to their respective SQL.Text properties. There are also two datasources (dsCustomers
and dsSuppliers), each connected to their respective dataset. Finally, there is an ADO
Connection object to which the queries are connected. (In practice, this connection object would not
be in a paired datamodule.)The datamodule (the udmRA_DM.pas unit) is accessed by the paired GUI-form
by methods instead of direct object references. This not only allows for a degree of isolation
(i.e., objects should only know about themselves), but also provides an easy means of translating the
datamodule into a COM "object". There can be more than one open query (this example has two), while in
practice only one of them is active in the sense of being worked on; the challenge is to identify the
active query in a manner that doesn't require the GUI-form to have any detailed "knowledge" of the
datamodule's objects. One way to achieve this is to invoke processes indirectly and at a high level
through methods of the datamodule, rather than manipulate the datamodule's objects directly.This is
done by exposing a set of datamodule methods (udmRA_DM.pas, starting at line 80) that in their turn
manipulate the object methods. (In practice, more methods would be required, but this is sufficient
for the example). Looking at one of the method's implementation (the NewRecord method) gives
a general overview of what all the other methods more or less look like:
procedure TdmRA_DM.NewRecord(ztQueryObject: TDMSections);
begin
if SetSourceQuery(ztQueryObject) then
begin
try
FqSourceQuery.Insert;
except
//*** Error Object Handling Here
end;
end;
end;
(Again, in practice the implementation would be more involved, for such things as error trapping,
validation, etc., but this is sufficient for the example.)
The NewRecord procedure has an argument, ztQueryObject, that will be described later on, but for here note that is a kind of index. (By convention, all function and procedure arguments begin with "z".) The first thing done is a call to SetSourceQuery, which is this code:
function TdmRA_DM.SetSourceQuery(ztQueryObject: TDMSections): boolean; begin FqSourceQuery := FarDMConfiguration[ztQueryObject].qryData; Result := Assigned(FqSourceQuery); end;This function uses the passed "kind of index" to find the actual data object (more accurately, its reference) and put it into the FqSourceQuery private variable, through which the data object's methods and properties can now be accessed. Instead of having ~.Insert code for each query, this allows having only one FqSourceQuery.Insert that can be used for all of the queries.
Several other items before we move on. In the DataModuleCreate event handler the ADO Connection object's ConnectionString property is set, inserting the Access database name with the assumption that it is in the same folder as the application's executable. Similarly, the DataModuleDestroy (udmRA_DM.pas, line 167) closes the ADO Connection. In an actual application these processes would typically be done elsewhere.The Paired GUI-FormThe GUI-form (the ufmRA_DM.pas unit) paired with the datamodule is essentially the same one used in the previous example project, the only obvious difference being that data-aware components have been added, as detailed in Figure 3. There have been several internal changesto integrate the paired datamodule.To begin with, the datamodule has been added to the interface uses section (ufmRA_DM.pas line 20), and several additions to the main page control's configuration record rMainTabSheetSectionConfiguration (ufmRA_DM.pas line 47), and the corresponding configuration population (ufmRA_DM.pas starting at line 307) have been made. In particular, two fields have been added: oEditFocus to set the focus when an edit mode is initiated, and qdPrimary, which is the sections main query obtained from the paired datamodule's TDMSections enumerated type array; this is part of the "glue" that accomplishes the pairing and integration of the GUI-form with the datamodule.For convenience, the datamodule is created in its paired GUI-form's OnCreate event handler (ufmRA_DM.pas line 496), where the active query is initialized (the FqdActiveQuery private variable) and the main queries are opened. Note that the datamodule as always referenced by the dmLinkedChild declaration, which greatly eases code by the simple trick of assigning this to the datamodule's type name (ufmRA_DM.pas line 267), moving the code closer to becoming a template. Having created the datamodule in code, it has be to be released in code, done in the GUI-form's OnClose event handler (ufmRA_DM.pas line 487).Again, the main action happens on the main page control's OnChange event handler:
procedure TfmMain_RA_DM.pcMainChange(Sender: TObject); begin FtmsCurrentMain := TMainTabSheetSections(pcMain.ActivePageIndex + 1); FqdActiveQuery := FarMainTabSheetSectionConfiguration[FtmsCurrentMain].qdPrimary; SetGridFocus(TRUE); DisplayPanelMessage(FarMainTabSheetSectionConfiguration[FtmsCurrentMain].sQueryDesc); end;Here, the current page control is maintained (FtmsCurrentMain), the main query associated with it is also maintained (FqdActiveQuery), and the query's description (if any) is displayed, all driven by field information in the configuration records. Regardless of how many tabsheets the main page control has, this core code will never have to be changed.The SetGridFocus (ufmRA_DM.pas starting at line 574) also does a lot. It is called whenever there's a transition in the dataset's mode (e.g., MODIFY, NEW), as in this example:
procedure TfmMain_RA_DM.actNewExecute(Sender: TObject);
begin
DisplayPanelMessage('Adding A New Record To The ' +
FarMainTabSheetSectionConfiguration[FtmsCurrentMain].sQueryDesc);
DisableToolBar;
actCancel.Enabled := TRUE;
actSave.Enabled := TRUE;
SetGridFocus(FALSE);
dmLinkedChild.NewRecord(FqdActiveQuery);
end;
More specifically, the SetGridFocus procedure: Controls the grid and editable sections
(by a call to EditControl, ufmRA_DM.pas starting at line 445); Goes to the active section's
first editable tabsheet; and sets the focus to the first editable object.
CONCLUSION
Using defined record arrays for programmatic configuration is only one of a number of tools by which
RAD can be done, but it has the advantage of allowing incremental changes to be applied at the edges
of a core implementation. Through these simple example projects, stripped of all but the absolute
essentials needed to demonstrate their exploitation, the outlines of a framework has been explored.
More importantly to the Delphi community, these defined record arrays readily lend themselves to the
Delphi environment, with some powerful side benefits: fast development, protecting investments in core
implementation, amazingly extensible, demonstrable reliability, and not least, a vehicle by which a
business case for Delphi can be made.
This article as an MS Word document and the Delphi sample projects can be downloaded as a ZIPed file, size 236.531KB.