如何从事件处理程序中移除自己?

11
我想做的基本上是从事件中删除一个函数,而不知道该函数的名称。
我有一个 FileSystemWatcher。如果创建/重命名文件,则检查其名称。如果匹配,则将其移动到特定位置。但是,如果文件被锁定,它会制作一个lambda,附加到计时器的tick事件,等待文件解锁。当文件解锁时,它会移动文件,然后从事件处理程序中删除自己。我已经看到了许多方法来实现这一点,例如保留实例或制作命名方法。但在这里我两者都不能使用。我的选择是什么?

有一些模式可以让你以一种松散耦合的方式订阅事件。请查看这个链接。它描述了如何订阅/取消订阅事件聚合器(在本例中是关于屏幕的)。实际的聚合器创建对象之间的引用。有很多不同的实现方法。我喜欢的是在Caliburn.Micro中实现的那个。 - Michael Schnerring
2个回答

31

没有简单的方法可以实现这一点。

首选方法:

我不明白为什么你不能保存委托。你不必将实例保存为某个字段。它可以是一个本地变量,由匿名事件处理程序捕获:

EventHandler<TypeOfEventArgs> handler = null;
handler = (s, e) =>
{
    // Do whatever you need to do here

    // Remove event:
    foo.Event -= handler;
}

foo.Event += handler;

我想不出任何情况下不能使用它的场景。

另一种方法,无需保存委托:

然而,如果你遇到这样的情况,那就变得非常棘手。
你需要找到已添加为事件处理程序的委托。因为你没有保存它,所以很难获取它。没有this可以获取当前执行方法的委托。

你也不能在事件上使用GetInvocationList(),因为访问超出其定义类的事件受到限制,只能添加和删除处理程序,即+=-=

创建新的委托也是不可能的。虽然你可以访问定义匿名方法的MethodInfo对象,但你无法访问该方法所声明的类的实例。这个类是由编译器自动生成的,在匿名方法内调用this将返回定义普通方法的类的实例。

我找到的唯一可行的方法是找到事件使用的字段(如果有的话),并在其上调用GetInvocationList()。以下代码演示了如何在一个虚拟类中实现这一点:

void Main()
{
    var foo = new Foo();
    foo.Bar += (s, e) => {
        Console.WriteLine("Executed");
        
        var self = new StackFrame().GetMethod();
        var eventField = foo.GetType()
                            .GetField("Bar", BindingFlags.NonPublic | 
                                             BindingFlags.Instance);
        if(eventField == null)
            return;
        var eventValue = eventField.GetValue(foo) as EventHandler;
        if(eventValue == null)
            return;
        var eventHandler = eventValue.GetInvocationList()
                                     .OfType<EventHandler>()
                                     .FirstOrDefault(x => x.Method == self)
                               as EventHandler;
        if(eventHandler != null)
            foo.Bar -= eventHandler;
    };
    
    foo.RaiseBar();
    foo.RaiseBar();
}

public class Foo
{
    public event EventHandler Bar;
    public void RaiseBar()
    { 
        var handler = Bar;
        if(handler != null)
            handler(this, EventArgs.Empty);
    }
}
请注意,传递给GetField的字符串"Bar"需要是事件使用的字段的确切名称。这会导致两个问题:
  1. 字段的名称可能不同,例如在使用显式事件实现时。您需要手动查找字段名称。
  2. 可能根本没有字段。如果事件使用显式事件实现,并且仅委托给另一个事件或以某种其他方式存储委托,则会发生这种情况。

结论:

备选方法依赖于实现细节,因此如果可以避免,请不要使用它。


我忘记了闭包!哎呀!谢谢你的答案,你甚至比我更进一步了。 - It'sNotALie.
我认为你在 Dark Falcon 的回答之前写了一个负面评论,然后在你喜欢的方法中基本上给出了相同的建议,这有点不公平。 - Ben Voigt
@BenVoigt嗯,他的评论是正确的,尽管如果可能的话,这是首选的解决方案,但从技术上讲,它并不是答案。此外请注意,他没有对Dark的回答进行负评,仅仅是评论说它在技术上没有满足所述标准。另一个关键区别是,Daniel在问题中包含了一个确实符合标准的解决方案。 - Servy
@Servy:谢谢,正是我想的。此外,我从未打算让Dark Falcon删除他的回答。 - Daniel Hilgarth

0

使用lambda表达式移除事件处理程序的步骤:

public partial class Form1 : Form
{
    private dynamic myEventHandler;
    public Form1()
    {
        InitializeComponent();
    }
    private void Form1_Load(object sender, EventArgs e)
    {
        myEventHandler = new System.EventHandler((sender2, e2) => this.button1_Click(sender, e, "Hi there"));
        this.button1.Click += myEventHandler;
    }

    private void button1_Click(object sender, EventArgs e, string additionalInfo)
    {
        MessageBox.Show(additionalInfo);
        button1.Click -= myEventHandler;
    }
}

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