Site menuContactSitemapProduktenInfoWie zijn wijWelkomStartDownload



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.

Binary schema

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.

Business Component Packages

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:

  • An interface defines methods that can be implemented by a class.
  • A variable of an interface type can reference an object whose class implements that interface.
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.

Sources

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.








[English] [welkom] [wie zijn wij] [info] [diensten] [producten] [download] [contact] [verwijzingen]