Firing events
Until now all the communication has been from client to server. The client is
calling methods on the server, the only information coming from server to
client are function results and property values. Eventsinks are the automation way to let
the server initiate a communication to the client, giving the client the opportunity
to react to events happening in the server.
In a server with a visual interface, like the FileManager
example, a lot of events happen. In this chapter I will create an eventsink through
which the FileManager can notify the client that the user has selected a file.
Eventsinks, eventsources and connectionpoints.
If a server object wants to make callbacks to its clients, it needs
something to make its calls to. The client has to supply the server a so called
outgoing interface, this is an interface to an object
which is implemented by the client. The server will make calls to the methods of
this interface, for every distinct event there will be a method. Which events
the client understands corresponds to the interface-methods the server can
call. This interface will be declared
in the typelibrary of the server, so every client will know the layout of the
outgoing interface it has to implement.
The client-side object which implements this interface is called an eventsink.
As you pour water into the kitchen sink, an automation server can pour events into
the eventsink of a client. What happens with the water is not your concern, what happens with
the events is not the servers concern. The server is the source of events. In
this scenario it no longer very clear who is calling who, so the names
client and server are somewhat confusing. That's why a server is called a
connectable object or an
event source.
The client connects its eventsink to the source using an IConnectionPoint
interface. A source can sink events to more than one client, all
IConnectionPoint instances are managed through the IConnectionPointContainer
interface. The client will get the IConnectionPointContainer interface and
create a connectionpoint in there to hook up it's eventsink. So if
a server wants to be an event source and support the sinking (firing) of events in the standard
automation way it has to provide an implementation of IConnectionPointContainer.
Creating the eventsink in the typelibrary
When you start the Delphi automation object wizard there is a checkbox to include event support

Having this box checked will lead to extra generated code
interface
uses ComObj, ActiveX, AxCtrls, Classes, FileManager_TLB, StdVcl;
type
TMyClassWithEvents = class(TAutoObject, IConnectionPointContainer, IMyClassWithEvents)
private
{ Private declarations }
FConnectionPoints: TConnectionPoints;
FConnectionPoint: TConnectionPoint;
FEvents: IMyClassWithEventsEvents;
{ note: FEvents maintains a *single* event sink. For access to more
than one event sink, use FConnectionPoint.SinkList, and iterate
through the list of sinks. }
public
procedure Initialize; override;
protected
{ Protected declarations }
property ConnectionPoints: TConnectionPoints read FConnectionPoints
implements IConnectionPointContainer;
procedure EventSinkChanged(const EventSink: IUnknown); override;
end;
implementation
uses ComServ;
procedure TMyClassWithEvents.EventSinkChanged(const EventSink: IUnknown);
begin
FEvents := EventSink as IMyClassWithEventsEvents;
end;
procedure TMyClassWithEvents.Initialize;
begin
inherited Initialize;
FConnectionPoints := TConnectionPoints.Create(Self);
if AutoFactory.EventTypeInfo <> nil then
FConnectionPoint := FConnectionPoints.CreateConnectionPoint(AutoFactory.EventIID, ckSingle, EventConnect)
else FConnectionPoint := nil;
end;
initialization
TAutoObjectFactory.Create(ComServer, TMyClassWithEvents, Class_MyClassWithEvents, ciMultiInstance, tmApartment);
end.
I want to add event support to an existing class so I will add the extra
code myself. In the process we will get an idea how Delphi implements automation
events. Let's start in the typelibrary. First I have to declare the layout of the eventsink. In the
typelibrary I will create a new dispinterface IfileZapperEvents and add
one method OnSelectionChanged.

This method will be called in the firing of the event.
Next I have to tell the automation CoClass declaration that the class does
implement events by adding the IfileZapperEvents dispinterface in the
implements tab

After inserting the interface I have to set two flags. First comes the source
flag, indicating that the class does not implement this interface but knows how
to sink events into instances of this interface, instances created by the
client. The second flag is the default flag, indicating that this
interface is part of the default interface returned when an object of the class
is created.
Now the declaration part of the events is finished and I can start with the
implementation.
Gluing the connectionpoint-container into the automation class
- Add AxCtrls to the uses clause.
Event support in the VCL has been mainly created for ActiveXcontrols, the
VCL classes used are found in this VCL unit.
- Add IConnectionPointContainer to the list of implemented interfaces
To find out if an automation object does support events, the client will
query the object for the IConnectionPointContainer interface. So I
will add the interface ID to the class declaration. The implementation of
the interface can be left to an object of type TconnectionPoint,
found in the AxCtrls unit of the VCL.
type
TFileZapper = class(TAutoObject, IConnectionPointContainer, IFileZapper)
...
protected
...
property ConnectionPoints: TConnectionPoints read FConnectionPoints
implements IConnectionPointContainer;
...
end;
Notice the implements keyword, it indicates that the property
Connectionpoints will provide the class tFileZapper with all methods and
properties tFileZapper promised to implement when it added IConnectionPointContainer
to its interface list.
- Declare the connectionpoints object
The implementing object is a private. Beside the container (IConnectionPointContainer)
the connection itself (IConnectionPoint) needs an implementing object. The
VCL class TConnectionPoint, found next to tConnectionPoints in
AxCtrls, provides an implementation for IConnectionpoint. The Delphi wizard
assumes an instance of a server object has only one client to sink events to
at the time. So it is sufficient to declare one private tConnectionPoint
variable.
private
FConnectionPoints: TConnectionPoints;
FConnectionPoint: TConnectionPoint;
- Catch the incoming eventsink
The eventsink is the interface whose methods will be called, the type of the
interface will be IFileZapperEvents. The client will pass an
outgoing interface of that type through a call to the EventSinkChanged
method. This method is introduced in tAutoObject, here I will override it to
catch the interface.
type
TFileZapper = class(TAutoObject, IConnectionPointContainer, IFileZapper)
private
FEvents: IFileZapperEvents;
....
protected
...
procedure EventSinkChanged(const EventSink: IUnknown); override;
end;
The implementation of EventSinkChanged is straightForward
procedure TFileZapper.EventSinkChanged(const EventSink: IInterface);
begin
inherited;
FEvents := EventSink as IFileZapperEvents;
end;
Eventsink is an interface to the sink object implemented by the client. It is
an interface, so it's type is at least an Iunknown. A proper client will implement
IFileZapperEvents, so I will typecast the sink to that type.
- Initialize the connectionpoint container
After declaring the private connectionpoint variables I can create the
connectionpointcontainer. This is done in the initialize method, a method of
tAutoObject which is called after the creation of the object
procedure TFileZapper.Initialize;
begin
inherited Initialize;
FConnectionPoints := TConnectionPoints.Create(Self);
if AutoFactory.EventTypeInfo <> nil then
FConnectionPoint := FConnectionPoints.CreateConnectionPoint(AutoFactory.EventIID, ckSingle, EventConnect)
else FConnectionPoint := nil;
end;
After calling the inherited implementation the object implementing IConnectionPointContainer
is created. After which the factory is checked if there
is a declaration of an eventsink in the typelibrary. If so the connectionpoint
is created in the connectionpointcontainer.
- Clean up after finishing
All of this code is generated by the Delphi wizard. But this wizard does
forget something, a tool checking for memory leaks will notice that
the Initialize method creates a tConnectionPoints object which
is never freed. I
will have to free it myself in the BeforeDestruction method,
together with the freeing of the form
procedure TFileZapper.BeforeDestruction;
begin
FConnectionPoints.Free;
Form1.Free;
inherited;
end;
Now the infrastructure is ready and I can start firing events.
Firing events
Whenever the user selects a file in the filelistbox I want to fire an event

This events corresponds to the OnChange event handler of tFileListBox.
First I will create an eventhandler in the FileZapper class
procedure TFileZapper.OnSelectChange(sender: tObject);
begin
if fEvents <> nil then
fEvents.OnSelectionChanged;
end;
This handler has to check whether fEvents is nil. Maybe the current client is
not interested in events at all and has not passed an outgoing interface to the
server. In which case fEvents will be nil. But if there is an eventsink I
can fire its OnSelectionChanged method. What will happen in there is entirely
up to the client.
To activate this eventhandler I will hook it to the visual component when
constructing the form
Form1:= TForm1.Create(nil);
Form1.FileListBox1.OnChange:= self.OnSelectChange;
Form1.Show;
This is Delphi at its best. Since the eventhandlers of VCL-controls are
properties, just like the control's position or color, I can assign a value to
the property. This value has to be a method having the same signature (return
type of the method, its number and type of parameters) as the eventhandler. Most
event handlers in VCL controls are of type tNotifyEvent, that is a procedure with one sender parameter of type
tObject. I have given tFileZapper.OnSelectChange
exactly the signature of a tNotifyEvent and now I can assign this
(private) method of my automation class to the eventhandler of a visual control.
Catching the events in VBA
As a client I will use MS Word. I will start the FileManager from a
Word macro and whenever the user selects a different file I want to insert the
names of the selected files into the current document.
I have to make sure the filemanager is added to the references
of my VBA project. In the project I will create a class to implement the
eventsink. The VBA equivalent for a class is a class module, I will create a new
one

In the source code of this unit I will declare FMapp as being an
object which supports events and is based on FileManager.FileZapper.
Public WithEvents FMapp As FileManager.FileZapper
The class has one method, the eventhandler for the onchange event
Private Sub FMapp_OnSelectionChanged()
If Not FMapp Is Nothing Then
For Each FSelected In FMapp.Selected
Selection.InsertAfter FSelected.Name
Selection.EndKey Unit:=wdLine
Selection.TypeParagraph
Next
End If
End Sub
The method has to check if the eventsource FMapp has been initialized.
If so it will iterate the collection of selected items and insert every filename
on a new line in the document.
Now I have an eventsink implementation ready which I can hook in a Word
document. This is the VBA code for the document.
Dim MyManager As New FileManager.FileZapper
Dim MyEvents As New Class1
Private Sub HookItUp()
Set MyEvents.FMapp = MyManager
MyManager.FileMask = "*.*"
End Sub
Private Sub Document_Open()
HookItUp
End Sub
First I will create MyManager, the FileManager object. Next I
will create MyEvents an object which implements my eventsink. In the HookItUp
method the server and the eventsink are hooked together. HookItUp is called when
opening the document and I will see all selection changes reflected in the
document.

Catching events in a Delphi client
Behind the scenes VBA is doing a lot of work to create the eventsink. To
realize how much I will take a short look at the amount of work needed for a
Delphi automation client which wants to sink the events coming from an
automation server.
The Delphi client has to create an object of a class which implements the
eventsink and will have to pass this object to the connectionpoint container of
the server. The eventsink is a dispinterface, a dispinterface is an Idispatch
interface on which all calls are made using Idispatch.invoke.
This leads to the following steps needed
- Declare a class which implements Idispatch
This class will be based on tInterfacedObject and will look like
TmySink = class(tInterFAcedObject, Iunknown, Idispatch)
The complex signature of the methods of Idispatch could be copied from the
VCL.
- Call the appropiate eventhandler in the implementation of Invoke
Every event has a dispId, in the invoke
implementation this dispId has to be mapped to some method in the client.
This will get more complex if the event does include parameters.
- Hook the eventsink to the server
The VCL procedure InterfaceConnect connects a sink to the
connectionpointcontainer of a server object.
This is a tedious amount of work. But the good thing is that there is a
wizard on the web to do this. Binh Ly has a nice Event
Sink Generator on his website

This tool will generate a tComponent which wraps up the implementation
of the sink. All events on the sink are translated to VCL events. This component
has to be installed into the Delphi IDE after which you can place a sink
component on your form and the code behind the events can be edited via the
object inspector. The component has a connect method which does the same
work as the VCL procedure InterfaceConnect, needing only one parameter.
All others are wrapped up in the component. More on this tool in a chapter to come.
On Binh Ly's web site you will find a large amount of examples how to work
with this tool. Have fun !
Where are we ?
We have seen that automation has a standard way of sending events from a
server to client. The server has to implement the IConnectionPointContainer
interface and has to describe the list and signature of the events in its
typelibrary. This list is declared as an interface and called an eventsink. An
automation client wishing to connect to a server has to implement this eventsink
and pass the server an interface to the object.
The implementation of the connectionpoint-container and its management can be
left almost entirely to the VCL and the Automation Object wizard. As a Delphi
developer no knowledge of the methods and other interfaces involved is needed.
The implementation of the eventsink in the client differs for every client
environment. The easiest way to implement an eventsink in Delphi is by using a
freeware wizard.
What's next ?
|