65.9K
CodeProject 正在变化。 阅读更多。
Home

如何在运行时将事件处理程序从一个控件复制到另一个控件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (11投票s)

2012年1月1日

CPOL

4分钟阅读

viewsIcon

62493

downloadIcon

2469

本文将向您展示如何使用一些技巧和 .NET 反射在运行时将事件处理程序从一个控件复制到另一个控件。

引言

今天在工作中,我的一个同事正在处理一个疯狂的客户需求,他问我是否有可能在运行时将事件处理程序从一个控件复制到另一个控件,并保持一切正常工作。我说是的,我认为这是可能的,而且应该很容易,让我们来看看……

几分钟后,我们发现 Control 类没有公开任何公共方法或属性来访问附加到控件事件的委托集合。

我做了大量的网络研究,发现了很多部分解决方案,但没有一个能满足我们的需求。所以我决定自己开发一个解决方案,发布它,并可能帮助一些遇到同样问题的人。

通常,当我们使用事件时,我们会将它们与处理程序连接起来,编写如下代码

textBox1.TextChanged += (s, e) => MessageBox.Show("Hi there from handler 1");

将多个处理程序附加到同一个事件也很常见

textBox1.TextChanged += (s, e) => MessageBox.Show("Hi there from handler 1");
textBox1.TextChanged += (s, e) => MessageBox.Show("Hi there from handler 2");
//*(I’m using lambdas for shortness)

需求是提供一些机制,在运行时创建一个新的文本框(比如 textBox2),并将设计时附加到 textBox1 的处理程序列表附加到它。(我们还需要销毁 textBox1,但这不是重点。)

在本文中,我假设您熟悉事件,并且了解事件的基本工作原理,因此我不会深入讨论 .NET 中的“事件机制”。但是,为了继续进行,我们需要回顾一下处理程序如何在 .NET 中(内部)附加到事件或从事件中分离。

上面的代码片段来自 .NET Framework 的(反编译的)Control

private static readonly object EventText = new object();

public event EventHandler TextChanged {
    add { 
        Events.AddHandler(EventText, value); 
    }
    remove { 
        Events.RemoveHandler(EventText, value);
    }
}

在这里,EventTextControl 类用于指向给定事件的处理程序(委托)列表的字段(键),在本例中为 TextChanged

所以当我们写

textBox1.TextChanged += some delegate…

在内部,我们将该委托添加到处理程序列表中。

add { 
    Events.AddHandler(EventText, value); 
}

现在我们了解了内部添加/删除事件处理程序的过程的基本知识,我们可以想象,如果我们可以某种方式获取 Events 属性的值并知道正确的键,我们也许能够提取附加到指定事件的委托集合,并遍历该集合,并希望能够获得委托并完成我们的任务;)

此解决方案大量使用反射,主要是因为正如我之前所说,没有公共 API 可以使用。例如,Events 属性在 Control 类中被标记为 protected,并且没有其他方法可以访问它(除非您使用扩展控件或其他类似的东西,但这不是我们的情况)。

当前的实现仅适用于在 Control 类上声明的事件。您可以使用 .NET Reflector 或类似工具获取支持事件的完整列表。如果您要查找的事件不受当前实现支持,您可以通过修改几行代码使其工作(稍后会详细介绍)。

首先,我们需要获取附加到事件的处理程序列表,这个方便的方法可以做到这一点。

public Dictionary<string, Dictionary<object, Delegate[]>> GetHandlersFrom(Control ctrl) {
    var ctrlEventsCollection = (EventHandlerList)typeof(Control)
    .GetProperty("Events", BF.GetProperty | BF.NonPublic | BF.Instance)
    .GetValue(ctrl, null);
           
    var headInfo = typeof(EventHandlerList)
    .GetField("head", BF.Instance | BF.NonPublic);
    var handlers = BuildList(headInfo, ctrlEventsCollection);
 
            var eventName = GetEventNameFromKey(ctrl, handlers.First().Key);
 
            //TODO: use key value pair.
            var result = new Dictionary<string, Dictionary<object, 
                         Delegate[]>> { { eventName, handlers } };
 
            return result;
}

现在我们有了委托,但我们(还)不知道在新控件中必须连接哪个事件。 在这一点上,我们将使用一个字典,其中包含事件名称和 Control 类中相应字段之间的一些映射。 基于我们已经拥有的键,并使用一些反射技巧,我们可以将这些委托附加到我们全新的控件中的正确事件。

//The mapping
_keyEventNameMapping = new Dictionary<string,>{
                {"EventAutoSizeChanged", "AutoSizeChanged"},
                {"EventBackColor", "BackColorChanged"},
                //etc, etc…

如果我感兴趣的事件未在 Control 类中定义,我该如何使其工作?

很简单,如果您想扩展此解决方案以使用未在 Control 类中定义的事件。 首先,在 _keyEventNameMapping 字段中添加映射,然后在 CopyTo 扩展方法中修改此行

//Instead of Control use the class you need
var info = typeof(Control).GetField(innerKeyFieldName,
          BF.GetField | BF.Static | BF.NonPublic | BF.DeclaredOnly);

运行示例应用程序

下载示例应用程序后,运行它,您将看到一个带有两个文本框和一个按钮的表单。 如果您在标记为Source的文本框中输入文本,您会注意到两个事件将被触发,并且附加到这些事件的处理程序将显示一条消息。 如果您在标记为Destination的文本框中输入文本,则不应发生任何事情。

现在,按下名为“Copy event handlers”的按钮,并在标记为Destination的文本框中输入一些文本。 如果一切顺利,您应该会看到附加到 Source 文本框的事件处理程序的消息,现在附加到 Destination 文本框。

使用代码

要将处理程序从一个控件复制到另一个控件,您只需要这两行代码

var _copyHelper = new CopyEventHandlers();
_copyHelper.GetHandlersFrom(textBox1).CopyTo(textBox2);

如果您对使用代码有任何疑问,请随时与我联系,我很乐意提供帮助。

© . All rights reserved.