Using interfaces and actions to create dynamic applications
Marco Wobben 12 maart 2002
[download]
[email]
Published at the 12th annual Borland Conference, 2001
Presented at the SDGN Conference to the Point, 14/3/2003
Introduction
My goal in application development is always to find an elegant way
to ease my coding efforts. The main goal of combining interfaces, actions
and packages is to reduce code and loosen couplings while maintaining maximum
flexibility with excellent (automatic) user interface support. This paper
will show routines which make it able for you to create your own interfaced
actions and have them address business entities contained in dynamically
loadable packages. Once set up it'll allow you to develop, test and deploy
applications with more ease. I'll briefly describe the three required technical
implementations: Interfaces, Actions and Packages.
Breakdown
This breakdown will describe the business components of which our application
should exist. For development and deployment ease we'll break up the application
in the main executable and several packages. The type of packages will
fall down into two categories, the runtime required packages (equal to
runtime packages in the Delphi IDE) and the runtime loadable packages (similar
to the Design time packages in the Delphi IDE). The loadable packages each
contain business services that extend the main applications functionality.
This simplifies deployment because only updated packages are required to
be re-deployed, without recompiling the entire application.
Typically the main executable will contain a mainform and there'll
be a main datamodule. The mainform will have the required menuitems and
toolbar buttons, referencing actions in the main datamodule with centralised
logic and code. This straightforward use of Delphi stuff will be used to
set-up our business components as well. The same description will go for
the customer entity, our business component: A customer form with buttons
that are connected to the actions on the customer datamodule. Business
components require some method of communicating to other business components.
This will be realised by using interfaces on the datamodules (we'll expand
on the technique later on). The figure shows all this together.
The business services will all be implemented with a central datamodule.
It'll contain database transactions, show end-user dialogs en allows users
to perform actions related to this business service. To be able to access
these services a generic interface should be declared. This paper describes
creating an interface for adding, modifying and deleting an entity.
As actions call interfaces on datamodules in business components they
should enable themselves only if the required datamodule class information
is loaded. Since business components reside in runtime loadable packages,
obviously the class information becomes available as soon as the business
component package is loaded. All code will be revealed later.
Off course we require some routines to make all this work together.
Routines for loading packages from a directory or specified in some inifile.
Routines to register datamodule class information. And some routines to
support a yet unmentioned element, the form dictionary, which is similar
to the datamodule registration but specifically for forms. This is simpler
then the required datamodule registration and will therefor be explained
briefly.
Interfaces
Let me start with two quotes from the Delphi help file, which describe
what an interface is:
These quotes tell us we can create several datamodules all addressing
each other using a generically declared interface instead of requiring
a uses-clause to other units and addressing them by hardcoded references.
The interface we require should contain the methods for adding, modifying
and deleting entities (E.g. Customers). The unit for declaring this interface
looks like this:
{: IBcpBasicTxUnit declares the IBcpBasicTx. Implement this interface in
datamodules which are to be supported by the Interfaced Actions. }
unit IBcpBasicTxUnit;
interface
type
{: Basic Tx interface allows Add,
Modify and
Delete. }
IBcpBasicTx = interface
['{CF50BAC0-E409-11D4-A728-0080C7F45A8D}']
function Add: boolean;
function Modify(KeyValues: variant):
boolean;
function Delete(KeyValues: variant):
boolean;
end;
implementation
end.
|
Data Modules
Our customer datamodule implements the IBcpBasicTx and the relevant
declaration code looks very similar to the interface itself. The datamodule
itself contains a Query, DataSource and some codelines for registration
purposes. Below is a compressed view of the code:
{: Simple sample datamodule to add, modify or delete a customer. } unit CustomerTxDmdUnit;
interface ... type TCustomerTxDmd = class(TDataModule, IBcpBasicTx) CustomerSrc: TDataSource; CustomerQry: TQuery; CustomerUpd: TUpdateSQL; private { Private declarations } procedure OpenQry(CustNo: integer); procedure ApplyQry(Post: boolean); procedure CloseQry; function ShowModalForm(FormClass: string): TModalResult; public { IBcpBasicTx } function Add: boolean; function Modify(KeyValues: Variant): boolean; function Delete(KeyValues: Variant): boolean; end;
var CustomerTxDmd: TCustomerTxDmd;
implementation
...
initialization BcpRegisterModule(TCustomerTxDmd, ciSingleInstance, 'Customer properties');
end.
|
Class registration
You've might have noticed two odd lines of code in the previous paragraph.
I will explain them here, since this is essential. Both have to do with
registration. The obvious line would be in the Initialization part of the
unit. The line mentions BcpRegisterModule and is meant to register DataModules.
The less obvious one is the ShowModalForm. The ShowModalForm might look
a bit weird to most Delphi developers since they're probably used to create
the datamodule from the code of a form and not the other way around. But,
we do it the other way around.
BcpRegisterModule registers the datamodule class, in this case TCustomerTxDmd.
It mentions that it is a single instance class, meaning that it may only
be instantiated once in the application at a given point in time. Finally
the description which will be used later on for end-user purposes.
The BcpRegisterModule enables other code to look for a Module classname
similar to the Class registration in Delphi. Actually it is using the Delphi
Class registration as part of its implementation. The implementation looks
like this:
procedure BcpRegisterModule( aDatamodule: TDatamoduleClass; ClassInstancing: TBcpModuleInstancing; Description: string); begin { let the vcl raise exceptions with duplicate registrations } RegisterClass(aDataModule); FModules.AddObject( aDataModule.ClassName+'='+Description, pointer(ClassInstancing){store the instancing mode}); end;
|
ShowModalForm is very straight forward and uses native VCL routines:
function TCustomerTxDmd.ShowModalForm( FormClass: string): TModalResult; begin Result := TFormClass(FindClass(FormClass)).Create(Self).ShowModal; end;
|
Actions
Although actions have been available for developers for
some time now, finding implementations which do not tie to a specific control
are rarely seen. Combining the generic use of Actions with the abstraction
enabled by Interfaces allows us to the flexibility of interfaced implementation
to automatically reflect to the user interfaces without extensive coding
techniques. Actions can be assigned to menuitems and buttons and other
controls. We require three actions which will perform an Add, Modify and
Delete of a customer. These actions are enabled when the Customer datamodule
is available in memory.
The published part of the Add action declaration looks
like this:
{: Action implementing the IBcpBasicTx's Add method. } TBcpBasicTxAddAction = class(TAction) ... published {: Semicolon seperated moduleclassnames which may be used to request the interface from if this action is executed. } property Module: string read FModule write SetModule; {: Allows manual refreshing after a succesfull add. } property OnAfterAdd: TNotifyEvent read FOnAfterAdd write SetOnAfterAdd; end;
|
The Module property contains datamodule classnames that are semicolon
separated. Several datamodules may be specified here but first we'll explain
stuff for the single datamodule classname and explain the multiple classnames
later. In addition to the Add action the relevant declaration for the Modify
and Delete actions are shown below:
{: Action implementing the IBcpBasicTx's Modify method. } TBcpBasicTxModifyAction = class(TAction) ... published {: Dataset information for keyvalues. } property DataSet: TDataSet read FDataSet write SetDataSet; {: Allows manual refreshing after a succesfull modify. } property OnAfterModify: TNotifyEvent read FOnAfterModify write SetOnAfterModify; {: Specify the keyfields which are used as identification for the record. } property KeyFields: string read FKeyFields write SetKeyFields; end;
{: Action implementing the IBcpBasicTx's Delete method. } TBcpBasicTxDeleteAction = class(TAction) ... published {: Dataset information for keyvalues. } property DataSet: TDataSet read FDataSet write SetDataSet; {: Allows manual refreshing after a succesfull delete. } property OnAfterDelete: TNotifyEvent read FOnAfterDelete write SetOnAfterDelete; {: Specify the keyfields which are used as identification for the record. } property KeyFields: string read FKeyFields write SetKeyFields; end;
|
The Modify and Delete of customers require the Keyfields and the
DataSet properties to provide identification. They both contain events
which is triggered when the operation was successful.
Since these three actions require similar logic for enabling and
calling we will introduce a basic action class from which these will inherit.
The enabling is then split into two methods. The first is the function
ModuleEnabled which returns True if the Module is registered and implements
IBcpBasicTxInterface. The second is meant to be overridden in the descendant
called MethodEnabled. The Add simply returns True, but the Modify and Delete
return False if the DataSet is empty. The declaration of this basic action
class looks like:
{: BcpBasicBasicTxAction implements a datamodule's interface by delegation. In the property Module the datamodule classname is specified. Semicolon seperated classnames may be entered to allow multiple. This action is enabled if any of the specified datamodules is registered and implements the IBcpBasicTx interface. } TBcpBasicTxAction = class(TAction, IBcpBasicTx) private FDataSet: TDataSet; FModule: string; FAvailableModules: TStringList; procedure SetDataSet(const Value: TDataSet); procedure SetModule(const Value: string); protected {: Update the list of datamodules, specified by Module, to locate the registered datamodules which implement the correct interface. } procedure UpdateModuleList; {: Return the IBcpBasicTx interface of a datamodule. } function GetBasicTx: IBcpBasicTx; {: Returns true if at least one datamodule is found. } function ModuleEnabled: boolean; {: This abstract method specifies if a specific interface method is enabled. } function MethodEnabled: boolean; virtual; abstract; {: This list contains all correct datamodules, update this list by calling UpdateModuleList. } property AvailableModules: TStringList read FAvailableModules; public constructor Create(aOwner: TComponent); override; destructor Destroy; override; procedure Notification(AComponent: TComponent; Operation: TOperation); override; {: TAction method to enable targets. } function HandlesTarget(Target: TObject): Boolean; override; {: TAction method to udpate this action for a target. } procedure UpdateTarget(Target: TObject); override; {: Property required to delegate the IBcpBasicTx. } property BasicTx: IBcpBasicTx read GetBasicTx implements IBcpBasicTx; published {: Semicolon seperated moduleclassnames which may be used to request the interface from if this action is executed. } property Module: string read FModule write SetModule; {: Dataset for descendants to use. } property DataSet: TDataSet read FDataSet write SetDataSet; end;
|
ModuleEnabled returns true if any of the specified DataModule classes
(using the property Module) is registered. The implementation of ModuleEnabled
calls the UpdateModuleList and then returns True if the AvailableModuleList
contains at least one classname.
procedure TBcpBasicTxAction.UpdateModuleList; var i: integer; begin FAvailableModules.BeginUpdate; try { Convert the semicolon seperated datamodules into seperate strings. } FAvailableModules.Text := StringReplace( Self.Module, ';', #13, [rfReplaceAll]); { Remove the unregistered and unsupported modules from the list. } i := 0; while (i < FAvailableModules.Count) do begin if (BcpModuleIsRegistered(FAvailableModules[i])) and (BcpModuleImplements(FAvailableModules[i], IBcpBasicTx)) then Inc(i) else FAvailableModules.Delete(i); end; finally FAvailableModules.EndUpdate; end; end;
function TBcpBasicTxAction.ModuleEnabled: boolean; begin UpdateModuleList; Result := (AvailableModules.Count > 0); end;
|
BcpModuleIsRegistered and BcpModuleImplements are described in the
support routines part of this paper. If we take a look at the action execution
code of the Modify action we'll see how the interface of a class is required
and what support routines we require for this. (Again these routines are
not yet described.)
function TBcpBasicTxAction.GetBasicTx: IBcpBasicTx; var idx: integer; ModuleName, aCaption: string; begin { Default the first datamodule is used. } ModuleName := AvailableModules[0]; if (AvailableModules.Count > 1) then begin { Transform the action.caption into a caption for the module selection } aCaption := StringReplace(Self.Caption,'&','',[])+'...'; { Allow the user to select a datamodule. } if ModuleSelect(aCaption, AvailableModules, idx) then ModuleName := AvailableModules[idx]; end; { Return the IBcpBasicTx of the datamodule. } BcpGetModuleInterface(ModuleName, IBcpBasicTx, Result); end;
procedure TBcpBasicTxModifyAction.ExecuteTarget(Target: TObject); begin { execute and if succeeded call the aftermodify event } if (IBcpBasicTx(Self).Modify( DataSet.FieldValues[Self.KeyFields])) and (Assigned(FOnAfterModify)) then FOnAftermodify(Self); end;
|
The GetBasicTx is implemented in the base action class and is involved
by the Interface by delegation. Which is a fancy way to typecast an object
to an interface where the interface method is not directly declared in
the class of that object, but is delegated by a property of the same interface
type. The read specifier of this property returns the interface of a datamodule.
In short: If we typecast the Action to the interface this method gets called.
The problem arises if more then one datamodule is available. We
need the end-user to choose which datamodule to use. This is where the
description of the datamodule comes in, which was passed when the datamodule
was registered. The user may select a datamodule at this point since the
routine ModuleSelect results in a dialog with descriptions for the user
to choose from.
Since we know now the datamodule exists and implements the interface
(otherwise the action would be disabled and this would never have been
called), we can skip checking datamodule presence and simply return the
interface using BcpGetModuleInterface.
The ExecuteTarget of TBcpBasicTxModifyAction typecasts self to the
IBcpBasicTx interface and calls the Modify method. Passing in the key values
retrieved from its DataSet. If that succeeded and we have an event assigned
to pass on the success we call it. Application developers may use this
to refresh the DataSet somehow.
Support routines
In the above code snippets we used several support
routine calls. BcpRegisterModule is already described in the section 'Class
registration'. Below I'll summarise others with a short implementation
snippet and description.
BcpGetModuleInterface is the main support
routine. It uses other routines of which two are publicly accessible, BcpModuleIsRegistered
and BcpModuleImplements. The first returns True if the DataModule is registered
by using the result of the VCL function GetClass. The second also uses
the internal routine GetModuleClass to retrieve the class information and
checks if it mentions the appropriate interface identification with GetInterfaceEntry.
function BcpModuleIsRegistered(ModuleClassName: string): boolean; begin Result := (GetClass(ModuleClassName) <> nil); end;
function GetModuleClass(ModuleClassName: string): TDataModuleClass; begin Result := TDataModuleClass(FindClass(ModuleClassName)); end;
function BcpModuleImplements(ModuleClassName: string; Guid: TGuid): boolean; begin Result := (BcpModuleIsRegistered(ModuleClassName)) and (GetModuleClass(ModuleClassName).GetInterfaceEntry( Guid) <> nil); end;
function BcpGetModuleInterface(ModuleClassName: string; Guid: TGuid; out Obj): boolean; begin Result := (BcpModuleIsRegistered(ModuleClassName)) and (BcpModuleImplements(ModuleClassName, Guid)) and (GetModule(ModuleClassName).GetInterface(Guid, Obj)); end;
|
The other internal routine returns the correct instance of a datamodule
requested. If it was registered as single instance it first tries to locate
an earlier instance by looking through the Screens datamodule list. If
nothing was found, or if it was registered as multiple instance it returns
a new instance.
function GetModule(ModuleClassName: string): TDataModule; var Instancing: TBcpModuleInstancing; i: integer; begin Result := nil; Instancing := TBcpModuleInstancing(FModules.Objects[ FModules.IndexOfName(ModuleClassName)]); { If SingleInstance locate an existing module first. } if (Instancing = ciSingleInstance) then begin i := 0; while (i < Screen.DataModuleCount) and (Result = nil) do begin if CompareText(Screen.DataModules[i].ClassName, ModuleClassName) = 0 then Result := Screen.DataModules[i] else Inc(i); end; end; { No instance located or MultiInstance creates a new instance } if (Result = nil) then Result := (GetModuleClass(ModuleClassName).Create(Application)); end;
function BcpModuleIsRegistered(ModuleClassName: string): boolean; begin Result := (GetClass(ModuleClassName) <> nil); end;
function BcpModuleImplements(ModuleClassName: string; Guid: TGuid): boolean; begin Result := (BcpModuleIsRegistered(ModuleClassName)) and (GetModuleClass(ModuleClassName).GetInterfaceEntry( Guid) <> nil); end;
|
Finally the actual routine BcpGetModuleInterface which is a concatenation
of routines and will return True to let the caller know that the Obj parameter
is set with the proper value. If the module is not registered or the module
doesn't implement the proper interface or somehow the retrieval of the
class information or its interface fails the result is simply False.
function BcpGetModuleInterface(ModuleClassName: string; Guid: TGuid; out Obj): boolean; begin Result := (BcpModuleIsRegistered(ModuleClassName)) and (BcpModuleImplements(ModuleClassName, Guid)) and (GetModule(ModuleClassName).GetInterface(Guid, Obj)); end;
|
Deployment
Deploying the code has to be in a specific order. First we need to
assemble the runtime package with the support routines, the actions and
the unit with the interface. This is because the application requires them
and the business component packages which we'll assemble. Secondly we compile
the application with the use of runtime packages, and add our package with
support routines. Finally we can compile our business component packages
which will be loadable at runtime. These require at least the runtime package
with support routines. The figure below shows where the code resides.
The table below shows the sources included in our sample application.
The locations are the executable, the runtime required package (bcp50)
and the business component package for customer (customertx).
| Filename |
Location |
Description |
| GuidActionFrmUnit |
guidactions.exe |
This is the main test form. Showing buttons for loading packages
and a list of customers which can be modified by the business components. |
| BcpBasicTxActionUnit |
bcp50.bpl |
This unit contains all actions. |
| BcpBusinessModulesUnit |
bcp50.bpl |
This unit contains the support routines for registering datamodules
and retrieving interfaces from them. |
| IBcpBasicTxUnit |
bcp50.bpl |
Interface declaration to be used by datamodules and actions. |
| BcpModuleSelectFrmUnit |
bcp50.bpl |
End-user selection dialog if multiple datamodules exist and the
end-user should choose one. |
| CustomerTxDmdUnit |
CustomerTx.bpl |
Datamodule that handles all customer properties manipulations:
add, modify and delete. |
| CustomerTxFrmUnit |
CustomerTx.bpl |
Form for CustomerTxDmd representation. |
| CustomerPhoneTxDmdUnit |
CustomerTx.bpl |
Datamodule that handles changing customer phone numbers. |
| CustomerPhoneTxFrmUnit |
CustomerTx.bpl |
Form for CustomerPhoneTxDmd representation. |
The way ahead
This example illustrates how the classes in Delphi can be used in an
elegant and simple way if you set your mind to it. Things for the future
may be a configurable Action which read their information from some database
table. This allows declarative application assembly. Another extension
could be improved support for menuitems, if multiple datamodules exist
perhaps a submenu would be appropriate. Lots of technical improvements
and extensions come to mind thinking about new possibilities. But the main
issue is code independence and runtime loadable packages which are optional.
Already I've got actions for searches, lookups and lots more. This
enables my application to be fully workable and customisable without recompiling,
but simply by distributing new business component packages and the application
will enable its interface as if it has always been there.
Conclusion
Delphi offers loads of classes, components and routines. Re-use of
this and combining them into a generic and powerful implementation allows
application developers faster development and re-use of their business
components. Generic however must never mean leaving the software developers
clueless in implemented abstract components. These developers require business
solution related components. This paper described a generic and powerful
code implementation which enables just that for application developers.
Accessing business components using actions greatly enhances application
development. The paper also has shown how to create loadable business component
packages which extend application functionality. All this simply by setting
action properties that automatically connect to other business components
and reflect their presence.
Having the application automatically respond to loadable packages
is a great advantage, which enables real parallel development, testing
and deployment of business components.
Copyright © 2001-2002 Bommeljé Crompvoets en partners
Het is toegestaan dit artikel in zijn geheel te kopiëren en te verspreiden,
mits de tekst woordelijk in tact blijft en deze notitie bevat. Het is toegestaan
om deze tekst te citeren, of te wijzigen, mits de oorspronkelijke auteur
en houder van het copyright vermeld worden.
You may republish this paper verbatim, including this
notation. You may update, correct, or expand the material, provided that
you include a notation stating the original author and copyright holder. |