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

本文关键字:类型 实例 事件 许一种 一种 | 更新日期: 2023-09-27 18:04:28

我有一个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
    }
所以在代码的某些地方,这些事件可以被触发,比如:
NotifierService.NotifyExclamation("Fail!");

在主表单中有用于通知目的的StatusStrip控件,并且由于主表单订阅了这些事件—它们的消息将显示在状态条中。
但是!,就像我之前说过的,用户可以打开其他表单,这些表单可以产生其他表单,等等……(它们是从一个类ManagedForm派生出来的,一旦创建了NotifierService,它就会被订阅)。
在这些表单中,还有另一种通知用户的逻辑——他们需要用消息显示 MessageBoxes。正如您所看到的,我在事件访问器中添加了一些魔法,只允许任何类型的一个订阅者,因为如果不这样做,所有打开的表单都将生成自己的MessageBox e。但是当一个孩子ManagedForm产生了另一个孩子并且第二个孩子已经关闭时,将不会显示MessageBox
我应该实现什么魔法来允许只从第一个ManagedForm订阅?谢谢你的建议。

EDIT:建议并不能解决这个问题。我试着把事件改成这样:

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;
    }
}

然后我打开了一个模态子窗体,并创建了一个事件被NotifierService触发的情况。已经生成并显示了一个MessageBox(没关系)。之后,我从第一个打开了另一个模态形式,并创建了另一个事件被触发的情况。已经生成并显示了一个MessageBox(这也可以)。现在我要关闭第二种形式,创造一个触发事件所需的情况。没有显示MessageBox es(但在事件的主要形式消息的状态条中已正确显示,因此从我的第一次实现中没有任何改变)。
我应该在remove条款中做些改变吗?我不需要只有一个订阅者应该是,我需要每个订阅者应该是不同的类型。对不起,我英语不好。

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

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

在我看来,以下是一些可行的选择。

(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));
    }
}

假设只有活动表单应该处理通知,MainFormManagedForm中的现有处理程序将在其方法体中使用类似的东西

if (this != ActiveForm) return;
// do the processing

你甚至可以创建一个像这样的基本表单

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
}

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

总之,这种方法将保留您的服务抽象,并将决策留给服务的客户端

(B)如果服务的唯一目的是充当窗体的通知协调器,那么您可以删除事件以及订阅/退订部分(因为Application.OpenFormsForm.ActiveForm已经提供了所需的足够信息)并处理服务中的逻辑。为了做到这一点,您需要某种基本接口或表单,最简单的方法是使用与选项(a)中可选的方法类似的方法,通过创建一个基本表单类,如下所示

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

,让您的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
}

,如果逻辑是只通知活动表单。

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
}

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

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

我希望你这样做:

  1. 从事件访问器方法中删除魔法,让所有订阅者订阅事件。现在你的主表单和所有其他表单都订阅了这个事件。

  2. 现在将魔术放入事件调用方法中。例如,在NotifyOK方法中,首先获取委托的调用列表,然后使用调用列表中每个委托的DynamicInvoke或invoke方法逐一调用每个委托,只有在尚未为特定的DeclaringType调用时才使用。参见下面的算法:

     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> 
     }
    

Try this:?)

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

我将NotifierService简化为:

public static class NotifierService
{
    public static event EventHandler<NotifierServiceEventArgs> OnOk = delegate { };
    public static void NotifyOk(string fullMessage = "Ok.", string shortMessage = null)
    {
        OnOk(typeof(NotifierService), 
             new NotifierServiceEventArgs(StatusType.Ok, fullMessage, shortMessage ?? fullMessage));
    }
}

,然后在ManagedForm中使用这个处理程序

NotifierService.OnOk += Notify;
private void Notify(object sender, NotifierServiceEventArgs e)
{
    // handle event in first open ManagedForm
    if (Application.OpenForms.OfType<ManagedForm>().FirstOrDefault() == this)
    {
       // notification logic
    }
}

如果窗体以Modal模式打开(使用ShowDialog()),则可以使用另一种变体(根据这个问题):

private void Notify(object sender, NotifierServiceEventArgs e)
{
    // handle event in active (last shown) ManagedForm
    if (this.CanFocus)
    {
       // notification logic
    }
}

所以这个想法是所有的ManagedForm接收事件数据,然后决定他们是否应该做某事

注::取消订阅Dispose

的处理程序
protected override void Dispose(bool disposing)
{
    if (disposing)
    {
        NotifierService.OnOk -= Notify;
    }
    // default
    if (disposing && (components != null))
    {
        components.Dispose();
    }
    base.Dispose(disposing);
}

我做了一个类似于你的设置&我明白问题所在了。

我将给出2个工作建议来解决这个问题(你可以根据需要的变化选择)-

  1. 最快修复与最小的改变你的原始代码-

所以这就是我从问题情况中理解的-您将事件NotifierService.OnOk钩到类ManagedForm &还编写了代码,以便在窗体关闭时从事件NotifierService.OnOk中解挂事件处理程序。

我假设你写的代码,从事件NotifierService.OnOk解开事件处理程序时,窗体关闭但我不确定的是,当你钩事件NotifierService.OnOk到它的事件处理程序在管理的形式。这很关键& &;我猜这是唯一的问题在你的设置。

我假设你已经把它设置在一个地方,只发生一次在生命周期的form - like构造函数或Load事件处理程序。这就是我重现这个问题的方法。

作为修复,只需将事件NotifierService.OnOk挂钩到它的事件处理程序在每次窗体激活时调用的地方就像像这样-

public partial class ManagedFrom : Form
{
    // this is the fix. Everytime the form comes up. It tries to register itself.
    //The existing magic will consider its request to register only when the other form is closed or if its the 1st of its type.
    protected override void OnActivated(EventArgs e)
    {
        base.OnActivated(e);
        NotifierService.OnOk += NotifierService_OnOk;
    }

不再需要更改,您在事件中的现有逻辑将照顾休息。我把原因写在上面的代码注释中。

  • 一个更好的方法但需要更多的改变
  • 我想将事件OnOk从所有额外的(&魔法)责任,我改变事件

     public static event NotifierServiceEventHandler OnOk
        {
            add
            {
                lock (Locker)  // I'm not removing the locks.  May be the publisher works in a multithreaded business layer.
                {
                    _notifierServiceEventHandler += value;                  
                }
            }
            remove
            {
                lock (Locker)
                {
                    _notifierServiceEventHandler -= value;
                }
            }
        }
    

    相反,订阅者应该知道何时开始和何时停止订阅。

    所以我改变了ManagedFrom

     public partial class ManagedFrom : Form
    {
        //start the subscription
        protected override void OnActivated(EventArgs e)
        {
            base.OnActivated(e);
            NotifierService.OnOk += NotifierService_OnOk;
        }
        //stop the subscription
        protected override void OnDeactivate(EventArgs e)
        {
            base.OnDeactivate(e);
            NotifierService.OnOk -= NotifierService_OnOk;
        }
    

    在这两个建议中,我的意图只是解决问题,而不引入任何新的模式。但是如果需要的话请告诉我。也请告诉我是否有帮助,或者如果你认为我做了错误的假设。

    总结:

    • 有多个事件源;
    • 有多个目标;
    • 有不同类型的事件,必须以不同的方式处理。

    使用静态管理器的想法是可以的(除非你有性能问题,然后分成多个不同的消息队列是一种选择),但是欺骗订阅/取消订阅感觉非常错误。

    创建一个简单的事件

    public enum MessageType { StatusText, MessageBox }
    public NotifyEventArgs: EventArgs
    {
        public MessageType Type { get; }
        public string Message { get; }
        public NotifyEventArgs(MessageType type, string message)
        {
            Type = type;
            Message = message;
        }
    }
    public static NotifyManager
    {
        public event EventHandler<NotifyMessageArgs> Notify;
        public static OnEventHandler(MessageType type, string message) =>
            Notify?.Invoke(null, new NotifyEventArgs(type, message));
    }
    

    每个表单必须在显示时订阅此事件,在隐藏时取消订阅。不确定这里哪些事件是最好的(习惯了WPF的Loaded, Unloaded,但在winforms中没有这样的,尝试使用ShownVisibilityChanged也许)。

    每个表单都将接收事件,但只有一个必须处理MessageBox类型(所有表单都显示StatusMessage是安全的)。为此,您需要某种机制来决定何时(用于显示消息框)。例如,它可以是主动形式:

    void NotifyManager_Event(object sender, NotifyEventArgs e)
    {
        if(e.Type == MessageType.MessageBox && this == Form.ActiveForm)
            MessageBox.Show(this, e.Message);
        else
            statusBar.Text = e.Message;
    }
    

    你确定NotifierService的任务是确保只有一个Form会显示通知吗?

    如果你想描述一个NotifierService的任务,你会描述它做什么和"当NotifierService有东西要通知时,它会通知所有说它想要被通知的人"

    这将使您的notifierservice对使用它的当前应用程序的依赖程度降低。如果你想要一个完全不同的应用,比如只有两个表单,你想让两个表单都对通知做出反应,你就不能使用这个notifierservice。

    但是在我的表单应用程序中,只有一个表单可以响应通知

    这是正确的:它是你的表单应用程序有这个约束,而不是通知服务。你可以创建一个表单应用程序,它可以使用任何类型的notifierservice,但是无论使用哪种notifierservice,在我的应用程序中只有一个表单可以显示通知。

    这意味着你应该有一些规则来知道表单是否应该显示通知

    例如:

    • 只有当前表单可以显示通知
    • 只有左上角的表单可以显示通知
    • 只有主表单可以显示通知,除非设置表单是可见的

    那么让我们假设你有一些东西来确定哪些表单或表单可能对通知做出反应。当某些事情发生时,这种情况就会发生变化:一个形式变得活跃,或者一个形式关闭,一个形式变得不可见,等等。

    为ManagedForm创建一个布尔属性,用于保存是否显示通知:

    class ManagedForm
    {
        public bool ShowNotifications {get; set;}
        public void OnEventNotification(object sender, ...)
        {
            if (this.ShowNotifications)
            {
                // show the notification
            }
        }
    

    现在必须有人知道哪个表单应该显示通知。这个人应该设置ShowNotification属性。

    例如,如果只有活动的ManagedForm应该显示通知,那么ManagedForm可以自己决定:

    public OnFormActiveChanged(object sender, ...)
    {
        this.ShowNotifications = this.Form.IsActive;
    }
    

    如果所有红色表单都应该显示通知:

    public OnFormBackColorChanged(object sender, ...)
    {
        this.ShowNotifications = this.Form.BackColor == Color.Red;
    }
    

    如果你有很多表单,只有少数显示通知,那么很多事件OnShowNotification将被调用为什么,但因为这只是一个函数调用它不会是一个问题,除非你显示1000个表单左右,我猜在你有更严重的问题。

    摘要

    • 决定ManagedForm显示通知的标准
    • 决定不同的表单何时应该显示通知
    • 创建一个事件处理程序,当表单改变时,让事件处理程序设置属性ShowNotification
    • 当显示通知的事件发生时,检查属性。

    如果您确实希望将这些事件传播到每个表单,订阅是有用的,但这似乎不是您想要做的。给定任何操作,您的代码只需要显示一个对话框并更新主表单的状态文本。

    也许你应该考虑使用单例模式。通过使用静态事件处理程序,这实际上是您已经在做的事情。

    public class MainAppForm : Form
    {
        static MainAppForm mainAppForm;
        public MainAppForm()
        {
            mainAppForm = this;
        }
        public static void NotifyOk(Form sender, string fullMessage = "Ok.", string shortMessage = null)
        {
            mainAppForm.NotifyOk(sender, fullMessage, shortMessage);
        }
        public void NotifyOk(Form sender, string fullMessage, string shortMessage)
        {
            this.statusStrip.Invoke(delegate {
                this.statusStrip.Text = shortMessage;
            });
        }
    }