【问题标题】:How to disable subscription to an event from many instances of one type and allow only one?如何禁用从一种类型的许多实例订阅事件并只允许一个?
【发布时间】:2015-10-14 13:25:10
【问题描述】:

我的 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 显示消息。正如你所看到的,我在事件访问器中添加了一些魔法,只允许任何类型的一个订阅者,因为没有这些,所有打开的表单都会生成它们自己的MessageBoxes。但是当一个孩子 ManagedForm 产生另一个孩子并且第二个孩子已经关闭时 - 不会显示 MessageBoxes。
我应该实施什么魔法才能只允许从第一个 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;
    }
}

然后我打开一个模态子窗体并创建一个事件已被NotifierService 触发的情况。一个MessageBox 已生成并显示(没关系)。之后,我从一开始就打开了另一个模态表单,并创建了另一个触发另一个事件的情况。一个MessageBox 已生成并显示(也可以)。现在我正在关闭第二种形式并制作触发事件所需的情况。没有显示MessageBoxes(但在主窗体的状态条中事件消息已正确显示,因此与我的第一个实现相比没有任何改变)。
我应该更改remove 子句中的某些内容吗?我不需要只有一个订阅者,我需要每个订阅者应该是不同的类型。对不起,如果英语不好。

【问题讨论】:

  • 你能添加一个布尔标志来表示第一次订阅吗?
  • @rawnald-gregory-erickson 谢谢,但不,我不能。这是个坏主意,因为当我的主表单订阅一个事件时——标志将变为true,因此所有其他子表单都将被取消资格。而且有很多事件,不仅仅是一个。
  • @xtnd8:根据页面msdn.microsoft.com/en-us/library/8edha89s.aspx += 运算符不能重载,但它使用 + 可以重载。也许你可以重载 + 运算符:)?
  • @P.K. : 不可能(看here)。
  • @xtnd: 所以你不能重载等于 x = x+y 的 x+=y 并将其重载到 x = 0 + y?只是想:)

标签: c# .net winforms events delegates


【解决方案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> 
     }
    

【讨论】:

  • 这对 xtnd8 有帮助吗?
【解决方案2】:

试试这个:?)

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

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

【讨论】:

    【解决方案3】:

    我已将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
        }
    }
    

    如果表单以模态打开(使用ShowDialog()),则可以使用另一个变体(根据question):

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

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

    P.S.:在 Dispose 上取消订阅处理程序

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

    【讨论】:

      【解决方案4】:

      我已经进行了类似于您的设置,我发现了问题。

      我会给出 2 个可行的建议来解决这个问题(您可以根据需要进行选择) -

      1. 最快的修复,只需对原始代码进行少量更改 -

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

      我假设您编写的代码是在表单关闭时从事件NotifierService.OnOk 中解开事件处理程序 但我不确定你什么时候将事件NotifierService.OnOk 以托管形式连接到它的事件处理程序。这很关键,我想这是您设置中的唯一问题

      我假设你已经将它设置在一个在表单的生命周期中只发生一次的地方——比如构造函数或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;
          }
      

      无需更多更改,您在事件中的现有逻辑将负责休息。 我已在上面的代码中将原因写为注释。

      1. 更好的方法,但需要更多的改变

      我想解除事件 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;
          }
      

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

      【讨论】:

        【解决方案5】:

        您尝试解决问题的方式在设计上是根本错误的。您的服务类定义了在某些情况下将触发的事件。一些客户端订阅了该事件,以这种方式请求在事件发生时得到通知。这只是实现Observer pattern 的 .NET 方式,因此您的服务(作为主题或可观察的)不应在订阅和通知部分应用任何逻辑,从而违背模式的全部目的。 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));
            }
        }
        

        假设只有活动表单应该处理通知,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
        }
        

        并让您的 MainFormManagedForm(以及任何其他需要的)从那里继承并覆盖 OnNotifyXXX 方法并应用它们的逻辑。

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

        (B) 如果您的服务的唯一目的是充当专门针对您的表单的通​​知协调器,那么您可以删除事件以及订阅/取消订阅部分(因为 Application.OpenForms 和 @ 987654332@ 已经提供了足够的信息)并处理您的服务中的逻辑。为此,您将需要某种基本接口或表单,最简单的方法是通过创建这样的基本表单类来使用与选项 (A) 中的可选内容类似的方法

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

        并让您的MainFormManagedForm 和其他需要的继承自它。请注意,这里没有逻辑(检查 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)更好,因为它需要更少的客户端(因此更不容易出错)并在一个地方提供集中的应用程序逻辑。

        【讨论】:

          【解决方案6】:

          总结一下:

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

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

          做一个简单的事件

          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 LoadedUnloaded,但在 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;
          }
          

          【讨论】:

            【解决方案7】:

            您确定是 NotifierService 的任务是确保只有一个表单会显示通知吗?

            如果您要描述 NotifierService 的任务,您会描述它的作用以及“只要 NotifierService 有要通知的内容,它就会通知所有说它希望收到通知的人”

            这将使您的通知服务更少依赖于使用它的当前应用程序。如果你想要一个完全不同的应用程序,例如只有两个表单,你希望两个表单都对通知做出反应,你不能使用这个通知服务。

            但在我的表单应用程序中,只有一个表单可以对通知做出反应

            没错:具有此约束的是您的 Forms 应用程序,而不是通知服务。您制作了一个可能使用任何类型的通知服务的表单应用程序,但无论使用什么通知服务,我的应用程序中只有一个表单可以显示通知。

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

            例如:

            • 只有当前表单可以显示通知
            • 只有左上角的表单可以显示通知
            • 只有主窗体可以显示通知,设置窗体可见时除外

            所以让我们假设您有一些东西可以确定哪些表单或哪些表单可能会对通知做出反应。这会随着某件事的发生而改变:一个表单变得活跃,或者一个表单关闭,一个表单变得不可见,等等。

            为 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
            • 当显示通知的事件发生时,检查属性。

            【讨论】:

              【解决方案8】:

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

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

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

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2020-05-09
                • 1970-01-01
                • 2019-11-03
                • 1970-01-01
                • 1970-01-01
                • 2020-03-19
                相关资源
                最近更新 更多