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

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
- EnumConnectionPoints(out Enum: IEnumConnectionPoints) provides an
interface to an automation collection of all
connectionpoints.
- FindConnectionPoint(const iid: TIID; out cp: IConnectionPoint) takes the
interface ID of the desired eventsink and will pass back the
associated IConnectionPoint interface.
The clients use these IConnectionPoint interfaces to connect their
eventsink. IconnectionPoint can handle this using five methods
- GetConnectionInterface(out iid: TIID) passes back the ID of the eventsink
interface it handles.
- GetConnectionPointContainer(out cpc: IConnectionPointContainer) passes
back an interface to the ConnectionPointContainer inside which it is
managed.
- Advise(const unkSink: IUnknown; out dwCookie: Longint) accepts an
eventsink after which the connectable object will fire its event methods. It
passes back a cookie to identify the connection.
- Unadvise(dwCookie: Longint) stops firing events in the sink associated
with the cookie.
- EnumConnections(out Enum:
IEnumConnections) passes back an automation
collection of all eventsinks connected to this ConnectionPoint.
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
- Query the object for IConnectionpointContainer
- Use IConnectionPoint.FindConnectionpoint to get the connectionpoint.
- Use IConnectionPoint.Advise to connect.
- Use IconnectionPoint.UnAdvise to disconnect.
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 :
- Create an empty collection of (eventsink) interfaces
- Typecast self to IConnectiopPointContainer. This typecast is
the same as performing a call to QueryInterface on the automation
object. After this typecast the methods of IconnectionPointContainer
are available.
- Use IConnectionPointContainer.FindConnectionPoint to obtain the
Connectionpoint for the IFileZapperEvents sink. This sink is
identified by the Guid in the typelibrary.
- Use IConnectioPoint.EnumConnections to obtain a collection of
connected sinks.
- Treat the connections as an automation collection
to obtain all sinks connected.
- Add every non nil sink to the result collection.
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 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
|