解讀WPF中的Binding


1.Overview

基於MVVM實現一段綁定大伙都不陌生,Binding是wpf整個體系中最核心的對象之一這里就來解讀一下我花了純兩周時間有哪些秘密。這里我先提出幾個問題應該是大家感興趣的,如下:

(1)INotifyPropertyChanged是如何被加載、觸發的(Binding如何完成數據更新的)?

(2)為什么需要開發者手動實現INotifyPropertyChanged接口來為每個成員實現數據通知,為什么不集成在wpf框架里?

(3)藏在WPF體系里的觀察者模式在哪里?

 

2.Detail

想了解以上問題,我們先補充以下前置知識點。

我們帶着以上幾個問題來看本文的后續內容,首先我們通過下面這張圖來了解綁定的過程。

 

 

根據以上過程我們可以基於MVVM模式下,在Xaml中寫出這樣的語句來表示綁定。

<TextBoxName="mytextbox"Height="25"Width="150"Text="{BindingPath=Name,Mode=**TwoWay**,UpdateSourceTrigger=**PropertyChanged**}"></TextBox>

那么如果把他轉換成c#代碼,將會是如下表示。

public string BeachName{ get; set; } private void Test() { BeachName="BikiniBeach"; TextBoxtextBox = newTextBox(); textBox.Name = "myTextBox"; Binding binding = new Binding(); binding.Source = BeachName; binding.Path = new PropertyPath("BeachName"); textBox.SetBinding(TextBox.TextProperty, binding); } 

(1-1)

上面這段代碼,包含了兩個關鍵對象Textbox和Binding它們里面大有文章首先我們逐個拆解這兩個對象里都有什么。

Textbox

在(1-1)的代碼中初始化一個Textbox對象,它會創建一個依賴屬性TextProperty用於綁定要素之一。

public static readonly DependencyProperty TextProperty = DependencyProperty.Register(nameof (Text), typeof (string), typeof (TextBox), (PropertyMetadata) new FrameworkPropertyMetadata((object) string.Empty, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault | FrameworkPropertyMetadataOptions.Journal, new PropertyChangedCallback(TextBox.OnTextPropertyChanged), new CoerceValueCallback(TextBox.CoerceText), true, UpdateSourceTrigger.LostFocus)); 

Binding

 

 

 

當我們在日常開發實現綁定過程當中,WPF的體系會默默幫你創建Binding對象,這里我們來看看Binding包含了哪些定義(為了觀看體驗刪除了大部分不相關代碼)。

namespace System.Windows.Data { public class Binding : BindingBase { //....其它代碼省略  public static void AddSourceUpdatedHandler( DependencyObject element, EventHandler<DataTransferEventArgs> handler) { UIElement.AddHandler(element, Binding.SourceUpdatedEvent, (Delegate) handler); } public static void RemoveSourceUpdatedHandler( DependencyObject element, EventHandler<DataTransferEventArgs> handler) { UIElement.RemoveHandler(element, Binding.SourceUpdatedEvent, (Delegate) handler); } public static void AddTargetUpdatedHandler( DependencyObject element, EventHandler<DataTransferEventArgs> handler) { UIElement.AddHandler(element, Binding.TargetUpdatedEvent, (Delegate) handler); } public static void RemoveTargetUpdatedHandler( DependencyObject element, EventHandler<DataTransferEventArgs> handler) { UIElement.RemoveHandler(element, Binding.TargetUpdatedEvent, (Delegate) handler); } public Binding(string path) { if (path == null) return; if (Dispatcher.CurrentDispatcher == null) throw new InvalidOperationException(); this.Path = new PropertyPath(path, (object[]) null); } public PropertyPath Path { get => this._ppath; set { this.CheckSealed(); this._ppath = value; this._attachedPropertiesInPath = -1; this.ClearFlag(BindingBase.BindingFlags.PathGeneratedInternally); if (this._ppath == null || !this._ppath.StartsWithStaticProperty) return; if (this._sourceInUse == Binding.SourceProperties.None || this._sourceInUse == Binding.SourceProperties.StaticSource || FrameworkCompatibilityPreferences.TargetsDesktop_V4_0) this.SourceReference = Binding.StaticSourceRef; else throw new InvalidOperationException(SR.Get("BindingConflict", (object) Binding.SourceProperties.StaticSource, (object) this._sourceInUse)); } }   [DefaultValue(BindingMode.Default)] public BindingMode Mode { get { switch (this.GetFlagsWithinMask(BindingBase.BindingFlags.PropagationMask)) { case BindingBase.BindingFlags.OneTime: return BindingMode.OneTime; case BindingBase.BindingFlags.OneWay: return BindingMode.OneWay; case BindingBase.BindingFlags.OneWayToSource: return BindingMode.OneWayToSource; case BindingBase.BindingFlags.TwoWay: return BindingMode.TwoWay; case BindingBase.BindingFlags.PropDefault: return BindingMode.Default; default: Invariant.Assert(false, "Unexpected BindingMode value"); return BindingMode.TwoWay; } } set { this.CheckSealed(); BindingBase.BindingFlags flags = BindingBase.FlagsFrom(value); if (flags == BindingBase.BindingFlags.IllegalInput) throw new InvalidEnumArgumentException(nameof (value), (int) value, typeof (BindingMode)); this.ChangeFlagsWithinMask(BindingBase.BindingFlags.PropagationMask, flags); } }   [DefaultValue(UpdateSourceTrigger.Default)] public UpdateSourceTrigger UpdateSourceTrigger { get { switch (this.GetFlagsWithinMask(BindingBase.BindingFlags.UpdateDefault)) { case BindingBase.BindingFlags.OneTime: return UpdateSourceTrigger.PropertyChanged; case BindingBase.BindingFlags.UpdateOnLostFocus: return UpdateSourceTrigger.LostFocus; case BindingBase.BindingFlags.UpdateExplicitly: return UpdateSourceTrigger.Explicit; case BindingBase.BindingFlags.UpdateDefault: return UpdateSourceTrigger.Default; default: Invariant.Assert(false, "Unexpected UpdateSourceTrigger value"); return UpdateSourceTrigger.Default; } } set { this.CheckSealed(); BindingBase.BindingFlags flags = BindingBase.FlagsFrom(value); if (flags == BindingBase.BindingFlags.IllegalInput) throw new InvalidEnumArgumentException(nameof (value), (int) value, typeof (UpdateSourceTrigger)); this.ChangeFlagsWithinMask(BindingBase.BindingFlags.UpdateDefault, flags); } }  [DefaultValue(false)] public bool NotifyOnSourceUpdated { get => this.TestFlag(BindingBase.BindingFlags.NotifyOnSourceUpdated); set { if (this.TestFlag(BindingBase.BindingFlags.NotifyOnSourceUpdated) == value) return; this.CheckSealed(); this.ChangeFlag(BindingBase.BindingFlags.NotifyOnSourceUpdated, value); } }  [DefaultValue(false)] public bool NotifyOnTargetUpdated { get => this.TestFlag(BindingBase.BindingFlags.NotifyOnTargetUpdated); set { if (this.TestFlag(BindingBase.BindingFlags.NotifyOnTargetUpdated) == value) return; this.CheckSealed(); this.ChangeFlag(BindingBase.BindingFlags.NotifyOnTargetUpdated, value); } }  [DefaultValue(null)] public IValueConverter Converter { get => (IValueConverter) this.GetValue(BindingBase.Feature.Converter, (object) null); set { this.CheckSealed(); this.SetValue(BindingBase.Feature.Converter, (object) value, (object) null); } } public object Source { get { WeakReference<object> weakReference = (WeakReference<object>) this.GetValue(BindingBase.Feature.ObjectSource, (object) null); if (weakReference == null) return (object) null; object target; return !weakReference.TryGetTarget(out target) ? (object) null : target; } set { this.CheckSealed(); if (this._sourceInUse == Binding.SourceProperties.None || this._sourceInUse == Binding.SourceProperties.Source) { if (value != DependencyProperty.UnsetValue) { this.SetValue(BindingBase.Feature.ObjectSource, (object) new WeakReference<object>(value)); this.SourceReference = (ObjectRef) new ExplicitObjectRef(value); } else { this.ClearValue(BindingBase.Feature.ObjectSource); this.SourceReference = (ObjectRef) null; } } else throw new InvalidOperationException(SR.Get("BindingConflict", (object) Binding.SourceProperties.Source, (object) this._sourceInUse)); } } internal override BindingExpressionBase CreateBindingExpressionOverride( DependencyObject target, DependencyProperty dp, BindingExpressionBase owner) { return (BindingExpressionBase) BindingExpression.CreateBindingExpression(target, dp, this, owner); } } } 

Binding對象繼承自BindingBase,在Binding類中我們可以看到CreateBindingExpressionOverride這個方法,這個方法來自父類BindingBase。代碼中的BindingExpression是“綁定表達式”的意思,在CreateBindingExpression中入參完美的闡述了綁定關系;

internal override BindingExpressionBase CreateBindingExpressionOverride( DependencyObject target, DependencyProperty dp, BindingExpressionBase owner) { return (BindingExpressionBase) BindingExpression.CreateBindingExpression(target, dp, this, owner); } internal static BindingExpression CreateBindingExpression( DependencyObject d, DependencyProperty dp, Binding binding, BindingExpressionBase parent) { if (dp.GetMetadata(d.DependencyObjectType) is FrameworkPropertyMetadata metadata && !metadata.IsDataBindingAllowed || dp.ReadOnly) throw new ArgumentException(System.Windows.SR.Get("PropertyNotBindable", (object) dp.Name), nameof (dp)); BindingExpression bindingExpression = new BindingExpression(binding, parent); bindingExpression.ResolvePropertyDefaultSettings(binding.Mode, binding.UpdateSourceTrigger, metadata); if (bindingExpression.IsReflective && binding.XPath == null && (binding.Path == null || string.IsNullOrEmpty(binding.Path.Path))) throw new InvalidOperationException(System.Windows.SR.Get("TwoWayBindingNeedsPath")); return bindingExpression; } 

(1)DependencyObject,是所有控件的基類這里我們在當前環境中可以理解為Textbox。

(2)DependencyProperty,是我們要綁定的控件中的TextProperty依賴屬性。

(3)Binding,表達了數據源、綁定目標、綁定模式、更新通知觸發類型等信息。

 

 

創建binding對象,建立綁定表達式CreateBindingExpression將依賴屬性和控件、綁定對象關聯起來->BindingExpression該方法將Path傳給 TraceData.Trace追蹤對象。在Binding繼承的BindingBase.cs中實現了CreateBindingExpression(創建綁定表達式,它的作用就是用來“描述”綁定的整個過程)

[BindingExpression作用-1]

該對象提供了綁定更新的機制,UpdateSourceTrigger.Explicit 模式用來控制源對象的更新時機。

(1)調用 BindingExpression.UpdateSource()和 UpdateTarget( )方法,觸發立即刷新行為。

(2)獲取 BindingExpression對象,需要使用 GetBindingExpression( )方法。

BindingExpiression binding = txtFontSize.GetBindingExpression(TextBox ,TextProperty); binding.UpdateSource()

要完全控制源對象的更新時機,可選擇 UpdateSourceTrigger.ExpUdt 模式。如果在文本框示 例中使用這種方法,當文本框失去焦點后不會發生任何事情 反而,由您編寫代碼手動觸發更 新。例如,可添加 Apply 按鈕,調用 BindingExpression.UpdateSource()方法,觸發立即刷新行為並更新字體尺寸。 當然,在調用 BindingExpressiorLUpdateSource( )之前 ,需要 一 種方法 來獲取 BindingExpression 對象。BindingExpressicm 對象僅是將兩項內容封裝到一起的較小組裝包,這 兩項內容是:己經學習過的 Binding 對象(通過 BindingExpression.ParentBinding 屬性提供)和由 源綁定的對象(BindingExpression.Dataltem)a 此外,BindingExpression 對象為觸發立即更新綁定 的-部分提供了兩個方法:UpdateSource( )和 UpdateTarget( )方法, 為聯取 BindingExpressiori 對象,需要使用 GetBindingExpression( )方法,並傳入具有綁定的 目標屬性,每個元素都從 FrameworkEkment 類繼承了該方法。

可為每個屬性引發事件。對於這種情況,事件必須以 的形式迸行命 名(如 UnitCostChanged)當屬性變化時,由您負責引發事件。 可實現 System.ComponentModel.INotifyPropertyChanged 接口,該接口需要名為 PropertyChanged 的事件。無論何時屬性發生變化,都必須引發 PropertyChanged 事件,並 且通過將屬性名稱作為字符串提供來指示哪個屬性發生了變化。當屬性發生變化時,仍 由您負責引發事件,但不必為每個屬性定義單獨的事件& 第一種方法依賴於 WPF 的依賴項屬性基礎架構,而第二種和第三種方法依賴於事件,通 常,當創建數據對象時,會使用第三種方法。對於非元素類而言,這是最簡單的選擇。實際上,還可使用另一種方法如果懷疑綁定對象已經發生變化,並且綁定對象不支持任 何恰當方 式的更改通知,這時可檢索 BindingExpression 對象(使用 FrameworkElement. GetBmdingExpression()方法),並調用 BindingExpresskm.UpdateTarget()方法來觸發更新, 這是最憨的解決方案。

[BindingExpression作用-2]

BindingExpression繼承自BindingExpressionBase除了表述綁定關系以外,還創建了BindingWorker對象(下面為關鍵代碼)這里所要講的就是INotifyPropertyChanged是如何被加載、觸發的。

private BindingWorker _worker; private void CreateWorker() { Invariant.Assert(this.Worker == null, "duplicate call to CreateWorker"); this._worker = (BindingWorker) new ClrBindingWorker(this, this.Engine); } 

上面代碼將_worker初始化為ClrBindingWorker,它里面又包含PropertyPathWorker對象,PropertyPathWorker這個對象中有一個方法UpdateSourceValueState,它會從上層引用中拿到ViewModel的引用(引用會逐層從Binding類的層面逐層傳遞進來)然后會判斷這個ViewModel是否繼承了INotifyPropertyChanged如果繼承了則找到public event PropertyChangedEventHandler PropertyChanged;的引用並進行管理。

else if (newO is INotifyPropertyChanged source15) PropertyChangedEventManager.AddHandler(source15, new EventHandler<PropertyChangedEventArgs>(this.OnPropertyChanged), this.SVI[k].propertyName); 

ViewModel.PropertyChangedEventHandler的我們開發者定義好的通知事件,添加進入到PropertyChangedEventManager中進行管理,這個時候我們在給ViewModel里的變量Set值能通知界面更改就這么來的;下面為PropertyChangedEventManager.cs部分源碼(這里的Manager類似於觀察者模式)。

//將ViewModel里的PropertyChangedEventHandler PropertyChanged;添加監聽 private void AddListener( INotifyPropertyChanged source, string propertyName, IWeakEventListener listener, EventHandler<PropertyChangedEventArgs> handler) { using (this.WriteLock) { HybridDictionary hybridDictionary = (HybridDictionary) this[(object) source]; if (hybridDictionary == null) { hybridDictionary = new HybridDictionary(true); this[(object) source] = (object) hybridDictionary; this.StartListening((object) source); } WeakEventManager.ListenerList list = (WeakEventManager.ListenerList) hybridDictionary[(object) propertyName]; if (list == null) { list = (WeakEventManager.ListenerList) new WeakEventManager.ListenerList<PropertyChangedEventArgs>(); hybridDictionary[(object) propertyName] = (object) list; } if (WeakEventManager.ListenerList.PrepareForWriting(ref list)) hybridDictionary[(object) propertyName] = (object) list; if (handler != null) list.AddHandler((Delegate) handler); else list.Add(listener); hybridDictionary.Remove((object) PropertyChangedEventManager.AllListenersKey); this._proposedAllListenersList = (WeakEventManager.ListenerList) null; this.ScheduleCleanup(); } } //這里每次在ViewModel里給變量Set值的之后就是通過OnPropertyChanged通知界面更改值,sender是ViewModel對象 private void OnPropertyChanged(object sender, PropertyChangedEventArgs args) { string propertyName = args.PropertyName; WeakEventManager.ListenerList list; using (this.ReadLock) { HybridDictionary hybridDictionary = (HybridDictionary) this[sender]; if (hybridDictionary == null) list = WeakEventManager.ListenerList.Empty; else if (!string.IsNullOrEmpty(propertyName)) { WeakEventManager.ListenerList<PropertyChangedEventArgs> listenerList1 = (WeakEventManager.ListenerList<PropertyChangedEventArgs>) hybridDictionary[(object) propertyName]; WeakEventManager.ListenerList<PropertyChangedEventArgs> listenerList2 = (WeakEventManager.ListenerList<PropertyChangedEventArgs>) hybridDictionary[(object) string.Empty]; if (listenerList2 == null) list = listenerList1 == null ? WeakEventManager.ListenerList.Empty : (WeakEventManager.ListenerList) listenerList1; else if (listenerList1 != null) { list = (WeakEventManager.ListenerList) new WeakEventManager.ListenerList<PropertyChangedEventArgs>(listenerList1.Count + listenerList2.Count); int index1 = 0; for (int count = listenerList1.Count; index1 < count; ++index1) list.Add(listenerList1.GetListener(index1)); int index2 = 0; for (int count = listenerList2.Count; index2 < count; ++index2) list.Add(listenerList2.GetListener(index2)); } else list = (WeakEventManager.ListenerList) listenerList2; } else { list = (WeakEventManager.ListenerList) hybridDictionary[(object) PropertyChangedEventManager.AllListenersKey]; if (list == null) { int capacity = 0; foreach (DictionaryEntry dictionaryEntry in hybridDictionary) capacity += ((WeakEventManager.ListenerList) dictionaryEntry.Value).Count; list = (WeakEventManager.ListenerList) new WeakEventManager.ListenerList<PropertyChangedEventArgs>(capacity); foreach (DictionaryEntry dictionaryEntry in hybridDictionary) { WeakEventManager.ListenerList listenerList = (WeakEventManager.ListenerList) dictionaryEntry.Value; int index = 0; for (int count = listenerList.Count; index < count; ++index) list.Add(listenerList.GetListener(index)); } this._proposedAllListenersList = list; } } list.BeginUse(); } try { this.DeliverEventToList(sender, (EventArgs) args, list); } finally { list.EndUse(); } if (this._proposedAllListenersList != list) return; using (this.WriteLock) { if (this._proposedAllListenersList != list) return; HybridDictionary hybridDictionary = (HybridDictionary) this[sender]; if (hybridDictionary != null) hybridDictionary[(object) PropertyChangedEventManager.AllListenersKey] = (object) list; this._proposedAllListenersList = (WeakEventManager.ListenerList) null; } } 

[BindingExpression作用-3]

 

 

這里主要講述,如果直接在文本框內直接修改數據Binding是如何更新通知的(View->ViewModel)。

1.創建Binding對象,建立綁定表達式CreateBindingExpression將依賴屬性和控件、綁定對象關聯起來->BindingExpression該方法將Path傳給 TraceData.Trace追蹤Path。

2.手動在Textbox中輸入內容則會被控件中的OnPreviewTextInput事件捕捉到,最后由BindingExpressionBase.OnPreviewTextInput觸發Drity方法。

[特別分享:這里的Dirty命名我覺得很有造詣,這里分享一下我的理解Dirty直接翻譯為‘臟’這個字如何去理解,舉例:下雨天雨點落在了車窗玻璃上,這時候雨刷器把落在玻璃上的雨點視為‘臟’東西然后雨刷器刷一下把所有雨點清理干凈了。借喻到代碼中就是當有數據需要更新調用Dirty方法解決所有的更新需求。]

internal void Dirty()
{
  if (ShouldReactToDirty())
  {
      NeedsUpdate = true;
      if (!HasValue(Feature.Timer))
      {
          ProcessDirty();
      }
      else
      {
          // restart the timer
          DispatcherTimer timer = (DispatcherTimer)GetValue(Feature.Timer, null);
                  timer.Stop();
                  timer.Start();
      }
            NotifyCommitManager();
      }
}

Drity方法會檢測是否有數據改動沒有改動則退出更新機制。如果在綁定表達式中用了Delay屬性,則會觸發BindingExpressionBase中的DispatcherTimer來達到數據延遲更新的效果。可見每創建一個綁定表達式里都會包含一個定時器只是大部分時間不會啟動而已。內部會有bool的標記來判斷更新過程是否開始或結束。

private void DetermineEffectiveUpdateBehavior() { if (!this.IsReflective) return; for (BindingExpressionBase bindingExpressionBase = this.ParentBindingExpressionBase; bindingExpressionBase != null; bindingExpressionBase = bindingExpressionBase.ParentBindingExpressionBase) { if (bindingExpressionBase is MultiBindingExpression) return; } int delay = this.ParentBindingBase.Delay; if (delay <= 0 || !this.IsUpdateOnPropertyChanged) return; DispatcherTimer dispatcherTimer = new DispatcherTimer(); this.SetValue(BindingExpressionBase.Feature.Timer, (object) dispatcherTimer); //這里的Interval就是根據我們在設置Binding對象Delay屬性來設置的。如果寫Delay=1000;那么就是1秒后觸發更新  dispatcherTimer.Interval = TimeSpan.FromMilliseconds((double) delay); dispatcherTimer.Tick += new EventHandler(this.OnTimerTick); } 

3.這時候訪問依賴屬性Text的內容去修改綁定在ViewModel的屬性BindingExpression.UpdateSource(object value)。

4.BindingExpressionBase.UpdateValue()里的object rawProposedValue = this.GetRawProposedValue();會去拿到依賴屬性的值這時候取到的內容是沒有被驗證是否合法的內容,然后會做兩件事情

(1)會判斷值是否合法能否通過驗證規則。

(2)如果在綁定表達式里寫了Convert轉換器,則進行值轉換。

完成以上兩步的值將會object obj = this.UpdateSource(convertedValue)來觸發更新;最終由依賴屬性中PropertyMetadata注冊的PropertyChangedCallback來落實值的修改。

internal bool UpdateValue() { ValidationError oldValidationError = BaseValidationError; if (StatusInternal == BindingStatusInternal.UpdateSourceError) SetStatus(BindingStatusInternal.Active); object value = GetRawProposedValue(); if (!Validate(value, ValidationStep.RawProposedValue)) return false; value = ConvertProposedValue(value); if (!Validate(value, ValidationStep.ConvertedProposedValue)) return false; value = UpdateSource(value); if (!Validate(value, ValidationStep.UpdatedValue)) return false; value = CommitSource(value); if (!Validate(value, ValidationStep.CommittedValue)) return false; if (BaseValidationError == oldValidationError) { // the binding is now valid - remove the old error  UpdateValidationError(null); } EndSourceUpdate(); NotifyCommitManager(); return !HasValue(Feature.ValidationError); } 

看到這里大家應該會明白設計者為什么不把ViewModel的每個字段默認集數據通知機制,我個人的理解是數據通知會帶來一定的性能損耗所以開放給開發者“按需”添加通知的成員。

3.Reference


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM