【问题标题】:C# Dynamic Event SubscriptionC# 动态事件订阅
【发布时间】:2010-09-07 22:18:01
【问题描述】:

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

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

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

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

【问题讨论】:

    标签: c# events reflection delegates


    【解决方案1】:

    您可以编译表达式树以使用不带任何参数的 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();
         }
     }
    

    【讨论】:

    • 见鬼,表达式树太酷了。我曾经通过 Reflection.Emit 写过类似的代码。多么痛苦。
    • 一段很棒的代码。你能不能展示如何改变它来支持论点?我更改了它以获取带有参数的方法,但是当我尝试创建委托时,我得到“从范围''引用的'System.String'类型的变量'x',但它没有定义”。谢谢
    • 完成——我添加了另一个示例。
    • 那么这对于 Eventhandler 的正常模式如何工作:(object sender, Eventargs e)?
    • 你必须添加一个重载,Delegate Create&lt;T1,T2&gt;(EventInfo evt, Action&lt;T1,T2&gt; d)
    【解决方案2】:

    这不是一个完全通用的解决方案,但如果你所有的事件都是形式 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!");
        }
    

    【讨论】:

      【解决方案3】:

      可以使用反射订阅事件

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

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

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

      希望这会有所帮助。

      【讨论】:

        【解决方案4】:
        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
        }
        

        【讨论】:

          【解决方案5】:

          试试 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

          【讨论】:

            【解决方案6】:

            我最近写了一系列描述单元测试事件的博客文章,我讨论的其中一个技术描述了动态事件订阅。我对动态方面使用了反射和 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/

            【讨论】:

            • @Robert Cheers 发表评论,我们昨天在修补我们的网络服务器后出现了一些中断。
            【解决方案7】:

            使用依赖注入可以实现您想要的。例如Microsoft Composite UI app block 完全按照您的描述进行

            【讨论】:

              【解决方案8】:

              该方法添加到事件中,动态处理程序调用方法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);
              

              【讨论】:

                【解决方案9】:

                你的意思是这样的:

                //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 一起使用。

                【讨论】:

                  猜你喜欢
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 2014-06-19
                  相关资源
                  最近更新 更多