Notify Me of Everything… – Part 2

By | September 21, 2017

Overview

This is a second article on notifications in the RAD Studio IDE and in it I’ll look at two slightly harder notifications to implement: Module and Project notifiers.

Module Notifier

The first notifier I’ll go through is the module notifier. Note that I’ve derived it from my own base-notifier object which handlers the common IOTANotifier methods.

Interface

There are 3 interfaces to be implemented for this notifier: IOTAModuleNotifer, IOTAModuleNotifier80 and IOTAModuleNotifier90 as shown below.

Type
  TDNModuleNotifier = Class(TDGHNotifierObject, IOTAModuleNotifier, IOTAModuleNotifier80,
    IOTAModuleNotifier90)
  Strict Private
  {$IFDEF D2010} Strict {$ENDIF} Protected
    // IOTAModuleNotifier
    Function CheckOverwrite: Boolean;
    Procedure ModuleRenamed(Const NewName: String);
    // IOTAModuleNotifier80
    Function AllowSave: Boolean;
    Function GetOverwriteFileNameCount: Integer;
    Function GetOverwriteFileName(Index: Integer): String;
    Procedure SetSaveFileName(Const FileName: String);
    // IOTAModuleNotifier90
    Procedure BeforeRename(Const OldFileName, NewFileName: String);
    Procedure AfterRename(Const OldFileName, NewFileName: String);
  Public
  End;

You will notice that I’ve had to put an IFDEF around the Strict keyword for RAD Studio 2009 and below. I put the interface methods in a Strict Protected section so that I don’t inadvertently use those methods from a class reference (cause its bad and causes reference counting problems). But it seems RAD Studio 2009 and below doesn’t like a Strict Protected section but can handle a Protected section.

Implementation

I’ve broken down the implementations into their interfaces below.

IOTAModuleNotifier Interface Methods

The below method ModuleRenamed of the notifier is called when a module has been renamed.

Procedure TDNModuleNotifier.ModuleRenamed(Const NewName: String);

Begin
  DoNotification(
    Format(
    '80(%s).ModuleRenamed = NewName: %s',
      [
        ExtractFileName(FileName),
        ExtractFileName(NewName)
      ])
  );
  FileName := NewName;
End;

The method CheckOverwrite of the notifier is called before a Save As operation to check if any files that are read only file will be overwritten. I think here you should return true if any files you manage along with the module are read-only.

Function TDNModuleNotifier.CheckOverwrite: Boolean;

Begin
  Result := True;
  DoNotification(Format('(%s).CheckOverwrite = Result: True', [ExtractFileName(FileName)]));
End;

IOTAModuleNotifier80 Interface Methods

The method AllowSave of the notifier is called to check whether your notifier will allow the module to be saved. Return true to allow the module to be saved by the IDE else return false to prevent saving the module.

Function TDNModuleNotifier.AllowSave: Boolean;

Begain
  Result := True;
  DoNotification(Format('80(%s).AllowSave = Result: True', [ExtractFileName(FileName)]));
End;

The method GetOverwriteFileName of the notifier is called so that you can return a number of files (in addition to those managed by the IDE) that you want to manage along with the module.

Function TDNModuleNotifier.GetOverwriteFileName(Index: Integer): String;

Begin
  Result := '';
  DoNotification(Format('(%s).GetOverwriteFileName = Index: %d, Result: ''''', [
    ExtractFileName(FileName), Index]));
End;

The method GetOverwriteFileNameCount of the notifier is called so that you can return the number of files (in addition to those managed by the IDE) that you want to manage along with the module and specifically to be checked by the IDE during a Save As operation.

Function TDNModuleNotifier.GetOverwriteFileNameCount: Integer;

Begin
  Result := 0;
  DoNotification(Format('(%s).GetOverwriteFileNameCount = Result: 0', [ExtractFileName(FileName)]));
End;

The method SetSaveFileName of the notifier is called with the fully qualified filename that the user entered in the Save As dialogue. This name can then be used to determine all the resulting names.

Procedure TDNModuleNotifier.SetSaveFileName(Const FileName: String);

Begin
  DoNotification(
    Format(
    '80(%s).SetSaveFileName = FileName: %s',
      [
        ExtractFileName(FileName),
        ExtractFileName(FileName)
      ])
  );
End;

IOTAModuleNotifier90 Interface Methods

The method AfterRename of the notifier is called after a module has been renamed providing the old and new filenames.

Procedure TDNModuleNotifier.AfterRename(Const OldFileName, NewFileName: String);

Begin
  DoNotification(
    Format(
    '90(%s).AfterRename = OldFileName: %s, NewFileName: %s',
      [
        ExtractFileName(FileName),
        ExtractFileName(OldFileName),
        ExtractFileName(NewFileName)
      ])
  );
End;

The method BeforeRename of the notifier is called before a module is renamed providing the old and new file names.

Procedure TDNModuleNotifier.BeforeRename(Const OldFileName, NewFileName: String);

Begin
  DoNotification(
    Format(
    '90(%s).BeforeRename = OldFileName: %s, NewFileName: %s',
      [
        ExtractFileName(FileName),
        ExtractFileName(OldFileName),
        ExtractFileName(NewFileName)
      ])
  );
End;

Project Notifier

The second notifer I’m going to look at is related to the module notifier and is a project [module] notifier.

Interface

I’ve derived this notifier from the above module notifier as IOTAProjectNotifier is inherited from IOTAModuleNotifier.

TDNProjectNotifier = Class(TDNModuleNotifier, IOTAProjectNotifier)
  Strict Private
  {$IFDEF D2010} Strict {$ENDIF} Protected
    // IOTAProjectModule
    Procedure ModuleAdded(Const AFileName: String);
    Procedure ModuleRemoved(Const AFileName: String);
    Procedure ModuleRenamed(Const AOldFileName, ANewFileName: String); {$IFNDEF D2010} Overload; {$ENDIF}
  Public
  End;

You should notice from the above that there is an IFDEFed Override directive for the ModuleRenamed method. This is required for RAD Studio 2009 and before to help the compiler resolve the reference. Later IDEs don’t seem to require it.

Implementation

Below are some explanations for the methods.

IOTAProjectNotifier Interface

This method of the notifier is called when a module is added to a project or a project is added to a project group.

Procedure TDNProjectNotifier.ModuleAdded(Const AFileName: String);

Begin
  DoNotification(Format('(%s).ModuleAdded = AFileName: %s', [FileName,
    ExtractFileName(AFileName)]));
End;

This method of the notifier is called when a module is removed from a project or a project is removed from a project group.

Procedure TDNProjectNotifier.ModuleRemoved(Const AFileName: String);

Begin
  DoNotification(Format('(%s).ModuleRemoved = AFileName: %s', [FileName,
    ExtractFileName(AFileName)]));
End;

This method is called when a project or project group has its name changed.

Procedure TDNProjectNotifier.ModuleRenamed(Const AOldFileName, ANewFileName: String);

Begin
  DoNotification(Format('(%s).ModuleRenamed = AOldFileName: %s, ANewFileName: %s',
    [FileName, ExtractFileName(AOldFileName), ExtractFileName(ANewFileName)]));
  FileName := ANewFileName;
End;

Adding and Removing the Notifiers

Now for the hard part, finding a sensible place to add and remove the notifiers. I’ve chosen the TDGHNotificationsIDENotifier notifier as the best place to do this as the FileNotification method informs you when a module is opened and closed.

In order to track the notifiers that are added I needed a collection to hold the files names and their notifier indexes so I created a record for the information which can then be transformed into a collection using a generic TList. Below is the definition of the record.

Type
  TModNotRec = Record
  Strict Private
    FFileName      : String;
    FNotifierIndex : Integer;
    FNotifierType  : TDGHIDENotification;
  Public
    Constructor Create(Const strFileName : String; Const iIndex : Integer;
      Const eNotifierType : TDGHIDENotification);
    Property FileName : String Read FFileName;
    Property NotifierIndex : Integer Read FNotifierIndex;
    Property NotifierType : TDGHIDENotification Read FNotifierType;
  End;

I’ve then defined an intermediate type for the collection as shown below. This is not necessary, I’ve only done this as it highlighted a bug in my Browse and Doc It parser that needs to be fixed.

TModNotRecList = TList<TModNotRec>;

Below is the updated definition of the IDE notifier class with the collection field.

Type
  TDGHNotificationsIDENotifier = Class(TDGHNotifierObject, IOTAIDENotifier,
    IOTAIDENotifier50, IOTAIDENotifier80)
  Strict Private
    FModuleNotifierRefs : TModNotRecList;
  {$IFDEF D2010} Strict {$ENDIF} Protected
    // IOTAIDENotifier
    Procedure FileNotification(NotifyCode: TOTAFileNotification;
      Const FileName: String; Var Cancel: Boolean);
    // IOTAIDENotifier
    Procedure BeforeCompile(Const Project: IOTAProject; Var Cancel: Boolean); Overload;
    Procedure AfterCompile(Succeeded: Boolean); Overload;
    // IOTAIDENotifier50
    Procedure BeforeCompile(Const Project: IOTAProject; IsCodeInsight: Boolean;
      Var Cancel: Boolean); Overload;
    Procedure AfterCompile(Succeeded: Boolean; IsCodeInsight: Boolean); Overload;
    // IOTAIDENotifier80
    Procedure AfterCompile(Const Project: IOTAProject; Succeeded:
      Boolean; IsCodeInsight: Boolean); Overload;
    Function Find(Const strFileName : String; Var iIndex : Integer) : Boolean;
    Property ModuleNotifierRefs : TModNotRecList Read FModuleNotifierRefs;
  Public
    Constructor Create(Const strNotifier, strFileName : String;
      Const iNotification : TDGHIDENotification); Override;
    Destructor Destroy; Override;
  End;

We need to add a constructor to the class so that we can create the collection to store the module and project notifiers as shown below.

Constructor TDGHNotificationsIDENotifier.Create(Const strNotifier, strFileName : String;
  Const iNotification : TDGHIDENotification);

Begin
  Inherited Create(strNotifier, strFileName, iNotification);
  FModuleNotifierRefs := TModNotRecList.Create;
End;

We also need to free the collection in a destructor as shown below. I also thought that I could remove any project and module notifiers that were still around but I realised that they were probably gone from the IDE at this point so they cannot be removed. Why would there be any dangling notifiers? Well there is a bug in the code at present. The notifier index reference is added to the collection using the original file name of the module. If a module is renamed then the removal process which will be described below will not work. I have 2 choices to fix this: the first is an event handler passed to each module / project notifier so they can update the collection file name or I pass an interface which does the same thing. The interface method would be the better way of doing it however I need to ensure I don’t repeat the mistake I recently made where one object as a reference to another and visa versa and hence neither will get freed.

Destructor TDGHNotificationsIDENotifier.Destroy;

Var
  iModule : Integer;

Begin
  For iModule := FModuleNotifierRefs.Count - 1 DownTo 0 Do
    Begin
      {$IFDEF DEBUG}
      CodeSite.Send('Destroy', FModuleNotifierRefs[iModule].FileName);
      {$ENDIF}
      FModuleNotifierRefs.Delete(iModule);
      //: @note Cannot remove any left over notifiers here as the module
      //:       is most likely closed at this point.
    End;
  FModuleNotifierRefs.Free;
  Inherited Destroy;
End;

Now for the final part. In the FileNotification method I’ve added some code to look for ofnFileOpened and ofnFileClosing notitications. In the ofFileOpened notification I check whether the file module implements the IOTAProject interface as this determines if its a Project or Project Group and if so I add and Project Notifier else I’ll create a Module Notifier and add then to the collection.

In the ofnFileClosing notification I look to the index associated with the file name and remove it from the IDE accordingly.

Procedure TDGHNotificationsIDENotifier.FileNotification(NotifyCode: TOTAFileNotification;
  Const FileName: String; Var Cancel: Boolean);

Const
  strNotifyCode : Array[Low(TOTAFileNotification)..High(TOTAFileNotification)] Of String = (
    'ofnFileOpening',
    'ofnFileOpened',
    'ofnFileClosing',
    'ofnDefaultDesktopLoad',
    'ofnDefaultDesktopSave',
    'ofnProjectDesktopLoad',
    'ofnProjectDesktopSave',
    'ofnPackageInstalled',
    'ofnPackageUninstalled',
    'ofnActiveProjectChanged' {$IFDEF DXE80},
    'ofnProjectOpenedFromTemplate' {$ENDIF}
  );

Var
  MS : IOTAModuleServices;
  M : IOTAModule;
  iModuleIndex: Integer;
  P : IOTAProject;
  eNotiferType : TDGHIDENotification;
  R: TModNotRec;
  MN : TDNModuleNotifier;

Begin
  DoNotification(
    Format(
    '.FileNotification = NotifyCode: %s, FileName: %s, Cancel: %s',
      [
        strNotifyCode[NotifyCode],
        ExtractFileName(FileName),
        strBoolean[Cancel]
      ])
  );
  If Not Cancel And Supports(BorlandIDEServices, IOTAModuleServices, MS) Then
    Case NotifyCode Of
      ofnFileOpened:
        Begin
          M := MS.OpenModule(FileName);
          If Supports(M, IOTAProject, P) Then
            Begin
              MN := TDNProjectNotifier.Create('IOTAProjectNotifier', FileName, dinProjectNotifier);
              iModuleIndex := M.AddNotifier(MN);
              eNotiferType := dinProjectNotifier;
            End Else
            Begin
              MN := TDNModuleNotifier.Create('IOTAModuleNotifier', FileName, dinModuleNotifier);
              iModuleIndex := M.AddNotifier(MN);
              eNotiferType := dinModuleNotifier;
            End;
          FModuleNotifierRefs.Add(TModNotRec.Create(FileName, iModuleIndex, eNotiferType));
        End;
      ofnFileClosing:
        Begin
          M := MS.OpenModule(FileName);
          If Find(M.FileName, iModuleIndex) Then
            Begin
              R := FModuleNotifierRefs[iModuleIndex];
              M.RemoveNotifier(R.NotifierIndex);
              FModuleNotifierRefs.Delete(iModuleIndex);
            End;
        End;
    End;
End;

The Find method is as follows:

Function TDGHNotificationsIDENotifier.Find(Const strFileName: String; Var iIndex: Integer): Boolean;

Var
  iModNotIdx : Integer;
  R: TModNotRec;

Begin
  Result := False;
  iIndex := -1;
  For iModNotIdx := 0 To FModuleNotifierRefs.Count - 1 Do
    Begin
      R := FModuleNotifierRefs.Items[iModNotIdx];
      If CompareText(R.FileName, strFileName) = 0 Then
        Begin
          iIndex := iModNotIdx;
          Result := True;
          Break;
        End;
    End;
End;

I hope all of the above is straight forward. The code and the binaries can be found with on the IDE Notifications page.

regards
Dave