【问题标题】:Is it possible to "steal" an event handler from one control and give it to another?是否可以从一个控件“窃取”事件处理程序并将其提供给另一个控件?
【发布时间】:2008-11-15 20:18:23
【问题描述】:

我想做这样的事情:

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

这不是我在这里寻找的。​​p>

这也不是我要找的:

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

【问题讨论】:

  • EventHandler btnClick = new EventHandler(btn1_Click); btn1.Click -= btnClick; btn2.Click += btnClick;如果这没有帮助,我不知道我是否明白你想要什么。
  • 请注意,从 C# 2 开始,您不需要“new EventHandler(...)” - 只需“EventHandler btnClick = btn1_Click”即可...很少需要“new WhatDelegateType( ...)" 从 C# 2 开始。
  • 抱歉,我在 sn-ps 中经常使用 new,所以我可以 TAB TAB。
  • 谢谢乔恩。我从上面的示例中复制了代码:)
  • @shahkalpesh:抱歉,我不清楚,但在我的情况下,EventHandler 实际上被分配给不同范围的 btn1,因此它不再被分配给 btn2。有关更多详细信息,请参阅 Jon 的答案中的 cmets。

标签: c# .net


【解决方案1】:

是的,这在技术上是可行的。反射是必需的,因为许多成员都是私有的和内部的。启动一个新的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 非常努力地阻止您这样做,那么您理解了代码。

【讨论】:

  • 在调用 typeof(Control).GetField("EventClick") 时,您将“EventClick”传递给名为“EventClick”的字段,应为 Control/Button 的 ParentChanged 事件传递什么字符串?
  • 得到它“EventParent”,之前我尝试过“EventParentChanged”得到空异常。遍历所有字段得到“EventParent”,谢谢。
  • @HansPassant 如何获取文本框控件的 TextChanged 事件?我没有找到任何事件,或者找不到其他一些事件:(
【解决方案2】:

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

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

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

编辑:我看到您已经接受了通过反射获取当前事件集的版本,但如果您对在原始控件中调用 OnXXX 处理程序的替代方案感兴趣,我在这里有一个示例.我最初复制了 all 事件,但这确实导致了一些非常奇怪的效果。请注意,此版本意味着如果有人在调用 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 });
        }
    }
}

【讨论】:

  • 我想在运行时获取一个表单,遍历它的控件集合并用图片框替换所有按钮。这样做很容易,但是您的替换图片框没有任何与原始按钮关联的事件。
  • 我知道这样做没有理智的理由。有时,作为一名程序员,您可能会受到外力的束缚,以至于疯狂成为最简单的出路。
  • 我认为如果没有反思,您将无法做到这一点。基本上你试图打破封装。这可能是一个很好的理由,但它仍然是一个破损。您可以使用反射调用 Button.OnClick,而不是通过反射获取处理程序。
  • 我可以这样调用 Button.OnClick,但我实际上不知道该调用什么,因为我不知道所有事件都分配给了按钮(可能有 getfocus 或 textchanged或其他)。
  • 我认为反射是必需的,但这超出了我的能力范围。
【解决方案3】:

我对@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 可能有助于澄清答案中的很多要点。

【讨论】:

    【解决方案4】:

    您可以为按钮和图片框使用通用事件处理程序(根据较早答案中的 cmets),然后使用“发送者”对象来确定如何在运行时处理事件。

    【讨论】:

      猜你喜欢
      • 2011-05-31
      • 1970-01-01
      • 2017-11-01
      • 2011-11-05
      • 1970-01-01
      • 2013-08-28
      • 2012-08-20
      • 2011-12-14
      • 1970-01-01
      相关资源
      最近更新 更多