是否有可能从一个控件“窃取”一个事件处理程序并将其交给另一个控件?

时间:2008-11-15 20:18:24

标签: c# .net

我想做这样的事情:

Button btn1 = new Button();
btn1.Click += new EventHandler(btn1_Click);
Button btn2 = new Button();
// Take whatever event got assigned to btn1 and assign it to btn2.
btn2.Click += btn1.Click; // The compiler says no...

其中btn1_Click已在类中定义:

void btn1_Click(object sender, EventArgs e)
{
    //
}

当然不会编译(“事件'System.Windows.Forms.Control.Click'只能出现在+ =或 - =”的左侧)。有没有办法从一个控件获取事件处理程序并在运行时将其分配给另一个控件?如果那是不可能的,重复事件处理程序并在运行时将其分配给另一个控件吗?

有几点:我已经用一段时间搜索了这个问题,但却发现还没有办法做到这一点。大多数尝试过的方法都涉及反射,因此如果您阅读我的问题并认为答案非常明显,请首先尝试在Visual Studio中编译代码。或者,如果答案确实非常明显,请随时给我打电话。谢谢,我真的很期待看到这是否可能。

我知道我可以这样做:

btn2.Click += new EventHandler(btn1_Click);

这不是我在这里寻找的。

这也不是我想要的:

EventHandler handy = new EventHandler(btn1_Click);
Button btn1 = new Button();
btn1.Click += handy;
Button btn2 = new Button();
btn2.Click += handy;

4 个答案:

答案 0 :(得分:19)

是的,这在技术上是可行的。需要反思,因为许多成员都是私人和内部成员。启动一个新的Windows Forms项目并添加两个按钮。然后:

using System;
using System.ComponentModel;
using System.Windows.Forms;
using System.Reflection;

namespace WindowsFormsApplication1 {
  public partial class Form1 : Form {
    public Form1() {
      InitializeComponent();
      button1.Click += new EventHandler(button1_Click);
      // Get secret click event key
      FieldInfo eventClick = typeof(Control).GetField("EventClick", BindingFlags.NonPublic | BindingFlags.Static);
      object secret = eventClick.GetValue(null);
      // Retrieve the click event
      PropertyInfo eventsProp = typeof(Component).GetProperty("Events", BindingFlags.NonPublic | BindingFlags.Instance);
      EventHandlerList events = (EventHandlerList)eventsProp.GetValue(button1, null);
      Delegate click = events[secret];
      // Remove it from button1, add it to button2
      events.RemoveHandler(secret, click);
      events = (EventHandlerList)eventsProp.GetValue(button2, null);
      events.AddHandler(secret, click);
    }

    void button1_Click(object sender, EventArgs e) {
      MessageBox.Show("Yada");
    }
  }
}

如果这让您确信Microsoft非常努力阻止您这样做,那么您就能理解代码。

答案 1 :(得分:3)

不,你不能这样做。原因是封装 - 事件是只是订阅/取消订阅,即他们不会让你“偷看”以查看已订阅的处理程序。

可以做的是从Button派生的,并创建一个调用OnClick的公共方法。然后你只需要让btn1成为该类的一个实例,并订阅一个调用btn2的处理程序,调用btn1.RaiseClickEvent()或你称之为方法的任何东西。

我不确定我是否真的推荐它。你究竟想做什么?什么是更大的图景?

编辑:我看到你已经接受了使用反射获取当前事件集的版本,但是如果你对在原始控件中调用OnXXX处理程序的替代方案感兴趣,我在这里有一个示例。我最初复制了所有事件,但这确实导致了一些非常奇怪的影响。请注意,此版本意味着如果有人在调用CopyEvents之后订阅原始按钮中的事件,那么它仍然“挂钩” - 也就是说当你将两者关联起来并不重要。

using System;
using System.Drawing;
using System.Reflection;
using System.Windows.Forms;

class Test
{
    static void Main()
    {

        TextBox output = new TextBox 
        { 
            Multiline = true,
            Height = 350,
            Width = 200,
            Location = new Point (5, 15)
        };
        Button original = new Button
        { 
            Text = "Original",
            Location = new Point (210, 15)
        };
        original.Click += Log(output, "Click!");
        original.MouseEnter += Log(output, "MouseEnter");
        original.MouseLeave += Log(output, "MouseLeave");

        Button copyCat = new Button
        {
            Text = "CopyCat",
            Location = new Point (210, 50)
        };

        CopyEvents(original, copyCat, "Click", "MouseEnter", "MouseLeave");

        Form form = new Form 
        { 
            Width = 400, 
            Height = 420,
            Controls = { output, original, copyCat }
        };

        Application.Run(form);
    }

    private static void CopyEvents(object source, object target, params string[] events)
    {
        Type sourceType = source.GetType();
        Type targetType = target.GetType();
        MethodInfo invoker = typeof(MethodAndSource).GetMethod("Invoke");
        foreach (String eventName in events)
        {
            EventInfo sourceEvent = sourceType.GetEvent(eventName);
            if (sourceEvent == null)
            {
                Console.WriteLine("Can't find {0}.{1}", sourceType.Name, eventName);
                continue;
            }

            // Note: we currently assume that all events are compatible with
            // EventHandler. This method could do with more error checks...

            MethodInfo raiseMethod = sourceType.GetMethod("On"+sourceEvent.Name, 
                                                          BindingFlags.Instance | 
                                                          BindingFlags.Public | 
                                                          BindingFlags.NonPublic);
            if (raiseMethod == null)
            {
                Console.WriteLine("Can't find {0}.On{1}", sourceType.Name, sourceEvent.Name);
                continue;
            }
            EventInfo targetEvent = targetType.GetEvent(sourceEvent.Name);
            if (targetEvent == null)
            {
                Console.WriteLine("Can't find {0}.{1}", targetType.Name, sourceEvent.Name);
                continue;
            }
            MethodAndSource methodAndSource = new MethodAndSource(raiseMethod, source);
            Delegate handler = Delegate.CreateDelegate(sourceEvent.EventHandlerType,
                                                       methodAndSource,
                                                       invoker);

            targetEvent.AddEventHandler(target, handler);
        }
    }

    private static EventHandler Log(TextBox output, string text)
    {
        return (sender, args) => output.Text += text + "\r\n";
    }

    private class MethodAndSource
    {
        private readonly MethodInfo method;
        private readonly object source;

        internal MethodAndSource(MethodInfo method, object source)
        {
            this.method = method;
            this.source = source;
        }

        public void Invoke(object sender, EventArgs args)
        {
            method.Invoke(source, new object[] { args });
        }
    }
}

答案 2 :(得分:1)

我使用@nobugz的解决方案进行了一些挖掘,并提出了可用于大多数通用对象的通用版本。

我发现,事实上,我敢说自动事件实际上是使用同名的支持委托字段编译的:

所以这里有一个窃取事件处理程序用于更简单的对象:

class Program
{
    static void Main(string[] args)
    {
        var d = new Dummy();
        var d2 = new Dummy();

        // Use anonymous methods without saving any references
        d.MyEvents += (sender, e) => { Console.WriteLine("One!"); };
        d.MyEvents += (sender, e) => { Console.WriteLine("Two!"); };

        // Find the backing field and get its value
        var theType = d.GetType();
        var bindingFlags = BindingFlags.NonPublic | BindingFlags.Instance;

        var backingField = theType.GetField("MyEvents", bindingFlags);
        var backingDelegate = backingField.GetValue(d) as Delegate;

        var handlers = backingDelegate.GetInvocationList();

        // Bind the handlers to the second instance
        foreach (var handler in handlers)
            d2.MyEvents += handler as EventHandler;

        // See if the handlers are fired
        d2.DoRaiseEvent();

        Console.ReadKey();
    }
}

class Dummy
{
    public event EventHandler MyEvents;

    public void DoRaiseEvent() { MyEvents(this, new EventArgs()); }
}

认为这可能对某些人有用。

但请注意,事件在Windows窗体组件中的连接方式有很大不同。它们经过优化,因此多个事件不占用大量内存而只占用空值。所以它需要更多的挖掘,但@nobugz已经做到了: - )

关于合并代表的文章 Delegates and events 可能有助于澄清答案中的许多要点。

答案 3 :(得分:0)

您可以为按钮和图片框使用公共事件处理程序(根据早期答案中的注释),然后使用“sender”对象确定如何在运行时处理事件。