Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / XML

Event Logging in Cx

4.86/5 (8 votes)
30 Sep 2009CPOL7 min read 35.3K   286  
Adding an event logger to Cx.

Image 1

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:

C#
[CxEvent] public event CxCharDlgt KeypadEvent;
C#
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:

C#
KeypadEvent(this, new CxEventArgs<char>(c));

Using EventHelpers.CreateEvent

An alternate form can also be used:

C#
[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:

C#
internal class ObjectEventHelper : EventHelper
{
  /// <summary>
  /// Events have to be implemented in our own class because
  /// events cannot be fired from anywhere but our class.
  /// </summary>
  // Too bad we can't use generics here for the delegate type.
  [CxEvent]
  public event CxObjectDlgt Event;
  ...

  /// <summary>
  /// The source (object's) event is handled here,
  /// and fires the Cx event, acquiring the value via
  /// reflection from the PropertyInfo instance.
  /// </summary>
  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:

C#
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:

C#
[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:

C#
[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:

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.

C#
protected void WireUp(string producer, string consumer)
{
  object producerTarget = GetProducerTarget(producer);
  object source = producerTarget;
  object consumerTarget = GetConsumerComponent(consumer).Instance;
  EventInfo ei = GetEventInfo(producerTarget, producer);

  // We pass in the consumerTarget here, because the consumer.Type
  // is the "open generic"--meaning that T hasn't been defined yet,
  // and we need the closed generic which is only found
  // by getting the type of the specific consumer target.
  // Oddly, we don't have this issue with the producer,
  // though I did modify the code to pass in the producer target as well.
  MethodInfo mi = GetMethodInfo(consumerTarget, consumer);

  if (ei == null)
  {
    // Is this an event transformed from an EventHandler
    // to a CxEvent using the EventHelpers?
    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:

C#
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:

C#
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:

C#
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.

C#
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);

          // Don't log events to the logger!
          LogEvents = false;

          if (wireupInfo != null)
          {
            string log = wireupInfo.Producer + " , " + wireupInfo.Consumer;
            EventLogHelper.Helper.LogEvent(log);
          }
          else
          {
            // This event was wired up some other way.
            // Just display the source, target, and target method.
            // If we wanted to be really tricky, we could inspect
            // the stack frame to get the method that is invoking the event.
            string log = args[0].ToString() + " , " + sink.Target.ToString() + 
                         "." + sink.Method.ToString();
            EventLogHelper.Helper.LogEvent(log);
          }
        }
        catch
        {
          // ignore any exceptions the logger creates!
        }
        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:

C#
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:

C#
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:

C#
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.

C#
[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:

Image 2

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:

Image 3

And finally, wire up the LogEvent event to the logger's LogEvent consumer:

Image 4

The Logging Component

The logging component is a UserControl consisting of a DataGridView, a Label, and a Clear button. The implementation is straightforward:

C#
[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:

C#
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:

C#
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:

Image 5

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:

C#
// Check whether the event is being logged by Cx.
object[] attrs = ei.GetCustomAttributes(typeof(CxEventAttribute), false);

// Events created with the EventHelpers.CreateEvent method
// have the event decorated with the CxEvent attribute.
if ( (attrs.Length == 0) && (!isEventTransformation) )
{
  // This is a direct wireup of a system or third party event
  // to a handler, and we have no mechanism for logging the event.
  // So instead, we need to add the logger to the event chain.
  // We set the instance with the wireupInfo so we can easily log the event.
  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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)