Thursday, January 9, 2014

Two Approaches to Sub-classing Components Compared

Delphi is an object oriented programming language. Unless you write nothing but console applications, this fact is obvious. For example, when your application includes either a form or a data module, the class that defines the form or data module is a descendent of an existing class. In the case of a form, it is a TForm descendant, and in the case of a data module it is a TDataModule descendant.

This process of extending an existing class, especially one that is not TObject, is used extensively by the visual component library (VCL), and to a lesser extent in the runtime library (RTL). Importantly, it is a technique that you can use to create your own custom classes, ones that inherit the power of an existing class, and which extend that class to add additional features. These features might include new properties, additional methods, or alternative behaviors for methods inherited from the ancestor class.

Overall, the VCL is a remarkable and rich component library. Nonetheless, it is not uncommon, especially with seasoned developers, to want to extend existing classes of the VCL or RTL to add custom capabilities.

In this post I am going to discuss two different, though not entirely dissimilar, techniques for creating a new class based on an existing VCL component. In most cases, these techniques can also be used to extend any component, whether created by you or your development team or a third party. Towards the end of this post I will compare these two techniques by discussing the particular strengths of each approach.

The Traditional Method

Most developers who sub-class components of the VCL do so by declaring that class as a descendant of the existing component, followed by compiling that class into a runtime package which they install in Delphi. I am going to demonstrate this technique by creating a component that extends the TDBGrid class, adding a handy feature for reading data from it's underlying TDataSet.

This sub-classed grid exposes a GetField method, which returns a TField associated with the current record of the data set being displayed by the grid based on the name of the underlying dataset field. This new class also includes an InitializeDictionary method which must be called at least once after the grid has been associated with a dataset, but before the first call to GetField.

Here is the declaration of this new class, named TEasyReaderDBGrid:

type
  TEasyReaderDBGrid = class(Vcl.DBGrids.TDBGrid)
  private
    { Private declarations }
    FDict: TDictionary< string, TField>;
  public
    { Public declarations }
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    procedure InitializeDictionary(DataSet: TDataSet);
    /// 
    /// You must call InitializeDictionary each time you assign the
    /// DataSet property of the grid before you can use GetField
    /// 
    function GetField(const Name: string): TField;
  end;

This class is not very complicated. It uses a generic TDictionary (declared in System.Generics.Collections) to implement the GetField method. This can be seen in the implementation of this class, shown here:
{ TDBGrid }
constructor TEasyReaderDBGrid.Create(AOwner: TComponent);
begin
  inherited;
  FDict := TDictionary< string, TField>.Create;
end;
destructor TEasyReaderDBGrid.Destroy;
begin
  FDict.Free;
  inherited;
end;
function TEasyReaderDBGrid.GetField(const Name: string): TField;
begin
  Result := FDict.Items[Name];
end;
procedure TEasyReaderDBGrid.InitializeDictionary(DataSet: TDataSet);
var
  Field: TField;
begin
  for Field in DataSet.Fields do
    FDict.Add(Field.FieldName, Field);
end;
All we need to do to make this component available on the component palette is to create a design time package, add this component's unit to the package, make a call to RegisterComponents from a Register procedure, and then install this new package.

I've added the forward declaration and implementation of the Register method to the same unit in which the TEasyReaderDBGrid class is declared, adding this new component to the Samples page of the Tool Palette. Finally, here is the source of the design time package:

package EasyReaderDBGrid;

{$R *.res}
{$IFDEF IMPLICITBUILDING This IFDEF should not be used by users}
{$ALIGN 8}
{$ASSERTIONS ON}
{$BOOLEVAL OFF}
{$DEBUGINFO ON}
{$EXTENDEDSYNTAX ON}
{$IMPORTEDDATA ON}
{$IOCHECKS ON}
{$LOCALSYMBOLS ON}
{$LONGSTRINGS ON}
{$OPENSTRINGS ON}
{$OPTIMIZATION OFF}
{$OVERFLOWCHECKS OFF}
{$RANGECHECKS OFF}
{$REFERENCEINFO ON}
{$SAFEDIVIDE OFF}
{$STACKFRAMES ON}
{$TYPEDADDRESS OFF}
{$VARSTRINGCHECKS ON}
{$WRITEABLECONST OFF}
{$MINENUMSIZE 1}
{$IMAGEBASE $400000}
{$DEFINE DEBUG}
{$ENDIF IMPLICITBUILDING}
{$DESCRIPTION 'Runtime package for TEasyReaderDBGrid'}
{$DESIGNONLY}
{$IMPLICITBUILD ON}

requires
  rtl,
  vcl,
  dbrtl,
  vcldb;

contains
  Traditionalu in 'Traditionalu.pas';

end.

Once we compile and install this new design time package, the new component, TEasyReaderDBGrid appears on the Samples page of the Tool Palette, as shown in Figure 1.

Figure 1. A traditionally sub-classed component appears on the Tool Palette

Figure 2 shows a VCL Forms application on which an instance of the TEasyReaderDBGrid class has been placed at design time. In this figure a call to GetField has been made from the OnClick event handler of a button, shown here:

procedure TForm1.Button1Click(Sender: TObject);
begin
  ShowMessage(EasyReaderDBGrid1.GetField('CustNo').AsString);
end;


Figure 2. The GetField method of the TEasyReaderDBGrid component returns a TField associated the grid's TDataSet.

The Interceptor Method

Unlike the traditional method, which involves adding a component to the component palette, the interceptor method "intercepts" the name of the existing VCL component, mapping it to a different class. Consider the form shown in Figure 3.


Figure 3. A VCL Forms application using a class that intercepts TDBGrid

The form in Figure 3 looks and behaves like that shown in Figure 2. The difference is that the grid that appears in Figure 3 is not a TEasyReaderDBGrid. Instead, the grid is actually an instance of the TDBGrid class. This class, however, is intercepted and extended, giving the TDBGrid class the methods necessary to support the features of the TEasyReaderDBGrid class.
There are two general approaches to class interception. One is to intercept the class within the module where the extended class is used. This approach is shown in the following type declaration:

type
  TDBGrid = class(Vcl.DBGrids.TDBGrid)
  private
    { Private declarations }
    FDict: TDictionary< string, TField>;
  public
    { Public declarations }
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    procedure InitializeDictionary(DataSet: TDataSet);
    /// 
    ///   You must call InitializeDictionary each time you assign the
    ///   DataSet property of the grid before you can use GetField
    /// 
    function GetField(const Name: string): TField;
  end;
  TForm1 = class(TForm)
    ClientDataSet1: TClientDataSet;
    DataSource1: TDataSource;
    Button1: TButton;
    DBGrid1: TDBGrid;
    procedure Button1Click(Sender: TObject);
    procedure FormCreate(Sender: TObject);
  end;

Alternatively, the interceptor class can be declared in its own unit. In that case, all you need to do is ensure that the unit in which the interceptor class is declared appears later in the uses clause than the unit of the class that is being extended. Here is an example of how the interface section of an interceptor unit might look (the implementation is identical to that shown earlier for the TEasyReaderDBGrid class):

unit Interceptoru;

interface

uses System.Classes, Vcl.Grids, Vcl.DBGrids,
     System.Generics.Collections, Data.DB;

type
  TDBGrid = class(Vcl.DBGrids.TDBGrid)
  private
    { Private declarations }
    FDict: TDictionary< string, TField>;
  public
    { Public declarations }
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    procedure InitializeDictionary(DataSet: TDataSet);
    /// 
    ///   You must call InitializeDictionary each time you assign the
    ///   DataSet property of the grid before you can use GetField
    /// 
    function GetField(const Name: string): TField;
  end;

When using an interceptor unit, I generally find it necessary to document the placement of the interceptor unit in the uses clause, as shown here:

unit MainformIu;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils,
  System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.Grids,
  Vcl.DBGrids, system.Generics.Collections, Data.DB,
  Vcl.StdCtrls,Datasnap.DBClient,
  Interceptoru;  //This unit must appear later in this
                 //uses clause than the Vcl.DBGrids unit

type
  TForm1 = class(TForm)
    ClientDataSet1: TClientDataSet;
    DataSource1: TDataSource;
    Button1: TButton;
    DBGrid1: TDBGrid;
    procedure Button1Click(Sender: TObject);
    procedure FormCreate(Sender: TObject);
  end;

The real difference in implementation between the traditional method and the interceptor method is that your code looks as though you are using the class that you sub-classed, as opposed to the sub-class itself. This can be seen in the following event handler, which is similar to the OnClick event handler for the button shown earlier in this article.

procedure TForm1.Button1Click(Sender: TObject);
begin
  ShowMessage(DBGrid1.GetField('CustNo').AsString);
end;

Comparing These Two Methods

While the end result of these two approaches is identical, there are significant differences. These differences make each of these mechanisms better suited for some uses over the other. I'll begin by considering the advantages of the traditional approach.

Advantage of the Traditional Approach

The primary advantage of the traditional approach is that your sub-classed component can appear in the Tool Palette. Having the component in the Tool Palette provides two benefits. First, any published properties that you add to your sub-classed component will appear in the Object Inspector at design time. Second, once you've placed the component onto your module from the Tool Palette, the unit in which your component is defined will be added to your interface section automatically the next time you save or compile your project.

Both of these benefits come down to convenience. Traditionally sub-classed components are easier to use.

Advantages of the Interceptor Approach

While the interceptor approach has the drawback that it is somewhat more complicated to use, it also introduces benefits that make it a powerful alternative. To begin with, creating a interceptor class takes less time. There is no need to create a design time package and a design time package does not need to be installed.

Not needing a design time package actually makes interceptor classes easier to share with a development team. Using the traditional approach, you need to provide each of the developers who will be working on the project with access to the package, which they will need to install into their copy of Delphi. When using an interceptor class, all you need to do is add the interceptor class unit into a directory on your library search path, and it will just work. Alternatively, you can make the interceptor unit a unit of your project, again making any classes defined in it immediately available to any unit that uses the interceptor unit.

Another advantage of an interceptor class is that it makes it remarkably easy to extend an existing class when you want to add only one or two new features. Furthermore, when you declare your interceptor class directly in the unit from which it will be used, you can customize each instance of the ancestor class for that module. For example, you might want to add an additional method to a TListBox interceptor on a given form. If you have a second form that also needs a modified TListBox, but with a different custom method, no problem. Create a different interceptor class for each form and you are done.

Finally, and what I think is absolutely the best advantage of interceptor classes is that it lets you make customizations to a component's sub-classes. For example, imagine that you want to add one or more custom properties to the individual menu items of a TMainMenu. This can be done very easily by creating an interceptor class for TMenuItem. After that, all of the menu items you add to your main menu will have those properties. Granted, you can only access those properties at runtime, but that is just a detail.

Here is a simple example of a TMenuItem interceptor type declaration that, if added to a unit prior to the TForm declaration, adds a MyInteger runtime property to any menu item appearing on that form.

type
  TMenuItem = class(Vcl.Menus.TMenuItem)
  strict private
    FMyInteger: Integer;
  public
    property MyInteger: Integer read FMyInteger write FMyInteger;
  end;

By comparison, if you use the traditional approach, you would have to actually sub-class TMainMenu and implement an extended version of TMenuItem from within the owner class.

Summary

Sub-classing existing classes of the VCL (or other libraries) is a powerful tool that every Delphi developer should take advantage of. In this article I have looked at the two primary mechanisms for sub-classing existing components. Each of these approaches have their strengths, making each best suited for some applications over the other.

Copyright (c) 2013 Cary Jensen. All Rights Reserved