用于禁用事件处理的设计模式

本文关键字:设计模式 事件处理 用于 | 更新日期: 2023-09-27 18:26:24

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

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更新属性时,不应该也通知他们。但我无法将他们与活动分开,因为我不认识他们
  • 如果同时有更多的线程可以与属性交互呢

破获的子弹越多越好。

原始问题

我最初的问题是一个TextBox(WPF),我想根据它的内容和重点来补充它。所以我需要对TextChanged事件做出反应,如果它的来源是从我的补码派生的,我也需要省略那个事件。在某些情况下,不应通知TextChanged事件的其他侦听器。某些特定状态和样式的字符串对其他字符串是不可见的。

用于禁用事件处理的设计模式

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

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

然后在你的处理程序中——简单地检查一下你不是发起者。

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

实际上,您正试图破坏事件委派机制,而任何"解决方案"都将是脆弱的,因为BCL的更新可能会破坏您的代码。您可以使用反射设置背景场。当然,这需要你有这样做的权限,看到问题的一般框架,你可能并不总是拥有所需的权限

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

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

在我看来,您的解决方案是可能的,尽管我会在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"修饰符。

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

好问题。

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

既然你提供了上下文。。。在WPF中,有一种方法可以做到这一点。所需要做的就是BTextBox.TextChanged的处理程序需要设置e.Handled = _dontDoThis。如果添加了B的侦听器作为第一个侦听器,那么它将为所有其他侦听器提供通知。如何确保这种情况发生?反射

UIElement只公开AddHandlerRemoveHandler方法,没有允许手动指定处理程序优先级的InsertHandler。然而,快速浏览一下.NET源代码(下载全部内容或查询所需内容)就会发现,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

我找到了一个关于连接到该属性的第三方的解决方案,当该属性更改时,我们不想取消通知。

尽管有以下要求:

  • 我们有能力超越A
  • A有一个虚拟方法,该方法在属性更改时调用,并允许挂起要引发的事件
  • 更改属性时会立即引发事件

解决方案是用MyA替换A,如下所示:

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建议的重构解决方案(此处)。首先,创建一个一次性包装:

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