C# Dynamic Event Subscription
您将如何动态订阅C#事件,以便在给定对象实例和包含事件名称的字符串名称的情况下,订阅该事件并在触发该事件时执行某些操作(例如,写入控制台)?
似乎无法使用Reflection,并且我想避免不得不使用Reflection.Emit,因为目前(对我而言)这似乎是唯一的方法。
/ EDIT:我不知道事件所需的委托人的签名,这是问题的核心
/ EDIT 2:尽管委派矛盾似乎是一个好计划,但我无法做出使用此解决方案所必需的假设
您可以编译表达式树以使用不带任何参数的void方法作为任何类型事件的事件处理程序。为了适应其他事件处理程序类型,您必须以某种方式将事件处理程序的参数映射到事件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
| using System;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
class ExampleEventArgs : EventArgs
{
public int IntArg {get; set;}
}
class EventRaiser
{
public event EventHandler SomethingHappened;
public event EventHandler<ExampleEventArgs> SomethingHappenedWithArg;
public void RaiseEvents()
{
if (SomethingHappened!=null) SomethingHappened(this, EventArgs.Empty);
if (SomethingHappenedWithArg!=null)
{
SomethingHappenedWithArg(this, new ExampleEventArgs{IntArg = 5});
}
}
}
class Handler
{
public void HandleEvent() { Console.WriteLine("Handler.HandleEvent() called.");}
public void HandleEventWithArg(int arg) { Console.WriteLine("Arg: {0}",arg); }
}
static class EventProxy
{
//void delegates with no parameters
static public Delegate Create(EventInfo evt, Action d)
{
var handlerType = evt.EventHandlerType;
var eventParams = handlerType.GetMethod("Invoke").GetParameters();
//lambda: (object x0, EventArgs x1) => d()
var parameters = eventParams.Select(p=>Expression.Parameter(p.ParameterType,"x"));
var body = Expression.Call(Expression.Constant(d),d.GetType().GetMethod("Invoke"));
var lambda = Expression.Lambda(body,parameters.ToArray());
return Delegate.CreateDelegate(handlerType, lambda.Compile(),"Invoke", false);
}
//void delegate with one parameter
static public Delegate Create< T >(EventInfo evt, Action< T > d)
{
var handlerType = evt.EventHandlerType;
var eventParams = handlerType.GetMethod("Invoke").GetParameters();
//lambda: (object x0, ExampleEventArgs x1) => d(x1.IntArg)
var parameters = eventParams.Select(p=>Expression.Parameter(p.ParameterType,"x")).ToArray();
var arg = getArgExpression(parameters[1], typeof(T));
var body = Expression.Call(Expression.Constant(d),d.GetType().GetMethod("Invoke"), arg);
var lambda = Expression.Lambda(body,parameters);
return Delegate.CreateDelegate(handlerType, lambda.Compile(),"Invoke", false);
}
//returns an expression that represents an argument to be passed to the delegate
static Expression getArgExpression(ParameterExpression eventArgs, Type handlerArgType)
{
if (eventArgs.Type==typeof(ExampleEventArgs) && handlerArgType==typeof(int))
{
//"x1.IntArg"
var memberInfo = eventArgs.Type.GetMember("IntArg")[0];
return Expression.MakeMemberAccess(eventArgs,memberInfo);
}
throw new NotSupportedException(eventArgs+"->"+handlerArgType);
}
}
static class Test
{
public static void Main()
{
var raiser = new EventRaiser();
var handler = new Handler();
//void delegate with no parameters
string eventName ="SomethingHappened";
var eventinfo = raiser.GetType().GetEvent(eventName);
eventinfo.AddEventHandler(raiser,EventProxy.Create(eventinfo,handler.HandleEvent));
//void delegate with one parameter
string eventName2 ="SomethingHappenedWithArg";
var eventInfo2 = raiser.GetType().GetEvent(eventName2);
eventInfo2.AddEventHandler(raiser,EventProxy.Create<int>(eventInfo2,handler.HandleEventWithArg));
//or even just:
eventinfo.AddEventHandler(raiser,EventProxy.Create(eventinfo,()=>Console.WriteLine("!")));
eventInfo2.AddEventHandler(raiser,EventProxy.Create<int>(eventInfo2,i=>Console.WriteLine(i+"!")));
raiser.RaiseEvents();
}
} |
这不是一个完全通用的解决方案,但是如果您所有的事件都是以下形式
void Foo(object o,T args),其中T是从EventArgs派生的,则可以使用委托自变量来摆脱它。像这样(其中KeyDown的签名与Click的签名不同):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| public Form1()
{
Button b = new Button();
TextBox tb = new TextBox();
this.Controls.Add(b);
this.Controls.Add(tb);
WireUp(b,"Click","Clickbutton");
WireUp(tb,"KeyDown","Clickbutton");
}
void WireUp(object o, string eventname, string methodname)
{
EventInfo ei = o.GetType().GetEvent(eventname);
MethodInfo mi = this.GetType().GetMethod(methodname, BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic);
Delegate del = Delegate.CreateDelegate(ei.EventHandlerType, this, mi);
ei.AddEventHandler(o, del);
}
void Clickbutton(object sender, System.EventArgs e)
{
MessageBox.Show("hello!");
} |
可以使用反射订阅事件
1 2
| var o = new SomeObjectWithEvent;
o.GetType().GetEvent("SomeEvent").AddEventHandler(...); |
http://msdn.microsoft.com/zh-CN/library/system.reflection.eventinfo.addeventhandler.aspx
现在这将是您必须解决的问题。每个事件处理程序所需的委托将具有不同的签名。您将不得不寻找动态创建这些方法的途径,这可能意味着Reflection.Emit,或者您必须将自己限制为某个委托,以便可以使用已编译的代码来处理它。
希望这可以帮助。
尝试使用LinFu-它具有通用事件处理程序,可让您在运行时绑定到任何事件。例如,在这里您可以将处理程序绑定到动态按钮的Click事件:
1 2 3 4 5 6 7 8 9 10 11
| // Note: The CustomDelegate signature is defined as:
// public delegate object CustomDelegate(params object[] args);
CustomDelegate handler = delegate
{
Console.WriteLine("Button Clicked!");
return null;
};
Button myButton = new Button();
// Connect the handler to the event
EventBinder.BindToEvent("Click", myButton, handler); |
LinFu允许您将处理程序绑定到任何事件,无论委托签名如何。请享用!
你可以在这里找到它:
http://www.codeproject.com/KB/cs/LinFuPart3.aspx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public TestForm()
{
Button b = new Button();
this.Controls.Add(b);
MethodInfo method = typeof(TestForm).GetMethod("Clickbutton",
BindingFlags.NonPublic | BindingFlags.Instance);
Type type = typeof(EventHandler);
Delegate handler = Delegate.CreateDelegate(type, this, method);
EventInfo eventInfo = cbo.GetType().GetEvent("Click");
eventInfo.AddEventHandler(b, handler);
}
void Clickbutton(object sender, System.EventArgs e)
{
// Code here
} |
我最近写了一系列博客文章,描述了单元测试事件,而我讨论的一种技术描述了动态事件订阅。我在动态方面使用了反射和MSIL(代码发出),但是这些都很好地包装了起来。使用DynamicEvent类,可以像下面这样动态地订阅事件:
1 2 3 4 5 6 7 8 9
| EventPublisher publisher = new EventPublisher();
foreach (EventInfo eventInfo in publisher.GetType().GetEvents())
{
DynamicEvent.Subscribe(eventInfo, publisher, (sender, e, eventName) =>
{
Console.WriteLine("Event raised:" + eventName);
});
} |
我实现的模式的功能之一是将事件名称注入事件处理程序的调用中,以便您知道引发了哪个事件。对于单元测试非常有用。
该博客文章相当长,因为它描述了事件单元测试技术,但是提供了完整的源代码和测试,并且在上一篇文章中详细介绍了如何实现动态事件订阅。
http://gojisoft.com/blog/2010/04/22/event-sequence-unit-testing-part-1/
此方法在事件中添加了动态处理程序,该处理程序调用方法OnRaised,并将事件参数作为对象数组传递:
1 2 3 4 5 6 7 8 9 10 11 12
| void Subscribe(object source, EventInfo ev)
{
var eventParams = ev.EventHandlerType.GetMethod("Invoke").GetParameters().Select(p => Expression.Parameter(p.ParameterType)).ToArray();
var eventHandler = Expression.Lambda(ev.EventHandlerType,
Expression.Call(
instance: Expression.Constant(this),
method: typeof(EventSubscriber).GetMethod(nameof(OnRaised), BindingFlags.NonPublic | BindingFlags.Instance),
arg0: Expression.Constant(ev.Name),
arg1: Expression.NewArrayInit(typeof(object), eventParams.Select(p => Expression.Convert(p, typeof(object))))),
eventParams);
ev.AddEventHandler(source, eventHandler.Compile());
} |
OnRaised具有此签名:
1
| void OnRaised(string name, object[] parameters); |
您可以使用依赖注入来实现。例如,Microsoft Composite UI应用程序块完全符合您的描述
您的意思是:
1 2 3 4 5 6 7
| //reflect out the method to fire as a delegate
EventHandler eventDelegate =
( EventHandler ) Delegate.CreateDelegate(
typeof( EventHandler ), //type of event delegate
objectWithEventSubscriber, //instance of the object with the matching method
eventSubscriberMethodName, //the name of the method
true ); |
这不会进行订阅,但会提供调用方法。
编辑:
此答案后澄清了帖子,如果您不知道类型,我的示例将无济于事。
但是,.Net中的所有事件都应遵循默认的事件模式,因此只要您遵循它,它将与基本EventHandler一起使用。
|