[英]A design pattern to disable event handling
為了簡單說明我的困境,讓我說我有以下代碼:
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事件作出反應,但在某些情況下也能夠自行交替該屬性。 不幸的是,其他對象也可以與Property進行交互。
我需要一個順序流的解決方案。 也許我可以使用一個變量來禁用一個動作:
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;
}
有更好的方法嗎?
解決的子彈越多越好。
我最初的問題是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的方式。 所有這一切需要做的是B
的處理程序TextBox.TextChanged
需要設置e.Handled = _dontDoThis
。 如果B
的一個被添加為第一個聽眾,那將會為所有其他聽眾提供通知。 如何確保這種情況發生? 反射!
UIElement
只公開AddHandler
和RemoveHandler
方法,沒有允許手動指定處理程序優先級的InsertHandler
。 但是,快速查看.NET源代碼(下載整個內容或查詢所需內容 )可以發現AddHandler
將參數轉發給Interal方法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
我找到了一個與第三方有關的解決方案,這些解決方案與該屬性相關聯,我們不希望在該屬性發生變化時對它們進行nofify。
雖然有要求:
解決方案是用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;
}
}
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.