如何删除“按钮”的“Click”事件的所有事件处理程序?

21

我有一个按钮控件,需要删除其Click事件绑定的所有事件处理程序。

该如何操作?

Button button = GetButton();
button.Click.RemoveAllEventHandlers();

你能用MyButtonClass替换Button吗? - H H
我认为更简单的方法是禁用该按钮。 - V4Vendetta
我想之后只添加一个事件处理程序,因此禁用无法帮助。 - The Light
可能是重复的问题:如何从控件中删除所有事件处理程序 - nawfal
1
我知道这是一个相当古老的问题,但我认为你应该将所选答案更改为Douglas提供的反射选项。 "你不能"是一个相当糟糕的答案,特别是现在有一个完美的示例可以展示你可以做到。 - tpartee
6个回答

41

注意: 由于我最初回答的那个问题已经被关闭作为重复问题,因此我将在此处跨贴发布我的改进版答案。 该答案仅适用于WPF。 它不适用于Windows Forms或任何其他UI框架。

以下是一个有用的实用方法,可以删除给定元素上订阅的路由事件的所有事件处理程序。如果您愿意,您可以轻松地将其转换为扩展方法。

/// <summary>
/// Removes all event handlers subscribed to the specified routed event from the specified element.
/// </summary>
/// <param name="element">The UI element on which the routed event is defined.</param>
/// <param name="routedEvent">The routed event for which to remove the event handlers.</param>
public static void RemoveRoutedEventHandlers(UIElement element, RoutedEvent routedEvent)
{
    // Get the EventHandlersStore instance which holds event handlers for the specified element.
    // The EventHandlersStore class is declared as internal.
    var eventHandlersStoreProperty = typeof(UIElement).GetProperty(
        "EventHandlersStore", BindingFlags.Instance | BindingFlags.NonPublic);
    object eventHandlersStore = eventHandlersStoreProperty.GetValue(element, null);

    // If no event handlers are subscribed, eventHandlersStore will be null.
    // Credit: https://dev59.com/K2w15IYBdhLWcg3wGn9c#16392387
    if (eventHandlersStore == null)
        return;

    // Invoke the GetRoutedEventHandlers method on the EventHandlersStore instance 
    // for getting an array of the subscribed event handlers.
    var getRoutedEventHandlers = eventHandlersStore.GetType().GetMethod(
        "GetRoutedEventHandlers", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
    var routedEventHandlers = (RoutedEventHandlerInfo[])getRoutedEventHandlers.Invoke(
        eventHandlersStore, new object[] { routedEvent });

    // Iteratively remove all routed event handlers from the element.
    foreach (var routedEventHandler in routedEventHandlers)
        element.RemoveHandler(routedEvent, routedEventHandler.Handler);
}

然后,您可以轻松地在按钮的Click事件中调用此实用方法:

RemoveRoutedEventHandlers(button, Button.ClickEvent);

编辑:我已经复制了corona实现的错误修复,该修复方法可以防止在没有事件处理程序订阅时抛出NullReferenceException。感谢(和点赞)应归功于他们的答案。


在Silverlight中能否做到这一点?Silverlight没有RoutedEventHandlerInfo。 - Syaiful Nizam Yahya
这个能扩展成一个扩展方法吗?那将会非常棒... - Will

15

我想稍微扩展一下道格拉斯的例程,我非常喜欢它。 我发现我需要添加额外的空值检查到eventHandlersStore中,以处理传递的元素尚未附加任何事件的情况。

/// <summary>
/// Removes all event handlers subscribed to the specified routed event from the specified element.
/// </summary>
/// <param name="element">The UI element on which the routed event is defined.</param>
/// <param name="routedEvent">The routed event for which to remove the event handlers.</param>
public static void RemoveRoutedEventHandlers(UIElement element, RoutedEvent routedEvent)
{
    // Get the EventHandlersStore instance which holds event handlers for the specified element.
    // The EventHandlersStore class is declared as internal.
    var eventHandlersStoreProperty = typeof(UIElement).GetProperty(
        "EventHandlersStore", BindingFlags.Instance | BindingFlags.NonPublic);
    object eventHandlersStore = eventHandlersStoreProperty.GetValue(element, null);

    if (eventHandlersStore == null) return;

    // Invoke the GetRoutedEventHandlers method on the EventHandlersStore instance 
    // for getting an array of the subscribed event handlers.
    var getRoutedEventHandlers = eventHandlersStore.GetType().GetMethod(
        "GetRoutedEventHandlers", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
    var routedEventHandlers = (RoutedEventHandlerInfo[])getRoutedEventHandlers.Invoke(
        eventHandlersStore, new object[] { routedEvent });

    // Iteratively remove all routed event handlers from the element.
    foreach (var routedEventHandler in routedEventHandlers)
        element.RemoveHandler(routedEvent, routedEventHandler.Handler);
}

10

基本上你是不能这样做的 - 至少不是没有反射和很多麻烦。

事件严格遵循“订阅、取消订阅”原则 - 你不能取消其他人的处理程序,就像你无法更改别人对对象的引用一样。


1
这正是我想要的:取消订阅别人的处理程序!我正在使用第三方的自定义按钮,并希望删除作者的点击事件处理程序... - The Light
@William:基本上你不能这样做 - 除非自定义按钮公开了这种行为,否则就不能依靠实现细节和反射。事件的封装是处理程序彼此不干扰的原因。 - Jon Skeet
4
@William: 1) 你正使用控件的方式不符合设计初衷,很容易引起问题。2) 你的临时解决方案可能会在未来的版本中出现问题。3) 在低信任环境下它将无法正常工作。基本上,你希望你的代码有多脆弱? - Jon Skeet
好的点:不仅考虑实现,还要思考设计。我创建了自己的自定义按钮,并从原始按钮中复制了所需的内容(图标等)。 - The Light

5
我在StackOverflow上找到了这个答案: 如何从控件中删除所有事件处理程序 请注意,此处不会提供答案解释。
private void RemoveClickEvent(Button b)
{
    FieldInfo f1 = typeof(Control).GetField("EventClick", 
        BindingFlags.Static | BindingFlags.NonPublic);
    object obj = f1.GetValue(b);
    PropertyInfo pi = b.GetType().GetProperty("Events",  
        BindingFlags.NonPublic | BindingFlags.Instance);
    EventHandlerList list = (EventHandlerList)pi.GetValue(b, null);
    list.RemoveHandler(obj, list[obj]);
}

以下内容是原帖作者在这里发现的:


2
这是具体实现相关的 - 答案出现在2008年,我甚至不想说它是否适用于.NET 4。依赖这种东西是一个非常糟糕的想法。 - Jon Skeet
2
谢谢,但是这一行总是返回 null:typeof(Control).GetField("EventClick", BindingFlags.Static | BindingFlags.NonPublic); - The Light
我实现了一个类似的解决方案,它使用了更多 EventHandlerList 对象的内部方法。请参考答案:https://dev59.com/02TWa4cB1Zd3GeqPIfdT - Dinis Cruz

0

我在使用Jamie Dixon发布的代码时遇到了空指针错误问题,因为它没有考虑到没有Click事件的情况。

private void RemoveClickEvent(Control control)
{
    // chenged "FieldInfo f1 = typeof(Control)" to "var f1 = b.GetType()". By changing to 
    // the type of the  passed in control we can use this for any control with a click event.
    // using var allows for null checking and lowering the chance of exceptions.

    var fi = control.GetType().GetField("EventClick", BindingFlags.Static | BindingFlags.NonPublic);
    if (fi != null)
    {
        object obj = fi.GetValue(control);
        PropertyInfo pi = control.GetType().GetProperty("Events", BindingFlags.NonPublic | BindingFlags.Instance);
        EventHandlerList list = (EventHandlerList)pi.GetValue(control, null);
        list.RemoveHandler(obj, list[obj]);
    }

}

稍作修改,它就可以适用于任何事件。

private void RemoveClickEvent(Control control, string theEvent)
{
    // chenged "FieldInfo f1 = typeof(Control)" to "var f1 = b.GetType()". By changing to 
    // the type of the  passed in control we can use this for any control with a click event.
    // using var allows for null checking and lowering the chance of exceptions.

    var fi = control.GetType().GetField(theEvent, BindingFlags.Static | BindingFlags.NonPublic);
    if (fi != null)
    {
        object obj = fi.GetValue(control);
        PropertyInfo pi = control.GetType().GetProperty("Events", BindingFlags.NonPublic | BindingFlags.Instance);
        EventHandlerList list = (EventHandlerList)pi.GetValue(control, null);
        list.RemoveHandler(obj, list[obj]);
    }

}

我想这个程序可能还有改进的空间,但它对我目前的需求来说已经足够好用了。希望对某些人有所帮助。


0
我正在开发一个WinForms项目,需要从ToolStripMenuItem中删除click-EventHandler并替换为自己的Handler。(我需要修改当单击ContextMenu项时执行的操作)
对我来说,user2113340的代码不起作用。我必须像这样修改它才能与ToolStripMenuItem一起使用:
    private void RemoveClickEvent(ToolStripMenuItem control)
    {
        FieldInfo eventClick = typeof(Control).GetField("EventClick", BindingFlags.NonPublic | BindingFlags.Static);
        PropertyInfo eventsProp = typeof(Component).GetProperty("Events", BindingFlags.NonPublic | BindingFlags.Instance);
        EventHandlerList events = (EventHandlerList)eventsProp.GetValue(control, null);
        FieldInfo headInfo = events.GetType().GetField("head", BindingFlags.NonPublic | BindingFlags.Instance);
        object head = headInfo.GetValue(events);
        FieldInfo keyType = head.GetType().GetField("key", BindingFlags.NonPublic | BindingFlags.Instance);
        object key = keyType.GetValue(head);
        Delegate d1 = events[key];
        events.RemoveHandler(key, d1);
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接