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.
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.
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;
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;
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.
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.
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.
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;
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.
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.
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.
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.