An Eventful Story

An Eventful Story

By Adrian Fagg

Overload, 7(30):, February 1999


In my previous article, I talked about 'events'. These are an intrinsic part of Delphi but the idea is equally valid in C++, though not as widely used. I'd like to tell the story of one use that I put this idea to in a project that I'm working on. The problems are common to many projects and this is only one approach from a choice of many.

Background

The project is based around a number of medium sized databases and consists of a main application plus a number of utilities. The main application is a conventional MDI application, where the documents don't actually correspond to files but instead are queries stored in a separate database. The queries are of a number of types, as are the data views. For reasons that I won't go into, the queries, query editors and data views are managed by a specially written singleton object that's independent of the user interface. A lot of information about the available queries is also cached in another cluster of objects that is independent of the aforementioned manager class.

All of these objects work in conjunction with the main application window that has the toolbars, menus etc.

This is a simplified part of the application's architecture. Associations drawn with a dashed line are application specific, solid lines are more fundamental. However, the solid lines are important only in one direction, e.g. a view must have a query but a query doesn't need to know about views.

Perhaps surprisingly, co-ordination of these parts of the application has taken a lot of time to get right, despite the framework given by the VCL (Delphi's framework).

My problem is that I needed to eliminate the dependencies that these objects had on each other. This was because I wanted to reuse some objects in contexts other than the main application. Specifically, I needed these objects in some of the utilities as well as some as yet unspecified applications.

For example, when a view window is closed, both the query manager and the main frame window need to be told. Likewise, when a query name is changed, just about every object needs to be told.

My original design allowed for these things to happen because there were associations between these objects. If it weren't for these co-ordination issues, these associations wouldn't have been needed.

By the time that the first version was delivered, these dependencies were beginning to become apparent. For the next version, I wanted to open the door to alternative contexts and to extend the degree of co-ordination between these and other objects.

An additional change that was made related to a deficiency in the early version of Delphi that was used originally. It wasn't possible to use inheritance on the windows, so that for example, an MDI child view couldn't have a common base class. This had led to unnecessary code duplication, mostly the co-ordination code in question! I only mention this as it meant that there was a good incentive to rewrite these aspects on two fronts.

So let's look at some of the alternatives that offered themselves.

Window Messages

This was an option that was already in use. When a query name changed, a message was posted to all interested windows with a pointer to the relevant object in the 'lparam'. The problem is to decide what windows need to be told. The query manager having a list of interested views and editors for each query object solved this originally. It additionally needed to tell the main window, which is a dependency that can be worked around by simply knowing that the main window is a window. That is, it doesn't need to know anything else about the main window. On the down side, I would prefer it if the manager didn't need to know that there was a main window, or that there was only one of them!

Unfortunately, not all objects involved are actually windows, so this can't be used as a general solution.

Broadcasting

An approach that we came up with was to make each object that needed to tell other objects anything a 'broadcaster'. Objects that needed to listen to the broadcaster registered themselves with it and received notifications whenever anything happened. The signature of the listening method called contained a message type parameter and a pointer, something like a window message. A listener would then get a look at each message and act according to the message type.

I wasn't comfortable with this approach for two reasons. Firstly, I didn't like the idea of having a switch statement in the listener method. This was too much like SDK Windows programming!

Secondly, each listener instance had to be connected to the broadcaster instances. This implied either that there would be knowledge of the broadcasters in the listener code or that this knowledge would have to be held in application specific code. This latter approach was feasible and could be invoked by firing an 'add listener' event for example. However, I felt that this was a clumsy way of making these associations. To make things even worse, these associations tended to 'compound up' so that you would need to connect many listeners to many broadcasters.

So, if I didn't like it, why bother at all? The above idea works by maintaining a list of events (i.e. listener object plus method). This is a useful thing in its own right but it does highlight a problem that is general for events. It is important that an event isn't called when the object that's handling the event no longer exists! For components whose lifetimes are coincident with their form, as is the most common case in Delphi, this problem doesn't arise.

By virtue of the listener maintaining a list of broadcasters it is simple for the listener to remove its handler from the broadcaster's event list in its destructor. Symmetrically, if a broadcaster is destroyed, it can let the remaining listener instances know.

Event List

From the broadcaster design, I took the guts of the event list and generalised it for any type of event. That is to say, you can safely cast these events in Delphi as they all have the same storage. In C++, you would want to use a template. This gave me storage for the events and methods to add and remove an instance's hander from the list. I specialised for both plain (no parameters) and notification (one 'sender' parameter, widely used in the VCL) events. These have methods called NotifyAll with parameters matching their event type. These classes are potentially useful in their own right:

EventList = class
public
  constructor Create; virtual;
  destructor Destroy; override;

  procedure
  StopNotifying( instance : Pointer );
end;

NotifyEventList = class(EventList)
public
  procedure
  PleaseNotify( event : TNotifyEvent );
  procedure 
  NotifyAll(sender : TObject );
end;

I've omitted the 'plain' version for simplicity. Note that I've declared 'instance' as a plain pointer (equivalent to void *) because Delphi allows events both on an instance and a class basis!

The implementation is simple. A union of the event and the two pointers that make up the event is used to store each event. The StopNotifying method simply scans the list for the 'instance' part of the event and 'removes it if found. Naturally, a class implementing an event handler would need to call StopNotifying in its destructor, passing 'this' as the instance ('self' in Delphi). So long as this is done in the same class that created the event in the first place, the handler's instance pointer should match, even with multiple inheritance.

A first rather feeble go at the event class:

class NotifyEvent
{
public:
  virtual void DoEvent(void *pSender)= 0;
  virtual bool InstanceMatches(void *pHandler) = 0;
};

template <class HandlerClass>
class NotifyEventHandler : public NotifyEvent
{
private:
  typedef void (HandlerClass::* FnNotifyEvent)
                (void *pSender);
  HandlerClass *m_pHandler;
  FnNotifyEvent m_OnNotifyEvent;
public:
  NotifyEventHandler
    (HandlerClass *pHandler, FnNotifyEvent ne)
    : m_pHandler(pHandler), 
      m_OnNotifyEvent(ne) {}
  
  void DoEvent(void *pSender)
  {
    (m_pHandler->*m_OnNotifyEvent)(pSender);
  }
  bool InstanceMatches(void *pHandler)
  {
    return m_pHandler == pHandler;
  }
};

There are some problems that the transition to C++ has highlighted!

Firstly, the TNotifyEvent signature in Delphi has a sender of type TObject. This is more comfortable than you might expect because all classes are derived from TObject and they have RTTI regardless!

This could perhaps be dealt with by using a template, an exercise for the reader.

The other problem lies in the InstanceMatches method. Again, I've hacked it with a void pointer, whether this can be made to work correctly I don't know.

When implementing a C++ version of NotifyEventList, you will need to work with pointers to NotifyEvent instances.

StopNotifying will require InstanceMatches and NotifyAll requires DoEvent.

In use, PleaseNotify will require a new instance of NotifyEventHandler for the handler class.

So, if you think that this is worth pursuing, I'm afraid that you'll have to do all the hard work.

Event Broker

This was my final solution. I called it an 'event broker' but don't read anything into the name.

Subsequent to my first version of this article, I found that a design very much like this is described by William Crowe (CUJ June 1998) and called a switchboard. Then I also discovered that a similar switchboard class is available in at least one C++ framework. The implementation is very like what I describe below but solves the lifetime problem more effectively using a more appropriate design in the C++ context. I leave my original text below as it illustrates the reality of human frailty in this kind of work!

The event list on its own is useful but in the broadcaster design, it would be accessible via the broadcaster instance, something that the listener would ideally not need to know about. A more interesting approach is to make the event list available as a central resource. The event broker fits this purpose. Here is my Delphi declaration:

TEventBroker = class
private
  m_EventLists : TMapStringToObject;

public
  constructor Create;
  destructor Destroy; override;

  function AddOrGet( const name : string; eventListClass : TTEventListClass ) : TEventList;

  procedure StopNotifying( instance : TObject );
  
  // may return nil.
  function EventList( const name : string ) : TEventList;
  
  // will create if not already present...
  function NotifyEventList( const name : string ) : TNotifyEventList;
  function SimpleEventList( const name : string ) : TSimpleEventList;
  function StringEventList( const name : string ) : TStringEventList;

  class function Instance : TEventBroker;
end;

This is designed to be a singleton, though it really doesn't have to be. What the event broker does is to maintain a collection of event lists, mapped by a string key. The choice of key type was based on convenience, other possibilities exist.

Creation of new event lists is done using a class reference, hence the virtual constructor in the EventList class.

In use, it is straightforward. To listen to particular events use something like:

with TEventBroker.Instance do begin
  NotifyEventList('QryVwClose').
                  PleaseNotify(QryViewClosed);
  NotifyEventList('QryVwActivate').
                PleaseNotify(QryViewActivate)
end

To notify do:

TEventBroker.Instance.
  NotifyEventList('QryVwClose').
  NotifyAll(self) 

To stop listening to any events:

TEventBroker.Instance.StopNotifying(self) 

Arranging for the last call to occur in a base class's destructor is not a problem in Delphi. Unfortunately, it may well be a problem in C++. This depends very much on being able to get the InstanceMatches method above to work correctly.

Perhaps the single broker managing all the different types of event list is a mistake, using templates, you could simply have it work with the correct type.

I'm leaving all of the above as an 'exercise for the reader' simply because I don't have the time to take it any further in C++. It may well be that significant changes will be needed.

So…

The above design has eliminated many dependencies from my original application design. The remaining dependencies are those that come with knowledge of the 'sender' parameter. For example, the above listeners might need to know about the query view that's closing or activating. Abstraction can be achieved by conventional means where this is needed, e.g. by using abstract base classes for the sender objects.

As a footnote, one way of implementing these classes as described would be to use COM. The pointer conversion problems could be overcome by using the IUnknown interface, which will always be the same for a given instance.

Back to now

Now I'm back in the present, knowing of the switchboard class! As can be seen above, I've dug a hole for myself by trying to give responsibility to stopping notifications to the handler class. William Crowe's switchboard solution simply makes the NotifyEventHandler instance responsible in its destructor. My problem was that in my Delphi implementation, the event objects are owned by the event list, whereas if they are simply part of the handler class, their lifetimes are just right for the job.

This is another good example of different language semantics forcing different approaches. Delphi doesn't handle lifetimes in the same way as C++ (except when using COM interface pointers), consequently, the event object is better owned by the event list. The switchboard class doesn't tackle the difficulties of emulating the 'sender' parameter; they remain a problem for now. I also note that the switchboard class is designed to work only as a singleton, which may also be an improvement.

I would be very interested to hear from anyone else who has any ideas in this area.






Your Privacy

By clicking "Accept Non-Essential Cookies" you agree ACCU can store non-essential cookies on your device and disclose information in accordance with our Privacy Policy and Cookie Policy.

Current Setting: Non-Essential Cookies REJECTED


By clicking "Include Third Party Content" you agree ACCU can forward your IP address to third-party sites (such as YouTube) to enhance the information presented on this site, and that third-party sites may store cookies on your device.

Current Setting: Third Party Content EXCLUDED



Settings can be changed at any time from the Cookie Policy page.