Dockable form handling within GExperts

Because I messed up the dockable form handling for the new Bookmarks expert which resulted in spurious cascading access violations when closing the Delphi IDE, I had to revisit that code. And so I can look up the information I gathered fixing that problem, I’m putting it here for later reference.

Basically, you create a new dockable form by descending from TfmIdeDockForm which is declared in GX_IdeDock. You then have to call IdeDockManager.RegisterDockableForm like this:

unit GX_MyExpert;

interface

uses
[...]
  GX_IdeDock;

type
  TfmMyExpertForm = class(TfmIdeDockForm)
    [...]
  end;

[...]

implementation

var
  fmMyExpert: TfmMyExpertForm = nil;

[...]

  IdeDockManager.RegisterDockableForm(TfmMyExpertForm, fmMyExpert, 'MyExpertForm');

This tells the IDE about this new form, what it is being called (the name must be unique within the IDE), what type it is and the address of the variable where it should store the form reference.

There is no need to instantiate the form before calling RegisterDockableForm because the method does not access the form variable but only the address of the form variable. But because it stores the address of that variable, it should not be an instance variable of your expert, because that would mean that the address becomes invalid when your expert instance gets freed. So you should use a global variable for this (but declare it in the implementation section to limit its scope to the current unit).

(This, by the way, was the bug that caused the above mentioned cascading access violations.)

In addition, since every expert in GExperts can be enabled and disabled using the GExperts configuration dialog, you should call RegisterDockableForm in the expert’s SetActive method, when it is being enabled, and likewise call UnregisterDockableForm when it is being disabled. And since it doesn’t make sense to keep the form itself around when the expert is disabled, free it and nil the global variable.

procedure TMyExpert.SetActive(New: Boolean);
begin
  if New <> Active then begin
    inherited SetActive(New);
    if New then
      IdeDockManager.RegisterDockableForm(TfmMyExpertForm, fmMyExpert, 'MyExpertForm')
    else begin
      IdeDockManager.UnRegisterDockableForm(fmMyExpert, 'MyExpertForm');
      FreeAndNil(fmMyExpert);
    end;
  end;
end;

But where do you instantiate the form?

There are two ways the form gets instantiated. First, it should of course be done when the user clicks on the GExperts menu item for your expert.

procedure TMyExpert.Click(Sender: TObject);
begin
  if fmMyExpert= nil then begin
    fmMyExpert:= TfmMyExpertForm.Create(nil);
  end;
  fmMyExpert.Init;
  IdeDockManager.ShowForm(fmMyExpert);
  EnsureFormVisible(fmMyExpert);
end;

As you can see, you don’t call your form’s show method either, you call IdeDockManager.ShowForm instead. Also, since the user probably wants your form to become active, you call the EnsureFormVisible global procedure (located in GX_GenericUtils).

The second way the form is instantiated is out of your control: The docking manager creates it, e.g. when the user has saved a desktop containing your form and activates that desktop later. So, never assume that your form does not exist just because your code hasn’t instantiated it yet.

To make sure your instance variable is always valid or nil, you should add code to your form’s destructor that sets it to nil:

destructor TfmMyExpertForm.Destroy;
begin
  fmMyExpert:= nil;
  inherited;
end;

Another little gem, just in case you haven’t noticed it yet: GExperts forms have their own distinctive icon, the same that is also shown in the GExperts menu. To make your form look the same, you have to call the expert’s SetFormIcon method after it has been instantiated. But since, as I said above, you haven’t got any control when the form is being instantiated, and SetFormIcon is a method of the expert and not the form, how do you do that?

Unfortunately this requires yet another global (but declared in implementation) variable, this time a reference to your expert. Initialize it in your expert’s constructor and set it to nil in the destructor. Using this global variable, you can then access the expert’s icon from your form’s constructor.

[...]
implementation

var
  MyExpert: TMyExpert = nil;

[...]

constructor TMyExpert.Create;
begin
  inherited Create;

  [...]

  MyExpert := Self;
end;

destructor TMyExpert.Destroy;
begin
  MyExpert := nil;

  [...]

  inherited;
end;

constructor TfmMyExpertForm.Create(_Owner: TComponent);
begin
  inherited;
  if Assigned(MyExpert) then
    MyExpert.SetFormIcon(Self);
end;

btw: If fixed that for the ToDo-Expert. It didn’t show its own icon because SetFormIcon was called in the Click method only but the window was already instantiated so that call was never executed. There are probably other experts that have the same bug.