如何从一个事件中移除所有的事件处理程序

434
为在控件上创建一个新的事件处理程序,您可以这样做。
c.Click += new EventHandler(mainFormButton_Click);

或者这个

c.Click += mainFormButton_Click;

要移除事件处理程序,您可以执行以下操作

c.Click -= mainFormButton_Click;

但是如何从一个事件中移除所有的事件处理程序?


15
如果有人来到这里寻找一个WPF解决方案,你可能想看一下这个答案 - Douglas
3
你可以直接将 c.Click 设置为 null 吗? - alexania
12
这是我认为过于复杂的事情之一。一个简单的“Clear”方法显然需要太多的努力。 - Zimano
在.NET 4.5中,如果List.Count > 2,例如您尝试删除第一个委托InvocatorList [0] == mainFormButton_Click...就像您所做的那样..它将删除所有这些委托。我认为这是一个错误! - Latency
18个回答

195

我在MSDN论坛上找到了解决方案。下面的示例代码将删除所有button1中的Click事件。

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();

        button1.Click += button1_Click;
        button1.Click += button1_Click2;
        button2.Click += button2_Click;
    }

    private void button1_Click(object sender, EventArgs e)  => MessageBox.Show("Hello");
    private void button1_Click2(object sender, EventArgs e) => MessageBox.Show("World");
    private void button2_Click(object sender, EventArgs e)  => RemoveClickEvent(button1);

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

4
如果我说错了,请纠正我,但是 RemoveClickEvent 的第一行不应该是这样吗:FieldInfo f1 = typeof(Button)?如果我使用 Control,在 GetField 中会得到空值。 - Protector one
2
这似乎不适用于ToolStripButtons。我已经在RemoveClickEvent中将Button替换为ToolStripButton,但调用RemoveClickEvent后事件仍然存在。有人有这个问题的解决方案吗? - Skalli
1
MSDN上面的链接还建议尝试使用myButton.Click += null;来删除所有委托(不仅适用于Click事件,也适用于其他事件..) - hello_earth
2
@hello_earth 对于 ObservableCollection.CollectionChanged += null; 似乎不起作用。 - Mike de Klerk
这会导致特定的C#软件出现奇怪的问题。(在我的情况下,此函数导致控件重复出现) - T.Todua
显示剩余4条评论

187

你们让这件事情变得太难了。其实很简单:

void OnFormClosing(object sender, FormClosingEventArgs e)
{
    foreach(Delegate d in FindClicked.GetInvocationList())
    {
        FindClicked -= (FindClickedHandler)d;
    }
}

70
只有当你拥有该事件时,这才有效。尝试在控件上操作将不起作用。 - Delyan
289
如果你拥有该事件,你只需写FindClicked = null;,这样更简单易懂。 - Jon Skeet
3
对于 Kinect 事件这种方式行不通 -- kinect.ColorFrameReady -= MyEventHandler 可以,但是在 Kinect 实例上没有 GetInvocationList() 方法来迭代它们的委托。 - Brent Faust
1
未找到 GetInvocationList。 - huang
1
@Timo:是的,必须是那个确切的类。 - Jon Skeet
显示剩余4条评论

92

来自 删除所有事件处理程序:

直接的答案是不行的,主要是因为你不能简单地将事件设置为空。

间接地,你可以将实际事件设为私有,并创建一个围绕它的属性来跟踪添加/删除到其中的所有委托。

看下面的例子:

List<EventHandler> delegates = new List<EventHandler>();

private event EventHandler MyRealEvent;

public event EventHandler MyEvent
{
    add
    {
        MyRealEvent += value;
        delegates.Add(value);
    }

    remove
    {
        MyRealEvent -= value;
        delegates.Remove(value);
    }
}

public void RemoveAllEvents()
{
    foreach(EventHandler eh in delegates)
    {
        MyRealEvent -= eh;
    }
    delegates.Clear();
}

6
我认为原帖是在提到一般的.NET控件......在这种情况下,可能无法进行这种包装。 - Gishu
5
你可以推导出控制方式,然后它就可以被应用。 - Tom Fobear
1
这也导致需要维护两个列表,请参见https://dev59.com/LHVD5IYBdhLWcg3wGHiu#0qOeEYcBWogLw_1bGSNW进行复位或https://dev59.com/LHVD5IYBdhLWcg3wGHiu#5475424以访问该列表。 - TN.
遗憾的是,我无法将此行为封装在接口中,因为它不能具有实例字段。 - Noman_1
@Noman_1 - 当你想要一个字段时,使用接口的解决方案是代之以一对方法(getter/setter)。然后每个类都要实现该方法对。 - ToolmakerSteve

73

被接受的答案不完整。对于声明为 {add; remove;} 的事件无效。

这里是可用的代码:

public static void ClearEventInvocations(this object obj, string eventName)
{
    var fi = obj.GetType().GetEventField(eventName);
    if (fi == null) return;
    fi.SetValue(obj, null);
}

private static FieldInfo GetEventField(this Type type, string eventName)
{
    FieldInfo field = null;
    while (type != null)
    {
        /* Find events defined as field */
        field = type.GetField(eventName, BindingFlags.Static | BindingFlags.Instance | BindingFlags.NonPublic);
        if (field != null && (field.FieldType == typeof(MulticastDelegate) || field.FieldType.IsSubclassOf(typeof(MulticastDelegate))))
            break;

        /* Find events defined as property { add; remove; } */
        field = type.GetField("EVENT_" + eventName.ToUpper(), BindingFlags.Static | BindingFlags.Instance | BindingFlags.NonPublic);
        if (field != null)
            break;
        type = type.BaseType;
    }
    return field;
}

4
这个版本适合我使用,但被接受的版本无法运行。为此点赞。 - Meister Schnitzel
2
直到我在第一个“GetField”调用中使用“BindingFlags.Public”,才能使 WPF 事件起作用。 - Lennart

51

删除不存在的事件处理程序不会造成任何损害。因此,如果您知道可能存在哪些处理程序,您可以简单地将它们全部删除。我曾经遇到过类似的情况。这在某些情况下可能会有所帮助。

例如:

// Add handlers...
if (something)
{
    c.Click += DoesSomething;
}
else
{
    c.Click += DoesSomethingElse;
}

// Remove handlers...
c.Click -= DoesSomething;
c.Click -= DoesSomethingElse;

3
是的,但问题在于您可以多次执行 +=,然后就无法知道有多少个相同的事件附加到处理程序上。因此,您不知道需要几个 -= 才能完全清除它。 - Tomislav3008
@Tomislav3008 - 我不知道有任何有效的情况需要多个完全相同的处理程序 - 那通常是一个错误 - 你可能会为不同的实例拥有相同类型的多个处理程序,但这不是这里的情况 - 这个答案是只在实例内才有意义的代码。事实上,我有时会编写代码,其中 += 首先执行 -=, 以确保不会意外地添加相同的处理程序两次。 - ToolmakerSteve
当您需要在控件中删除处理程序后从类中删除处理程序时怎么办?如果您正在使用 GridView 并在第一阶段附加处理程序,则随着 ContainerContent 的更改,您将继续生成处理程序,因此您需要确保相同的控件在删除旧处理程序的同时获得新处理程序。在这种情况下,您需要保留一个 Map。 - John Glen
@ToolmakerSteve 如果你在 GridView 的项容器中有处理程序,因为你需要每个元素的控件,那么该怎么办?你必须在容器更改其内容时不断更换处理程序。 - John Glen
@JohnGlen - 我需要看到具体的代码,这超出了评论范围。除非我误解了,否则我会有一个单一的处理程序,并将项目作为参数传递。我不声称这涵盖了所有可能性,但对我的需求已经足够了。我同意网格项可能比列表视图更棘手。如果我没记错,在某些情况下,我在每个单元格的视图模型数据中添加了一个 int ID 字段,页面上有一个从 ID 到该单元格视图的映射。因此,我可以传递一个 int 参数并找到相应的视图。(我的 ID 是 N * row + column,其中 N 大于最大列数。) - ToolmakerSteve

21

我讨厌这里展示的任何完整解决方案,现在做了一些混合并测试,适用于任何事件处理程序:

public class MyMain()
    public void MyMethod() {
        AnotherClass.TheEventHandler += DoSomeThing;
    }

    private void DoSomething(object sender, EventArgs e) {
        Debug.WriteLine("I did something");
        AnotherClass.ClearAllDelegatesOfTheEventHandler();
    }

}

public static class AnotherClass {

    public static event EventHandler TheEventHandler;

    public static void ClearAllDelegatesOfTheEventHandler() {

        foreach (Delegate d in TheEventHandler.GetInvocationList())
        {
            TheEventHandler -= (EventHandler)d;
        }
    }
}

太简单了!感谢Stephen Punak。

我使用它是因为我使用一种通用的本地方法来移除委托,当不同的委托被设置时,该本地方法会在不同情况下被调用。


3
根据 Jon Skeet 在其他地方的评论,如果您拥有 AnotherClass 的源代码,则清除方法可以只有一行:TheEventHandler = null;。(如果 AnotherClass 不在您的代码中,则不允许在事件上调用 GetInvocationList() 。) - ToolmakerSteve

16

实际上我正在使用这种方法,它完美地工作。我从Aeonhack写的代码这里得到了启发。


    Public Event MyEvent()
    Protected Overrides Sub Dispose(ByVal disposing As Boolean)
        If MyEventEvent IsNot Nothing Then
            For Each d In MyEventEvent.GetInvocationList ' If this throws an exception, try using .ToArray
                RemoveHandler MyEvent, d
            Next
        End If
    End Sub
    ~MyClass()
    {
        if (MyEventEvent != null)
        {
            foreach (var d in MyEventEvent.GetInvocationList())
            {
                MyEventEvent -= (MyEvent)d;
            }
        }

    }

MyEventEvent字段已隐藏,但它确实存在。

在调试时,可以看到d.target是实际处理事件的对象,d.method是其方法。您只需要将其删除。

它运作得很好。不再因为事件处理程序而导致对象无法被垃圾回收。


9
我不同意 - 这是一个.NET的问题和疑问。VB.NET和其他任何.NET语言一样有效地解决这个问题。我也会包括一个C#的例子,因为它更常见,但偶尔看到一些VB.NET还是很不错的! - Thomas Phaneuf
@kiddailey,Finalyzer清理没有意义,因为对象已经标记为收集并将被GC回收,因此它不会再引用订阅者。实际上,添加Finalyzer到对象会延迟内存回收到下一个GC。原始VB代码使用“Dispose”模式。 - OwnageIsMagic

4
如果你确实需要这样做……那么需要反思并花费相当长的时间来做。事件处理程序在控件内部的事件到委托映射中进行管理。你需要:
  • 反射并获取控件实例中的映射。
  • 为每个事件迭代,获取委托
    • 依次处理每个委托可能是一系列链接的事件处理程序。所以调用obControl.RemoveHandler(event, handler)
简而言之,工作量很大。理论上是可能的……我从未尝试过这样的事情。
看看能否更好地控制/遵守控件的订阅-取消订阅阶段。

3

我刚刚发现了如何在设置WinForms控件属性时暂停事件。它将从控件中删除所有事件:

namespace CMessWin05
{
    public class EventSuppressor
    {
        Control _source;
        EventHandlerList _sourceEventHandlerList;
        FieldInfo _headFI;
        Dictionary<object, Delegate[]> _handlers;
        PropertyInfo _sourceEventsInfo;
        Type _eventHandlerListType;
        Type _sourceType;


        public EventSuppressor(Control control)
        {
            if (control == null)
                throw new ArgumentNullException("control", "An instance of a control must be provided.");

            _source = control;
            _sourceType = _source.GetType();
            _sourceEventsInfo = _sourceType.GetProperty("Events", BindingFlags.Instance | BindingFlags.NonPublic);
            _sourceEventHandlerList = (EventHandlerList)_sourceEventsInfo.GetValue(_source, null);
            _eventHandlerListType = _sourceEventHandlerList.GetType();
            _headFI = _eventHandlerListType.GetField("head", BindingFlags.Instance | BindingFlags.NonPublic);
        }

        private void BuildList()
        {
            _handlers = new Dictionary<object, Delegate[]>();
            object head = _headFI.GetValue(_sourceEventHandlerList);
            if (head != null)
            {
                Type listEntryType = head.GetType();
                FieldInfo delegateFI = listEntryType.GetField("handler", BindingFlags.Instance | BindingFlags.NonPublic);
                FieldInfo keyFI = listEntryType.GetField("key", BindingFlags.Instance | BindingFlags.NonPublic);
                FieldInfo nextFI = listEntryType.GetField("next", BindingFlags.Instance | BindingFlags.NonPublic);
                BuildListWalk(head, delegateFI, keyFI, nextFI);
            }
        }

        private void BuildListWalk(object entry, FieldInfo delegateFI, FieldInfo keyFI, FieldInfo nextFI)
        {
            if (entry != null)
            {
                Delegate dele = (Delegate)delegateFI.GetValue(entry);
                object key = keyFI.GetValue(entry);
                object next = nextFI.GetValue(entry);

                Delegate[] listeners = dele.GetInvocationList();
                if(listeners != null && listeners.Length > 0)
                    _handlers.Add(key, listeners);

                if (next != null)
                {
                    BuildListWalk(next, delegateFI, keyFI, nextFI);
                }
            }
        }

        public void Resume()
        {
            if (_handlers == null)
                throw new ApplicationException("Events have not been suppressed.");

            foreach (KeyValuePair<object, Delegate[]> pair in _handlers)
            {
                for (int x = 0; x < pair.Value.Length; x++)
                    _sourceEventHandlerList.AddHandler(pair.Key, pair.Value[x]);
            }

            _handlers = null;
        }

        public void Suppress()
        {
            if (_handlers != null)
                throw new ApplicationException("Events are already being suppressed.");

            BuildList();

            foreach (KeyValuePair<object, Delegate[]> pair in _handlers)
            {
                for (int x = pair.Value.Length - 1; x >= 0; x--)
                    _sourceEventHandlerList.RemoveHandler(pair.Key, pair.Value[x]);
            }
        }

    }
}

2
这非常有帮助,但有一件事需要改变:在Resume()中,您以相反的顺序添加处理程序(我假设这是从Suppress复制/粘贴的,因为您希望向后工作,以免干扰正在迭代的集合)。某些代码依赖于处理程序按特定顺序触发,因此不应更改该顺序。 - Michael

2

这个页面对我帮助很大。我从这里得到的代码是为了从一个按钮中移除点击事件的。现在我需要从一些面板中移除双击事件以及从一些按钮中移除点击事件。因此,我创建了一个控制扩展,它可以移除特定事件的所有事件处理程序。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Windows.Forms;
using System.Reflection;
public static class EventExtension
{
    public static void RemoveEvents<T>(this T target, string eventName) where T:Control
    {
        if (ReferenceEquals(target, null)) throw new NullReferenceException("Argument \"target\" may not be null.");
        FieldInfo fieldInfo = typeof(Control).GetField(eventName, BindingFlags.Static | BindingFlags.NonPublic);
        if (ReferenceEquals(fieldInfo, null)) throw new ArgumentException(
            string.Concat("The control ", typeof(T).Name, " does not have a property with the name \"", eventName, "\""), nameof(eventName));
        object eventInstance = fieldInfo.GetValue(target);
        PropertyInfo propInfo = typeof(T).GetProperty("Events", BindingFlags.NonPublic | BindingFlags.Instance);
        EventHandlerList list = (EventHandlerList)propInfo.GetValue(target, null);
        list.RemoveHandler(eventInstance, list[eventInstance]);
    }
}

现在,这个扩展的用法是:如果您需要从按钮中移除点击事件,可以使用它。
Button button = new Button();
button.RemoveEvents(nameof(button.EventClick));

如果您需要从面板中删除双击事件,
Panel panel = new Panel();
panel.RemoveEvents(nameof(panel.EventDoubleClick));

我不是C#专家,如果有任何错误,请谅解并告诉我。


1
.CastTo<>() 扩展方法在哪里可以找到? - IbrarMumtaz
你可以自己写一个: public static T CastTo<T>(this object objectToCast) { return (T)objectToCast; } - KingOfHypocrites

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