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

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.

private
   FConnectionPoints: TConnectionPoints; 
   FConnectionPoint: TConnectionPoint;
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.

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.

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

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 ?