65.9K
CodeProject 正在变化。 阅读更多。
Home

Cx 中的事件日志记录

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.86/5 (8投票s)

2009年9月30日

CPOL

7分钟阅读

viewsIcon

35779

downloadIcon

303

在 Cx 中添加事件记录器。

引言

这是 Cx 系列的第三部分,其中描述了如何向 Cx 框架添加事件记录器。Cx 中的事件日志记录是一个简单的实现,在各种选项(例如日志记录到 log4net)中,我选择了一个无模式窗体和一个 `DataGridView` 来记录事件。

回顾:事件在 Cx 中是如何工作的?

首先回顾事件在 Cx 中是如何声明和连接的可能很有用。

在组件中声明事件(生产者)

Cx 使用生产者和消费者组件之间的事件来通信状态和数据更改、用户操作等。

作为事件

事件可以在代码中声明为典型的 .NET 事件

[CxEvent] public event CxCharDlgt KeypadEvent;
protected virtual void RaiseKeypadEvent(char c)
{
  EventHelpers.Fire(KeypadEvent, this, new CxEventArgs<char>(c));
}

在上面的示例中,请注意事件实际上是通过 `EventHelpers` 静态类触发的。如果您如上声明事件,则只有在使用 `EventHelpers.Fire` 方法而不是典型的实现时,事件才会被记录。

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

使用 EventHelpers.CreateEvent

也可以使用另一种形式

[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);
  }

在上面的示例中,事件实际上存在于支持泛型类型(在本例中为 "object")的几个类之一中。Cx 目前支持以下泛型参数类型

  • bool
  • 字符串
  • CxStringPair
  • CxObjectState
  • IEnumerable
  • object

在内部,会实例化一个实现所需签名的委托的特定类。在上面的示例中,它将是这个类

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));
  }

注意 `CxObjectDlgt` 被定义为

public delegate void CxObjectDlgt(object sender, CxEventArgs<object> args);

它会传递在 `EventHelpers.CreateEvent` 方法调用中指定的泛型类型。

处理源实例事件的 `CommonHandler` 调用 `EventHelpers.Fire`,类似于第一个示例中使用的机制。

使用 EventHelpers.Transform

最后,事件可以是 .NET 事件的转换

[CxExplicitEvent("TextSet")]
public partial class CxTextBox : UserControl, ICxVisualComponentClass
{

  public CxTextBox()
  {
    InitializeComponent();
    EventHelpers.Transform(this, tbText, "LostFocus", "Text").To("TextSet");
  }
}

消费者当然必须匹配事件签名

[CxConsumer]
public void OnNameSet(object sender, CxEventArgs<string> args)
{
  Name = args.Data;
}

`EventHelpers.Transform` 方法根据属性类型创建辅助方法,如前面的示例所示,在上面的示例中属性类型是 "Text"。Cx 支持非常有限的属性类型(可以轻松扩展)

  • bool
  • 字符串
  • IEnumerable
  • object

连接事件

生产者与消费者之间的连接在 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>

在不编辑 XML 的情况下完成此连接是我在上一篇文章中介绍的 Cx Designer 的目的之一。

执行连接的代码使用反射来构造 `EventInfo` 和 `MethodInfo` 对象,这些对象对于将消费者连接到生产者事件是必需的。

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;
}

接下来我将解释关于 `WireupInfo` 类的最后三行代码的目的。

记录事件

保留用于连接事件的事件元数据

在上面的代码中,注意最后一行

eventWireupMap[new ProducerEventInfo(source, consumerTarget, mi)] = wireupInfo;

源实例、目标实例和 `MethodInfo` 结构构成了一个唯一的键,用于查找特定连接的生产者(事件源)和消费者(事件处理程序)的信息。通过此键,我们可以获取 `WireupInfo` 实例。此映射假设相同的生产者-消费者-方法元组是唯一的,这意味着生产者实例中的相同事件不会两次映射到同一消费者实例中的同一处理程序方法。

`ProducerEventInfo` 是一个结构(因此映射使用值类型作为键),它比较复合键中的三个字段

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();
  }
}

这可能不是 `CompareTo` 和 `GetHashCode` 的最佳实现,因此欢迎提出建议!

`WireupInfo` 类本身保留了元数据中指定的信息,这些信息将在日志记录期间使用

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);
  }
}

事件触发时获取元数据

我们显然不能仅仅触发事件。相反,我们必须遍历委托的调用列表。这实际上是一个好主意,因为我们希望调用调用列表中的所有方法,即使其中一些方法抛出异常,它还创建了异步调用这些方法的可能性。然而,它确实要求开发人员使用 `EventHelpers.Fire` 方法,传入委托,而不是直接调用委托,这会绕过所有日志记录。

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.");
    }
  }
}

在上面的代码中,这行

IWireupInfo wireupInfo = App.GetWireupInfo(args[0], del.Target, sink.Method);

检索与生产者-消费者-方法键关联的 `WireupInfo`,从中我们可以获取元数据中描述的生产者和消费者名称

string log = wireupInfo.Producer + " , " + wireupInfo.Consumer;
EventLogHelper.Helper.LogEvent(log);

如果连接信息不可用,那么我们假设使用了一些其他机制来连接事件,记录器会转而使用更通用的事件描述

string log = args[0].ToString() + " , " + sink.Target.ToString() + 
             "." + sink.Method.ToString();
EventLogHelper.Helper.LogEvent(log);

在这两种情况下,Cx 的消息传递机制(事件)都用于通知侦听组件记录事件。因此,在触发此事件时,事件日志记录会关闭。

EventLogHelper

此类被视为“业务”组件,并具有触发事件的方法。

[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);
  }
}

当您将 *Cx.EventArgs* 程序集添加到您的应用程序(在元数据中)时,您可以实例化此组件,并且它可以连接到执行实际日志记录的消费者。例如,在计算器应用程序中,我添加了一个我编写的用于查看事件的组件(本文开头的窗体)。在设计器中,我添加了 `CxEventLogger` 组件

另请注意,我将属性 `FloatingWindow` 指定为“true”,以便该窗口被视为无模式对话框。

我还在设计器中添加了 `EventLogHelper` 业务组件

最后,将 `LogEvent` 事件连接到记录器的 `LogEvent` 消费者

日志记录组件

日志记录组件是一个由 `DataGridView`、`Label` 和一个“清除”按钮组成的 `UserControl`。实现很简单

[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();
  }
}

问题:直接从 API 消费事件

当我们直接从 API(例如 .NET Framework)消费事件时会出现问题。例如,在计算器应用程序中,运算符按钮事件直接连接到消费者。这完全绕过了 Cx 事件记录器。现在,我真的很想能够记录所有这些事件,所以我决定,除了消费者之外,我还会附加一个事件处理程序,然后该处理程序将记录事件。我上面用 `WireupInfo` 类解决的同一个问题现在又出现了:事件处理程序没有关于触发事件的对象和消费事件的对象的上下文。

我选择了一个解决方案,该解决方案涉及在运行时编译一个事件消费者,在该消费者中我可以设置 `CxApp` 实例和 `WireupInfo` 实例。事件处理程序对 Cx 执行回调,将 `WireupInfo` 实例传递给 Cx,然后 Cx 记录事件。我对这个解决方案感到喜忧参半,因为虽然很巧妙,但我不喜欢运行时编译。无论如何,这是生成运行时程序集的类

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;
    }
  }
}

日志记录然后变得微不足道

public void GEL(object data)
{
  WireupInfo wireupInfo = (WireupInfo)data;
  EventHelpers.LogEvents = false;
  EventLogHelper.Helper.LogEvent(wireupInfo.Producer, 
                 wireupInfo.Consumer, String.Empty);
  EventHelpers.LogEvents = true;
}

事实上,这非常容易,我几乎会考虑使用这种方法,即使是 Cx 可以记录的连接,除非这会导致创建数百个运行时程序集,这是我非常非常不愿意做的。

结果是 Cx 现在可以记录直接连接的事件,这些事件不通过 Cx 事件助手。如屏幕截图所示,现在记录了加法和等于运算符

不过,还有一个最后的问题:Cx 如何知道使用这种机制来加载事件?答案是通过检查事件是否具有 `CxEvent` 属性或是否是转换事件。如果两者都不是,则事件不通过 Cx 事件机制。是的,这意味着如果您(开发人员)正在使用本文开头描述的第一种方法来定义事件,则必须指定 `CxEvent` 属性 *并* 使用 `EventHelpers.Fire` 机制。或者,您可以省略 `CxEvent` 属性 *并* 使用执行事件的常用形式。Cx 执行以下测试

// 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);
}

结论

我对向 Cx 添加事件记录器如此困难感到惊讶。尽管我控制着连接,但很快就显而易见,当事件触发时,所有关于连接的上下文都丢失了。这是不幸的,需要两种方法:跟踪足够的信息以获取我需要的信息,并在运行时编译专门处理不通过 Cx 事件触发机制的事件的代码。后一种方法可能对希望将某种监视附加到现有事件的人有用。

© . All rights reserved.