Advanced events

Events can be used to pass information to the clients. In this chapter I will describe a technique to pass data from the event source to the client using the parameters of the eventhandler.

The Delphi wizard assumes that the server eventsource only has to handle one eventsink at  the time. For some clients this will limit the number of different events that can be sinked to just one. In this chapter I will describe the way to support multiple sinks and show how a C# client can function fully with all events of my eventsource including their data.

Passing arguments in an event

The event described in the first chapter on events only passed a notification to the client. This was informed that the user had selected an other file. If the client  wanted to know which file had been selected it had to query the properties of the FileManager to find out.

The event itself can pass information to the client as well. A notification is nothing else than the calling of a user defined method. If I give this method parameters, I can use those to pass the information. I will create a second event, it will be fired when the users selects another directory. The name of the selected directory will be passed to the client.

To do so I will give the IFileZapperEvents a new method an this method will have a string type parameter to hold the directory name. 

Having done this in the typelibrary editor I can start implementing the event.

Firing OnDirectoryChanged

The new event has to be fired when the user selects another directory in the DirectoryOutline component. This is a VCL event, so again I can create a private method in the server class to handle the event. There is a little problem, the event itself has already got some code attached, code which sets the directory property of the FileListBox. Setting the eventhandler to my method would disable this code. One solution would be to fire the event in the existing handler but I do not want to mix up the code concerning the automation class with the code concerning the visual form itself. Instead I will declare a private variable of type tNotifyEvent to store the existing eventhandler:

type
TFileZapper = class(TAutoObject, IConnectionPointContainer, IFileZapper)
   private

   Form1: TForm1;
   Form1DirChange : tNotifyEvent;

When creating the form I can now store the original eventhandler and install my own:

procedure TFileZapper.AfterConstruction;
   begin
   inherited;
   Form1:= TForm1.Create(nil);
   Form1.FileListBox1.OnChange:= self.OnSelectChange;
   Form1DirChange:= Form1.DirectoryOutline1.OnChange;
   Form1.DirectoryOutline1.OnChange:= self.OnDirChange;

Now the new eventhandler can do its own thing and fire the former eventhandler

procedure TFileZapper.OnDirChange(sender: tObject);
begin
   if assigned(Form1DirChange) then
      Form1DirChange(sender);
   if fEvents <> nil then
      fEvents.OnDirectoryChanged(Form1.DirectoryOutline1.Directory);
end;

First it will check if the saved eventhandler points to any code (if assigned(Form1DirChange), if so it will fire the event (Form1DirChange(sender)). After which it can fire the event in the eventsink just like firing the OnSelectionchanged event, this time passing the directory name in the parameter.

Sinking OndirectoryChanged

The first target for my new event is the VBA class module. This class module has a very straightforward way of mapping its methods to the methods in the sink. It takes the class name, adds an underscore, adds the eventname and then it assumes the method has to be mapped on the event in the sink. Doing so for OnDirectoryChanged will result in an error :

VBA is complaining about the signature of the method. This has to match the signature of the event in the typelibrary and include the string parameter to catch the incoming directoryname

Private Sub FMapp_OnDirectoryChanged(ByVal DirName As String)
   If Not FMapp Is Nothing Then
      Selection.InsertAfter "Directory changed to : " & DirName
      Selection.EndKey Unit:=wdLine
      Selection.TypeParagraph
End If

VBA will keep complaining until the parameter is declared as being ByVal, the eventhandler cannot change its value. Having done this the document will now keep track of the directories visited

IConnectionpointContainer revisited

Before changing the automation class to a connectable class which supports multiple client sinks, we will have to take a closer look at the IConnectionPointContainer interface.

In the type library I have defined my IFileZapperEvents eventsink interface. I can define more interfaces, a connectable object can support any number of eventsink interfaces. Each different interface type is handled by an IConnectionPoint interface. IConnectionPointContainer manages all these interfaces, to do so it needs two methods

The clients use these IConnectionPoint interfaces to connect their eventsink. IconnectionPoint can handle this using five methods 

This is sufficient information to see how I can change the way eventsinks are connected to my object.

Supporting multiple clients

The Delphi wizard assumed that my class only supports one type of eventsink, IFileZapperEvents and had only client connected at the time. We have seen that the IConnectionPointContainer actually describes a collection of multiple eventsink types and that each of those holds a collection of multiple connected clients. As the connectionpoints and the connections are both automation collections, they are not that difficult to work with. Let's go to the Delphi implementation.

I will fire events to all clients, clients I will address using just the IConnectionPointContainer. This means I can get rid of the private fConnectionPoint and the private fEvents variable. The only thing the overriden EventSinkChanged method did was setting the fEvents variable, so this method can be scrapped as well. The only survivor is the fConnectionPoints variable, it is still initiated in the Initialize method

procedure TFileZapper.Initialize;
  begin
  inherited Initialize;
  FConnectionPoints := TConnectionPoints.Create(Self);
  if AutoFactory.EventTypeInfo <> nil then
      FConnectionPoints.CreateConnectionPoint(AutoFactory.EventIID, ckMulti, EventConnect);
end;

The connectionpointcontainer is created and a connectionpoint for the IFileZapperEvents is created. A client will connect using the following scenario

Now the ConnectionpointContainer is up and running I have to take care of the firing of the events. In the former, wizard generated situation, this was easy. There was only one eventsink which was available as a private. All I had to do was check whether there was a client connected after which I could fire away. Now I have to check all connections and fire the event on every one of them. The VCL has a very handy helperclass tInterfaceList which can hold a collection of interfaces. I will use this class in a helper function to gather all eventsinks connected.

function TFileZapper.GetSinks: tInterfaceList;
   var connections : IenumConnections;
   conPoint : IconnectionPoint;
   ConnectData : tConnectData;
   NoFetched : cardinal;

   begin
   result:= tInterfaceList.Create;
   (self as IConnectionPointContainer).FindConnectionPoint(DIID_IFileZapperEvents ,conPoint);
   conPoint.EnumConnections(connections);
   if connections <> nil then
      while connections.Next(1, ConnectData, @NoFetched) = S_OK do
         if ConnectData.pUnk <> nil then
            result.Add(ConnectData.pUnk)
   end;

The function does this : 

I will use this function to get a collection of all eventsinks in which I have to fire the event.

procedure TFileZapper.OnSelectChange(sender: tObject);
   var SinkList : tInterFaceList;
   i : integer;
   begin
   SinkList:= GetSinks;
   for i:= 0 to SinkList.Count -1 do
      (SinkList.Items[i] as IFileZapperEvents).OnSelectionChanged;
   SinkList.Free;
end;

GetSinks return a list of all connected sinks, after typecasting each interface to IFileZapperEvents I can call the OnSelectionChanged method. Now my automation class will sink its events to any number of clients attached.

A C# client

A nice client to test multiple clients is C#, the leading language in the Microsoft .NET platform. Dot Net does support automation and the handling of events is excellent.

In Visual Studio I will create a new Windows Application. Its is a form with a listbox, a trackbar and a progressbar. I will add the filemanager to the references of the project. The dialog to do this can be found in the solution explorer, my FileManager is found under the COM tab.

The dot net interop will now generate a .net-class wrapper for my automation-class. In the form I will declare a private for the object. If I want to be able to work with the events it is important that I will declare the variable as a FileManager object and not as an IFileManager interface. The interface knows nothing about the events, the class does.

private FileManager.FileZapper Ido;

The FileManager object is created in a menu handler. In C# al the eventhandlers of controls are methods of the form which hosts the controls, just like Delphi.

private void menuItem5_Click(object sender, System.EventArgs e)
   {
      Ido = new FileManager.FileZapper();
      Ido.FileMask = "*.*";
   }

I will not dive to deep into C#. It is sufficient to know that the FileManager object is created just like any other C# (.net) object. The VS IDE has a code completion comparable to Delphi. I can use it to see that VS is aware of the fact that the FileManager has events

The best thing about events in C# is that they are multicast. Events in the VCL are singlecast, when the event happens only one method will be fired. When setting the OnDirectoryOutLineChange I had to save the original eventhandler and fire that from within the new eventhandler. A C# multicast will fire events to any number of targets, all I have to do is add the new eventhandler using the += operator.

Ido.OnSelectionChanged+= new FileManager.IFileZapperEvents_OnSelectionChangedEventHandler(this.OnChange);

C# eventhandlers are so called delegate objects. All events in the typelibrary are translated to declarations of delegate classes. In the code I am adding a new delegate object to the Ido.OnSelectionChanged eventhandler of my automation object. The constructor takes the C# method which will fire as a parameter. This is the form class itself, OnChange is a method which does something with the trackbar.

private void  OnChange()
   {       trackBar1.Value+= 1;    }

In the beta editions of .NET the code only compiled right when Onchange was a function which returned an integer. To understand why you have to realize that all methods of an automation object are actually functions which return an hResult. And an hResult stands for an integer. C# does the translation from hResult to a void method, a function with no result, since beta 2.

So far this C# app would have worked with the wizard generated events. But as it is seems so easy to add existing methods to an eventhandler I will add some more.

private void menuItem7_Click(object sender, System.EventArgs e)
   {
      Ido.OnSelectionChanged+= new  FileManager.IFileZapperEvents_OnSelectionChangedEventHandler(this.OnChange);
      Ido.OnSelectionChanged+= new FileManager.IFileZapperEvents_OnSelectionChangedEventHandler(this.OnChange2);
      Ido.OnDirectoryChanged+= new FileManager.IFileZapperEvents_OnDirectoryChangedEventHandler(this.OnDirChange);
   }

When the user selects another file I want the trackbar and the progressbar to advance. When the users selects another directory I want to see the directoryname in the listbox.

The forms method for the OnDirectoryChangeEvent catches the incoming directoryname in a string

private void OnDirChange(string DirName)
   {
      listBox1.Items.Add(DirName);
   }

With all these events to sink the app would not have worked with the wizard generated code. For every eventhandler I am creating a new delegate object. The delegate is the object which implements the eventsink and so this C# app has a separate sink for every event. The generated code supported only a single sink and I did get an exception when adding the second event.

Using the new implementation of the connectionpoints the C# client works just fine:

Here we see automation at work. It is a nice way to integrate Delphi code with C# code.

Where are we ?

Events themselves can be used to pass information to the client. The signature of the method implementing the sinking of the event, has to match the signature as it is declared in the typelibrary. Parameters in this signature can be used to pass information.

The Delphi wizard limits the number of eventsinks the connectable object can handle to one. This is a serious limitation for a C# client who will pass a new eventsink for every event consumer. The interfaces which implement the connecting of sinks to sources are easy to understand, creating a Delphi implementation for multiple sinks has not proven to be very difficult.

What's next

  • Objects and interfaces
  • Using automation objects
  • Building automation servers in .net
  • Demo application