Introduction
This is the third installment in the Cx series, in which is described how an event logger is added to the Cx framework. Event logging in Cx is a straightforward implementation, and from the various options (such as logging to log4net), I chose a modeless form and a DataGridView
to which to log the events.
Review: How Do Events Work in Cx?
It might be useful to first review how events are declared and wired up in Cx.
Declaring Events (Producers) in a Component
Cx uses events between producer and consumer components to communicate state and data changes, user actions, and so forth.
As An Event
Events can be declared in code as a typical .NET event:
[CxEvent] public event CxCharDlgt KeypadEvent;
protected virtual void RaiseKeypadEvent(char c)
{
EventHelpers.Fire(KeypadEvent, this, new CxEventArgs<char>(c));
}
In the above example, notice that the event is actually fired through the EventHelpers
static class. If you declare an event as above, the event will be logged only if you use the EventHelpers.Fire
method, rather than the typical implementation:
KeypadEvent(this, new CxEventArgs<char>(c));
Using EventHelpers.CreateEvent
An alternate form can also be used:
[CxExplicitEvent("ItemSelected")]
public class SomeClass
{
protected EventHelper itemSelected;
public SomeClass()
{
itemSelected = EventHelpers.CreateEvent<object>(this, "ItemSelected");
}
protected void OnSelectionChanged(object sender, System.EventArgs e)
{
itemSelected.Fire(bsData.Current);
}
In the above example, the event actually lives in one of several classes that support the generic type (in this case, "object
"). Cx currently supports the following generic argument types:
bool
string
CxStringPair
CxObjectState
IEnumerable
object
Internally, a specific class is instantiated that implements a delegate with the desired signature. In the above example, it would be this class:
internal class ObjectEventHelper : EventHelper
{
[CxEvent]
public event CxObjectDlgt Event;
...
protected void CommonHandler(object sender, System.EventArgs e)
{
object val = PropertyInfo.GetValue(Object, null);
EventHelpers.Fire(Event, Component, new CxEventArgs<object>(val));
}
Notice that a CxObjectDlgt
is defined as:
public delegate void CxObjectDlgt(object sender, CxEventArgs<object> args);
which carries forward the generic type that was specified in the EventHelpers.CreateEvent
method call.
The CommonHandler
that handles the source instance event makes a call to EventHelpers.Fire
, similar to the mechanism used in the first example.
Using EventHelpers.Transform
Lastly, an event can be a transformation of a .NET event:
[CxExplicitEvent("TextSet")]
public partial class CxTextBox : UserControl, ICxVisualComponentClass
{
public CxTextBox()
{
InitializeComponent();
EventHelpers.Transform(this, tbText, "LostFocus", "Text").To("TextSet");
}
}
Consumers must, of course, match the event signature:
[CxConsumer]
public void OnNameSet(object sender, CxEventArgs<string> args)
{
Name = args.Data;
}
The EventHelpers.Transform
method creates helper methods as in the previous example, based on the property type, which in the above example is "Text". Cx supports a very limited number of property types (which can be easily extended):
bool
string
IEnumerable
object
Wiring Up Events
The wire-up of producers to their consumers is declared in XML:
<WireUps>
<WireUp Producer="App.Initialize"
Consumer="CxDesigner.InitializeDesigner" />
<WireUp Producer="CxDesigner.RequestLoadComponents"
Consumer="DesignerDataService.OnLoadComponents" />
<WireUp Producer="DesignerDataService.ComponentListLoaded"
Consumer="CxDesigner.OnComponentsLoaded" />
<WireUp Producer="DesignerDataService.WireupsLoaded"
Consumer="CxDesigner.OnWireupsLoaded" />
...
</WireUps>
Doing this wire-up without editing the XML is one of the purposes of the Cx Designer that I presented in the previous article.
The code that performs the wire-up uses Reflection to construct the EventInfo
and MethodInfo
objects, which are necessary to wire up the consumer to the producer event.
protected void WireUp(string producer, string consumer)
{
object producerTarget = GetProducerTarget(producer);
object source = producerTarget;
object consumerTarget = GetConsumerComponent(consumer).Instance;
EventInfo ei = GetEventInfo(producerTarget, producer);
MethodInfo mi = GetMethodInfo(consumerTarget, consumer);
if (ei == null)
{
ei = TryEventTransformation(producer, producerTarget, out producerTarget);
}
Verify.IsNotNull(ei, "EventInfo did not initialize for wireup of " +
producer + " to " + consumer);
Verify.IsNotNull(mi, "MethodInfo did not initialize for wireup of " +
producer + " to " + consumer);
Type eventHandlerType = ei.EventHandlerType;
Delegate dlgt = Delegate.CreateDelegate(eventHandlerType, consumerTarget, mi);
ei.AddEventHandler(producerTarget, dlgt);
WireupInfo wireupInfo=
new WireupInfo(ei, producerTarget, consumerTarget,
dlgt, producer, consumer);
wireupList.Add(wireupInfo);
eventWireupMap[new ProducerEventInfo(source, consumerTarget, mi)] = wireupInfo;
}
I will explain next the purpose of the last three lines of code regarding the WireupInfo
class.
Logging Events
Preserving the Event Metadata Used to Wire-up the Event
In the above code, notice the last line:
eventWireupMap[new ProducerEventInfo(source, consumerTarget, mi)] = wireupInfo;
The source instance, target instance, and the MethodInfo
structure make up a unique key to look up the information on the producer (event source) and the consumer (event handler) for the specific wire-up. From this key, we can get the WireupInfo
instance. This mapping assumes that the same producer-consumer-method tuple is unique, meaning that the same event in a producer instance will not be mapped twice to the same handler method in the same consumer instance.
The ProducerEventInfo
is a structure (so the mapping uses a value type as the key) that compares the three fields in the composite key:
public struct ProducerEventInfo : IComparable
{
public object source;
public object target;
public MethodInfo methodInfo;
public ProducerEventInfo(object source, object target,
MethodInfo methodInfo)
{
this.source = source;
this.target = target;
this.methodInfo = methodInfo;
}
public int CompareTo(object obj)
{
ProducerEventInfo pei = (ProducerEventInfo)obj;
int ret = 1;
if ((source == pei.source) && (target == pei.target) &&
(methodInfo == pei.methodInfo))
{
ret = 0;
}
return ret;
}
public override bool Equals(object obj)
{
return CompareTo(obj) == 0;
}
public override int GetHashCode()
{
return source.GetHashCode() | target.GetHashCode() |
methodInfo.GetHashCode();
}
}
This probably isn't the best implementation of CompareTo
and GetHashCode
, so suggestions are welcome!
The WireupInfo
class itself preserves the information specified in the metadata, which will be used during logging:
public class WireupInfo : IWireupInfo
{
public EventInfo EventInfo { get; protected set; }
public string Producer { get; protected set; }
public string Consumer { get; protected set; }
public object Source { get; protected set; }
public object Target { get; protected set; }
protected Delegate dlgt;
public WireupInfo(EventInfo eventInfo, object source, object target,
Delegate dlgt, string producer, string consumer)
{
this.EventInfo = eventInfo;
Source = source;
Target = target;
this.dlgt = dlgt;
Producer = producer;
Consumer = consumer;
}
public void Remove()
{
EventInfo.RemoveEventHandler(Target, dlgt);
}
}
Acquiring the Metadata When the Event Fires
We obviously can't just fire the event. Instead, we have to iterate through the delegate's invocation list. This is actually a good idea, because we want to call all the methods in the invocation list, even if some of them throw exceptions, and it also creates the possibility of calling these methods asynchronously. It does, however, require that the developer uses the EventHelpers.Fire
method, passing in the delegate, rather than invoking the delegate directly, which would bypass all of the logging.
public static void Fire(Delegate del, params object[] args)
{
if (del == null)
{
return;
}
List<Exception> exceptions = new List<Exception>();
Delegate[] delegates = del.GetInvocationList();
foreach (Delegate sink in delegates)
{
try
{
if (LogEvents)
{
try
{
IWireupInfo wireupInfo =
App.GetWireupInfo(args[0], del.Target, sink.Method);
LogEvents = false;
if (wireupInfo != null)
{
string log = wireupInfo.Producer + " , " + wireupInfo.Consumer;
EventLogHelper.Helper.LogEvent(log);
}
else
{
string log = args[0].ToString() + " , " + sink.Target.ToString() +
"." + sink.Method.ToString();
EventLogHelper.Helper.LogEvent(log);
}
}
catch
{
}
finally
{
LogEvents = true;
}
}
sink.DynamicInvoke(args);
}
catch (Exception e)
{
exceptions.Add(e);
}
}
if (exceptions.Count > 0)
{
if (ThrowExceptionOnEventException)
{
throw new CxException(exceptions, "Event exceptions have occurred.");
}
}
}
In the above code, this line:
IWireupInfo wireupInfo = App.GetWireupInfo(args[0], del.Target, sink.Method);
retrieves the WireupInfo
associated with the producer-consumer-method key, from which we can obtain the producer and consumer names as described in the metadata:
string log = wireupInfo.Producer + " , " + wireupInfo.Consumer;
EventLogHelper.Helper.LogEvent(log);
If the wire-up information is not available, then we assume that some other mechanism was used to wire-up the event, and the logger instead reverts to a more generic description of the event:
string log = args[0].ToString() + " , " + sink.Target.ToString() +
"." + sink.Method.ToString();
EventLogHelper.Helper.LogEvent(log);
In both cases, Cx's messaging mechanism (events) is used to inform a listening component to log the event. For this reason, event logging is turned off while firing this event.
The EventLogHelper
This class is treated as a "business" component, and has a method for firing an event.
[CxComponentName("EventLogHelper")]
[CxExplicitEvent("LogEvent")]
public class EventLogHelper : ICxBusinessComponentClass
{
public static EventLogHelper Helper;
protected EventHelper logEvent;
public EventLogHelper()
{
Helper = this;
logEvent = EventHelpers.CreateEvent<string>(this, "LogEvent");
}
public void LogEvent(string log)
{
logEvent.Fire(log);
}
}
When you add the Cx.EventArgs assembly to your application (in the metadata), you can instantiate this component and it can be wired up to a consumer that performs the actual logging. For example, in the calculator application, I added a component that I wrote for viewing events (the form at the beginning of this article). In the designer, I added the CxEventLogger
component:
Note also that I specified the property FloatingWindow
with the value "true", so that the window is treated as a modeless dialog.
I also, again in the designer, added the EventLogHelper
business component:
And finally, wire up the LogEvent
event to the logger's LogEvent
consumer:
The Logging Component
The logging component is a UserControl
consisting of a DataGridView
, a Label
, and a Clear button. The implementation is straightforward:
[CxComponentName("CxEventLogger")]
public partial class CxEventLogger : UserControl, ICxVisualComponentClass
{
protected bool floatingWindow;
protected Form modelessDlg;
protected DataTable dtLog;
[CxComponentProperty]
public bool FloatingWindow
{
get { return floatingWindow; }
set { floatingWindow = value; }
}
public CxEventLogger()
{
InitializeComponent();
dtLog = new DataTable();
dtLog.Columns.Add(new DataColumn("Timestamp"));
dtLog.Columns.Add(new DataColumn("Producer"));
dtLog.Columns.Add(new DataColumn("Consumer"));
dgvEventLog.DataSource = dtLog;
Dock = DockStyle.Fill;
btnClear.Click += new EventHandler(OnClear);
}
public void Register(object form, ICxVisualComponent component)
{
if (floatingWindow)
{
modelessDlg = new Form();
modelessDlg.Text = "Event Logger";
modelessDlg.Controls.Add(this);
modelessDlg.Size = new Size(425, 300);
modelessDlg.Show();
}
else
{
this.RegisterControl((Form)form, component);
}
}
[CxConsumer]
public void LogEvent(object sender, CxEventArgs<string> args)
{
string msg = args.Data;
string[] prodCons = msg.Split(',');
DataRow row = dtLog.NewRow();
row[0] = System.DateTime.Now.ToString("hh:mm:ss.fff");
row[1] = prodCons[0].Trim();
row[2] = prodCons[1].Trim();
dtLog.Rows.Add(row);
}
protected void OnClear(object sender, System.EventArgs e)
{
dtLog.Rows.Clear();
}
}
The Problem: Consuming Events Directly from an API
A problem arises when we consume events directly from an API, like the .NET Framework. For example, in the Calculator application, the operator button events are wired up directly to the consumer. This completely bypasses the Cx event logger. Now, I really want to be able to log all of these events, so I decided that, in addition to the consumer, I would attach an event handler that would then log the event. The same problem that I solved above with the WireupInfo
class now rears its ugly head again: the event handler has no context regarding the object that fired the event and the object that is consuming the event.
I opted for a solution that involves compiling, at runtime, an event consumer, in which I can set the CxApp
instance and the WireupInfo
instance. The event handler performs a callback to Cx, passing the WireupInfo
instance to Cx, which then logs the event. I have mixed emotions about this solution, because while nifty, I'm not thrilled with the runtime compilation. Regardless, here's the class that generates the runtime assembly:
namespace Cx.CodeDom
{
public static class CxGeneralEventLogger
{
static string sourceCode =
"using System;\r\n" +
"using Cx.Interfaces;\r\n" +
"namespace CxCodeDom\r\n" +
"{\r\n" +
"public class CxGeneralEventLogger : ICxGeneralEventLogger\r\n" +
"{\r\n" +
"protected object data;\r\n" +
"protected ICxApp app;\r\n" +
"public object Data {get {return data;} set {data=value;}}\r\n" +
"public ICxApp App {get {return app;} set {app=value;}}\r\n" +
"public void GeneralEventLogger(object sender, EventArgs e)\r\n" +
"{\r\n" +
"app.GEL(data);\r\n" +
"}\r\n" +
"}\r\n" +
"}\r\n";
public static ICxGeneralEventLogger GenerateAssembly()
{
CodeDomProvider cdp = CodeDomProvider.CreateProvider("CSharp");
CompilerParameters cp = new CompilerParameters();
cp.ReferencedAssemblies.Add("System.dll");
cp.ReferencedAssemblies.Add("Cx.Interfaces.dll");
cp.GenerateExecutable = false;
cp.GenerateInMemory = true;
cp.TreatWarningsAsErrors = false;
string[] sources = new string[] { sourceCode };
CompilerResults cr = cdp.CompileAssemblyFromSource(cp, sources);
ICxGeneralEventLogger gel = (ICxGeneralEventLogger)
cr.CompiledAssembly.CreateInstance("CxCodeDom.CxGeneralEventLogger");
return gel;
}
}
}
The logging is then trivial:
public void GEL(object data)
{
WireupInfo wireupInfo = (WireupInfo)data;
EventHelpers.LogEvents = false;
EventLogHelper.Helper.LogEvent(wireupInfo.Producer,
wireupInfo.Consumer, String.Empty);
EventHelpers.LogEvents = true;
}
In fact, this is so easy, I'd almost consider using this approach even for wire-ups that Cx can log, except that this would result in hundreds of runtime assemblies being created, something I'm very, very reluctant to do.
The result is that Cx can now log the events wired up directly, that do not go through the Cx event helper. As the screenshot illustrates, the Add and Equal operators are now logged:
There is one final question though: how does Cx know to use this mechanism for loading events? The answer is by checking whether the event has a CxEvent
attribute or is a transformation event. If it's not either, then the event is not going through the Cx eventing mechanism. Yes, this does mean that if you, the developer, are using the first approach described at the beginning of the article for defining events, you must specify the CxEvent
attribute and use the EventHelpers.Fire
mechanism. Or, you could omit the CxEvent
attribute and use the usual form for executing the event. Cx performs the following test:
object[] attrs = ei.GetCustomAttributes(typeof(CxEventAttribute), false);
if ( (attrs.Length == 0) && (!isEventTransformation) )
{
ICxGeneralEventLogger gel = Cx.CodeDom.CxGeneralEventLogger.GenerateAssembly();
gel.App = this;
gel.Data = wireupInfo;
MethodInfo listenerMethodInfo =
gel.GetType().GetMethod("GeneralEventLogger",
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
Delegate listenerDlgt =
Delegate.CreateDelegate(eventHandlerType, gel, listenerMethodInfo);
ei.AddEventHandler(producerTarget, listenerDlgt);
}
Conclusion
I was surprised how difficult it was to actually add an event logger to Cx. Even though I'm in control of the wire-up, it became obvious right away that all the context regarding the wire-up is lost by the time the event fires. This is unfortunate and required two approaches: tracking enough information to obtain the information I needed, and to compile, at run-time, code that specifically handles events that don't go through the Cx event fire mechanism. This latter approach might be useful to people that want to attach some sort of monitoring to existing events.