Introduction
In previous article titled "How to Change Order of Event Handlers Execution at
Run Time" (http://www.c-sharpcorner.com/UploadFile/b81385/8741/)
we have provided certain level of insight into implementation of event handling
mechanism in .NET. In this article we are pushing further down into murky depths
of .NET event driven application internals. Several concepts used to implement
event subscriptions in practice (all strictly based on MSDN) will be explained
and their functioning used to implement otherwise hardly implementable entity:
event auditor.
Event auditing is a common task in programming and it boils down to getting
informed about events (all or some) that have been raised and about their exact
order of being raised. First goal can be relatively easily met by simply adding
subscribers to all events of interest. But the second goal may become a source
of trouble because entity which audits events would have to be the first one to
subscribe to all events or otherwise recursive event raising might cause auditor
to receive invocations in reversed order of events.
We have discussed this issue of reversed event handlers invocations in full
detail in previous article titled How to Change Order of Event Handlers
Execution at Run Time and we will not repeat the analysis in this article.
Instead, solution presented there will be pushed further in this article so that
it now covers wider set of classes that can be audited.
Solution presented in previous article is not complete because it covers only
the simplest method in which event handlers are declared. But most of the
classes that expose multiple events (like most of the GUI classes) do not use
this method, but rather employ data structure to store handlers for all their
events at one place. This method is explained in MSDN article
How to: Handle
Multiple Events Using Event Properties, so please refer to that text before
proceeding.
Our previous solution solves only the case when invocation list of the event
handler can be accessed directly by searching for the non-public field with name
equal to event name. However, when class uses collection, like EventHandlerList
class, then we cannot be certain which EventHandler delegate instance is
connected with which event. This is because EventHandler instances are inserted
into collection using System.Object keys which are only known to the class which
implements the events.
In the following sections we will first examine the problem of accessing
EventHandler instances for events, and then we will propose a solution which
allows auditor to insert own event handlers at the beginning of every invocation
list in the system, thus ensuring that all event notifications will be received
by the auditor in their correct order.
Problem Statement
In this section we will try to find all EventHandler instances within a given
object such that they are either declared directly, or stored in an
EventHandlerList instance. We will not cover other endless possibilities, like
using custom collections to store event handlers.
We will start with a custom form which publishes event named CustomEvent, in
addition to all events inherited from System.Windows.Forms.Form class:
class
MyForm: Form
{
public event
EventHandler CustomEvent;
public void
DoWork()
{
if (CustomEvent !=
null)
CustomEvent(this, new
EventArgs());
}
}
This form can be instantiated and events handled in the application like this:
static
void CustomEventHandler(object
sender, EventArgs e)
{
Console.WriteLine("CustomEvent
handled.");
((Form)sender).Text =
"Changed text";
}
static
void FormTextChanged(object
sender, EventArgs e)
{
Console.WriteLine("TextChanged
handled.");
}
static
void FormShown(object
sender, EventArgs e)
{
Console.WriteLine("Shown
handled.");
((MyForm)sender).DoWork();
}
static
void Main(string[]
args)
{
MyForm form = new
MyForm();
form.Shown +=
new EventHandler(FormShown);
form.TextChanged += new EventHandler(FormTextChanged);
form.CustomEvent += new EventHandler(CustomEventHandler);
form.ShowDialog();
}
This is simple case in which events
are raised recursively: Shown event causes form's DoWork method to be invoked,
which in turn raises CustomEvent, further causing form's text to be changed.
However, since main function is the only subscriber in the system, all events
will be noted on the output in their correct order. Output of this program looks
like this:
Shown handled.
CustomEvent handled.
TextChanged handled.
Now we can try to build a new class, called MyFormAuditor, which subscribes the
same set of events and simply prints them out to the console. Note that auditor
should not change state of the system because that could cause changes in other
events order and argument values. Auditing class could look like this:
class
MyFormAuditor
{
public MyFormAuditor(MyForm
form)
{
form.CustomEvent += new
EventHandler(CustomEvent);
form.Shown += new
EventHandler(Shown);
form.TextChanged += new
EventHandler(TextChanged);
}
void CustomEvent(object
sender, EventArgs e) {
Console.WriteLine("Audit
CustomEvent."); }
void Shown(object
sender, EventArgs e) {
Console.WriteLine("Audit
Shown."); }
void TextChanged(object
sender, EventArgs e) {
Console.WriteLine("Audit
TextChanged."); }
}
This class can be used to subscribe all events of interest on any instance of
MyForm class, but we must understand that such subscriptions typically come last
in invocation lists, simply because objects are instantiated and linked before
auditor comes into play. Now we can change the main function, so that auditor is
added and linked with the form:
static
void Main(string[]
args)
{
MyForm form = new
MyForm();
form.Shown +=
new EventHandler(FormShown);
form.TextChanged += new EventHandler(FormTextChanged);
form.CustomEvent += new EventHandler(CustomEventHandler);
MyFormAuditor auditor =
new MyFormAuditor(form);
form.ShowDialog();
}
This is typical situation in
practice โ all objects are instantiated and all event handlers subscribed before
auditor is created. Output now looks like this:
Shown handled.
CustomEvent handled.
TextChanged handled.
Audit TextChanged.
Audit CustomEvent.
Audit Shown.
As we can see, auditor has received event notifications in reverse order. This
is consequence of recursive events, i.e. events being raised from inside other
event handlers. In order to correct the issue, we will have to discover where
event handlers are stored in the MyForm instance and then to insert new
delegates at the front of their invocation lists.
Listing Event Handlers
First step in solving the auditing problem is to find EventHandler delegates
contained in target object. As stated above, event handlers are either found
instantiated on their own (default implementation in .NET), or as members of
EventHandlerList instance, which is typical for most of the complex classes. For
instance, all classes deriving from System.ComponentModel.Component class, and
that means Windows forms and controls, use EventHandlerList to store subscribed
event handlers.
In the first case, EventHandler instance is declared as a non-public field with
name equal to corresponding event's name. In the second case, object contains
non-public field named events, of type EventHandlerList, and it also declares
number of static non-public fields of type System.Object, which are used as keys
to the list. In both cases any particular EventHandler instance can be
discovered by examining contained fields, and it is done by the following
function:
static
void ExamineSubscribers(object
target)
{
Hashtable objects =
new Hashtable();
List<EventHandlerList>
handlerLists = new
List<EventHandlerList>();
BindingFlags bf =
BindingFlags.Instance |
BindingFlags.NonPublic |
BindingFlags.Public |
BindingFlags.Static;
Type t = target.GetType();
while (t != null)
{
FieldInfo[] fields = t.GetFields(bf);
for (int i = 0;
i < fields.Length; i++)
{
if (fields[i].FieldType ==
typeof(System.Object)
&& fields[i].GetValue(target) != null)
objects.Add(fields[i].GetValue(target), fields[i].Name);
else if
(fields[i].FieldType == typeof(EventHandlerList))
handlerLists.Add((EventHandlerList)fields[i].GetValue(target));
else if
(fields[i].FieldType == typeof(EventHandler)
&& t.GetEvent(fields[i].Name, bf) != null)
{
EventHandler eh = (EventHandler)fields[i].GetValue(target);
Delegate[] invocationList =
eh.GetInvocationList();
if (invocationList.Length > 0)
Console.WriteLine("{0}
event handler(s) directly subscribed to event {1}", invocationList.Length,
fields[i].Name);
}
}
t = t.BaseType;
}
foreach (EventHandlerList handlerList
in handlerLists)
foreach (object
key in objects.Keys)
if (handlerList[key] !=
null)
{
Delegate dlg =
handlerList[key];
Delegate[] invocationList =
dlg.GetInvocationList();
if (invocationList.Length > 0)
Console.WriteLine("{0}
event handler(s) mapped by key {1}", invocationList.Length, (string)objects[key]);
}
}
This function iterates through type's derivation chain and for every type along
it visits all fields. If field is of type System.Object, then it is possibly a
key in some EventHandlerList associated with some event. If it is
EventHandlerList then it might contain event handlers of interest. Otherwise, if
it is EventHandler and has name equal to some event published by the target
object, then it is treated as handler for that event. When this function is
invoked on our MyForm instance, it prints out the following report:
2 event handler(s) directly subscribed to event CustomEvent
2 event handler(s) mapped by key EventText
2 event handler(s) mapped by key EVENT_SHOWN
This report shows that three events have two subscribers each, as expected.
However, locations holding these EventHandler instances are different. In first
case, CustomEvent is implemented as private EventHandler field with same name.
TextChanged event handler is located in the list under key indicated by private
static System.Object field named EventText, which is defined in the
System.Windows.Forms.Control class. Last event, Shown, is also located in the
same list, but under key indicated by private static System.Object field named
EVENT_SHOWN, which is defined in the System.Windows.Forms.Form class.
These results show how complicate it is to locate each particular event handler
in a given object. Locations and naming conventions are not uniform, not even
within classes provided by the same vendor. Further on, we will address the
auditing problem in the MyForm instance, as it closely resembles most of the
practical cases.
Solution to Auditing Problem
In previous section we have seen that some event handlers are subscribed under
key which are class-specific. Every class using EventHandlerList is free to
create and name keys as it wants and there is no way to discover the naming in
general case. One consequence of such state of matters is that we cannot say
which EventHandler in the list handles which event.
In previous example we have identified objects named EventText and EVENT_SHOWN
that are connected with existing handlers. From these names, and having source
code which has subscribed to events, we know that first one marks TextChanged
event and the second marks Shown event, both raised by the Form object. However,
in general case, this could be all different โ there is no way to say what was
the naming convention obeyed by the coder.
So here is the major question: how do we audit events CustomEvent, TextChanged
and Shown, all raised by the MyForm class? To do so, we want to remove existing
subscriptions, then to subscribe the auditor to all three events, and then to
return back all previous subscriptions. In that way, auditor will always be the
first one to receive every event notification. However, in any practical case,
we cannot determine which subscribers are subscribed to which event, simply
because we do not know which contained EventHandler instance is bound to which
event, due to arbitrary naming of the keys to the EventHandlerList.
Hard solution would be to somehow understand naming conventions for every
particular class. But this way is not general and it is not easily extendible.
Much simpler solution is to remove all existing event handlers, then add
auditor's event handlers, and finally re-subscribe all originally subscribed
handlers. This solution might look too heavy, especially if number of existing
subscriptions is large, but it essentially solves the problem of unknown keys to
known events.
As a matter of demonstration, we will implement this solution in the
MyFormAuditor class. Complete source code of this class is as follows:
class
MyFormAuditor
{
public MyFormAuditor(MyForm
form)
{
Queue<object>
subscriptions = RemoveExistingSubscriptions(form);
form.CustomEvent +=
new EventHandler(CustomEvent);
form.Shown += new
EventHandler(Shown);
form.TextChanged += new
EventHandler(TextChanged);
RestoreOriginalSubscriptions(form, subscriptions);
}
void CustomEvent(object
sender, EventArgs e) {
Console.WriteLine("Audit
CustomEvent."); }
void Shown(object
sender, EventArgs e) {
Console.WriteLine("Audit
Shown."); }
void TextChanged(object
sender, EventArgs e) {
Console.WriteLine("Audit
TextChanged."); }
Queue<object>
RemoveExistingSubscriptions(object target)
{
Queue<object>
subscriptions = new
Queue<object>();
Hashtable objects =
new Hashtable();
List<EventHandlerList>
handlerLists = new
List<EventHandlerList>();
BindingFlags bf =
BindingFlags.Instance |
BindingFlags.NonPublic |
BindingFlags.Public |
BindingFlags.Static;
Type t = target.GetType();
while (t !=
null)
{
FieldInfo[] fields =
t.GetFields(bf);
for (int i = 0;
i < fields.Length; i++)
{
if (fields[i].FieldType ==
typeof(System.Object)
&& fields[i].GetValue(target) != null)
{
object value =
fields[i].GetValue(target);
if (!objects.ContainsKey(value))
objects.Add(value, null);
}
else if
(fields[i].FieldType == typeof(EventHandlerList))
handlerLists.Add((EventHandlerList)fields[i].GetValue(target));
else if
(fields[i].FieldType == typeof(EventHandler)
&& t.GetEvent(fields[i].Name, bf) != null)
{
EventHandler eh = (EventHandler)fields[i].GetValue(target);
Delegate[] invocationList
= eh.GetInvocationList();
Delegate dlg = (Delegate)eh;
for (int
j = 0; j < invocationList.Length; j++)
dlg = Delegate.Remove(dlg,
invocationList[j]);
fields[i].SetValue(target, dlg);
subscriptions.Enqueue(fields[i]); // This means
that EventHandler field indicated by fields[i]
subscriptions.Enqueue(invocationList);
// should be subscribed by delegates
listed in the invocation list
}
}
t = t.BaseType;
}
foreach (EventHandlerList
handlerList in handlerLists)
foreach (object
key in objects.Keys)
if (handlerList[key] !=
null)
{
Delegate dlg =
handlerList[key];
Delegate[] invocationList
= dlg.GetInvocationList();
for (int i = 0;
i < invocationList.Length; i++)
handlerList.RemoveHandler(key, invocationList[i]);
subscriptions.Enqueue(handlerList); // This means
that in specified handler list under specified key
subscriptions.Enqueue(key);
// delegates listed in the invocation list should be
subscribed
subscriptions.Enqueue(invocationList);
}
return subscriptions;
}
void RestoreOriginalSubscriptions(object
target, Queue<object>
subscriptions)
{
while (subscriptions.Count > 0)
{
if (subscriptions.Peek()
is FieldInfo)
{
FieldInfo fi = (FieldInfo)subscriptions.Dequeue();
Delegate[] invocationList = (Delegate[])subscriptions.Dequeue();
Delegate dlg = (Delegate)fi.GetValue(target);
for (int
i = 0; i < invocationList.Length; i++)
dlg = Delegate.Combine(dlg,
invocationList[i]);
fi.SetValue(target, dlg);
}
else
{
EventHandlerList handlerList = (EventHandlerList)subscriptions.Dequeue();
object key =
subscriptions.Dequeue();
Delegate[] invocationList = (Delegate[])subscriptions.Dequeue();
for (int i = 0;
i < invocationList.Length; i++)
handlerList.AddHandler(key, invocationList[i]);
}
}
}
}
In this version, two methods have been added. First method, named
RemoveExistingSubscriptions, iterates through the fields contained in the MyForm
instance, and determines whether each field has anything to do with events
exposed by the same instance. All subscriptions are carefully pushed to the
queue and then removed. Second method, named RestoreOriginalSubscriptions, reads
items from the queue and subscribes all previously removed handlers back to
their original EventHandler delegates.
Now take a look at the MyFormAuditor's constructor, which is now changed. Before
subscribing to the events of interest, it first invokes
RemoveExistingSubscriptions method to clear the target object of all existing
event subscriptions. Once done, event handlers for the three events are
registered, and only after that RestoreOriginalSubscriptions method is invoked
so that all original subscriptions can be returned into place, but after
auditor's handlers in all invocation lists.
When program is started again, we can see that auditor has been first to know
about all events raised by the MyForm instance. Output of the program now looks
as expected:
Audit Shown.
Shown handled.
Audit CustomEvent.
CustomEvent handled.
Audit TextChanged.
TextChanged handled.
Conclusion
In this article we have demonstrated how application can force specific event
handlers to be subscribed as first in event's invocation list, regardless of any
other objects that may have subscribed to same events before.
Prerequisites for this method to be functional is that target object implements
events only in default manner, or using EventHandlerList class in cooperation
with System.Object fields that contain keys to the list. If these conditions are
met, then specific event handlers may be registered as event auditors and order
of their invocations is guaranteed to be the same as the order of events being
raised by the target objects.
This text continues in the next article titled "General Event Handling in .NET" (http://www.c-sharpcorner.com/UploadFile/b81385/8798/), which uses all knowledge about CLR event handling obtained so far to design a completely general event handler capable to subscribe to events with signatures unknown at compile time.