C#模式防止事件处理程序挂钩两次

Ali*_*fai 93 c# delegates event-handling

重复:如何确保事件仅订阅一次 并且已添加事件处理程序?

我有一个提供一些服务的单例,我的类挂钩到它上面的一些事件,有时一个类挂起两次事件然后被调用两次.我正在寻找一种经典的方法来防止这种情况发生.不知怎的,我需要检查一下我是否已经迷上了这个事件......

Pri*_*TSS 165

-=如果没有找到,则首先删除事件如何不抛出异常

/// -= Removes the event if it has been already added, this prevents multiple firing of the event
((System.Windows.Forms.WebBrowser)sender).Document.Click -= new System.Windows.Forms.HtmlElementEventHandler(testii);
((System.Windows.Forms.WebBrowser)sender).Document.Click += new System.Windows.Forms.HtmlElementEventHandler(testii);
Run Code Online (Sandbox Code Playgroud)

  • +1我想这取决于程序员,但我认为这是最好的(不是"最好的"要求最终开发人员这样做,但是订阅者不能阻止多个事件发生器的错误不是订阅,所以,让他们弄清楚删除等...此外,为什么要阻止某人如果想要多次订阅同一个处理程序?) (4认同)
  • 这应该是公认的答案,因为它很简单,不需要自定义实现。@LoxLox 也显示了与实现相同的模式。我没有测试,所以我接受他们的话。非常好。 (2认同)
  • 为此,请注意线程安全,如下所述:/sf/ask/25726641/#comment68420379_7065833 (2认同)

Jud*_*ngo 144

显式实现事件并检查调用列表.您还需要检查null:

using System.Linq; // Required for the .Contains call below:

...

private EventHandler foo;
public event EventHandler Foo
{
    add
    {
        if (foo == null || !foo.GetInvocationList().Contains(value))
        {
            foo += value;
        }
    }
    remove
    {
        foo -= value;
    }
}
Run Code Online (Sandbox Code Playgroud)

使用上面的代码,如果调用者多次订阅该事件,它将被简单地忽略.

  • 您需要使用System.Linq. (14认同)
  • Contains是一种扩展方法,可让您*查询*列表.它带有LINQ,因为它是一个查询辅助工具.它与EventHandlers和调用列表有关,因为这些是可以查询的列表. (6认同)
  • 喜欢这个!我删除了if语句中的第二个条件,所以我只能将一个委托附加到Foo事件,这就是我需要的!好的解决方案;) (3认同)
  • 尽管此答案确实可以解决问题,但是[工程师在指定事件源而不是事件处理程序本身时指定事件处理程序的行为时应格外小心](http://stackoverflow.com/a/937321/3367144)。@PrimeTSS答案提供了将事件处理行为(如重复订阅)留给处理程序本身的替代方法。 (2认同)
  • 对`Invoke()` 的调用必须从公共事件切换到私有委托,例如`Foo?.Invoke()` 现在将变成`foo?.Invoke()`。否则你会得到一个错误。 (2认同)

Lox*_*Lox 34

我测试了每个解决方案,最好的解决方案(考虑性能)是:

private EventHandler _foo;
public event EventHandler Foo {

    add {
        _foo -= value;
        _foo += value;
    }
    remove {
        _foo -= value;
    }
}
Run Code Online (Sandbox Code Playgroud)

没有Linq使用必需.在取消订阅之前无需检查null(有关详细信息,请参阅MS EventHandler).无需记住在任何地方取消订阅.


JP *_*oto 19

你真的应该在接收器级而不是源级别处理这个问题.也就是说,不要在事件源处规定事件处理程序逻辑 - 将其留给处理程序(接收器)本身.

作为服务的开发者,你是说谁只能注册一次?如果他们出于某种原因想要注册两次怎么办?如果你试图通过修改源来纠正接收器中的错误,那么这也是在接收器级纠正这些问题的一个很好的理由.

我相信你有理由; 重复接收器非法的事件源并不是不可思议的.但也许您应该考虑一种替代架构,使事件的语义保持不变.


ang*_*son 14

您需要在事件上实现添加和删除访问器,然后检查委托的目标列表,或将目标存储在列表中.

在add方法中,您可以使用Delegate.GetInvocationList方法获取已添加到委托的目标列表.

由于委托被定义为比较相等,如果它们链接到同一目标对象上的相同方法,您可能可以运行该列表并进行比较,如果您发现没有比较相等,则添加新的.

这是示例代码,编译为控制台应用程序:

using System;
using System.Linq;

namespace DemoApp
{
    public class TestClass
    {
        private EventHandler _Test;

        public event EventHandler Test
        {
            add
            {
                if (_Test == null || !_Test.GetInvocationList().Contains(value))
                    _Test += value;
            }

            remove
            {
                _Test -= value;
            }
        }

        public void OnTest()
        {
            if (_Test != null)
                _Test(this, EventArgs.Empty);
        }
    }

    class Program
    {
        static void Main()
        {
            TestClass tc = new TestClass();
            tc.Test += tc_Test;
            tc.Test += tc_Test;
            tc.OnTest();
            Console.In.ReadLine();
        }

        static void tc_Test(object sender, EventArgs e)
        {
            Console.Out.WriteLine("tc_Test called");
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

输出:

tc_Test called
Run Code Online (Sandbox Code Playgroud)

(即只有一次)


Jud*_*ngo 5

Microsoft的Reactive Extensions(Rx)框架也可用于"仅订阅一次".

给定一个鼠标事件foo.Clicked,这里是如何订阅和只接收一个调用:

Observable.FromEvent<MouseEventArgs>(foo, nameof(foo.Clicked))
    .Take(1)
    .Subscribe(MyHandler);

...

private void MyHandler(IEvent<MouseEventArgs> eventInfo)
{
   // This will be called just once!
   var sender = eventInfo.Sender;
   var args = eventInfo.EventArgs;
}
Run Code Online (Sandbox Code Playgroud)

除了提供"订阅一次"功能外,RX方法还提供了将事件组合在一起或过滤事件的功能.这很漂亮.