【问题标题】:A design pattern to disable event handling禁用事件处理的设计模式
【发布时间】:2013-11-13 18:04:45
【问题描述】:

为了简单说明我的困境,假设我有以下代码:

class A
{
    // May be set by a code or by an user.
    public string Property
    {
        set { PropertyChanged(this, EventArgs.Empty); }
    }

    public EventHandler PropertyChanged;
}

class B
{
    private A _a;

    public B(A a)
    {
        _a = a;
        _a.PropertyChanged += Handler;
    }

    void Handler(object s, EventArgs e)
    {
        // Who changed the Property?
    }

    public void MakeProblem()
    {
        _a.Property = "make a problem";
    }
}

为了履行职责,B 类必须对 A 的 PropertyChanged 事件做出反应,但也能够在某些情况下自行更改该属性。不幸的是,其他对象也可以与该属性交互。

我需要一个顺序流的解决方案。也许我可以使用一个变量来禁用一个动作:

bool _dontDoThis;

void Handler(object s, EventArgs e)
{
    if (_dontDoThis)
        return;

    // Do this!
}

public void MakeProblem()
{
    _dontDoThis = true;
    _a.Property = "make a problem";
    _dontDoThis = false;
}

有没有更好的方法?

其他注意事项

  • 我们无法更改 A。
  • A 已密封。
  • 还有其他各方与 PropertyChanged 事件有关,我不知道他们是谁。但是当我从 B 更新属性时,他们也不应该被通知。但我无法将他们与活动断开连接,因为我不认识他们。
  • 如果同时还有更多线程可以与属性交互呢?

解决的子弹越多越好。

原来的问题

我最初的问题是一个文本框 (WPF),我想根据它的内容和焦点来补充它。所以我需要对 TextChanged 事件做出反应,如果它的起源来自我的补充,我还需要省略该事件。在某些情况下,不应通知 TextChanged 事件的其他侦听器。某些状态和样式的某些字符串对其他人是不可见的。

【问题讨论】:

  • 我认为有一个 TextInput 事件可以满足您的需求。我也遇到了这个问题(另一个事件),我就是这样做的。
  • @It'sNotALie。是的,在那个特定的问题上,它可能是一个足够的解决方案。谢谢你。但我仍然会寻找更一般的东西......
  • 要回答您的原始问题,解决此类问题的一般方法是使用某种标志。在使用模式方面,我已经回答了与 here 类似的问题,但还有其他建议绝对可以为您完成这项工作。
  • @James,我想链接我自己的问题,只是想知道你打败了我。所以 +1 给你 :)
  • @Jordy 它与重复并不遥远,但因为 OP 并没有完全关注它是一个 UI 问题,所以我决定不标记它。

标签: c# wpf design-patterns


【解决方案1】:

如果不处理您发起的事件非常重要,也许您应该更改设置属性的方式以包含更改的发起者?

public class MyEventArgs : EventArgs
{
    public object Changer;
}

public void SetProperty(string p_newValue, object p_changer)
{
   MyEventArgs eventArgs = new MyEventArgs { Changer = p_changer };
   PropertyChanged(this, eventArgs); 
}

然后在您的处理程序中 - 只需检查您不是发起者。

我发现注册和成员的所有这些变化在多线程和可扩展性方面都存在很大问题。

【讨论】:

    【解决方案2】:

    本质上,您正在尝试破坏事件委托机制,并且任何“解决方案”都会变得脆弱,因为对 BCL 的更新可能会破坏您的代码。您可以使用反射设置支持字段。这当然需要您确实有权执行此操作,并且看到问题的一般框架可能并不总是您拥有所需的权限

    public void MakeProblem()
    {
      if (_backingField == null) {
        _backingField = = _a.GetType().GetField(fieldname)
      }
      _backingField.SetValue(_a,"make a problem");
    }
    

    但在我刚开始时,您正试图破坏事件委托机制。这个想法是事件的接收者是独立的。禁用可能会导致很难找到错误,因为查看任何给定的代码看起来都是正确的,但只有当您意识到某些狡猾的开发人员已经破解了委托机制时,您才会意识到为什么屏幕上显示的信息似乎是实际值的缓存版本。调试器显示属性的预期值,但由于事件被隐藏,负责更新显示的处理程序从未被触发,因此显示旧版本(或日志显示不正确的信息,因此当您尝试重新创建问题时用户已根据日志内容报告您将无法报告,因为日志中的信息不正确,因为它基于没有人破解事件委托机制

    【讨论】:

      【解决方案3】:

      在我看来,您的解决方案是可能的,尽管我会在 B 中创建一个嵌套的 IDisposable 类,它使用“using”执行相同的操作,或者将“_dontDoThis = false”放在“finally”子句中。

      class A
      {
          // May be set by a code or by an user.
          public string Property
          {
              set { if (!_dontDoThis) PropertyChanged(this, EventArgs.Empty); }
          }
      
          public EventHandler PropertyChanged;
          bool _dontDoThis;
      }
      
      class B
      {
      
          private class ACallWrapper : IDisposable
          {
              private B _parent;
              public ACallWrapper(B parent)
              {
                  _parent = parent;
                  _parent._a._dontDoThis = true;
              }
      
              public void Dispose()
              {
                  _parent._a._dontDoThis = false;
              }
          }
      
          private A _a;
      
          public B(A a)
          {
              _a = a;
              _a.PropertyChanged += Handler;
          }
      
          void Handler(object s, EventArgs e)
          {
              // Who changed the Property?
          }
      
          public void MakeProblem()
          {
              using (new ACallWrapper(this))
                  _a.Property = "make a problem";
          }
      }
      

      另一方面,如果这两个类在同一个程序集中,我会使用“内部”修饰符来处理这些事情。

      internal bool _dontDoThis;
      

      这样,您可以保持更好的 OOP 设计。

      此外,如果两个类都在同一个程序集中,我会在 A 中编写以下代码:

          // May be set by a code or by an user.
          public string Property
          {
              set 
              { 
                  internalSetProperty(value);
                  PropertyChanged(this, EventArgs.Empty); 
              }
          }
          internal internalSetProperty(string value)
          {
              // Code of set.
          }
      

      在这种情况下,B 可以访问 internalSetProperty 而不会触发 PropertyChanged 事件。

      线程同步:
      注意:下一部分适用于 WinForms - 我不知道它是否适用于 WPF好吧。
      对于线程同步,因为我们指的是控件。您可以使用 GUI 线程机制进行同步:

      class A : Control
      {
          public string Property
          {
              set 
              { 
                  if (this.InvokeRequired) 
                  {
                      this.Invoke((Action<string>)setProperty, value);
                      reutrn;
                  }
                  setProperty(value);
              }
          }
      
      
          private void setProperty string()
          {
              PropertyChanged(this, EventArgs.Empty); 
          }
      }
      

      【讨论】:

        【解决方案4】:

        好问题。

        一般情况下,您不能乱用密封类的事件处理程序。通常,您可以覆盖A 的假设OnPropertyChanged 并根据某些标志引发或不引发事件。或者,您可以提供一个不引发事件的 setter 方法,正如@Vadim 所建议的那样。但是,如果 A 是密封的,您最好的选择是向列表器添加标志,就像您所做的那样。这将使您能够识别由B 引发的PropertyChanged 事件,但您将无法为其他侦听器抑制该事件。

        现在,既然您提供了上下文... 一种在 WPF 中执行此操作的方法。所有需要做的是BTextBox.TextChanged 处理程序需要设置e.Handled = _dontDoThis。如果B 的一个被添加为第一个,这将抑制所有其他听众的通知。如何确保发生这种情况?反思!

        UIElement 仅公开AddHandlerRemoveHandler 方法,没有InsertHandler 允许手动指定处理程序的优先级。但是,快速查看 .NET 源代码(下载 the whole thingquery what you need)会发现 AddHandler 将参数转发给内部方法 EventHandlersStore.AddRoutedEventHandler,它执行以下操作:

        // Create a new RoutedEventHandler 
        RoutedEventHandlerInfo routedEventHandlerInfo =
            new RoutedEventHandlerInfo(handler, handledEventsToo); 
        
        // Get the entry corresponding to the given RoutedEvent
        FrugalObjectList<RoutedEventHandlerInfo> handlers = (FrugalObjectList<RoutedEventHandlerInfo>)this[routedEvent];
        if (handlers == null) 
        {
            _entries[routedEvent.GlobalIndex] = handlers = new FrugalObjectList<RoutedEventHandlerInfo>(1); 
        } 
        
        // Add the RoutedEventHandlerInfo to the list 
        handlers.Add(routedEventHandlerInfo);
        

        所有这些东西都是内部的,但可以使用反射重新创建:

        public static class UIElementExtensions
        {
            public static void InsertEventHandler(this UIElement element, int index, RoutedEvent routedEvent, Delegate handler)
            {
                // get EventHandlerStore
                var prop = typeof(UIElement).GetProperty("EventHandlersStore", BindingFlags.NonPublic | BindingFlags.Instance);
                var eventHandlerStoreType = prop.PropertyType;
                var eventHandlerStore = prop.GetValue(element, new object[0]);
        
                // get indexing operator
                PropertyInfo indexingProperty = eventHandlerStoreType.GetProperties(BindingFlags.NonPublic | BindingFlags.Instance)
                    .Single(x => x.Name == "Item" && x.GetIndexParameters().Length == 1 && x.GetIndexParameters()[0].ParameterType == typeof(RoutedEvent));
        
                object handlers = indexingProperty.GetValue(eventHandlerStore, new object[] { routedEvent });
                if (handlers == null)
                {
                    // just add the handler as there are none at the moment so it is going to be the first one
                    if (index != 0)
                    {
                        throw new ArgumentOutOfRangeException("index");
                    }
                    element.AddHandler(routedEvent, handler);
                }
                else
                {
                    // create routed event handler info
                    var constructor = typeof(RoutedEventHandlerInfo).GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance).Single();
                    var handlerInfo = constructor.Invoke(new object[] { handler, false });
        
                    var insertMethod = handlers.GetType().GetMethod("Insert");
                    insertMethod.Invoke(handlers, new object[] { index, handlerInfo });
                }
            }
        }
        

        现在调用 InsertEventHandler(0, textBox, TextBox.TextChangedEvent, new TextChangedEventHandler(textBox_TextChanged)) 将确保您的处理程序将是列表中的第一个,使您能够禁止其他侦听器的通知!

        public partial class MainWindow : Window
        {
            public MainWindow()
            {
                InitializeComponent();
        
                var textBox = new TextBox();
                textBox.TextChanged += (o, e) => Console.WriteLine("External handler");
                var b = new B(textBox);
                textBox.Text = "foo";
                b.MakeProblem();
            }
        }
        
        class B
        {
            private TextBox _a;
            bool _dontDoThis;
        
            public B(TextBox a)
            {
                _a = a;
                a.InsertEventHandler(0, TextBox.TextChangedEvent, new TextChangedEventHandler(Handler));
            }
        
            void Handler(object sender, TextChangedEventArgs e)
            {
                Console.WriteLine("B.Handler");
                e.Handled = _dontDoThis;
                if (_dontDoThis)
                {
                    e.Handled = true;
                    return;
                }
                // do this!
            }
        
            public void MakeProblem()
            {
                try
                {
                    _dontDoThis = true;
                    _a.Text = "make a problem";
                }
                finally
                {
                    _dontDoThis = false;
                }
        
            }
        }
        

        输出:

        B.Handler
        External handler
        B.Handler
        

        【讨论】:

        • 我认为为了总结这一点,您的建议是插入一个事件处理程序作为第一个,然后抑制事件传播。当我们有两件事时,这听起来是个不错的选择:1. 事件插入方法和 2. 带有抑制的事件参数。那么我们就不需要基类中的任何虚方法负责事件分发,也不需要用子类替换类。
        • 没错。据我了解,这是最初的情况,不是吗?
        • 是的,原来是这样的(WPF中的TextBox.Text)
        • 是的,这是解决方案之一。
        【解决方案5】:

        我找到了一种与第三方相关的解决方案,该解决方案与财产相关,我们不想在财产发生变化时通知他们。

        虽然有要求:

        • 我们能够覆盖 A.
        • A 有一个虚拟方法,在属性更改时调用该方法并允许暂停要引发的事件。
        • 更改属性时立即引发该事件。

        解决办法是把A换成MyA,如下:

        class A
        {
            // May be set by a code or by an user.
            public string Property
            {
                set { OnPropertyChanged(EventArgs.Empty); }
            }
        
            // This is required
            protected virtual void OnPropertyChanged(EventArgs e)
            {
                PropertyChanged(this, e);
            }
        
            public EventHandler PropertyChanged;
        }
        
        // Inject MyA instead of A
        class MyA : A
        {
            private bool _dontDoThis;
        
            public string MyProperty
            {
                set
                {
                    try
                    {
                        _dontDoThis = true;
                        Property = value;
                    }
                    finally
                    {
                        _dontDoThis = false;
                    }
                }
            }
        
            protected override void OnPropertyChanged(EventArgs e)
            {
                // Also third parties will be not notified
                if (_dontDoThis)
                    return;
        
                base.OnPropertyChanged(e);
            }
        }
        
        class B
        {
            private MyA _a;
        
            public B(MyA a)
            {
                _a = a;
                _a.PropertyChanged += Handler;
            }
        
            void Handler(object s, EventArgs e)
            {
                // Now we know, that the event is not raised by us.
            }
        
            public void MakeProblem()
            {
                _a.MyProperty = "no problem";
            }
        }
        

        不幸的是,我们仍然使用 back bool 字段并且我们假设一个线程。为了摆脱第一个,我们可以使用EZSlaver (here) 建议的重构解决方案。首先,创建一个一次性包装器:

        class Scope
        {
            public bool IsLocked { get; set; }
        
            public static implicit operator bool(Scope scope)
            {
                return scope.IsLocked;
            }
        }
        
        class ScopeGuard : IDisposable
        {
            private Scope _scope;
        
            public ScopeGuard(Scope scope)
            {
                _scope = scope;
                _scope.IsLocked = true;
            }
        
            public void Dispose()
            {
                _scope.IsLocked = false;
            }
        }
        

        那么 MyProperty 可能会被重构为:

        private readonly Scope _dontDoThisScope = new Scope();
        
        public string MyProperty
        {
            set
            {
                using (new ScopeGuard (_dontDoThisScope))
                    Property = value;
            }
        }
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2012-11-03
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多