Supporting automation events in Delphi

I needed to add a new capability to the SpamFilter program , an Outlook 2000 add-in that I am developing: that of trapping the addition of new items to the in-box folder of Outlook in order to filter out known spammers, the list of which is maintained by the application. This caused me to investigate the way that events are dealt with in COM. This article is a summary of my notes on the subject.

Delphi programmers take events for granted in their everyday programming tasks. The Delphi event model is extremely simple but lacks the power that is needed for COM communication. The automation event model is powerful but much less simple. Supporting automation events in Delphi requires supplementary work that we will describe here.

The typical COM Client/Server model that you have probably worked with before allows the client to call the server through a supported interface. This is fine when a client calls the server to perform an action or retrieve data, but what about when the server wants to ask things of the client? Incoming interfaces are just that: incoming. There is no way for the server to talk to any of its clients. This is where the Server Events model comes into play. A Server that supports events not only responds to calls from the client, but can also report status and make its own requests to the client.

In the forthcoming sections, we will examine the automation event model is some depth and will also show how wrapper components use this model.

Connectable objects

Automation objects supply services to their clients by letting them invoke methods of the interfaces that they expose. This is what every one would expect from an automation object: it has incoming interfaces. By "incoming", we imply that the COM object "listens" to its client. That is, the client calls methods of the interface and, in this way, "talks" to the object. If an automation object has something to say to its client and the client is interested in listening to the COM object, something known as an outgoing interface is required. This interface is located on the client and its methods are called by the server. This situation is illustrated in the following diagram.

Connectable Object

A COM object that supports one or more outgoing interfaces is known as a Connectable Object. A connectable object can support as many outgoing interfaces as it likes. Each method of the outgoing interface represents a single event or request. All COM objects, regardless of whether they are non-visual COM objects or ActiveX controls, use the same mechanism for firing events to their clients.

Events are used to tell a client that something of interest has occurred in the object - a property has changed or the user has clicked a button. Events are particularly important for COM controls. Events are fired by COM objects and no response from the client is expected. In other words, they are simple notifications. Requests, on the other hand, is how a COM object asks the client a question and expects a response in return. In both cases, the client of the COM object must listen to what the object has to say and then use that information appropriately. It is the client, therefore, that implements the outgoing interfaces which are also known as sinks.

Connection Points And Connection Point Containers

For each outgoing interface that a COM object supports (note the use of the word support, the COM object itself does not implement this interface, it invokes it), the COM object exposes a small object called a connection point. This connection point object implements the IConnectionPoint interface. It is through this IConnectionPoint interface that the client passes its Sink's outgoing interface implementation to the COM object. Reference counts of these IConnectionPoint objects are kept by both the client and the COM object itself to ensure the lifespan of the two-way communications.

The IConnectionPointContainer interface

The IConnectionPointContainer interface

The situation is depicted in the diagram next to this text. The server is connectable and supports the IConnectionPointContainer interface. It is through this interface that the client requests the appropriate connection point object of the outgoing interface.

It is not a big problem as the interface contains only two methods:

The IConnectionPoint interface

Using the methods of IConnectionPointContainer, the client can find which outgoing interfaces are supported by the server and, by the same occasion, obtain a reference on their connection points. The client is then ready to establish a connection with the server. The connections are established or terminated using the methods of the TConnectionPoint interface:

The method to call (from the client side) to establish event communication with the COM object is the IConnectionPoint Advise() method. The converse of Advise() is IConnectionPoint Unadvise() which terminates a connection.

Automation Server

In order to illustrate this theory, we will now develop a simple automation server that fires events and two clients that use it. The example itself is taken from Reference 1.

Developing the server

The first step in the development of the server consists in creating a new application. We choose File|New|Application and call the unit ServerMain.pas and the project Server.dpr. On the form, we drop a TMemo that we call Memo and we assign the value alClient to its Align property.

To build the core of the server, we choose File|New|Others and select the Automation object on the ActiveX page. In the automation object wizard, we check the "Generate event support code" and give the co-class the name ServerWithEvents.

The fact that "Generate event support code" has been checked generates the code that will provide accessibility to the outgoing interface of the automation object. It will also generate the outgoing interface in the type library. In the type library, the automation interface and the outgoing interface are shown as IServerWithEvent and IServerWithEventEvent respectively.

We will add the methods AddText() and Clear() to the IServerWithEvents interface and OnTextChanged() and OnClear() to the IServerWithEventsEvents interface.

The source code of the first two methods is as follows:

procedure TServerWithEvents.AddText(const NewString: WideString);
begin   
	ServerForm.Memo.Lines.Add(NewText)
end

and

procedure TServerWithEvents.Clear;
begin
  ServerForm.Memo.Lines.Clear;
  if FEvents <> nil then FEvents.OnClear; // trigger event handler if sink exists
end;

Now, let's manage the OnChange event of the memo by assigning it the method MemoChanged() in the Initialize() method. This methods is as follows:

procedure TServerWithEvents.MemoChanged(Sender: TObject);
begin
// triggers event handler if sink exists if FEvents <> nil then FEvents.OnTextChanged((Sender as TMemo).Text) end;

As the server is an out-of-process server, it will automatically be registered once it has been run once. That's all that is needed for the server. Let's now develop the two clients.

Developing a standard client

The client consists of a form with 5 components on it: a TEdit, a TMemo and three TButtons. We choose File|New|Application. Now, we put the components on the form, we call it ClientForm, we call its unit ClientMain.pas and call the project Client.dpr. We will now add a reference to the unit Server_tlb.pas in order to access the types and methods that it defines.

We will create a reference to the server's interface IServerWithEvents as the private variable FServer. In the FormCreate method of the form, we will create the server as follows:

FServer:= CoServerWithEvents.Create

Now that this has been done, let's implement the event sink that will trap the events and handle them.

The event sink

This class is called by the server via automation. As a consequence, it must implement IDispatch (and therefore IUnknown). Its declaration is as follows:

TEventSink = class(TObject, IUnknown, IDispatch)
private
FController: TClientForm;
// IUnknown methods
function _AddRef: Integer; stdcall;
function _Release: Integer; stdcall;
function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
// IDispatch methods
function GetTypeInfoCount(out Count: Integer): HResult; stdcall;
function GetTypeInfo(Index, LocaleID: Integer; out TypeInfo): HResult; stdcall;
function GetIDsOfNames(const IID: TGUID; Names: Pointer;
NameCount, LocaleID: Integer; DispIDs: Pointer): HResult; stdcall;
function Invoke(DispID: Integer; const IID: TGUID; LocaleID: Integer; Flag: Word;
var Params; VarResult, ExceptInfo, ArgErr: Pointer): HResult; stdcall;
public
constructor Create(Controller: TClientForm);
end;

Of all these methods, there is no reason to implement  them all. We will implement only QueryInterface() and Invoke() as follows:

function TEventSink.QueryInterface(const IID: TGUID; out Obj): HResult;
begin
if GetInterface(IID, Obj) then Result:= S_OK else if IsEqualIID(IID, IServerWithEventsEvents) then Result:= QueryInterface(IDispatch, Obj)
else Result:= E_NOINTERFACE;
end;

and

function TEventSink.Invoke(DispID: Integer; const IID: TGUID;
LocaleID: Integer; Flag: Word; var Params; VarResult, ExceptInfo,
ArgErr: Pointer): HResult;
var
V: OleVariant;
begin
Result:= S_OK;
case DispID of
1: begin // the first parameter is a new string
V:= OleVariant(TDispParams(Params).rgvarg^[0]);
FController.OnServerMemoChanged(V);
end;
2: FController.OnClear;
end
end;

In this context, QueryInterface() returns an instance of the interface if and only if it is IUnknown, IDispatch or IServerWithEventsEvents. On the other hand, Invoke() links the event handlers with the events of the server. Here, Invoke() is coded for a simple case where the DispID are 1 and 2 only.

Once this is accomplished, we put a reference to this class as the private variable FEventSink. In the FormCreate() method, we create the TEventSink object and we destroy it in the FormDestroy() method.

Event handlers

Let's develop the two event handlers that we need:

procedure TClientForm.OnClear;
begin
  Memo.Clear;
end;

and

procedure TClientForm.OnServerMemoChanged(const NewText: string)
begin
  Memo.Text:= NewText;
end;

Connection

Now we have to see the connection between the event sink and the source of events on the server. To do this, we use the function InterfaceConnect() defined as

procedure InterfaceConnect(const Source: IUnknown; const IID: TIID;
const Sink: IUnknown; 
var Connection: Longint);

in ComObj.pas. This function verify if the Source supports the IConnectionPointContainer interface. (is connectable). If so, it finds the proper IConnectionPoint interface using the FindConnectionPoint() method usingh the IID argument. Finally, it links the sink and the source and delivers the variable Connection, a token that identifies the connection.

Developing a simple client

Now that we have developed a client the hard way, let's use a simpler way to reach the same goal. With the registration of the automation server, we can import the type library of the server and obtain, in addition to the declaration of the interfaces and methods, a component wrapper of type TServerWithEvents.

With this wrapper at hand, we develop our simple client the same way that we developed our client but without declaring or implementing the TEventSink class. Instead, we will use the reference to the TServerWithEvents of Server_Tlb.pas to make the connection as follows in the FormCreate() method:

procedure TSimpleClientForm.FormCreate(Sender: TObject);
begin  
  FServer:= CoServerWithEvents.Create; // create the server  
  FLocalServer:= TServerWithEvents.Create(nil); // create the local wrapper of the server  
  with FLocalServer do  
  begin    
    AutoConnect:= False;
    ConnectTo(FServer); // connect it to the server interface
    OnTextChanged:= OnServerMemoChanged; // identify the 1st event handler    
    OnClear:= OnClear; // identify the 2nd event handler
  end;
end;

When true with the client, FLocalServer is freed in the FormDestroy() method.

Component wrappers

In the development of the simple client, we have wrapped a component around the server rather than generating the event sink. It is simpler, much simpler but it is nearly equivalent to the other method. Let's consider the theory.

When creating a client program for an automation server, one can wrap a component around the server. Such component is the result of choosing Project|Import TypeLibrary, checking the "Generate component wrapper" in the Import Type Library dialog box and clicking on the "Create unit" button. If the registered type libraries contain creatable CoClasses, this dialog allows the generation of a unit containing not only the coclasses but also component wrappers around these coclasses. As a matter of fact, these components are wrappers around interfaces.

TOleServer

TOleServer is the base class for COM servers that are imported this way. TOleServer is derived from TComponent and implements the IUnknown interface. It introduces several properties and methods that manage the connection to a COM server and for invoking its default events. Descendants of TOleServer expose the properties, methods, and events of the server CoClass. When an application calls one of the TOleServer descendants methods or sets one of its properties, the component automatically establishes a connection to the COM server. You can also connect explicitly by calling the Connect method.

In the Server_tlb.pas unit generated by Delphi, the class TServerWithEvents that wraps the IServerWithEvents interface has the following declarations (some conditional defines have been removed for clarity):

TServerWithEvents = class(TOleServer)
private
  FOnTextChanged: TServerWithEventsOnTextChanged;
  FOnClear: TNotifyEvent;
  FIntf: IServerWithEvents;
  FProps: TServerWithEventsProperties;
  function GetServerProperties: TServerWithEventsProperties;
  function GetDefaultInterface: IServerWithEvents;
protected
  procedure InitServerData; override;
  procedure InvokeEvent(DispID: TDispID; var Params: TVariantArray); override;
public
  constructor Create(AOwner: TComponent); override;
  destructor Destroy; override;
  procedure Connect; override;
  procedure ConnectTo(svrIntf: IServerWithEvents);
  procedure Disconnect; override;
  procedure AddText(const NewText: WideString);
  procedure Clear;
  property DefaultInterface: IServerWithEvents read GetDefaultInterface;
published
  property Server: TServerWithEventsProperties read GetServerProperties;
  property OnTextChanged: TServerWithEventsOnTextChanged read FOnTextChanged write FOnTextChanged;
  property OnClear: TNotifyEvent read FOnClear write FOnClear;
end;

We will now examine the implementation of some of these methods.

Create

The constructor of the class creates and initializes an instance of TOleServer. It calls the InitServerData method so that each TOleServer descendant can initialize the ServerData property to reflect the particular COM interface it represents.

InitServerData

This method initializes the ServerData property to reflect the type library information of the COM server. In TOleServer, it is an abstract method that must be overridden in the descendant classes. It takes the following form in Server_tlb.pas:

procedure TServerWithEvents.InitServerData;
const
  CServerData: TServerData = (
  ClassID: '';
  IntfIID: '';
  EventIID: '';
  LicenseKey: nil;
  Version: 500);
begin
ServerData := @CServerData;
end;

where ServerData is a property of type pServerData, a pointer to the class TServerData. In this class, ClassID is the CLSID of CoClass, IntfIID is IID of default interface, EventIID is  IID of default event source interface, LicenseKey is a pointer to license string (not implemented) and Version is version of this structure.

Connect

This method establishes a connection to the COM server. Its code for the TItems wrapper component is:

procedure TItems.Connect;
var
  punk: IUnknown;
begin
  if FIntf = nil then
  begin
    punk := GetServer;
    ConnectEvents(punk);
    Fintf:= punk as _Items; // can be any interface exposed by the server
  end;
end;

TOleServer descendants have a private variable FIntf which is a reference to the specific interface that the wrapper aims at connecting to. If it has not been assigned, the GetServer method is called. Its return value is dependant on the value of the ConnectKind property. If the ConnectKind property is

The value ckAttachToInterface does not bind to the server. In this case, the application must use the ConnectTo method of the descendant.

ConnectTo

When the ConnectKind property is ckAttachToInterface, the Connect() method has no effect. In this case, the TOleServer descendant must declare a ConnectTo() public method (this method is automatically declared when the wrapper class is imported from the type library).

procedure TItems.ConnectTo(svrIntf: _Items)
begin
  Disconnect;
  FIntf := svrIntf;
  ConnectEvents(FIntf);
end;

This method calls the ConnectEvent() method of the ancestor which calls the InterfaceConnect() procedure declared in ComObj.pas. This procedure verifies if the interface is connectable, i.e, supports the IConnectionPointContainer interface . If it does, it uses its FindConnectionPoint() method to retrieve its IConnectionPoint interface. Given that this succeeds, the Advise() method of this interface is used and returns a cookie. It is actually a token that uniquely identifies a connection between the connection point object and the client's sink. Note that the cookies have meaning only to the object that produces them.

InvokeEvents

Last but not the least, InvokeEvents() dispatches the event to the proper event handlers.

procedure TServerWithEvents.InvokeEvent(DispID: TDispID; var Params: TVariantArray);
begin case DispID of -1: Exit; // DISPID_UNKNOWN 1: if Assigned(FOnTextChanged) then FOnTextChanged(Self, Params[0] {const WideString}); 2: if Assigned(FOnClear) then FOnClear(Self); end; {case DispID} end;

Note the differences between this code and the code of Invoke() that we codes for the client.

Conclusion

This article shows that enabling events between a COM automation server and its client is not that difficult though it is not as simple as the Delphi event model is. I have tried to show the technical points which are required to accomplish it. This lead to the development of two clients: a standard client for which we had to develop the event sink and deal with the events in detail, and a simple one where we used the wrapper components obtained by importing the server's type library.

The second method is much simpler and quicker to code than the second. Except for the dispatch of events, the two methods seem to be equivalent but their execution leads to somewhat different behavior. In adding text to the server's memo, the two methods are undistinguishable as far as their behavior is concerned.

The code of the Server and its two clients is available for download.

References

The following reference have been used to put together this article. Their contributions are hereby acknowledged.

  1. "Borland Delphi 6" by Steve Teixera and Xavier Pacheco, Campus Press - French translation of "Delphi 6 Developer's Guide", Copyright 2001 Sams Publishing.
  2. Brian Long in his article entitled "More automation in Delphi" provides very solid basic information on automation techniques.
  3. The article entitled "Writing COM Automation Events" by the Borland Developer Support Staff provides a short tutorial on writing simple automation server and client classes. [Article ID: 27126 Publish Date: March 26, 2001 Last Modified: March 29, 2001]
  4. The article entitled "Visio Automation Controller Using Delphi" by Graham Wideman (1/2/99) - Controlling Visio via OLE Automation from Delphi is a frequent topic on Visio's discussion groups - the article provides a Delphi unit that does the job.
  5. The article entitled "Advanced events" describe a technique to pass data from the event source to the client using the parameters of the eventhandler.
  6. Gekko Software's Delphi pages are dedicated to building automation server objects using Delphi. It is a very general article that provides sections on:
    • Introduction to creating automation servers
    • Working with automation objects
    • Expanding the possibilities
    • Events - covering events and advanced events
    • Sharing automation objects
  7. The COM Events and Callbacks section of the article entitled "Automation in Delphi COM Programming" by Eric Harmon shows how to build automation controllers that fire events.
  8. The article entitled "Using Speech Technology with your Delphi App" by Glenn Stephens of Code Rage Software Pty Ltd is a tutorial where it is shown how one can gain access to Speech Libraries compatible with Microsoft's Speech API (SAPI) using Delphi's Automation Objects.
  9. The article Understanding COM Event Handling by Lim Bio Liong on "The code project" site exposes some fundamental principles of COM Event Handling via a C++ template class that allows for generic handling of dispinterface COM events.

Warning!
This code was developed for the pleasure of it. Anyone who decides to use it does so at its own risk and agrees not to hold the author responsible for its failure.


Questions or comments?
E-Mail
Last modified: September 3rd 2014 12:44:48. []