Pacific Blue Software Logo

Interposer Classes in Delphi

Interposer Classes in Delphi

An interposer class is a class with exactly the same name as a standard class, descended from it, and declared in a unit that comes later in the uses clause. Every TEdit or TDateTimeField on a form that uses that unit becomes your class instead of the original - no new component, no package to install, no change to the form file.

It is the quickest way to change how a component behaves across a whole application, and it works the same in the VCL, FireMonkey and TMS WEB Core.


Why Use Them


The Smallest Example

An edit box that selects all its text when you tab into it:

unit Interposers.Edits;

interface

uses
  Vcl.StdCtrls;

type
  TEdit = class (Vcl.StdCtrls.TEdit)
  protected
    procedure DoEnter; override;
  end;

implementation

procedure TEdit.DoEnter;
begin
  inherited;
  SelectAll;
end;

end.

Then add the unit to the form, after Vcl.StdCtrls:

uses
  Winapi.Windows, System.SysUtils, System.Classes, Vcl.Controls, Vcl.Forms,
  Vcl.StdCtrls,
  Interposers.Edits;

Every edit on that form now selects its contents on entry. Nothing in the form designer changes; the form file still says TEdit.


How It Works


A Practical Example - Date-Times Stored as UTC

A database that stores every date-time as UTC is easy to get wrong: each read must convert to local time and each write back to UTC. Miss one and the time is out by hours. Interposing TDateTimeField does the conversion once, for every persistent date-time field on every form and data module that uses the unit:

unit Interposers.Fields;

interface

uses
  System.Classes, Data.DB;

type
  TDateTimeField = class (Data.DB.TDateTimeField)
  private
    class var GStore_As_UTC : Boolean;
          var FStore_As_UTC : Boolean;
  protected
    function  GetAsDateTime : TDateTime; override;
    procedure SetAsDateTime (Value : TDateTime); override;
    procedure GetText (var Text : string; DisplayText : Boolean); override;
  public
    constructor Create (AOwner : TComponent); override;

    class property Default_Store_As_UTC : Boolean read  GStore_As_UTC
                                                  write GStore_As_UTC;
    property Store_As_UTC               : Boolean read  FStore_As_UTC
                                                  write FStore_As_UTC;
  end;

implementation

uses
  System.SysUtils, System.DateUtils;

constructor TDateTimeField.Create (AOwner : TComponent);
begin
  inherited;
  FStore_As_UTC := GStore_As_UTC;
end;

function TDateTimeField.GetAsDateTime : TDateTime;
begin
  Result := inherited GetAsDateTime;
  if FStore_As_UTC and (not IsNull)
  then Result := TTimeZone.Local.ToLocalTime (Result);
end;

procedure TDateTimeField.SetAsDateTime (Value : TDateTime);
begin
  if FStore_As_UTC
  then inherited SetAsDateTime (TTimeZone.Local.ToUniversalTime (Value))
  else inherited SetAsDateTime (Value);
end;

procedure TDateTimeField.GetText (var Text : string; DisplayText : Boolean);
var
  LFormat : string;
begin
  if FStore_As_UTC and (not IsNull)
  then begin
         if DisplayText
         then LFormat := DisplayFormat
         else LFormat := '';
         DateTimeToString (Text, LFormat, AsDateTime);
       end
  else inherited;
end;

initialization
  TDateTimeField.GStore_As_UTC := True;
end.

Data-aware controls, AsDateTime, Value, AsString and DisplayText all go through these methods, so the rest of the application only ever sees local time. A field that holds a real local time can opt out with Store_As_UTC := False.

Points to watch:

I use the same field in TMS WEB Core, where the XData server stores UTC and the browser shows local time; only the time zone calls differ.


Adding Features - A Code Combo Box in TMS WEB Core

Interposers can add fields, properties and methods as well as override. A common need is a combo box that shows descriptions but stores a code - Active on screen, A in the database:

unit Interposers.Web_Combo;

interface

uses
  System.Classes, WEBLib.StdCtrls;

type
  TWebComboBox = class (WEBLib.StdCtrls.TWebComboBox)
  private
    FCodes : TStringList;
    function  Get_Selected_Code : string;
    procedure Set_Selected_Code (const AValue : string);
  public
    constructor Create (AOwner : TComponent); override;
    destructor  Destroy; override;
    procedure Load_Pairs (APairs : TStrings);
    property  Selected_Code : string read  Get_Selected_Code
                                     write Set_Selected_Code;
  end;

implementation

constructor TWebComboBox.Create (AOwner : TComponent);
begin
  inherited;
  FCodes := TStringList.Create;
end;

destructor TWebComboBox.Destroy;
begin
  FCodes.Free;
  inherited;
end;

procedure TWebComboBox.Load_Pairs (APairs : TStrings);
var
  I : Integer;
begin
  Items.Clear;
  FCodes.Clear;
  for I := 0 to APairs.Count - 1
  do begin
       FCodes.Add (APairs.Names [I]);
       Items.Add  (APairs.ValueFromIndex [I]);
     end;
end;

function TWebComboBox.Get_Selected_Code : string;
begin
  if ItemIndex < 0
  then Result := ''
  else Result := FCodes [ItemIndex];
end;

procedure TWebComboBox.Set_Selected_Code (const AValue : string);
begin
  ItemIndex := FCodes.IndexOf (AValue);
end;

end.

Using it:

LPairs := TStringList.Create;
try
  LPairs.Add ('A=Active');
  LPairs.Add ('S=Suspended');
  LPairs.Add ('C=Closed');
  Combo_Status.Load_Pairs (LPairs);
finally
  LPairs.Free;
end;

Combo_Status.Selected_Code := Query_Status.AsString;
...
Query_Status.AsString := Combo_Status.Selected_Code;

Every combo box already dropped on your TMS forms gains Load_Pairs and Selected_Code, without replacing a single control. My own version goes further - integer codes, a default code, yes/no lists and writing straight to a bound field - but the principle is the same.


Getting at Private Behaviour

Private methods cannot be overridden - they are invisible outside their own unit, and an interposer always lives in another one. Two things get close:

Message Handlers

Much of the VCL's behaviour sits in private message methods, such as TControl's handlers for CM_MOUSEENTER and CM_MOUSELEAVE. Messages are dispatched by number, not by method name, so an interposer can declare its own handler for the same message. Call inherited and the private original still runs:

unit Interposers.Panels;

interface

uses
  Winapi.Messages, Vcl.Controls, Vcl.ExtCtrls;

type
  TPanel = class (Vcl.ExtCtrls.TPanel)
  private
    procedure CM_Mouse_Enter (var Message : TMessage); message CM_MOUSEENTER;
    procedure CM_Mouse_Leave (var Message : TMessage); message CM_MOUSELEAVE;
  end;

implementation

procedure TPanel.CM_Mouse_Enter (var Message : TMessage);
begin
  inherited;
  BevelOuter := bvRaised;
end;

procedure TPanel.CM_Mouse_Leave (var Message : TMessage);
begin
  inherited;
  BevelOuter := bvNone;
end;

end.

Every panel now lifts when the mouse is over it. The same approach works for any WM_ or CM_ message, whatever visibility the original handler has. This is VCL only - FireMonkey and TMS WEB Core do not dispatch Windows messages.

Replacing Private Getters and Setters

A property's getter and setter are usually private, so they cannot be overridden. An interposer can instead redeclare the property itself with its own getter and setter. Code that reads or assigns the property now goes through yours, and inside them inherited PropertyName still reaches the original.

A good use is an event the interposer needs for itself. The combo box below always runs its own change handler first, and then whatever handler the form assigned - the form's code does not change at all:

type
  TComboBox = class (Vcl.StdCtrls.TComboBox)
  private
    FUser_On_Change : TNotifyEvent;
    procedure Internal_Change (Sender : TObject);
    function  Get_On_Change : TNotifyEvent;
    procedure Set_On_Change (AValue : TNotifyEvent);
  protected
    procedure Loaded; override;
  public
    constructor Create (AOwner : TComponent); override;
    property OnChange : TNotifyEvent read  Get_On_Change
                                     write Set_On_Change;
  end;

implementation

constructor TComboBox.Create (AOwner : TComponent);
begin
  inherited;
  inherited OnChange := Internal_Change;
end;

procedure TComboBox.Loaded;
var
  LStreamed : TNotifyEvent;
begin
  inherited;
  LStreamed := inherited OnChange;
  if TMethod (LStreamed).Code <> @TComboBox.Internal_Change
  then FUser_On_Change := LStreamed;
  inherited OnChange := Internal_Change;
end;

function TComboBox.Get_On_Change : TNotifyEvent;
begin
  Result := FUser_On_Change;
end;

procedure TComboBox.Set_On_Change (AValue : TNotifyEvent);
begin
  FUser_On_Change    := AValue;
  inherited OnChange := Internal_Change;
end;

procedure TComboBox.Internal_Change (Sender : TObject);
begin
  { the interposer's own work - eg. writing the selected code to a field }
  if Assigned (FUser_On_Change)
  then FUser_On_Change (Sender);
end;

Two things to know:

My TMS WEB Core combo box uses exactly this to keep its bound field in step while the form keeps its own OnChange.


Interposer or Class Helper?

Class helpers also add methods to an existing class without touching its declaration, and they are often the better choice. The difference:

The two mix well - one unit can carry helpers for the conveniences and interposers for the behaviour.


Chaining Interposers

An interposer can itself be interposed. A reporting unit can declare TStringField = class (Data_Fields.TStringField) on top of an application-wide Data_Fields interposer, adding report-only behaviour for forms that list both units - in that order.


Pitfalls


History

Stephen Posey named and described the technique in Interposer Classes, The Delphi Magazine, Issue 33 (May 1998). I used it in print the following year, in Better Calculated Fields, The Delphi Magazine, Issue 43 (March 1999) - interposing TStringField and TTable so calculated fields could be read and written at any time. That article is republished here. The technique has not changed since Delphi 3.


PBS Delphi - Changing Components with Interposer Classes


Back to Articles for Developers
Back to Articles on Delphi
Better Calculated Fields in Delphi (The Delphi Magazine, 1999)
Delegating an Interface to a Property in Delphi (implements)
Free tool : Delphi Version Lookup

If you found this useful, then please consider making a donation.

paypal
QR Code for donation Please donate if helpful