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

时间:2009-06-01 22:46:49

标签: c# event-handling delegates

重复:How to ensure an event is only subscribed to onceHas an event handler already been added?

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

9 个答案:

答案 0 :(得分:161)

如果仅使用-=删除事件,如果未找到,则不会抛出异常

/// -= 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);

答案 1 :(得分:139)

显式实现事件并检查调用列表。您还需要检查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;
    }
}

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

答案 2 :(得分:29)

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

private EventHandler _foo;
public event EventHandler Foo {

    add {
        _foo -= value;
        _foo += value;
    }
    remove {
        _foo -= value;
    }
}

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

答案 3 :(得分:19)

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

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

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

答案 4 :(得分: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");
        }
    }
}

输出:

tc_Test called

(即只有一次)

答案 5 :(得分:6)

Microsoft的Reactive Extensions (Rx) framework也可用于“仅订阅一次”。

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

Observable.FromEvent<MouseEventArgs>(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;
}

除了提供“订阅一次”功能外,RX方法还可以将事件组合在一起或过滤事件。这很漂亮。

答案 6 :(得分:2)

创建一个Action而不是一个事件。你的班级可能如下:

public class MyClass
{
                // sender   arguments       <-----     Use this action instead of an event
     public Action<object, EventArgs> OnSomeEventOccured;

     public void SomeMethod()
     {
          if(OnSomeEventOccured!=null)
              OnSomeEventOccured(this, null);
     }

}

答案 7 :(得分:0)

让你的单例对象检查它通知的列表,如果重复则只调用一次。或者,如果可能,拒绝事件附件请求。

答案 8 :(得分:0)

在silverlight中你需要说e.Handled = true;在事件代码中。

void image_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
    e.Handled = true; //this fixes the double event fire problem.
    string name = (e.OriginalSource as Image).Tag.ToString();
    DoSomething(name);
}

如果这有帮助,请打勾我。