如何从一种类型的多个实例中禁用对事件的订阅,并且只允许一个?

9 .net c# events delegates winforms

我有一个主窗体的Windows窗体应用程序(从基础派生Form).可以在那里打开的其他模态形式来自我的类ManagedForm,它也来自Form.
此外,我有一个静态通知程序服务,它会触发这样的事件:

    public static class NotifierService
    {
        public delegate void NotifierServiceEventHandler(object sender, NotifierServiceEventArgs e);

        private static readonly object Locker = new object();
        private static NotifierServiceEventHandler _notifierServiceEventHandler;

        #region Events

        public static event NotifierServiceEventHandler OnOk
        {
            add
            {
                lock (Locker)
                {
                    _notifierServiceEventHandler += value;

                    if (
                        _notifierServiceEventHandler.GetInvocationList()
                                                    .Count(
                                                        _ =>
                                                        _.Method.DeclaringType != null &&
                                                        value.Method.DeclaringType != null &&
                                                        _.Method.DeclaringType == value.Method.DeclaringType) <= 1)
                        return;

                    _notifierServiceEventHandler -= value;
                }
            }
            remove
            {
                lock (Locker)
                {
                    _notifierServiceEventHandler -= value;
                }
            }
        }

        // and many more events similar to previous...

        #endregion

        #region Event firing methods

        public static void NotifyOk(string fullMessage = "Ok.", string shortMessage = null)
        {
            NotifierServiceEventHandler handler;

            lock (Locker)
            {
                handler = _notifierServiceEventHandler;
            }

            if (handler == null) return;

            handler(typeof (NotifierService),
                    new NotifierServiceEventArgs(StatusType.Ok, fullMessage, shortMessage ?? fullMessage));
        }

        #endregion
    }
Run Code Online (Sandbox Code Playgroud)

因此,在某些代码处,这些事件可能被触发:

NotifierService.NotifyExclamation("Fail!");
Run Code Online (Sandbox Code Playgroud)

在主窗体中,有StatusStrip用于通知目的的控件,并且由于主窗体已订阅这些事件 - 它们的消息将显示在状态条中.
但是,正如我之前所说,用户可能会打开其他表单,这些表单可能会生成其他表单等等...(它们来自一个类ManagedForm,它将NotifierService在创建后立即订阅).
在这些表单中,还有另一个逻辑如何通知用户 - 他们需要显示MessageBox带有消息的es.正如您所看到的,我在事件访问器中添加了一些魔法,只允许任何类型的一个订阅者,因为没有这个所有打开的表单将生成他们自己的MessageBoxes.但是当一个孩子ManagedForm生产了另一个孩子而第二个孩子已经关闭时 - MessageBox将不会显示任何一个孩子.
我应该实现什么样的魔法来允许首先订阅ManagedForm?非常感谢任何想法.

编辑:建议的想法并没有解决这个问题.我试图将事件更改为:

private static readonly object Locker = new object();

private static EventHandler<NotifierServiceEventArgs> _myEvent;

public static event EventHandler<NotifierServiceEventArgs> OnOk
{
    add
    {
        if (_myEvent == null || _myEvent.GetInvocationList().All(_ => _.Method.DeclaringType != value.Method.DeclaringType))
        {
            _myEvent += value;
        }
    }
    remove
    {
        _myEvent -= value;
    }
}
Run Code Online (Sandbox Code Playgroud)

然后我打开一个模态子表单并创建一个事件被触发的情境NotifierService.一个MessageBox已生成并显示(这是确定).之后我从第一个开始另一个模态形式,并创建另一个事件已被触发的情况.一个MessageBox已生成并显示(这也行).现在我正在关闭第二种形式,并制定一个触发事件所需的情况.没有显示MessageBoxes(但是在事件的主要表单消息的状态条中已经正确显示,因此我的第一个实现没有任何改变).
我应该改变remove条款吗?我不需要只有一个订阅者,我需要每个订阅者都应该是不同的类型.抱歉如果英语不好.

Iva*_*oev 6

您试图解决问题的方式在设计上是根本错误的.您的服务类定义了在某些情况下将触发的事件.有些客户订阅了该事件,这样就要求在事件发生时收到通知.这只是实现Observer模式的.NET方式,因此您的服务(作为主题或可观察的)不应该在subscribe和notify部分都不应用任何逻辑,从而破坏了模式的整个目的.Hans Passant已经指出了你的设计中的一些缺陷,但即使他的解决方案并不完美,因为查看事件签名,完全不清楚只有表单实例方法应该注册 - 可以尝试使用静态方法,匿名lambda /方法,一些类方法等

所以,IMO以下是你有一些可行的选择.

(A)保留您的NotificationService活动,但从订阅和通知部分中删除任何"魔法"(不久,使用常规方式定义和触发事件)并将订阅者所需的逻辑放入:

public static class NotifierService
{
    public delegate void NotifierServiceEventHandler(object sender, NotifierServiceEventArgs e);
    public static event NotifierServiceEventHandler OnOk;
    public static void NotifyOk(string fullMessage = "Ok.", string shortMessage = null)
    {
        var handler = OnOk;
        if (handler != null)
            handler(typeof(NotifierService), new NotifierServiceEventArgs(StatusType.Ok, fullMessage, shortMessage ?? fullMessage));
    }
}
Run Code Online (Sandbox Code Playgroud)

假设只有活动表单应该处理通知,你MainFormManagedForm他们的现有处理程序将在他们的方法体内使用这样的东西

if (this != ActiveForm) return;
// do the processing
Run Code Online (Sandbox Code Playgroud)

您甚至可以创建这样的基本表单

class NotifiedForm : Form
{
    protected override void OnActivated(EventArgs e)
    {
        base.OnActivated(e);
        NotifierService.OnOk += OnNotifyOK;
        // similar for other events
    }
    protected override void OnDeactivate(EventArgs e)
    {
        base.OnDeactivate(e);
        NotifierService.OnOk -= OnNotifyOK;
        // similar for other events
    }
    protected virtual void OnNotifyOK(object sender, NotifierServiceEventArgs e) { }
    // similar for other events
}
Run Code Online (Sandbox Code Playgroud)

让你的MainForm,ManagedForm(以及任何其他需要)从继承,只是覆盖OnNotifyXXX的方法和应用他们的逻辑.

总而言之,这种方法可以使您的服务保持抽象,并将决策留给服务的客户.

(B)如果您的服务的唯一目的是作为专门针对您的表单的通​​知协调员,那么您可以删除事件以及订阅/取消订阅部分(因为Application.OpenForms并且Form.ActiveForm已经提供了足够的信息)并处理您服务中的逻辑.为了做到这一点,你需要某种基本接口或表单,最简单的方法是通过创建一个像这样的基本表单类,在选项(A)中使用类似于可选的方法

class NotifiedForm : Form
{
    public virtual void OnNotifyOK(object sender, NotifierServiceEventArgs e) { }
    // similar for other notifications
}
Run Code Online (Sandbox Code Playgroud)

让你的MainForm,ManagedForm和其他需要继承它.请注意,这里没有逻辑(检查ActiveForm等),因为现在这是调用者的责任.那么服务可能是这样的:

public static class NotifierService
{
    public static void NotifyOk(string fullMessage = "Ok.", string shortMessage = null)
    {
        var target = Form.ActiveForm as NotifiedForm;
        if (target != null)
            target.OnNotifyOK(typeof(NotifierService), new NotifierServiceEventArgs(StatusType.Ok, fullMessage, shortMessage ?? fullMessage));
    }
    // similar for other notifications
}
Run Code Online (Sandbox Code Playgroud)

如果逻辑是仅通知活动表单.

要么

public static class NotifierService
{
    public static void NotifyOk(string fullMessage = "Ok.", string shortMessage = null)
    {
        // Could also be a forward for, forach etc.
        for (int i = Application.OpenForms.Count - 1; i >= 0; i--)
        {
            var target = Application.OpenForms[i] as NotifiedForm;
            if (target != null /* && someOtherCritaria(target) */)
            {
                target.OnNotifyOK(typeof(NotifierService), new NotifierServiceEventArgs(StatusType.Ok, fullMessage, shortMessage ?? fullMessage));
                // Could also continue
                break;
            }
        }
    }
    // similar for other notifications
}
Run Code Online (Sandbox Code Playgroud)

如果需要一些其他逻辑(我怀疑).

希望有所帮助.在任何情况下,选项(A)都更灵活,允许更多的使用场景,但如果使用场景是通过设计修复的,则选项(B)更好,因为它对客户端的要求较少(因此不易出错)并在一个地方提供集中的应用程序逻辑.


Dee*_*tia 1

我希望您按如下方式进行:

  1. 消除事件访问器方法的魔力,让所有订阅者订阅该事件。现在您的主表单和所有其他表单都订阅了该事件。

  2. 现在将魔法放入您的事件调用方法中。例如,在您的 NotifyOK 方法中,首先获取 deligate 的调用列表,现在仅当您尚未调用特定 DeclaringType 时,才使用调用列表中每个 deligate 的 DynamicInvoke 或 Invoke 方法一一调用每个 deligate。请参阅下面的算法:

     public static void NotifyOk(string fullMessage = "Ok.", string shortMessage = null)
    
     {
        NotifierServiceEventHandler handler;
    
        lock (Locker)
        {
            handler = _notifierServiceEventHandler;
        }
    
        if (handler == null) return;
    
        // Get invocation list of handler as you have done in event accessor
    
        //initialise a new List<T> to hold the declaring types
    
        // loop through each member (delegate) of invocation list
    
          // if the current member declaration type is not in List<t>
    
           // Invoke or DynamicInvoke current delegate
           // add the declaration type of current delegate to List<t> 
     }
    
    Run Code Online (Sandbox Code Playgroud)