另一个通用事件处理器






4.94/5 (42投票s)
一个与众不同的通用事件处理器:无需 MSIL。
引言
如果您想记录任何对象生成的每一个事件,您会怎么做?本文描述了我认为是一种新的(即使只是略有不同)方法,允许一个通用点来处理任何事件。
为什么?
您为什么要从一个点处理多个事件?原因很多,我的原因是我正在编写一个应用程序,用于测试某些位置的 3G 和 4G 无线移动网络的场强。
三个独立的模块生成事件,指示位置变化(来自 GPS)、无线场强变化(来自 3G/4G 调制解调器)以及数据下载速率变化(来自另一个反复下载同一文件的类)。
需要一个单一的点来整合每个源的数据,并将其记录在公共数据结构中。由于实际编程的性质,每个模块之前都已出于不同目的开发,而我实现应用程序的时间非常有限,因此重写每个模块以使用通用的事件结构是不切实际的。
背景
我必须以最诚挚的敬意开始,向以下作为灵感的文章致敬:commoneventhandler.aspx。这篇文章介绍了一种无需事先知道事件签名即可附加到任何事件的出色方法。
我创建自己的方法和后续文章的原因是,上述方法并不*完全*符合我的目的,而且我相信我已经为这个问题提出了一个可能(即使只是略有不同)的新解决方案。
问题的核心:我的代码中的一个点如何能够处理来自任何类的事件?
主要问题是,虽然微软试图通过 (Object sender, EventArgs e)
的签名来为所有事件处理器建立一个通用基础 - 这绝非强制性。要将委托绑定到事件,*无论如何*,委托的 Invoke
方法的参数必须与事件处理器委托的参数匹配。
还会出现另一个问题 - 假设您确实成功地将多个事件绑定到同一个处理器(以记录或重定向这些事件)- 事件处理器不一定包含关于该事件的任何信息。在正常实践中,您应该确切地知道正在引发哪个事件,因为您在每个处理器中只处理一个事件。然而,使用通用事件意味着一个事件处理器正在处理多个事件和事件源,如果您知道那个事件是什么,那将非常有帮助。
事件处理器的方法签名必须与事件处理器委托匹配,因此在方法签名中没有包含事件信息的余地(除非您可以规定事件处理器的签名,而我们不能)。
此外,我仔细研究了反射,虽然似乎有一种方法可以获取调用方法的签名,但没有功能可以找到调用事件。
解决方案最终是在其自己的类上放置一个事件处理器方法,并将事件信息设置为该类的属性。
这是“EventHandler
”标准委托的事件路由类的默认实现。
/// <summary>
/// default implementation of InternalEventRouter for standard events.
/// The signature of the HandleEvent method matches that of the
/// EventHandler delegate.
/// </summary>
public class DefaultEventRouter : InternalEventRouter
{
/// <summary>
/// construct and pass the event-info to the base constructor.
/// </summary>
/// <param name="info"></param>
public DefaultEventRouter(EventInfo info)
: base(info)
{
}
/// <summary>
/// handles EventHandler type events:
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void HandleEvent(object sender, EventArgs e)
{
// capture the event parameters:
Object[] args = new Object[2];
args[0] = sender;
args[1] = e;
// submit the event data to the common event broker.
CommonEventBroker.SubmitEvent(sender, Info, args);
}
}
事件信息通过构造函数传递给类。HandleEvent
方法现在可以将该事件信息结构与事件的发送者以及包含参数的对象数组一起传递给通用事件代理。
// submit the event data to the common event broker.
CommonEventBroker.SubmitEvent(sender, Info, args);
通过为每个事件生成该类的新实例,然后让该类的事件处理器方法引发一个通用事件,传入事件信息和源事件的参数,我们就能在通用事件处理器中区分事件类型。
此时您可能会想,是的,这很棒,但它并不那么通用,对吧?
您说得对。是时候解决问题的下一部分了:如何让一个事件处理器方法绑定到任何事件?
其他解决方案似乎都回到了使用 MSIL 和 Reflection.Emit
在运行时生成事件处理器签名。
实际上,这会为每种类型的事件编写一个新的类和方法来处理。我遇到的问题是,为这些事件处理器生成的 MSIL 完全难以理解 - 它只比汇编语言低一步。
我不仅不想直接处理 IL(它从未 intended be human coded),我也不想给未来可能处理我代码的程序员带来负担,让他们不得不阅读和理解那些东西。
如果事件处理器可以在运行时用 C# 编写,并在需要时编译并附加到事件,那将很棒。只需要调整方法签名、类名和几行代码,使其与通用模板不同。
事实证明这不止是可能的。System.CodeDom
命名空间中的 CodeDomProvider
拥有该方法
CompilerResults CompileAssemblyFromSource(CompilerParameters options, params string[] sources);
这会从源代码生成一个新的程序集,并授予访问程序集中类型的权限。您甚至可以告诉该过程生成一个仅在内存中的程序集。
现在我只需要能够以编程方式编写一个事件处理器类。
我提出了以下模板
// define the class template: spots are marked for replacement (ie %NAME%)
string baseCode = @"using System;
using System.Collections.Generic;
using System.Text;
using System.Reflection;
namespace Utility.Events
{
public class %NAME%EventRouter : InternalEventRouter
{
public %NAME%EventRouter(EventInfo info) : base(info)
{
}
public virtual void HandleEvent(%PARAMETERS%)
{
Object[] args = new Object[%PARAMCOUNT%];
%PARAMASSIGNMENT%
// submit the event to the broker
CommonEventBroker.SubmitEvent(sender, Info, args);
}
}
}";
我使用 %% 标记了需要调整代码的位置,并使用 EventInfo
结构体的 EventHandlerType
属性来构建替换的代码段。
然后将生成的代码编译到内存中的程序集中,并根据事件处理器类型存储在字典中。可以使用该类的每个使用相同事件处理器类型的事件的新实例。
绑定事件
/// <summary>
/// subscribes the specified event from the specified object to the universal event broker.
/// when the event is raised, it will be rerouted to the CommonEventBroker.CommonEvent
/// </summary>
/// <param name="obj"></param>
/// <param name="eventName"></param>
public static void Subscribe(object obj, string eventName)
{
// get the event-info:
EventInfo info = obj.GetType().GetEvent(eventName);
// get the event-handler:
var eventHandler = CreateEventHandler(info);
// attach the event handler to the source object:
eventHandler.AttachToEventOn(obj);
}
CommonEventBroker
类的 Subscribe
方法订阅该类上命名的一个事件。
从那时起,每当引发该事件时,CommonEventBroker
类中的通用事件也会被引发。通用事件将具有与之关联的事件源、事件信息和参数。
这是通用事件的委托
/// <summary>
/// delegate for a universal event handler. passes the source object,
/// the event-info, and all the parameters of the event.
/// </summary>
/// <param name="source"></param>
/// <param name="eventInfo"></param>
/// <param name="arguments"></param>
public delegate void CommonEventHandler(object source, EventInfo eventInfo,
CommonEventParameter[] parameters);
Using the Code
在示例项目中,“Test”窗体上托管了一个浏览器控件,窗体和浏览器上的所有事件都由通用事件代理处理,代码如下:
CommonEventBroker.CommonEvent += new CommonEventHandler(CommonEventBroker_CommonEvent);
CommonEventBroker.SubscribeAll(this);
CommonEventBroker.SubscribeAll(this.webBrowser1);
通用事件的处理器只是写出发送者、事件名称和事件签名。
void CommonEventBroker_CommonEvent(object source,
System.Reflection.EventInfo eventInfo, CommonEventParameter[] parameters)
{
Console.Write(source.ToString() + " fired: " + eventInfo.Name + " (");
foreach (var p in parameters)
Console.Write(p.ParameterType.Name + " " + p.Name + " ");
Console.WriteLine(")");
}
关注点
当任何类引发事件时,引发事件的线程也必须执行事件处理器方法。因此,附加复杂的事件处理器可能会引入延迟,或导致应用程序无响应。如果您使用单个事件处理器记录大量事件,这个问题会加剧。
为了解决这个问题,我正在使用一个单独的线程来处理事件。CommonEventBroker
的 SubmitEvent
方法将事件详细信息添加到 FIFO 队列,设置一个 AutoResetEvent
,然后将控制权返回给事件源。消费者线程由 AutoResetEvent
信号,然后从队列中取出事件数据并引发 CommonEvent
。通用事件处理器中的代码然后由消费者线程执行,而不是引发事件的线程。这解耦了事件及其处理,意味着您可以为事件编写非常繁重的处理代码,而不会牺牲响应能力。
我可以做得更好的地方
我敢肯定还有很多,我没有时间全部使用最佳实践。主要是,CommonEventHandler
委托应该只使用两个参数,第二个参数应该是 EventArgs
的一个派生类。
我还将 CommonEventBroker
设置为静态。这限制您只有一个通用事件。我本可以使用单例模式代替。
最新更新
首先,感谢那些对本文发表了积极评论的人。我希望它有所帮助。
我对代码进行了一些升级,这些升级可能会提供更高的可用性。主要是,我已经将事件代理从一个名为 CommonEventBroker
的静态类更改为一个名为 EventBroker
的实例类。该类现在有一个名为 CommonEventBroker
的静态属性,非常类似于单例模式,但与单例不同的是,构造函数仍然是公共的,因此您可以创建该类的实例。我认为这提供了两全其美:有一个真正通用的静态事件代理实例,以及特定的实例。
测试窗体已更新,以利用其自身的事件代理实例。
此外,我还将通用事件委托的签名更改为匹配最佳实践,它现在有两个参数:第一个是发送者,第二个是 EventArgs
的一个派生类。
我还将事件代理类设置为实现 IDisposable
接口,该接口将在 Dispose()
方法中停止事件处理线程。
以下代码是更新后的 Test
窗体
public partial class Test : Form
{
/// <summary>
/// instance event broker for this form.
/// </summary>
private EventBroker myEventBroker = new EventBroker();
public Test()
{
// initialise designer components
InitializeComponent();
// attach to the common event:
myEventBroker.CommonEvent +=
new CommonEventHandler(myEventBroker_CommonEvent);
// subscribe to this form and the web-browser control's events.
// note: the web-browser offers several un-subscribable events.
myEventBroker.SubscribeAll(this);
myEventBroker.SubscribeAll(this.webBrowser1);
// handle the form-closed event:
this.FormClosed += new FormClosedEventHandler(Test_FormClosed);
}
void myEventBroker_CommonEvent(object source, CommonEventArgs e)
{
// write out the event signature:
Console.WriteLine(source.ToString() + " fired: " + e.ToString());
}
void Test_FormClosed(object sender, FormClosedEventArgs e)
{
// stop the event broker.
myEventBroker.Stop();
}
private void btnGo_Click(object sender, EventArgs e)
{
// navigate to the selected url:
this.webBrowser1.Navigate(this.txtAddress.Text);
}
}