如何确保只订阅一次事件

时间:2008-12-15 05:10:02

标签: c# events event-handling subscription

我想确保我只在特定的类中为实例上的事件订阅一次。

例如,我希望能够做到以下几点:

if (*not already subscribed*)
{
    member.Event += new MemeberClass.Delegate(handler);
}

我如何实施这样的警卫?

9 个答案:

答案 0 :(得分:57)

我在所有重复的问题中添加了这个,只是为了记录。这种模式对我有用:

myClass.MyEvent -= MyHandler;
myClass.MyEvent += MyHandler;

请注意,每次注册处理程序时执行此操作都将确保您的处理程序只注册一次。

答案 1 :(得分:35)

如果您正在讨论可以访问源的类的事件,那么您可以将警卫放在事件定义中。

private bool _eventHasSubscribers = false;
private EventHandler<MyDelegateType> _myEvent;

public event EventHandler<MyDelegateType> MyEvent
{
   add 
   {
      if (_myEvent == null)
      {
         _myEvent += value;
      }
   }
   remove
   {
      _myEvent -= value;
   }
}

这将确保只有一个订阅者可以在提供该事件的类的这个实例上订阅该事件。

编辑请参阅有关上述代码为何不好主意而非线程安全的评论。

如果您的问题是客户端的单个实例多次订阅(并且您需要多个订阅者),则客户端代码将需要处理该问题。所以替换

  

尚未订阅

使用客户端类的bool成员,当您第一次订阅该事件时,该成员将被设置。

编辑(接受后):根据来自@Glen T(问题的提交者)的评论,他接受的解决方案的代码位于客户端类中:

if (alreadySubscribedFlag)
{
    member.Event += new MemeberClass.Delegate(handler);
}

其中alreadySubscribedFlag是客户端类中的成员变量,用于跟踪对特定事件的第一次订阅。 人们在这里查看第一个代码片段,请注意@ Rune的评论 - 以非显而易见的方式更改订阅事件的行为并不是一个好主意。

编辑31/7/2009:请参阅@Sam Saffron的评论。正如我已经说过的那样,Sam同意这里介绍的第一种方法并不是修改事件订阅行为的明智方法。该类的消费者需要了解其内部实现以了解其行为。不是很好。
@Sam Saffron还评论了线程安全性。我假设他指的是可能的竞争条件,其中两个订阅者(接近)同时尝试订阅,他们可能最终都订阅。可以使用锁来改善这一点。如果您打算更改事件订阅的工作方式,那么我建议您read about how to make the subscription add/remove properties thread safe

答案 2 :(得分:7)

正如其他人所示,您可以覆盖事件的添加/删除属性。或者,您可能想要抛弃事件,只需让类在其构造函数(或其他方法)中将委托作为参数,而不是触发事件,调用提供的委托。

事件暗示任何人都可以订阅它们,而委托是一个方法,您可以传递给该类。如果您只是在实际使用它通常提供的一对多语义时才使用事件,那么对于您的库的用户来说可能不那么令人惊讶。

答案 3 :(得分:4)

你可以使用Postsharper只编写一个属性,并在普通事件中使用它。重用代码。代码示例如下。

[Serializable]
public class PreventEventHookedTwiceAttribute: EventInterceptionAspect
{
    private readonly object _lockObject = new object();
    readonly List<Delegate> _delegates = new List<Delegate>();

    public override void OnAddHandler(EventInterceptionArgs args)
    {
        lock(_lockObject)
        {
            if(!_delegates.Contains(args.Handler))
            {
                _delegates.Add(args.Handler);
                args.ProceedAddHandler();
            }
        }
    }

    public override void OnRemoveHandler(EventInterceptionArgs args)
    {
        lock(_lockObject)
        {
            if(_delegates.Contains(args.Handler))
            {
                _delegates.Remove(args.Handler);
                args.ProceedRemoveHandler();
            }
        }
    }
}

就这样使用它。

[PreventEventHookedTwice]
public static event Action<string> GoodEvent;

有关详细信息,请查看Implement Postsharp EventInterceptionAspect to prevent an event Handler hooked twice

答案 4 :(得分:3)

您需要存储一个单独的标志,指示您是否已订阅,或者,如果您可以控制MemberClass,则提供该事件的添加和删除方法的实现:

class MemberClass
{
        private EventHandler _event;

        public event EventHandler Event
        {
            add
            {
                if( /* handler not already added */ )
                {
                    _event+= value;
                }
            }
            remove
            {
                _event-= value;
            }
        }
}

要决定是否添加了处理程序,您需要比较在_event和value上从GetInvocationList()返回的委托。

答案 5 :(得分:1)

我知道这是一个老问题,但是当前的答案对我不起作用。

查看C# pattern to prevent an event handler hooked twice(标记为该问题的重复项),给出的答案更加接近,但仍然无法正常工作,这可能是由于多线程导致新事件对象有所不同,或者可能是因为我正在使用自定义事件类。最后,我为上述问题的答案提供了类似的解决方案。

private EventHandler<bar> foo;
public event EventHandler<bar> Foo
{
    add
    {
        if (foo == null || 
            !foo.GetInvocationList().Select(il => il.Method).Contains(value.Method))
        {
            foo += value;
        }
    }

    remove
    {
        if (foo != null)
        {
            EventHandler<bar> eventMethod = (EventHandler<bar>)foo .GetInvocationList().FirstOrDefault(il => il.Method == value.Method);

            if (eventMethod != null)
            {
                foo -= eventMethod;
            }
        }
    }
}

与此同时,您还必须使用foo.Invoke(...)而不是Foo.Invoke(...)来触发事件。如果您还没有使用System.Linq,则还需要包含它。

此解决方案并不十分美观,但可以。

答案 6 :(得分:1)

在我看来,这样做的一个简单方法是取消订阅您的处理程序(如果没有订阅则会无声地失败)和然后订阅。

<a href="http://google.com" class="dropbtn">Login</a>
<?php
if (!isset($_SESSION['username'])) // the session variable is not set
{
    ?>
    <div class="dropdown-content">
        <div id="Register">
            <form>
                // form content here
            </form>
        </div>
    </div>
    <?php
}
?>

确实给开发者带来了负担,这是错误的做法,但是对于快速和肮脏的这种做法很快而且很脏。

答案 7 :(得分:0)

我最近做了这个,我将其放在这里,这样就可以了:

private bool subscribed;

if(!subscribed)
{
    myClass.MyEvent += MyHandler;
    subscribed = true;
} 

private void MyHandler()
{
    // Do stuff
    myClass.MyEvent -= MyHandler;
    subscribed = false;
}

答案 8 :(得分:-1)

在引发时仅调用来自 GetInvocationList 的不同元素:

using System.Linq;
....
public event HandlerType SomeEvent;
....
//Raising code
foreach (HandlerType d in (SomeEvent?.GetInvocationList().Distinct() ?? Enumerable.Empty<Delegate>()).ToArray())
     d.Invoke(sender, arg);

示例单元测试:

class CA 
{
    public CA()
    { }
    public void Inc()
        => count++;
    public int count;
}
[TestMethod]
public void TestDistinctDelegates()
{
    var a = new CA();
    Action d0 = () => a.Inc();
    var d = d0;
    d += () => a.Inc();
    d += d0;
    d.Invoke();
    Assert.AreEqual(3, a.count);
    var l = d.GetInvocationList();
    Assert.AreEqual(3, l.Length);
    var distinct = l.Distinct().ToArray();
    Assert.AreEqual(2, distinct.Length);
    foreach (Action di in distinct)
        di.Invoke();
    Assert.AreEqual(3 + distinct.Length, a.count);
}
[TestMethod]
public void TestDistinctDelegates2()
{
    var a = new CA();
    Action d = a.Inc;
    d += a.Inc;
    d.Invoke();
    Assert.AreEqual(2, a.count);
    var distinct = d.GetInvocationList().Distinct().ToArray();
    Assert.AreEqual(1, distinct.Length);
    foreach (Action di in distinct)
        di.Invoke();
    Assert.AreEqual(3, a.count);
}