Midnight Beach logo

Models, Views, and Frames

This originally appeared as a chapter in High Performance Delphi 3 Programming and then as a two part magazine series that started in the October 1997 issue of Visual Developer magazine as the cover article.

  Sections
  1. Introduction
  2. Real Live Code
  3. From Embedded Forms To Views
  4. Interfaced Forms
  5. Abstract, Valid, and Fickle Views
  6. Model Editors
  7. The sample model
  8. Other Applications
Full Source
  Listings
  1. A special constructor for embedded forms
  2. Model, View, and Frame Behavior
  3. InterfacedForms.pas
  4. Views.pas
  5. The IValid methods in ValidViews. pas
  6. Extracts from FickleViews.pas
  7. Running model editors
  8. TAbstractPropertySheet .InitializeSheet
  9. TAbstractWizard .SetCurrentPage
  10. EmployeeIdViews .pas
  Figures
  1. This wizard shares 'view' code with the property sheet in Figure 2.
  2. This property sheet shares 'view' code with the wizard in Figure 1.
  3. Views look like normal forms at designtime.
  4. Views don't look like normal forms at runtime.
  5. The different sorts of interfaced forms in the sample project.
  6. This view is also a frame. At runtime, it places PersonIdView's on the blank panels.
  7. The PersonIdView at designtime.


One of Delphi's subtler virtues is the way it simplifies many aspects of the Windows API. Things that once required lots of cumbersome code have been reduced to a property assignment. What was so arcane as to be unimaginable is now quite simply trivial. While the Canvas property is probably the most familiar example of this, the Parent property deserves our attention, too. When you set a control's Parent property, you have told Windows to make that control a child window of its new Parent. It is now visible and enabled whenever its parent is visible and enabled. (This is how tabbed notebooks work: Each page is, in effect, a panel. All the components on it are children of the notebook page. When a page is brought to the front, over the other pages, it becomes visible, and so do all the components on it.) If you set the Parent property at runtime, you can get all sorts of special effects, from dynamic creation of notebook-like controls to placing one form on a blank area of another form.

Why might you want to embed one form in another form? Consider these four scenarios:

  1. You are building wizards to walk users through the creation of an object. You also want to give your users property sheets with tabbed notebooks, so that they have random access to the object's properties and don't have to go through each step of the wizard to change a single property. The only real difference between a wizard and a property sheet is that a wizard walks the user from one page to the next (and will only allow them to move on when each page contains valid data) while a property sheet allows the user to access pages in any order they like. That is, as in Figures 1 and 2, the tabs of a property sheet contain the exact same view of a part of the object as the pages of a wizard . If you can use the same code for wizard pages as for property sheet tabs, there is no possibility of the two getting out of synch.
  2. You have objects that can appear in more than one context. For example, a Person may be an Employee and an Employee's Supervisor. If you can use the same code to display the supervisor record as to display the employee record, you can save code and, again, eliminate the possibility of the two getting out of synch.
  3. You are part of a team developing a large and complicated tabbed dialog. Because it is so big, the team has decided to put several people to work on the same dialog, with each person working on one or more of the tabs. Rather than always merging changes into a single humongous unit, you would like each tab to 'live' in its own unit. This will keep you from stepping on each others toes, and it will also keep the code clearer and more focused.
  4. You have an object hierarchy, and you need a form that lets your users see and/or change any member of the hierarchy. Some things can be done to all members of the hierarchy; other actions are only possible on some members of the hierarchy. You would like to have a single form with the controls appropriate to the common actions. At runtime you will add the specialized controls appropriate to each object type, and change them as you change objects.
The first two cases are clearly the most similar. You have objects and standardized ways for your user to examine and manipulate the objects. Models and views. What you need now is a frame. A frame can hold any view. When a frame is visible, so is the view it contains. The same view can be placed in more than one frame. View may, in turn, act as frames for views of embedded or referenced objects.

Frames can handle the third scenario rather neatly, too. If you build a tabbed dialog with a frame on each tab, you have a generic tabbed dialog that can hold any views that you need it to hold. The form's unit has been pared down from an unwieldy unit containing all sorts of (at least potentially) unrelated code to a simple container with little more than the code to fill each frame and, perhaps, some OnChanging logic that tells the active frame to ask its view if it's OK to change to another tab.Your large tabbed dialog can now be split into multiple units, so that each member of a team can check in her own updates. No more frustrating losses of updates or reversal of bug fixes due to sloppy merging.

Somewhat similarly, a form for editing the class hierarchy in scenario four might contain a few controls for the things that can be done to all members of the hierarchy and a frame where you can place the view specific to each member of the hierarchy. (You could make the object responsible for telling the editing form which view to use.) To switch from one type of object to another, you'd simply put a different view in the frame.

Real Live Code

This chapter might have seemed a bit abstract, so far. Day in, day out, you work with objects, components, forms, and event handlers - not models, views, and frames. But these abstractions are useful and even necessary. The standard model/view abstraction helps us avoid the "RAD trap" of scattering our applications' logic throughout a bunch of event handlers where it's hard to follow, hard to modify, and hard to reuse. Adding the notion of a frame helps us avoid the similar trap of tying a view to a particular container object, like a form, panel, or notebook page. Still, the theory's only as good as the code it helps you write - how do you implement a view so that you can plop it into more than one frame?

One approach might be to use the new D3 notion of component templates. These let us bundle up a group of interacting components - complete with names and event handlers - and place them on the component palette for reuse. This is a great idea, but it doesn't quite do what we want, here, because what we end up with is components on a form. When we want to put the same view on two different forms we find that the collection as a whole is not an object that we can give methods to - so how can we tell the view to read or write its model? When we have, say, two different Person views in the same Employee view, we find that placing two instances of a component template on the same form, means that all the controls in the second instance lose their saved names and end up as Label1, Edit1, and so forth. When we want to do team development of complex dialogs, we find both that all the pages end up in the same unit, and also that changing the template doesn't change the instance.

A similar but 'stronger' approach is to make a view into a compound component, whose private variable include other visual components. However, if you've ever actually done this, you know that creating and resizing the component is a real nightmare. Instead of simply placing components on a form the way we've grown used to, we have to explicitly create each subcomponent. Instead of dragging components around and setting properties in the Object Inspector until things look right, we have to manually set each and every property. This is doable, of course, but it's very slow, tedious, and error-prone, and we can end up with a lot of code that's hard to read and/or maintain. Worse, because the process is so painful, one tends to try to do as little as possible and ends up with ugly, poorly laid out messes. Views built this way may 'have all the widgets they need', but often aren't very usable.

We could avoid all the pain and ugliness of manually building compound components if we could build a a form visually and then turn it into a component. Delphi, does in fact, allow us to do just this, but it's not particularly easy or straightforward. You have to buy or build a special design-time-only component to set the form's undocumented IsControl property; add some code to the form; and manually edit the .dfm file to change the form object's parent class. If you really want to do this, Ray Lischner's excellent book Secrets Of Delphi 2 supplies the special component to tweak IsControl, and tells you step-by-step how to proceed - but while I found this section of his book fascinating reading, I've never actually used the technique and recommend that you don't either. Why? Because you have to go through the same procedure each time you create a new view, or change an existing one. Turning a form into a component in this way seems to make more sense for really generic components - such as bundling a TMemo or a TRichEdit with a toolbar - than for views.

Instead, I use form inheritance to derive perfectly ordinary forms from TEmbeddedForm. As you can see in Listing 1, which is an extract from Embedded.pas, embedded forms have a special constructor which lets them be treated as 'lightweight controls' which can be placed on any container control - panel, notebook page, or group box - at runtime. Since forms are themselves objects, you can freely add any methods you need to make them act like views. Since these lightweight controls are normal forms, it's just as easy to change an embedded form as your project evolves as it is to change any other form.

Listing 1 - A special constructor for embedded forms

type
  EmbeddedFormMode = (efmZoomed, efmTopLeft, efmCentered);

function ALZ(Number: integer): Cardinal; // At Least Zero
begin
  if Number > 0
    then Result := Number
    else Result := 0;
end;

constructor TEmbeddedForm.CreateEmbedded(
  _Owner: TComponent;
  Frame:  TWinControl;
  Mode:   EmbeddedFormMode );
begin
  Inherited Create(_Owner);

  Parent      := Frame;
  BorderIcons := [];
  BorderStyle := bsNone;

  case Mode of
    efmZoomed:   Align := alClient;
    efmTopLeft:  begin
                 Top  := 0;
                 Left := 0;
                 end; // efmTopLeft
    efmCentered: begin
                 Top  := ALZ((Frame.Height - Height) div 2);
                 Left := ALZ((Frame.Width  - Width)  div 2);
                 end; // efmCentered
    else         Assert(False);
    end; // case
  Visible := True;
end; 

The most important line in Listing 1 is "Parent := Frame", which makes the Frame control the embedded form's Parent. This is just like what is done 'behind the scenes' for a form's design-time controls when a form is loaded, and it has three consequences. First, a child control is visible whenever its parent is visible. Thus, hiding the frame hides the view, while making the frame visible or bringing it to the front also makes the view it contains visible. Second, child controls are clipped to their parent's client area; large views are automatically clipped to fit within their frame. Third, the child control is positioned relative to its parent's client area; just as with any other control, the embedded form's Top and Left properties are relative to the container it's placed on.

This last means that when an embedded form is "zoomed" by setting Align to alClient, it acts just like any other alClient aligned control - it sizes itself to fill its frame, and automatically resizes itself (and calls any OnResize handler) whenever the frame is resized. Conversely, unzoomed views retain their design-time size, and may be centered or placed at the topleft corner of their frame. You can match the view's starting size to its frame's starting size by setting its ClientHeight and ClientWidth properties at design time - or you can size frame windows based on the design size of their embedded forms, as the generic wizard and property sheets I'll talk about in the Model Editors section do.

Before we get too far from Listing 1, though, I should point out that the lines "BorderIcons := []" and "BorderStyle := bsNone" mean that what actually appears at runtime is the view form's client area - there's no border or caption to show the frame actually contains a whole independent form. Thus, as you can see in Figures 3 and 4, a view's designtime Caption will have no effect at all at runtime.

From Embedded Forms To Views

The ability to use a form as a lightweight control is obviously a good start. We can place the form wherever we like. We can make as many copies of the form as we need. We can add methods to the form object so that it 'acts like' a view.

How should a view act?

  • It needs to be able to read data from a model object, and it needs to be able to write data back to a model object.
  • It may need to be able to validate itself - a wizard typically doesn't let the user move to the next page until the current one is valid, and a property sheet usually won't let you save invalid data. Then again, it may not - you may not care whether a free form Notes field is filled in or not.
  • It needs some way to tell its frame when it's Valid property changes, so the frame can enable or disable a Next or OK button.
  • It may need the ability to display data without allowing the user to edit it. A property sheet may be readonly if the current user doesn't have permission to edit a model object, or simply because the user hasn't locked that object so as to avoid race conditions.
These 'rules' are captured in Listing 2, which is an extract from Models.pas.

Listing 2 - Model, View, and Frame Behavior

type
  TModel      = TObject; 

  // Both IView and IModelEdit have a ReadOnly property
  IReadOnly =  interface
               function  GetReadOnly: boolean;
               procedure SetReadOnly(Value: boolean);
               property  ReadOnly: boolean read  GetReadOnly
                                           write SetReadOnly;
               end;

  // Fill a view from a model & write changes back;
  // frame/view interactions
  IFrame =     interface;
  IView =      interface (IReadOnly)
               procedure ReadFromModel(Model: TModel);
               procedure WriteToModel(Model: TModel);

               function  GetValid: boolean;
               procedure SetValid(Value: boolean);
               property  Valid: boolean read GetValid
                                        write SetValid;


               procedure AddNotifiee(   Notify: IFrame);
               procedure RemoveNotifiee(Notify: IFrame);
               end;
  IFrame =     interface
               procedure OnValidChanged(
                 ChangingObject: TObject;
                 View:           IView );
               end;

  // Wizards and Property Sheets are "model editors"
  IModelEdit = interface (IReadOnly)
               // Low level routines that allow an app to setup
               // an editor once, and run it several times.
               procedure Initialize;
               function RunEditor(Model: TModel): boolean;
               procedure Finalize;
               // Initialize/RunEditor/Finalize
               function EditModel(Model: TModel): boolean;
               end;

It's probably pretty clear that we're working with a simple architecture here. Models are totally passive containers of data that are usually created, loaded, or stored by a data module Views can read and write models on command, and can tell their frames when they become valid or invalid. Model editors - both wizards and property sheets - simply setup and run, and report back on whether they've actually made any changes to the model object because the user hit OK.

What we don't have is a full-blown Model-View-Controller architecture, where models can tell views to update themselves because the model has changed, and so on. It certainly wouldn't be all that hard to add this, but it would only further complicate a chapter on embedded forms - and it's not as if this simple Model-View-Frame architecture is weak or simplistic. I've used it with great success in several different projects.

Before we move on, you may wondering why I use interfaces, instead of simply defining view objects and frame objects and deriving from them. Am I just using interfaces for the sake of using the new whizbang features in D3?

Not really. To start with, views and frames have a circular relationship. Frames need to tell their views when to read or write models; views need to tell frames when their Valid property changes. Obviously, I could have implemented this circularity using forward class declarations instead of a forward interface declaration, but I think using interfaces makes the interactions more obvious than when they're obscured by various unrelated object properties and methods. Also, though I don't take advantage of this in EmbeddedForms.dpr, using interfaces means that a view might be implemented in several different ways; views don't have to be descendants of TEmbeddedForm. The most important reason, though, is that a view may also be a frame. For example, an Employee object might contain references to 'employee' and 'supervisor' People objects, each of which contains name and address information. A view of the Employee object might contain a view of the employee or supervisor field. Delphi doesn't support multiple inheritance, so an object can't be simultaneously a TView and a TFrame, but an object can easily implement both the IView and IFrame interfaces.

Before Delphi supported interfaces, the best way to implement something like the INotify protocol was to use procedural types:

type
  TOnValidChanged = procedure(ChangingObject: TObject) of object;

procedure TView.AddNotifiee(Callback: TOnValidChanged)

This worked, but TOnvalidChanged is basically the same as TNotifyProc, and obviously every Delphi program has a lot of those. You can pass any TNotifyProc to this AddNotifiee, and the compiler has no way to prevent you from passing the wrong one by mistake. With interfaces, the frame's callback has to have the right name and the right signature, and it has to be in an object that has promised to implement the IFrame protocol - so it's much harder to make a careless mistake.

Interfaced Forms

When I began to implement the interfaces in Listing 2, I ran into a snag. Though the resolution was simple, it took me many hours of generating and testing hypotheses before I could stop my system from crashing whenever I called AddNotifiee(Self) from a form that implemented IFrame. To understand what was going on, you're going to need a bit of background.

The D3 documentation is quite clear that any object that implements an interface must also implement the IUnknown interface, which supports reference counting and interface querying. If you declare

type
  IFoo = interface
         procedure Foo;
         end;
  TFoo = class (TObject, IFoo)
         procedure Foo;
         end;

procedure TFoo.Foo; begin end;

the compiler will complain about the undeclared identifiers QueryInterface, _AddRef, and _Release. You have to explicitly implement IUnknown, or you have to derive your object from TInterfacedObject instead of from TObject. On the other hand, the compiler won't complain at all about

type
  TFoo = class (TForm, IFoo)
         procedure Foo;
         end;

procedure TFoo.Foo; begin end;

That means that Borland implemented IUnknown somewhere in the VCL, and we're home free, right?

Well, no. Whenever you pass a TForm as an interface reference, you get GPF's in the VCL. It turns out that while TComponent does implement IUnknown's methods, it does so in a way which isn't very useful to those of us who want to use interfaces within an application.. IUnknown calls are delegated to FVCLComObject, which is a pointer that is only set when you call GetComObject to get an interface reference for the object. What's more, GetComObject only sets FVCLComObject if you have used VclCom in your project. If you do that, your call to GetComObject generates complaints about class factories not being registered - at which point I stopped caring. This is probably great if you are sharing COM objects with other applications, but if all you wanted was to add interfaces to your forms?

It's far simpler to just look at the implementation of TInterfacedObject and add a simple, self-contained IUnknown implementation to TForm, and then just derive from TInterfacedForm instead of from TForm.

Listing 3 - InterfacedForms.pas

unit InterfacedForms;

// Copyright © 1997 by Jon Shemitz, all rights reserved.
// Permission is hereby granted to freely use, modify, and
// distribute this source code PROVIDED that all six lines of
// this copyright and contact notice are included without any
// changes. Questions? Comments? Offers of work?
// mailto:jon@midnightbeach.com

// ------------------------------------------------------------

// Adds a functional IUnknown implementation to TForm.

interface

uses Classes, Forms;

type
  TInterfacedForm =
    class (TForm, IUnknown)
    private
      fRefCount: integer;
    protected
      function QueryInterface( const IID: TGUID;
                               out Obj): Integer; stdcall;
      function _AddRef: Integer;                  stdcall;
      function _Release: Integer;                 stdcall;
    public
      property RefCount: integer read fRefCount write fRefCount;
    end;

implementation

uses Windows; // for E_NOINTERFACE

// IUnknown code based on the TInterfacedObject source

function TInterfacedForm.QueryInterface( const IID: TGUID;
                                         out Obj): Integer;
begin
  if GetInterface(IID, Obj)
    then Result := 0
    else Result := E_NOINTERFACE;
end;

function TInterfacedForm._AddRef: Integer;
begin
  Inc(fRefCount);
  Result := fRefCount;
end;

function TInterfacedForm._Release: Integer;
begin
  Dec(fRefCount);
  Result := fRefCount;
  if fRefCount = 0 then Destroy;
end;

end.

Pretty simple, actually. In retrospect, I don't know what took me so long, though I suppose I was sort of blindsided by the assumption that adding interfaces to a form wouldn't compile unless it was safe. While I was experimenting, however, I discovered one more problem with D3's implementation of interfaces that I should mention before we move on.

Interface references are reference counted, just like huge strings are. Every time you make a copy of an interface reference variable (either by direct assignment, or by passing it to a procedure), the object's _AddRef method gets called to increment the reference count. Every time you remove a reference to the interface (either by explicitly reassigning a variable, or when it goes out of scope), the object's _Release method gets called to decrement the reference count. When the reference count goes to 0, the object frees itself. 'Old-style' object references don't affect the reference count at all.

This works perfectly - if you only ever interact with the object through interface references. For example, if you have

type
  IFoo = interface
         procedure Foo;
         end;
  TFoo = class (TForm, IFoo)
         procedure Foo;
         end;

procedure TFoo.Foo; begin end;

procedure Bar(InterfaceReference: IFoo); begin end;

begin
  Bar(TFoo.Create);
end.

the TFoo created in the call to Bar will be automatically freed when Bar returns. But consider a slightly different scenario, where we mix object reference and interface references:

var
  ObjectReference: TFoo;

begin
  ObjectReference := TFoo.Create;
  try
    Bar(ObjectReference);
  finally ObjectReference.Free; end;
end.

The problem is that setting ObjectReference to TFoo.Create does not affect the object's reference count. After we set ObjectReference, the RefCount property is still 0, just as it was when the object was created. However, when we call procedure Bar, we do an implicit assignment to its InterfaceReference parameter. This generates a call to _AddRef, which sets RefCount to 1. When Bar returns, InterfaceReference goes out of scope. This in turn generates a call to _Release, which sets RefCount back to 0, and thus frees the object. ObjectReference is now invalid! The next time we refer to it - when we Free it, in this case - we will get a GPF.

Obviously, this scenario is a bit contrived, but it serves to illustrate the sort of problems you will face if you start adding interfaces to legacy code. For that matter, even in new code there will be times when you find that the most natural way to work with an object is through a mixture of object and interface references. (In particular, there's nothing like TList that works with interface references.)

In these cases, what you will need to do is to artificially set the object's reference count to 1, before you ever use an interface reference to it. For example, the next section's TAbstractView class has the following OnCreate handler:

procedure TAbstractView.FormCreate(Sender: TObject);
begin
  inherited;
  _AddRef;  // so can pass Self as an interface reference
end;

Explicitly calling _AddRef this way means that the first interface reference sets RefCount to 2, and that RefCount will never go to 0. Thus, the object will never destroy itself and invalidate your object references; it will survive until you explicitly free it, in the old-fashioned way.

Of course, you only need to call _AddRef explicitly if you will be mixing object and interface references. If you will only interact with the object via interface references, you should be very chary of explicitly calling _AddRef lest you mess up the reference counting so that your object doesn't free itself when it should. Conversely, when you do have such a 'pure interface' object, you should be careful to never create an object reference to it, lest that object reference become invalid when the interface reference count goes to 0 and the object frees itself. One simple precaution you can take is to make all the interfaced methods protected - they will still be publically accessible via the interface, but if you can't access them from an object reference, you're not particularly likely to ever create one.

Abstract, Valid, and Fickle Views

As you can see from the inheritance tree in Figure 5, the EmbeddedForms project uses interfaced forms to build two basic sorts of views: valid views, whose Valid property is always True, and fickle views, whose Valid property may change. You might use a valid view for something like a memo field, where you don't care about the contents in any way, or perhaps for an initial or final panel of a wizard. You would use a fickle view any time it's possible for users to enter invalid data that they should not be allowed to save - a date like February 31st, proposed expenditures that exceed the budget, and so on. Since both implement the IView interface, both can be handled identically by generic code that knows only that it has a collection of views to deal with.

Both types of view descend from Listing 4's TAbstractView:

Listing 4 - Views.pas

unit Views;

// Copyright © 1997 by Jon Shemitz, all rights reserved.
// Permission is hereby granted to freely use, modify, and
// distribute this source code PROVIDED that all six lines of
// this copyright and contact notice are included without any
// changes. Questions? Comments? Offers of work?
// mailto:jon@midnightbeach.com

// ------------------------------------------------------------

// Maps the IView contract onto an embedded form. Typically,
// you would derive views from TValidView or TFickleView.

interface

uses
  Models, Embedded;

type
  TAbstractView = class(TEmbeddedForm, IView)
    procedure FormCreate(Sender: TObject);
  private
    fReadOnly: boolean;
  protected
    function  GetValid: boolean;              virtual; abstract;
    procedure SetValid(Value: boolean);       virtual; abstract;

    function  GetReadOnly: boolean;           virtual;
    procedure SetReadOnly(Value: boolean);    virtual;
  public
    procedure ReadFromModel(Model: TModel);   virtual;
    procedure WriteToModel(Model:  TModel);   virtual;

    procedure AddNotifiee(   Notify: IFrame); virtual; abstract;
    procedure RemoveNotifiee(Notify: IFrame); virtual; abstract;

    property Valid:    boolean read GetValid write SetValid;
    property ReadOnly: boolean read  fReadOnly
                               write SetReadOnly;
  end;

  TViewClass = class of TAbstractView;

implementation

{$R *.DFM}

function  TAbstractView.GetReadOnly: boolean;
begin
  Result := fReadOnly;
end; // TAbstractView.GetReadOnly

procedure TAbstractView.SetReadOnly(Value: boolean);
begin
  fReadOnly := Value;
  Enabled   := not Value;
            // A read only view will display information but not
            // let users change it; you can override SetReadOnly
            // to change the appearance of read only views.
end; // TAbstractView.SetReadOnly

procedure TAbstractView.ReadFromModel(Model: TModel);
begin
end; // TAbstractView.ReadFromModel

procedure TAbstractView.WriteToModel(Model: TModel);
begin
end; // TAbstractView.WriteToModel

procedure TAbstractView.FormCreate(Sender: TObject);
begin
  inherited;
  _AddRef;  // so can pass Self as an interface reference
end;

end.

TAbstractView effectively splits the IView protocol into three parts - read-only, validation, and model IO - and handles each a bit differently.

  • It implements the basic read-only functionality - users can't change the data on a disabled form - though actual views will typically override SetReadOnly so as to change the visual appearance of read-only views.
  • It defers all implementation of validation to its TValidView and TFickleView descendants.
  • It provides do-nothing stub code for ReadFromModel and WriteToModel. Since these are typically overridden by each actual view object, it seemed desirable to let views always call inherited.

As you might imagine from the name, you're not meant to actually use a TAbstractView nor to inherit directly from one. Rather, you would use a TValidView or a TFickleView.

Listing 5 - The IValid methods in ValidViews.pas

function TValidView.GetValid: boolean;
begin
  Result := True;
end; // TValidView.GetValid

procedure TValidView.SetValid(Value: boolean);
begin
  // A TValidView is always Valid - ignore the Value
end; // TValidView.SetValid

procedure TValidView.AddNotifiee(Notify: IFrame);
begin
  // A TValidView is always Valid - ignore the Add request
end; // TValidView.AddNotifiee

procedure TValidView.RemoveNotifiee(Notify: IFrame);
begin
  // A TValidView is always Valid - ignore the Remove request
end; 

Listing 6 - Extracts from FickleViews.pas

type
  TFickleView = class(TAbstractView)
  private
    fValid:  boolean;
    fNotify: IFrame;
    // This implementation of IValid only supports 1 Notifiee
  public
    procedure AddNotifiee(   Notify: IFrame); override;
    procedure RemoveNotifiee(Notify: IFrame); override;

    function  GetValid: boolean;              override;
    procedure SetValid(Value: boolean);       override;
  end;

procedure TFickleView.AddNotifiee(Notify: IFrame);
begin
  fNotify := Notify;
end; // TFickleView.AddNotifiee

procedure TFickleView.RemoveNotifiee(Notify: IFrame);
begin
  fNotify := Nil;
end; // TFickleView.RemoveNotifiee

function  TFickleView.GetValid: boolean;
begin
  Result := fValid;
end; // TFickleView.GetValid

procedure TFickleView.SetValid(Value: boolean);
begin
  if Value <> fValid then
    begin
    fValid := Value;
    if Assigned(fNotify) then
      fNotify.OnValidChanged(Self, Self);
    end; // Value <> fValid
end; 

Obviously, I could simply have collapsed all 'abstract', 'valid', and 'fickle' views into a single TView class. Splitting them up as I did has two main benefits: First, because valid views have special code to flat-out ignore the validation parts of the IView protocol, they are a trifle faster and take a bit less memory. More importantly, when you have derived a particular view from TValidView instead of TFickleView, its Valid property is always True, even if you carelessly set it to False.

Model Editors

Wizards and property sheets are both model editors - you pass them a model object, they run, and then they return control. If the user pressed the OK button and changed the model, they return True; otherwise they return False. The abstract wizard and property sheet in the EmbeddedForms.dpr project let you create actual wizards and property sheets that can share model objects and views. All you have to do is:

  • Create a new form that inherits from TAbstractWizard or TAbstractPropertSheet.
  • Set the caption.
  • For wizards, supply an image and adjust the image panel's width.
  • Write a small Initialize procedure that supplies page captions and view classes as in the following snippet from TestSheet.pas:
    procedure TPropertySheet.Initialize;
    begin
      InitializeSheet(
        ['Name/Supervisor', 'Birthday', 'Address'],
        [TEmployeeIdView, TBirthdayView, TAddressView] );
    end; 

The abstract wizard and property sheet code does all the rest: Both automatically size themselves to fit the largest view. The wizard has standard Prev/Next/OK button logic; the property sheet will disable the OK button whenever a page is invalid, unless at least one page was already invalid when you called EditModel. Both call all views' ReadFromModel method on entry, and call all views' WriteToModel method if the user clicks OK. The property sheet has a ReadOnly property so you can let users look at objects without being able to edit them. Both are 'pure interface' objects with no public methods, so that you don't have to bother with Free or try finally. Listing 7, for example, is the code from main.pas that actually creates and runs the sample wizard and property sheet:

Listing 7 - Running model editors

procedure TTestForm.EditModel(Editor: IModelEdit; Model: TModel);
begin
  {$ifdef ReadOnly}
    Editor.ReadOnly := True;
  {$endif} // ReadOnly
  if Editor.EditModel(Model)
    then ShowMessage('OK!')
    else ShowMessage('Abort ...');
end; // TTestForm.EditModel

procedure TTestForm.RunWizard(Sender: TObject);
var
  Employee: TEmployee;
begin
  Employee := DataModel.NewEmployee;
  try
    EditModel(TWizard.Create(Self), Employee);
  finally Employee.Free; end;
end;

procedure TTestForm.RunSheet(Sender: TObject);
var
  Employee: TEmployee;
begin
  Employee := DataModel.LoadEmployee(3);
  try
    EditModel(TPropertySheet.Create(Self), Employee);
  finally Employee.Free; end;
end;

To me, the really amazing part of the wizard and property sheet code is how simple such generic code is, in Delphi. The key is the array of TViewClass argument to InitializeSheet() and InitializeWizard():

Listing 8 - TAbstractPropertySheet.InitializeSheet

// From PropertySheets.pas

procedure TAbstractPropertySheet.InitializeSheet(
  Captions:   array of string;
  Views:      array of TViewClass );
var
  MaxSpan:    TSpan;
  Index:      integer;
  Sheet:      TTabSheet;
  ActualView: TAbstractView;
begin
  Assert( fViews.Count = 0,
          'Should only call ' + Name + '.InitializeSheet once' );
  Assert( High(Captions) >= Low(Captions), // can use Slice() to
          'Must have at least one tab' );  // pass empty arrays
  Assert( High(Captions) = High(Views),
          'Must have same number of Captions as of Views' );

  MaxSpan := Point(0, 0);

  for Index := Low(Captions) to High(Captions) do
    begin
    Sheet := TTabSheet.Create(Self);
    with Sheet do
      begin
      PageControl := Self.PageControl;
      Caption     := Captions[Index];
      end; // with Sheet
    ActualView := Views[Index].CreateEmbedded( Self,
                                               Sheet,
                                               efmTopLeft );
    fViews.Add(ActualView);
    ActualView.AddNotifiee(Self);
    MaxSpan := UnionSpan(MaxSpan, ActualView.Span);
    end; // for

  Sheet  := PageControl.ActivePage;
  Width  := (Width  - Sheet.Width)  + MaxSpan.X;
  Height := (Height - Sheet.Height) + MaxSpan.Y;
end; 

The three Assert statements check that this property sheet hasn't already been setup; that there is at least one caption; and that there are the same number of captions as view classes. (I really love Assert - I never quite realized just how cumbersome {$IfOpt D+} {$endif} was until I didn't have to use it anymore. Assertions are easier to type, as well as being smaller and easier to read.)

Spans are defined in Embedded.pas. They're just a width/height pair, the BottomRight of a TRect whose Top and Left are 0:

function TEmbeddedForm.Span: TSpan;
begin
  Result.X := Width;
  Result.Y := Height;
end; 

UnionSpan is very similar to the Windows API's UnionRect except that it works on spans instead of rectangles. Setting MaxSpan to (0, 0) is just setup for calculating the smallest rectangle that will hold all the Views.

The real work is done in the loop on the Captions array. For each item in the array, we create a new tab sheet; place it on the page control; and set its caption. Then we use the array of class of TAbstractView argument, Views, to create a new view. We then add the new view to a TList of views; tell it to call its frame whenever Valid changes; and inflate MaxSpan.

Once we've added all the views, we calculate how much room to allow 'around' MaxSpan for the frame, caption, buttons, and notebook tabs. We get this by looking at the difference between the form's height/width and the PageControl.ActivePage's height/width.

TAbstractWizard is very similar, but is a bit more complicated because instead of notebook pages, we use three panels: An outer panel, a top-aligned caption panel; and a client-aligned frame panel. Activating a particular page is simply a matter of bringing the appropriate outer panel to the front:

Listing 9 - TAbstractWizard.SetCurrentPage

// From Wizards.pas

property CurrentPage: integer read  fCurrentPage
                              write SetCurrentPage;

procedure TAbstractWizard.SetCurrentPage(Value: integer);
var
  LastPage, PageIsValid: boolean;
begin
  Assert(TObject(fPanels[Value]) is TPanel);
  Assert(TObject(fViews[Value]) is TAbstractView);
  // Using Assert(is) with 'blind' casts gives us the debugging
  // safety of "as" without the (modest) performance hit

  fCurrentPage := Value;
  TPanel(fPanels[Value]).BringToFront;

  LastPage := Value = fPageCount;
  PageIsValid := TAbstractView(fViews[Value]).Valid;

  PrevBtn.Enabled := Value > 0;
  NextBtn.Enabled := PageIsValid and (not LastPage);
  OkBtn.Enabled   := PageIsValid and LastPage;
end; 

As you can see in Listing 9, another nice feature of Assert statements is the way the assert-and-blind-cast pair gives us all the develop-time type checking of as without any deployment-time speed penalty. Otherwise, the code is pretty straightforward: we set fCurrentPage and bring the appropriate panel to the front. Then we check to see if it is the first or last page and if the page is Valid, and then we set the Previous, Next, and OK buttons accordingly.

The rest of the code in Wizards.pas and PropertySheets.pas is pretty straightforward. While I'll be pleased and flattered if you think it's worth studying, you don't need to understand it to use it, and it's certainly not worth killing trees to print any more of it. (Of course, it's all available online.)

The sample model

While the real point of EmbeddedForms.pas is to give an extended example of embedded forms in action, and to give you a usable wizard and property sheet framework, it also contains a simplistic data model and four views, both to show you how to use the framework and to serve as an example of a view containing another view.

The Data unit is a skeletal data module that contains methods to create, 'load', and 'save' Employee objects. In a real application, these methods would probably be wrappers around database access routines; here, the load method just retrieves some compiled-in dummy data and the save method does nothing at all. The Employee object contains references to two People objects, which contain personal data about the employee and his or her supervisor. The Employee ID view in Figures 1 and 2 allows the user to select the employee's supervisor from a pulldown list and to edit the employee's name and tax identification.

The interesting part of this is that we are displaying the same view of the supervisor field as of the employee field - and using two different copies of the same view object to do so. At design-time, both views are just blank place holders (Figure 6). When we create the form, we create two instances of the Person ID view of Figure 7 and place each on the appropriate panel of the Employee ID view form.

Listing 10 - EmployeeIdViews.pas

unit EmployeeIdViews;

// Copyright © 1997 by Jon Shemitz, all rights reserved.
// Permission is hereby granted to freely use, modify, and
// distribute this source code PROVIDED that all six lines of
// this copyright and contact notice are included without any
// changes. Questions? Comments? Offers of work?
// mailto:jon@midnightbeach.com

// ------------------------------------------------------------

// A reasonably plausible employee id view - select/view the
// supervisor; set name and tax id.

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls,
  Forms, Dialogs, StdCtrls, ExtCtrls,
  Models, Embedded, FickleViews, PersonIdViews;

type
  TEmployeeIdView = class(TFickleView, IFrame)
    SupervisorPnl: TPanel;
    SupervisorCaptionPnl: TPanel;
    SupervisorFrame: TPanel;
    SelectSupervisor: TComboBox;
    SupervisorLbl: TLabel;
    EmployeeIdFrame: TPanel;
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure SelectSupervisorChange(Sender: TObject);
  private
    SupervisorView,
    EmployeeView:   TPersonIdView;
  protected
    procedure ReadFromModel(Model: TModel);   override;
    procedure WriteToModel(Model:  TModel);   override;
    procedure SetReadOnly(Value: boolean);    override;
    procedure OnValidChanged( ChangingObject: TObject;
                              View:           IView );
  end;

implementation

{$R *.DFM}

uses Data;

// create/destroy

procedure TEmployeeIdView.FormCreate(Sender: TObject);
var
  Index: integer;
begin
  inherited;
  SupervisorView :=
    TPersonIdView.CreateEmbedded( Self,
                                  SupervisorFrame,
                                  efmCentered );
  SupervisorView.ReadOnly := True;
  SupervisorView.AddNotifiee(Self);

  EmployeeView :=
    TPersonIdView.CreateEmbedded( Self,
                                  EmployeeIdFrame,
                                  efmCentered );
  EmployeeView.AddNotifiee(Self);

  with DataModel do
    for Index := 0 to SupervisorCount - 1 do
      SelectSupervisor.Items.Add(
        GetEmployeeName(Supervisor[Index]) );
end; // TEmployeeIdView.FormCreate

procedure TEmployeeIdView.FormDestroy(Sender: TObject);
begin
  inherited;
  SupervisorView.RemoveNotifiee(Self);
  SupervisorView.Free;

  EmployeeView.RemoveNotifiee(Self);
  EmployeeView.Free;
end; // TEmployeeIdView.FormDestroy

// IView overrides

procedure TEmployeeIdView.ReadFromModel(Model: TModel);
begin
  Assert(Model is TEmployee);

  with TEmployee(Model) do
    begin
    SupervisorView.ReadFromModel(Supervisor);
    EmployeeView.ReadFromModel(Employee);

    SelectSupervisor.ItemIndex :=
      DataModel.IndexOfSupervisor(Supervisor.ID);
    end; // with
end; // TEmployeeIdView.ReadFromModel

procedure TEmployeeIdView.WriteToModel(Model:  TModel);
begin
  Assert(Model is TEmployee);

  with TEmployee(Model) do
    begin
    SupervisorView.WriteToModel(Supervisor);
    EmployeeView.WriteToModel(Employee);
    end; // with
end; // TEmployeeIdView.WriteToModel

procedure TEmployeeIdView.SetReadOnly(Value: boolean);
begin
  inherited;
  EmployeeView.ReadOnly  := ReadOnly;
  SelectSupervisor.Color := ShowReadOnly_EditColors[ReadOnly];
end; // TEmployeeIdView.SetReadOnly

// change supervisor

procedure TEmployeeIdView.SelectSupervisorChange(Sender: TObject);
var
  ID:         TPersonID;
  Supervisor: TPerson;
begin
  inherited;
  ID := DataModel.Supervisor[SelectSupervisor.ItemIndex];
  Supervisor := DataModel.LoadPerson(ID);
  try
    SupervisorView.ReadFromModel(Supervisor);
  finally Supervisor.Free; end;
end; // TEmployeeIdView.SelectSupervisorChange

// frame notification

procedure TEmployeeIdView.OnValidChanged(
  ChangingObject: TObject;
  View:           IView );
begin
  Valid := SupervisorView.Valid and EmployeeView.Valid;
end; // TEmployeeIdView.OnValidChanged

end.

FormCreate creates the two TPersonID views and registers itself as their frame. The supervisor view is read-only; the only way to change it is via the drop-down box. FormDestroy undoes the registration (releases the interface reference) and frees the embedded forms.

ReadFromModel and WriteToModel basically just delegate their job to the embedded views. In general, it's a good idea for all model IO functions to do as these do, and Assert that their Model argument is of the type that they expect. This will cause a runtime error during development if you pass the wrong model type to the model editor, or pass the wrong view type to the model editor's setup routine.

Other Applications

EmbeddedForms.dpr really only illustrates the first two scenarios that I laid out at the beginning of this chapter: Using the same form in a wizard and a property sheet, and using forms as lightweight components. I haven't shown you any actual examples of the second two scenarios: Using embedded forms for team development of a tabbed dialog, or to build a generic editor that can handle any member of an object hierarchy. However, I have illustrated the techniques you would need to handle these less common cases.

To build a dialog out of multiple independent forms, just have each derive from TEmbeddedForm. Create a tab for each page, and in the dialog's OnCreate handler, call CreateEmbedded for each page's form. I generally make a point of freeing each page in the OnDestroy handler, honoring the general Free What You Create rule, but this isn't strictly necessary as freeing the dialog will free all its child components. If you need any sort of per page validation - perhaps you won't let users tab off of invalid pages - you can derive from TFickleView instead of TEmbeddedForm.

You could model a generic editor on the abstract model editors: A container object with all the standard controls you need, and a blank frame panel where the object-specific controls will go. Each member of the object hierarchy might have a class function which returns a TViewClass. This would let the generic editor fill the frame with the right view for each object it's asked to edit.

I've gotten a lot of mileage out of embedded forms over the last year or so. They have great ability to make your code simpler and clearer, more reliable and more flexible. They're yet another example of the sort of thing that's always been possible under Windows, but which was impossibly complex until Delphi made it easy.


Jon Shemitz has been programming since he was 12, and has been programming professionally since 1981. After an overly long period making very little money as an independent developer, he's been "crazy busy" doing contract programming for the last few years. He lives in Santa Cruz, California with his partner and their two homeschooled children, and fills the odd free moment with reading, gardening, cooking, and exercise. He can be reached at jon@midnightbeach.com or via http://www.midnightbeach.com/jon.

This chapter first appeared in High Performance Delphi 3 Programming

Copyright © 1997 Jon Shemitz - jon@midnightbeach.com - May 18, 1996..July 29, 1997

Created on May 18, 1996, last updated December 27, 2002 • Contact jon@midnightbeach.com