|
Implementing an Event Framework
by Binh Ly
Download the source code for this article
One of the complexities of developing
COM-based distributed applications is implementing a stable and consistent event
infrastructure. A lot of professionals often ask me:
- I can implement connection points
easily, but how do I trigger events from outside of the object that contains
the connection point? Or better yet, how do I trigger events from a second
thread or an external application?
- How do I combine threads and
connection points to produce some sort of an asynchronous event
infrastructure?
Event Classes
In a nutshell, the answer to these
questions is to develop a loosely coupled event system. A loosely
coupled event system introduces the idea of separating an event into an
abstraction called an event class. An event class is simply an entity
that represents any custom event that we define in our system together with
all of that event's interested parties. For instance, we define a trading stock
update event class as the trading stock update event plus all
clients/subscribers that are
interested in this particular event:
Stock Update event class = Stock Update
event + Subscribers to Stock Update event
In this context, an event subscriber
is simply a party that's interested in a particular event. To reiterate, an
event class is the event itself combined with all known subscribers of that
event. An interesting aspect of the event class
is that we can instantiate an event class just like any other COM object.
Furthermore, the event class exposes the interface of the event it
defines. When we call a method on an instantiated event
class, we are actually triggering the event method to all subscribers of that
event.
The introduction of an event class
enables us to build sophisticated event mechanisms than possible when simply using the normal
tightly coupled event system such as the COM connection point technology. When
using connection points, a client that wishes to receive events must first find
an instance of a server object that it can connect to. Once it finds and
connects to a server object, it will normally receive events that originate from
that server object. This means that in order to trigger the event from outside
of the server object, such as from a separate thread, object, or application,
the source of the event needs to somehow acquire a pointer to the connected
server object and then trigger the event from there. Although it is possible to
programmatically do this kind of thing, it is very cumbersome and error-prone.
This is why we call it a tightly coupled event system - the event source and the
event receiver have a coupling that can be hard to break to satisfy some event
requirements.
You'll notice that with the event
class, it doesn't matter anymore how events are triggered. Whether it's in a
separate thread, object, or application, the event source simply creates an
instance of the event class and then calls a method on the event class to
trigger the event. Unlike the connection points mechanism, the source of an
event doesn't care and doesn't need to know how to get to the list of
subscribers of a given event - this is now the responsibility of the event
class. An interesting aspect of this mechanism is that an event class can then do
some interesting things such as instantiating subscribers on the fly (for
subscribers that may want to be instantiated and notified only when the event
arrives), filtering events, and enabling administrative
event configuration and maintenance. In fact, the COM+ event services, which is
based on event classes, allows us to configure event classes and subscriptions
easily within the Component Services administrative tool.
EventService - A Simple Event Framework
So the question you're probably
anxiously waiting to ask is "How do I implement event classes? Do I wait
for COM+ and Windows 2000 to be able to do this?" Fortunately, the answer
is no! As I was doing research in this area, I found out that it is relatively
easy to produce an event framework using the concepts of event classes that will
work today (on NT & 9x) and on Windows 2000, when it arrives. We'll call
this framework "EventService". Note that
we're not going to build an event system that's similar to what COM+ provides -
that's way too complicated. What we'll do is build a framework that enables us to
easily develop applications that can make use of event classes without forcing
you to learn much about the nitty-gritty details of how to properly implement
events. I believe this framework will be very valuable for beginners who know
what they need but don't have much experience in the intricacies of COM events,
as well as experts who know the details but would rather not have to duplicate code from project to project to reproduce similar event requirements.
An Overview of EventService
Before going into the details of
implementing EventService, I'd like to show you first how you, as a
developer, would develop your COM applications that make use of events.
What I'm going to show you is a bit of a shift in the model of how you used to
implement events using the old COM connection points technology. So please read carefully
to make sure you understand what I'm talking about.
First, before starting to implement
events, we should always design the event interfaces and methods up front. Once
we come up with the design, we create a "dummy" COM server and define
the events in this server. The purpose of this server is to serve
as a repository for definitions of all events that we'll use in our
applications. Therefore this server must only contain event definitions, and
only event definitions. To further illustrate, let's assume we have a need to create an
event interface named IChatEvent for a chat room event notification:
IChatEvent = interface
procedure OnMessage (Sender, Message)
end;
To create an event definition for
IChatEvent, we simply create a new ActiveX DLL server. Then
we create a new automation object named ChatEvent, after which we come up with something like
this:

Figure: Defining IChatEvent
Looking at the above definition,
ChatEvent defines our event class. In other words, anyone who wants to trigger
an IChatEvent event method will instantiate ChatEvent asking for IChatEvent, and then
call the desired IChatEvent method:
var
Event : IChatEvent
//create ChatEvent event class
Event = CreateEventClass ("ChatEvents.ChatEvent")
//trigger OnMessage event method
Event.OnMessage ("From Me", "Hello
World")
Note that the above 3 lines is all
that's required for us to be able to trigger events, regardless of whether or
not we're in a separate thread, object, or application. Imagine how simple this
is for beginners, as well as hard-core experts. Of course, EventService will have to somehow kick in to be able to handle the details involved in
realizing ChatEvent as an event class but we'll get to that later.
Now that we've seen how to trigger
events, let's look at how we can easily act as event subscribers to be able to
receive events. Using EventService, we'll need to create something called a
subscription object every time we want to receive a particular event. In simple terms, the subscription object represents our connection as a
subscriber to a particular event. As long as the subscription object is alive,
we'll be able to receive events from a particular event class. Then when
we're not interested in the event anymore, we simply destroy the subscription
object.
Note that the subscription object does
not receive the events, per se. We'll need a separate event handler object for that.
This object is the one that actually implements the event interface. What the subscription
does is bind this event handler object to the event class so that it can start
receiving events from the event class. Once we destroy the subscription, the
subscription object will automatically disconnect our event handler object from
the event class. We'll talk more about the event handler object later in this
article.
With that said, creating an event
subscription is as simple as instantiating a subscription object and passing in
the desired parameters necessary to establish an event connection:
var
Subscription : IUnknown
//create event subscription object connecting
//EventHandlerObject to
ChatEvents.ChatEvent
Subscription = CreateEventSubscription (
"ChatEvents.ChatEvent", //event class name
EventHandlerObject //event handler object
)
//at this point, any events triggered on the ChatEvents.ChatEvent
//event class will be received by EventHandlerObject
...
//destroy subscription object to disconnect EventHandlerObject
Subscription = NIL
The above lines will be the standard
mechanism in establishing an application that wants to receive events using
EventService. If you've noticed, there as absolutely no mention of what's going on
under-the-hood which is how it should be. Over the past few months, a lot of
developers ask me things like how to handle threads, how to trigger events
from separate objects, how to perform marshaling, etc. so hopefully, this
framework will answer most, if not all, of your questions.
Inside EventService
This section
is for those of you who are interested to know how I built EventService. If
you have no inclination to learn the details, you can simply skip to the next
section where you can learn how to use EventService immediately.
|
I built EventService using Delphi. In
the interest of readership of both Delphi and C++ Builder developers, I will
demonstrate the details using pseudocode instead of Delphi code. I believe there
is no reason for me to duplicate EventService in C++ Builder because the
Delphi-built framework also works for C++ Builder applications.
EventService is implemented as a COM
server named EventService. EventService can hold any number of event classes
from any number of applications. In other words, if we have 10 COM applications
wanting to use EventService for 10 different event classes, we'll only need 1
EventService application running on our system. Inside EventService is a global
event catalog that stores:
- All event classes that are currently
available
- A list of subscribers for each event
class
The event class list is stored in a
class named TEvents. TEvents allows us to do 2 things, among others:
- Obtain a list of subscribers for any
given event class. This is used to trigger events to subscribers of a
specific event class
- Add a new event class to the list
In simple terms:
TEvents = class
//adds a new event class whose name is specified by EventName
function Add (const EventName : string) : ISubscribers;
//obtains the subscribers list for the event class whose name is
specified by EventName
property EventSubscribers [const EventName : string] : ISubscribers;
end;
ISubscribers represents an internal
list of subscribers per event class and is implemented in a class named
TSubscribers. It is defined as follows:
ISubscribers =
interface
//add a new subscriber/event handler and return subscriber ID into Cookie
procedure Add (const Subscriber: IUnknown; out Cookie: Integer); safecall;
//delete a subscriber specified by the ID in Cookie
procedure Delete (Cookie: Integer); safecall;
//returns a subscriber at specified index
property Item [Index: Integer]: ISubscriber;
//returns the count of subscribers in this list
property Count: Integer;
end;
Given TEvents and TSubscribers, it
should be easy to see how an event class is implemented. We simply create a
separate object that references the list of subscribers for a given event class
extracted from TEvents, and at the same time, have this object implement the
event interface for the particular event. In order to quickly produce such an
object, I rely on the COM dispinterface (IDispatch.Invoke) mechanism which
requires that our event interfaces support IDispatch. Note that this is not late
binding - it uses the dispinterface binding technique that we all know from the
vast majority of connection point implementations today.
The event class construction technique
I mentioned above is implemented in a class named TSimpleEventClass. The most
important part of TSimpleEventClass is it's implementation of IDispatch.Invoke
and how it triggers an event method for each subscriber:
procedure
TSimpleEventClass.Invoke;
begin
i = 0;
//iterate subscribers list
while (i < Subscribers.Count) do
begin
//get a single subscriber
Subscriber := Subscribers.Item [i];
//invoke subscriber as IDispatch
//note: this implicitly assumes that the subscriber/sink is
//handling dispinterface events!
if Succeeded (Subscriber.QueryInterface (IDispatch)) then
Subscriber.Invoke (...);
//move to next subscriber
i = i + 1;
end;
end;
That's basically how the event class
works. When the client creates an event class, it will eventually get a pointer
to an IDispatch interface implemented by TSimpleEventClass. You can inspect the source code to trace into the details of this.
Using TEvents and TSubscribers, it is
also very easy to implement the subscription object. We simply ask the TEvents
instance to either add a new subscriber (using the TEvents.EventSubscribers []
property and then the ISubscribers.Add method) or create a new event class
subscriber list and add a new subscriber to it (using the TEvents.Add and
ISubscribers.Add methods). The subscription object will then represent a live
connection from a subscriber to an event class. Once the client destroys the
subscription object, it simply calls ISubscribers.Delete to remove the live
connection from the associated TSubscribers instance. I won't go into the
details of this but you look at the source code and inspect the
TSubscription class together with the TSimpleEventCatalog SubscribeEvent and
UnsubscribeEvent methods to see how this is done.
So what about COM+?
EventService can
be instantiated to use either the event mechanism described above or using the
native COM+ event services. We do this through the InitializeEventService
function that we have to call in every application that wishes to use
EventService. Once we specify that we are using COM+ events, our event
class simply becomes the native event class instance that COM+ fabricates, and
our subscription object simply becomes a COM+ transient event subscription. Note
that this is all transparent to you as a developer. I won't go into the details
of COM+ events because that's not my goal here. My goal is to not force you to
learn the COM+ event services while still being able to implement COM+ events in
your applications. Regardless of COM or COM+
deployment, all we'll ever see are the concepts of an event class and a
subscription object, nothing more, nothing less.
Using EventService
For the sake of simplicity, let's
create a simple multi-user chat application. Readers who are familiar with my
COM events article from last year will notice that this application is similar
to the sample application in that article. However, this application is probably
10x much simpler in terms of implementation than the one in that tutorial. This
is exactly the goal I had in mind for this article - provide a
mechanism/framework to implement COM events in the simplest manner possible!
We'll start by designing our simple
event interface, IChatEvent:
IChatEvent = interface
procedure OnMessage (Sender, Message)
end;
IChatEvent is an event interface
implemented by clients who are interested in receiving messages from a
conceptual online chat room. In simple terms, if we have a group of online
chatters talking to each other, a chatter who wishes to broadcast a message
triggers the IChatEvent.OnMessage event method. Correspondingly, a chatter who
wishes to receive the IChatEvent.OnMessage event will subscribe to the ChatEvent
event class using the subscription mechanism described above.
Implementation
First we define IChatEvent in a
separate COM server that serves as our event repository. We do this by creating
a new ActiveX DLL using the using the File | New |
ActiveX | Active X Library menu from the IDE. Let's save this project as
"ChatEvents.dpr". Then we add a new automation object to it using the File
| New | ActiveX | Automation Object menu from the IDE. Let's name this
object ChatEvent so that Delphi will create a coclass named ChatEvent with an
IChatEvent interface. Then we go into the type library editor (TLE) and define
the IChatEvent interface based on it's definition:

Figure: Defining IChatEvent
This will be our ChatEvent event class definition. Strictly
speaking, this event class is named "ChatEvents.ChatEvent", which is
simply the ProgID of this coclass. That's it for our ChatEvent event class. Note
that this server is simply a repository and does not need to be deployed nor
registered on your system. The sole purpose of this server is for development
and for ease of configuration of COM+ events as we shall see later.
Let's now look at the chat client
application:

Figure: Chat client application
main form
You'll find the definition of the
above form in ChatFrm.pas. Whenever a chatter wants to broadcast a chat message,
he first types in a message into the message edit box (ChatMessage) and then
hits the Send button. The Send button simply creates a ChatEvent event class
and triggers the IChatEvent.OnMessage method. Here's what the send code actually
looks like:
uses
EventUtils, ChatEvents_TLB;
procedure TChatForm.SendClick(Sender: TObject);
var
//note we can only use dispinterface events as
//discussed in the article
Events : IChatEventDisp;
begin
//check if Chat Message edit box has text in it
if ChatMessage.Text <> '' then
begin
//create ChatEvent event class
CreateEventClass (
'ChatEvents.ChatEvent', //event class name
Events //output event class
);
//trigger OnMessage event method
Events.OnMessage (FUserID, ChatMessage.Text);
//clear message for next one
ChatMessage.Text := '';
end
else
//beep if no message to send
Beep;
//restore focus to edit
ActiveControl := ChatMessage;
end;
I've highlighted the relevant lines
above where the ChatEvent event class is instantiated and the OnMessage method
is called. Also note that Events is defined as type IChatEventsDisp because we
can only use dispinterface calls as discussed above [this is not a limitation,
however, when we use the native COM+ event services].
And to prove that we can trigger events
as easily in a separate thread, I've also included a Send from Thread button on
the form whose code is surprisingly similar to the above:
uses
EventUtils, ChatEvents_TLB;
procedure TChatForm.SendFromThreadClick(Sender: TObject);
var
SendThread : TSendThread;
begin
//check if Chat Message edit box has text in it
if ChatMessage.Text <> '' then
begin
SendThread := TSendThread.Create (FUserID, ChatMessage.Text);
SendThread.Resume;
//clear message for next one
ChatMessage.Text := '';
end
else
//beep if no message to send
Beep;
//restore focus to edit
ActiveControl := ChatMessage;
end;
procedure TSendThread.Execute;
var
//note we can only use dispinterface events as
//discussed in the article
Events : IChatEventDisp;
begin
//initialize COM
CoInitialize (NIL);
//create ChatEvent event class
CreateEventClass (
'ChatEvents.ChatEvent', //event class name
Events //output event class
);
//trigger OnMessage event method
Events.OnMessage (FUserID, FMessage);
//free all COM pointers before CoUninit or else we're in trouble
Events := NIL;
//uninitialize COM
CoUninitialize;
end;
As you can see, there is absolutely no
difference in triggering an event between a secondary thread and the normal way.
No marshaling, no synchronization, nothing extra!
Since we've seen how to trigger events,
let's now look at how we subscribe to events. The subscription process is
actually very simple: we create a subscription object to establish our
connection as an event handler to a particular event class, and we destroy the
subscription object when we're no longer interested in receiving events. In our
chat client, the Login button establishes a subscription and the Logout button
terminates a subscription. This is what the Login button code looks like:
procedure TChatForm.LoginClick(Sender: TObject);
begin
//create event subscription
CreateEventSubscription (
'ChatEvents.ChatEvent', //event class name
ChatEventHandler, //event handler object
IChatEvent, //event IID
FSubscription //output event subscription
);
//notify user the we're ready to receive events
ShowMessage ('Now ready to accept incoming chat messages');
end;
In the highlighted code,
ChatEventHandler is our event handler object, IChatEvent is the interface
ID of the event that we want to handle, and FSubscription is an output IUnknown pointer
returned from CreateEventSubscription. FSubscription represents our subscription
object. What you're probably wondering right now is what exactly is the
ChatEventHandler object. If you're a fan of my EventSinkImp utility, it's simply
a component generated by EventSinkImp. This means that we don't have to worry
about writing code for this object, we'll let EventSinkImp do the hard work for
us.
EventSinkImp
is now on version 1.7. This new version has an enhancement that enables
you to produce event handler objects mentioned in this article. If you
haven't downloaded the latest copy, you can visit the EventSinkImp site by
navigating to the Products section on this
site. If you're unsure what your EventSinkImp version is, run it and
click the About button.
|
ChatEventHandler is simply
the TIChatEvent class generated by EventSinkImp. I was able to do this by
running EventSinkImp, manually loading our ChatEvents.tlb file (use the
"..." button in EventSinkImp's main form to browse to ChatEvents.tlb), checking the IChatEvent
interface, and then performing an import on this type library.
That does it for creating
subscriptions. To terminate a subscription, we simply set the FSubscription
variable to NIL at the appropriate time:
procedure TChatForm.LogoutClick(Sender: TObject);
begin
//destroy subscription
FSubscription := NIL;
end;
One last thing before I forget. Before
your application can make use of EventService, you'll need to initialize it and
then uninitialize it when you're done. In our chat application, the following
code in ChatClient.dpr shows you how to do this:
uses
EventUtils;
begin
Application.Initialize;
//initialize event service
InitializeEventService;
Application.CreateForm(TChatForm, ChatForm);
Application.Run;
//uninitialize event service
UninitializeEventService;
end.
If you want to make use of the COM+
event services, simply call InitializeService and pass in True as a parameter:
procedure InitializeEventService
(UseCOMPlusEvents : boolean = False);
A
Note on COM+
If you are using the COM+ event
services as mentioned above, you'll need to build and install the event
repository project that we created earlier. First build ChatEvents.dpr
into ChatEvents.dll. Then go into the Component Services administrative
tool and install ChatEvents.dll as an event class into a COM+ application
(you can create a new COM+ application, perhaps named ChatEvents, if you need to). To do this, when
in the COM Component Install Wizard dialog, select the Install new event
class(es) option and pick ChatEvents.dll from there.
|
EventService is is normally deployed and registered
as an EXE COM server. To register EventService, simply run EventService.exe /regserver
once.
If
you want to deploy EventService in Microsoft Transaction Server (MTS), simply create a separate package for it (make sure the package Activation is set up as
a Server package instead of Library package) and install EventService.dll into
the package.
If you want to deploy EventService as
a COM+ application in Windows 2000, simply create a separate COM+ application for it (make sure
the application Activation is set up as Server application instead of Library
application) and install EventService.dll into the COM+ application.
When
deploying EventService as a COM+ application or in an MTS package, make sure that you
do not also deploy and register EventService.exe as a standard EXE on the
same machine. Also, my tests on Windows 2000 Beta RC2 indicate that
EventService does not work when deployed as a COM+ application and you are
not using the COM+ event services. Whatever reason is causing this remains
to be seen once Windows 2000 ships. In other words, if you deploy
EventService into a COM+ application, make sure all your client
applications initialize it as follows:
InitializeEventService (True);
//True means use COM+ event services
When using EventService as a
standalone EXE in a NT or 9x environment, make sure you have an NT4 SP3
equivalent installation of DCOM. For NT, this is Service Pack 3. For 9x,
this is at least DCOM 9x 1.2 and above.
|
In this tutorial, I've shown you a
very easy-to-use yet very effective COM event framework. This framework does not
replace COM connection points or any hand-coded callback mechanisms you've come
to learn and love for the past few years. However, I can say for sure that this
framework can tremendously help professionals who want a simple, quick, and
effective way to build COM event mechanisms into their applications. Good luck
and have fun!
A Word of Caution
All source code presented here is not
necessarily production quality code. In particular, tasks like error handling,
multithreading, optimizations, etc. were not taken into consideration because
they are not relevant to the tutorial topic. Therefore, I do not make any
guarantees that the code will work for you, and you cannot hold me liable for
any damages resulting from the use (or misuse) of this material. In other words,
don't blame me if you get ****ed!
|