2008-09-05 303 views
30

如何动态订阅C#事件,以便给定一个Object实例和一个包含事件名称的String名称,您订阅该事件并执行一些操作(例如写入控制台)该事件已被解雇?C#动态事件订阅

它似乎使用反射这是不可能的,我想避免必须使用Reflection.Emit,如果可能的话,因为目前(对我来说)似乎是唯一的方法。

/编辑:我不知道需要事件的委托的签名,这是问题的核心

/EDIT 2:虽然委托逆变似乎是个不错的计划,我不能作出使用此解决方案的必要假设

回答

28

您可以编译表达式树来使用void方法,而无需任何参数作为任何类型事件的事件处理程序。为了适应其他事件处理程序类型,您必须以某种方式将事件处理程序的参数映射到事件。

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

地狱,表情树太酷了。我通过Reflection.Emit写了一次类似的代码。多么痛苦。 – 2009-02-11 04:07:13

+0

伟大的一段代码。你能否展示如何改变它以支持参数?我改变了它以获得带有参数的方法,但是当我尝试创建委托时,我得到了'范围''引用的'System.String'类型的变量'x',但没有定义'。谢谢 – 2012-01-23 12:38:31

2

有可能使用反射

var o = new SomeObjectWithEvent; 
o.GetType().GetEvent("SomeEvent").AddEventHandler(...); 
订阅事件

http://msdn.microsoft.com/en-us/library/system.reflection.eventinfo.addeventhandler.aspx

现在,这里将是你将不得不解决的问题。每个事件处理程序所需的委托将具有不同的签名。您将不得不动态创建这些方法,这可能意味着Reflection.Emit,或者您将不得不将自己限制为某个委托,以便您可以使用编译代码处理它。

希望这会有所帮助。

-1

你的意思是这样的:

//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。

1
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 
} 
8

这不是一个完全通用的解决方案,但如果您的所有活动的形式是 无效美孚(对象o,T参数),其中T从EventArgs的派生,那么你可以使用委托逆变逃脱它。像这样的(其中的KeyDown的签名是不一样的点击的):

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

尝试李林甫 - 这是一个普遍的事件处理程序,可以让你在运行时绑定到任何事件。例如,这里是你可以处理程序绑定到一个动态按钮的Click事件:

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

李林甫,您可以绑定你处理任何事件,无论是委托签名的。请享用!

你可以在这里找到: http://www.codeproject.com/KB/cs/LinFuPart3.aspx

1

我最近写了一系列描述单元测试活动的博客文章,和我讨论技术之一,介绍动态事件订阅。我使用反射和MSIL(代码发射)的动态方面,但这一切都很好地包装起来。使用DynamicEvent类,事件可以订阅到动态,像这样:

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/