32

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

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

/编辑:我不知道事件所需的代表签名,这是问题的核心

/ EDIT 2:虽然委托逆变似乎是一个好计划,但我无法做出使用此解决方案所必需的假设

4

9 回答 9

29

您可以编译表达式树以使用不带任何参数的 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();
     }
 }
于 2008-09-05T14:14:37.080 回答
9

这不是一个完全通用的解决方案,但如果您的所有事件都是 void Foo(object o, T args) 的形式,其中 T 派生自 EventArgs,那么您可以使用委托逆变来摆脱它。像这样(KeyDown的签名与Click的签名不同):

    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!");
    }
于 2008-09-05T14:25:09.113 回答
3

可以使用反射订阅事件

var o = new SomeObjectWithEvent;
o.GetType().GetEvent("SomeEvent").AddEventHandler(...);

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

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

希望这可以帮助。

于 2008-09-05T13:23:01.370 回答
2
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
}
于 2008-09-05T13:25:27.430 回答
2

试试 LinFu——它有一个通用的事件处理程序,可以让您在运行时绑定到任何事件。例如,您可以将处理程序绑定到动态按钮的 Click 事件:

// 注意:CustomDelegate 签名定义为:
// 公共委托对象 CustomDelegate(params object[] args);
CustomDelegate 处理程序 = 委托
                         {
                           Console.WriteLine("按钮点击了!");
                           返回空值;
                         };

按钮 myButton = new Button();
// 将处理程序连接到事件
EventBinder.BindToEvent("Click", myButton, handler);

LinFu 允许您将处理程序绑定到任何事件,而不管委托签名如何。享受!

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

于 2008-12-18T05:16:40.797 回答
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/

于 2010-04-22T18:25:05.783 回答
0

你想要的可以使用依赖注入来实现。例如,Microsoft Composite UI 应用程序块完全符合您的描述

于 2008-09-05T13:21:21.700 回答
0

此方法添加到事件,即调用方法的动态处理程序,OnRaised将事件参数作为对象数组传递:

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有这个签名:

void OnRaised(string name, object[] parameters);
于 2018-04-11T17:09:32.780 回答
-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 一起使用。

于 2008-09-05T13:25:10.413 回答