The tAuto(Intf)ObjectWithEvents class

In the chapter on advanced events I described a way to have an automation object sink events to multiple sinks. In this chapter I will use this to create two classes which can be used as a replacement for tAutoObject and tAutoIntfObject to add this level of event support to any automation object.

tAutoObjectWithEvents

To be usable this class class will have to be as simple as possible :

The management of all needed objects should be internal to its implementation.

The interface of the tAutoObjectWithEvents class

To meet the first two points the class will inherit from tAutoObject and will add IConnectionPointContainer to the list of implemented interfaces. To meet the third point the class will expose a public property EventSinks which is typed as an interfacelist, this property can be used by the automation object to fire all events. This leads to the following declaration of the interface of the class :

type
  TAutoObjectWithEvents = class(TAutoObject, IConnectionPointContainer)
  public
     procedure Initialize; override;
     procedure BeforeDestruction; override;
     property EventSinks : tInterFaceList read GetSinks;
end;

The internals of the tAutoObjectWithEvents class

To implement these features, the class has to manage two objects :

Using TConnectionPoint and TInterfaceList, provided by the VCL, leads to the following private declarations :

private
   FConnectionPoints: TConnectionPoints;
   FsinkList : tInterFaceList;
   procedure Clearsinks;
   function GetSinks : tInterfaceList;
   property ConnectionPoints: TConnectionPoints read FConnectionPoints
      implements IConnectionPointContainer;

The ClearSinks methods cleans up the interfacelist

procedure TAutoObjectWithEvents.Clearsinks;
  begin
  if Assigned(fSinkList) then
     FreeAndNil(fSinkList);
end;

The interfacelist is created on demand in GetSinks, the property getter of the EventSinks property :

function TAutoObjectWithEvents.GetSinks : tInterfaceList;
   var connections : IenumConnections;
   conPoint : IconnectionPoint;
   ConnectData : tConnectData;
   NoFetched : cardinal;

begin
   if not Assigned(fSinkList) then
      begin
      fSinkList:= tInterfaceList.Create;
      (self as IConnectionPointContainer).FindConnectionPoint(AutoFactory.EventIID, conPoint);
      conPoint.EnumConnections(connections);
      if connections <> nil then
         while connections.Next(1, ConnectData, @NoFetched) = S_OK do
            if ConnectData.pUnk <> nil then
               fSinkList.Add(ConnectData.pUnk)
      end;
   result:= fSinkList;
end;

The implementation of this function is almost identical to the GetSinks helper function  in the FileZapper class. The only difference is that I buffer the result in the private fSinkList variable.

The Initialize method is called by the factory which creates the object. In this method the connectionpoints object is created and the private fSinkList variable is initialized as nil

procedure TAutoObjectWithEvents.Initialize;
begin
   inherited Initialize;
  FConnectionPoints := TConnectionPoints.Create(Self);
  if AutoFactory.EventTypeInfo <> nil then
     FConnectionPoints.CreateConnectionPoint(AutoFactory.EventIID, ckMulti, EventConnect);
   fSinkList:= nil;
end;

The initialization of the connectionpoints is identical to the implementation in the FileZapper class.

In the BeforeDestruction method the connectionpoints and the interfacelist are freed :

procedure TAutoObjectWithEvents.BeforeDestruction;
   begin
   ClearSinks;
   FConnectionPoints.Free;
   inherited;
   end;

Connecting and disconnecting sinks to tAutoObjectWithEvents

Clients of my object can connect and disconnect eventsinks at their own will. Every change in the list of connected sinks has to be reflected in the EventSinks property. To be more efficient I stored the list of connected sinks in a private variable, every time the property is used I will only rebuild the list when it is nil. How do I know this list is still valid ? To get a notification I can use the EventSinkChanged method of tAutoObject. Every time an eventsink connects or disconnects to my object this method will be called

type
TAutoObjectWithEvents = class(TAutoObject, IConnectionPointContainer)
   protected
      procedure EventSinkChanged(const EventSink: IUnknown); override;
end;

The method is virtual so I can override it and act :

procedure TAutoObjectWithEvents.EventSinkChanged(const EventSink: IInterface);
   begin
   ClearSinks;
   inherited;
end;

My implementation will call the ClearSinks method which will free the list. So it will be recreated the next time the EventSinks property is used. The newly connected eventsink will then be in the list. And a disconnected eventsink will no longer be in the list. 

Using tAutoObjectWithEvents

The Delphi wizard did two things when generating event support. It added the eventsink to the typelibrary and created event support code in the implementing unit. The work in the typelib is done quite well, there is no need in doing that by hand. I suggest that you do use the wizard, be satisfied with the typelib and throw away all the fiddling in the implementation unit. After adding the AutoObjectWithEvents unit to the uses clause of the unit you can base your automation object on tAutoObjectWithEvents like this.

type
   TFileZapper = class(TAutoObjectWithEvents, IFileZapper)
  private

There is no need to include IconnectionPointContainer to the list of implemented interfaces, it is taken care of by tAutoObjectsWithEvents. Just like tAutoObject takes care of the implementation of Idispatch

Changing the base class of your automation object is all that is needed to enable event support. The EventSinksproperty can now be used to fire the events

procedure TFileZapper.OnSelectChange(sender: tObject);
   var i : integer;
   begin
   for i:= 0 to EventSinks.Count -1 do
      (EventSinks.Items[i] as IFileZapperEvents).OnSelectionChanged;
end;

That's all that there is to it.

tAutoIntfObjectWithEvents

The tAutoIntfObject class is used to create automation objects which have no factory. These objects are always created by an other automation object and are ideal for implementing object type properties. The same techniques used for tAutoObjectWithEvents can be used to add event support to these tAutoIntfObject's. The common ancestor of tAutoObject and tAutoIntfObject goes as far as tObject, so there is little I can reuse and I will have to override other methods to get things done.

The interface of the tAutoIntfObjectWithEvents class

tAutoIntfObjectWithEvents will inherit from tAutoIntfObject and will add IConnectionPointContainer to its implements list. The class exposes all connected sinks in an EventSinks properties, just like tAutoObjectWithEvents. The objects are created in Delphi code, so the constructor CreateWithSink can do all the initialization. tAutoIntfObject does not have an Initialize method which does the work in tAutoObject. The public beforedestruction method will do the clean up when the object is released.

type
  TAutoIntfObjectWithEvents = class(TAutoIntfObject, IConnectionPointContainer)

     public
     constructor CreateWithSink(IntfID, SinkID : TIID);
     procedure BeforeDestruction; override;
     property EventSinks : tInterFaceList read GetSinks;
end;

The internals of the tAutoIntfObjectWithEvents class

To get things done the class uses the same private declarations as tAutoObjectWithEvents:

FConnectionPoints: TConnectionPoints;
   FsinkList : tInterFaceList;
   FsinkID : TIID;
   procedure Clearsinks;
   function GetSinks : tInterfaceList;
   property ConnectionPoints: TConnectionPoints read FConnectionPoints
      implements IConnectionPointContainer;

New is FsinkID, it is the GUID which identifies the eventsink. In tAutoObjectWithEvents this GUID is stored in the factory and available through the AutoFactory.EventIID property. As an AutoIntfObject has no factory I have to store this ID myself. All initialization of the object is done in the constructor.

constructor TAutoIntfObjectWithEvents.CreateWithSink(IntfID, SinkID: TIID);
   begin
   create(ComServer.TypeLib, IntfId);
   fSinkID:= SinkID;
   FConnectionPoints := TConnectionPoints.Create(Self);
   FConnectionPoints.CreateConnectionPoint(fSinkID, ckMulti, EventConnect);
   fSinkList:= nil;
end;

CreateWithSink is passed the ID of the implemented interface and the ID of the supported sink. The interface ID is passed to the constructor of tAutoIntfObject and the sink ID is stored in the private fSinkID. This constructor initiates the connectionpoints, just like Initialize in tAutoObjectWithEvents. The implementation of Clearsinks is identical to the implementation in tAutoObjectWithEvents, the implementation of BeforeDestruction is identical to the implementation in tAutoObjectWithEvents.

Connecting and disconnecting sinks to tAutoIntfObjectWithEvents

Every time a client connected or disconnected a sink to tAutoObjectWithEvents, it's EventSinkCanged method was called and I could react. tAutoIntfObject does not have an EventSinkChanged method, I will have to find an other way to recieve a notification of a sink (dis-)connecting.

When the connectionpoints are created they are passed a notifier, the parameter EventConnect is of type tConnectEvent, which can be found in the VCL :

TConnectEvent = procedure (const Sink: IUnknown; Connecting: Boolean) of object;

tAutoObject has a protected EventConnect method with this signature. It's implementation is also found in the VCL :

procedure TAutoObject.EventConnect(const Sink: IUnknown;  Connecting: Boolean);
   begin
   if Connecting then
      begin
      OleCheck(Sink.QueryInterface(FAutoFactory.FEventIID, FEventSink));
      EventSinkChanged(TDispatchSilencer.Create(Sink, FAutoFactory.FEventIID));
      end
   else
      begin
      FEventSink := nil;
      EventSinkChanged(nil);
      end;
end;

The method first checks if the sink really is an implementation of the intended interface. When not an exception is raised by OleCheck. After the check EventSinkChanged is called and I will recieve a notification in my overriden implementation of EventSinkChanged.

tAutoIntfObject does not have an EventConnect or an EventSinkChanged method so I will create one myself.

type
TAutoIntfObjectWithEvents = class(TAutoIntfObject, IConnectionPointContainer)

   protected
      procedure EventConnect(const Sink: IUnknown; Connecting: Boolean);
end;

When creating a connectionpoint this EventConnect method is passed. Every time a sink (dis-) connects from this connectionpoint my EventConnect will be called. The implementation can be very straightforward :

procedure TAutoIntfObjectWithEvents.EventConnect(const Sink: IInterface; Connecting: Boolean);
   begin
   ClearSinks;
   end;

Now I have found a place for every piece of code for event support.

Using tAutoIntfObjectWithEvents

The fastest way to include a tAutoIntfObjectWithevents is a two step approach. First make sure the typelibrary is OK by using the Delphi wizzard, just like a tAutoObjectWithEvents. After which the tAutoObject descendant can be changed to an tAutoIntfObject descendant.

The demoapp uses tAutoIntObjectWithEvents to notify the client that a file has been succesfully selected using the Add method of the SelectedFiles collection. The constructor of the collection sets up the sink support

constructor TSelectedFileCollection.CreateEx(FileListBox : TFileListBox);
   begin
   CreateWithSink(ISelectedFileCollection, ISelectedFileCollectionEvents);
   fFileListBox:= FileListBox;
end;

The tSelectedFile.Add method will sink the events just the same way as in tAutoObjectWithEvents:

function TSelectedFileCollection.Add(const FileName: WideString): ISelectedFile;
   var I,j : integer;
   begin
   for i:= 0 to fFileListBox.Items.Count - 1 do
      if CompareText(fFileListBox.Items[i], FileName) = 0 then
         begin
         fFileListBox.Selected[i]:= True;
         result:= tSelectedFile.CreateEx(FileName);

         for j:= 0 to EventSinks.Count -1 do
            (EventSinks.Items[j] as ISelectedFileCollectionEvents).OnAdd(FileName);

         end;
end;

The word document will provide a VBA sink. It has to implement a new class module for the sink:

Public WithEvents FMselected As FileManager.SelectedFileCollection

Private Sub FMselected_OnAdd(ByVal FileName As String)
   If Not FMselected Is Nothing Then
      Selection.EndKey Unit:=wdStory
      Selection.TypeParagraph
      Selection.InsertAfter "File added :" & FileName
      End If
End Sub

When opening the document VBA will create the sink and hook the selected object to the VBA sink

Dim MyManager As New FileManager.FileZapper
Dim SelectedEvents As New ClassSink2
Dim MyEvents As New ClassSink

Private Sub Document_Open()
   HookItUp
End Sub

Private Sub HookItUp()
   Set MyEvents.FMapp = MyManager
   MyManager.FileMask = "*.*"
   Set SelectedEvents.FMselected = MyManager.selected
End Sub

Now a macro in the document can call the Add method

Sub SelectFile()
  MyManager.selected.Add (Selection.Text)
End Sub

And every time selection.text corresponds to a filename FMselected_OnAdd will write a notification to the end of the document.

Where are we ?

Using the tAutoObjectWithEvents class any automation object can sink its events to multiple clients. According to the specification of the IconnectionPointcontainer interface an automation object should be able to support a number of connectionpoints, each with a different signature of methods on the eventsink interface. This number is still limited to one, due to the VCL.

The same techniques can be used to add event support to any automation object implemented using tAutoIntfObject. The fact that tAutoIntfObject has total different roots than tAutoObject makes it impossible to combine both in an elegant object oriented way.

What's next