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.
To be usable this class class will have to be as simple as possible :
- Provide all tAutoObject functionality.
- Provide an implementation of IConnectionPointContainer.
- Expose a collection of all connected eventsinks
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 :
- A connectionpoint container, which implements the IconnectionPointContainer
interface.
- An interfacelist, which implements the EventSinks property. This
interfacelist is (re-)created when needed.
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.
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
|