Introduction
Creating a design pattern bridge between the user interface and its event implementation can often improve the code portability. Furthermore, such an object can be instrumented to record all GUI-implementation interactions.
The event manager presented in this article describes such a bridge. It is intended to separate the event sink from the event source. Especially in MFC programming, event sinks are often implemented as part of the dialog or form class. This makes it nearly impossible to access the implementation, should it be required outside of the scope of the dialog or form. In C#, the form designer also places the event handler in the form class, which is not necessarily desirable.
Furthermore, the event manager provides capability that the default form designer does not provide. It allows for events to be invoked asynchronously, and event sinks can be associated with their sources by either using the System.EventHandler
delegate or via reflection.
An Introduction To Events
Events cause the program to break out of its current process flow (usually an idle loop) and execute a specific set of instructions. It is important to get this definition right (and I hope I did a fair job of it) because it then allows further discussion of two important aspects of event management: the event source and the event sink. These terms derive from hardware engineering, where a device �sourced� current and another device was the current "sink". Thus, the event is the go-between for the event source and the event sink.
The event source is the object that generates the event. Typical event sources are GUI controls such as buttons, but they can also be hardware related, such as a timer, a mouse button, a busy indicator, etc.
The event sink is the object that receives the event notification. It performs specific actions based on the event.
Why make the distinction between event source, event and event sink? Why not just say that the event sink is coupled to the event source? Because this removes an important consideration when dealing with events:
- One event source can source one or more events
- One or more event sinks can sink one or more events
Both of these are �many to many� relationships. Now, I will state something that may be controversial. In my opinion, one event source should generate only one event, and one event should be received by one and only one event sink. This reduces the possible combinations to one-to-one for event sources to events, and one-to-one for event sinks to events. Why do I have this opinion? Because, an event source that generates multiple events adds a layer of complexity with regards to the sequence that the events are �sinked�. Similarly, a single event (or multiple events) handled by multiple sinks creates the same layer of complexity: what order are the event sinks invoked, and are there synchronization issues between them? This kind of complexity falls outside of the purview of a basic event manager. Furthermore, such a system should be implemented with additional meta-control event manager objects, leaving the core event manager as presented here in tact. Hopefully this would give the programmer a finer level of control and possibly address issues such as semaphores and synchronization between event sinks. The point though is that a set of meta-classes to handle these issues would bring to consciousness the complexity of many-to-many event relationships, and heaven only knows how unconscious programmers can be!
Thus, embedded in the above paragraph is a criticism of .NET�s ability to associate several event sinks to a single event source. All too often, it seems that Microsoft gives the programmer powerful capabilities without explaining the dangers and pitfalls of their usage!
One final note�while I�ve talked about three separate entities, the event source, the event itself, and the event sink, in implementation the concept of the event is abstracted out. It exists in the layers of the operating system and the hardware, but usually the actual concept of the event doesn�t need to be implemented. This differs from a message manager, for example, in which the concept of a "message" is a very real entity and is passed between the addresser and the addressee.
The Event Object Model
The event object model, as illustrated in the above diagram, consists of the abstract base class EventSink
:
public abstract class EventSink
{
public abstract IAsyncResult Invoke(object sender, object[] args);
}
This class defines a single abstract method, IAsyncResult
.
Two abstract classes are derived from the base class, which respectively refine the abstraction a little bit more, into events that are invoked using the System.EventHandler
delegate, and events that are invoked using reflection.
The EventHandlerSink
abstract class:
public abstract class EventHandlerSink : Event
{
protected EventHandler eh;
public EventHandlerSink(System.EventHandler eh)
{
this.eh=eh;
}
}
simply consists of a constructor that takes the System.EventHandler
delegate as a parameter.
The ReflectionSink
abstract class:
public abstract class ReflectionSink : EventSink
{
protected MethodInfo mi;
protected object instance;
protected BindingFlags bindingFlags;
public ReflectionSink(MethodInfo mi)
{
this.mi=mi;
instance=null;
bindingFlags=BindingFlags.Public |
BindingFlags.NonPublic |
BindingFlags.InvokeMethod |
BindingFlags.Static;
}
public ReflectionSink(object instance, MethodInfo mi)
{
this.mi=mi;
this.instance=instance;
bindingFlags=BindingFlags.Public |
BindingFlags.NonPublic |
BindingFlags.InvokeMethod |
BindingFlags.Instance;
}
}
maintains information about the reflected method and the instance (if non-static) of the method to invoke and provides two constructors, one to handle static reflected methods and the other to handle reflected methods that are associated with an object instance.
The real work is done in the following four classes, derived from the abstract classes described above. SyncEventHandlerSink
(sorry about the alliteration!) handles invoking the event sink as specified in the System.EventHandler
delegate and consists of the constructor and the Invoke
method, which is very trivial in this case.
public class SyncEventHandlerSink : EventHandlerSink
{
public SyncEventHandlerSink(System.EventHandler eh) : base(eh) {}
public override IAsyncResult Invoke(object sender, object[] args)
{
try
{
eh(sender, new EventArgs(args));
}
catch (Exception e)
{
Dbg.Warn(false, new DbgKey("EvMgrInvokeError"), e.ToString());
}
return null;
}
}
Compare this to the asynchronous version of the EventHandler
delegate call. Here we take advantage of a very poorly documented BeginInvoke
method.
public class AsyncEventHandlerSink : EventHandlerSink
{
public AsyncEventHandlerSink(System.EventHandler eh) : base(eh) {}
public override IAsyncResult Invoke(object sender, object[] args)
{
IAsyncResult res=null;
try
{
res=eh.BeginInvoke(sender, new EventArgs(args), null, null);
}
catch (Exception e)
{
Dbg.Warn(false, new DbgKey("EvMgrInvokeError"), e.ToString());
}
return res;
}
}
The synchronous event invocation using reflection isn�t that much more complicated. Here, we use the MethodInfo
class to invoke the event sink. The parameters for this object (instance
and bindingFlags
) have already been set up previously (discussed below).
public class SyncReflectionSink : ReflectionSink
{
public SyncReflectionSink(MethodInfo mi) : base(mi) {}
public SyncReflectionSink(object instance,
MethodInfo mi) : base(instance, mi) {}
public override IAsyncResult Invoke(object sender, object[] args)
{
try
{
mi.Invoke(instance, bindingFlags, null,
new object[] {sender, new EventArgs(args)}, null);
}
catch (Exception e)
{
Dbg.Warn(false, new DbgKey("EvMgrInvokeError"), e.ToString());
}
return null;
}
}
The most interesting implementation is the asynchronous reflected event sink. This requires using an intermediate delegate so that the event sink can be invoked as a separate thread. The MethodInfo
class doesn�t appear to have a BeginInvoke
as does System.EventHandler
.
public class AsyncReflectionSink : ReflectionSink
{
private delegate void RunAsync(object sender, object[] args);
private RunAsync run;
public AsyncReflectionSink(MethodInfo mi) : base(mi)
{
InitializeAsyncDelegate();
}
public AsyncReflectionSink(object instance,
MethodInfo mi) : base(instance, mi)
{
InitializeAsyncDelegate();
}
private void InitializeAsyncDelegate()
{
run=new RunAsync(Run);
}
public override IAsyncResult Invoke(object sender, object[] args)
{
return run.BeginInvoke(sender, args, null, new Object());
}
private void Run(object sender, object[] args)
{
try
{
mi.Invoke(instance, bindingFlags, null,
new object[] {sender, new EventArgs(args)}, null);
}
catch (Exception e)
{
Dbg.Warn(false, new DbgKey("EvMgrInvokeError"), e.ToString());
}
}
}
To construct the MethodInfo
object, a private member function parses the name of the event sink method. This name must be of the form:
assembly/namespace.class/method
The parser is quite simple:
private static MethodInfo GetMethodInfo(string reflection)
{
MethodInfo mi=null;
try
{
string[] info=reflection.Split(new char[] {'/'});
Assembly mainAssembly=Assembly.LoadFrom(info[0]);
Type[] types=mainAssembly.GetTypes();
Type type=mainAssembly.GetType(info[1]);
MethodInfo[] mis=type.GetMethods();
mi=type.GetMethod(info[2]);
}
catch (Exception e)
{
Dbg.Warn(false, new DbgKey("EvMgrNoMethod"),
e.ToString()+"\n"+"Method:"+reflection);
}
return mi;
}
The EventManager
class consists of several overloaded methods for adding event sinks:
public static void AddSyncEventSink(string name, System.EventHandler eh)�
public static void AddAsyncEventSink(string name, System.EventHandler eh)�
public static void AddSyncEventSink(string name, string reflection)�
public static void AddSyncEventSink(string name,
object instance, string reflection)�
public static void AddAsyncEventSink(string name, string reflection)�
public static void AddAsyncEventSink(string name,
object instance, string reflection)�
which allows both static and non-static (instance based) events sink to be specified as either synchronous or asynchronous calls.
Similarly, there are several flavors of the Invoke
method based on the information you need to supply:
public static IAsyncResult Invoke(string name)�
public static IAsyncResult Invoke(string name, object sender)�
public static IAsyncResult Invoke(string name, object[] args)�
public static IAsyncResult Invoke(string name, object sender, object[] args)�
For synchronous calls, the Invoke
method always returns null
. For asynchronous calls, the Invoke
method returns an IAsyncResult
which can be useful for obtaining thread results.
One caveat here: the way the EventManager
is designed, whether the event sink is called synchronously or asynchronously is determined when the event sink is added to the Event
collection. It would be fairly trivial to expand this object so that the sync/async determination can be made when the event occurs.
Usage
I�ve included in the project, a very simple demonstration of the usage:
static void Main()
{
Dbg.Initialize();
Dbg.LogOutput("out.txt");
EventManager.Initialize();
Events evs=new Events();
EventManager.AddAsyncEventSink("event1",
new EventHandler(MyApp.Events.Event1));
EventManager.AddSyncEventSink("event2", evs,
"csEventManager.exe/MyApp.Events/Event2");
EventManager.AddAsyncEventSink("event3",
"csEventManager.exe/MyApp.Events/Event3");
EventManager.Invoke("event1");
EventManager.Invoke("event2");
EventManager.Invoke("event3");
MessageBox.Show("Application will exit when you" +
" click on the OK button\nand running" +
" threads will terminate.");
Dbg.Close();
}
Instrumentation
Aside from several warnings issued when an exception occurs, the EventManager
logs the event sinks in a text file specified by the Dbg
object (see my other articles):
Debug Logging Initialized: 10/16/2002 5:50:13 PM
EventManager.Invoke: event1
EventManager.Invoke: event2
EventManager.Invoke: event3
Conclusion
In previous programming efforts, I have found that creating an instrumented bridge between GUI generated events and event handlers has been both a productivity enhancement and has helped in debugging complex applications. An event manager can be a general solution which increases the flexibility of your code.