前言
WPF的一大基礎就是Data Binding。在基於MVVM架構的基礎上,只有通過實現INotifyPropertyChanged接口的ViewModel才能夠用於Data Binding。
要實現INotifyPropertyChanged接口,只需要實現一個事件,event PropertyChangedEventHandler PropertyChange。
作為一個剛接觸WPF沒多久的人來說,我最不可理解的就是data binding的時候到底誰自動幫我完成了訂閱PropertyChanged這個事件?
delegate & event基礎知識回顧
先來回顧下C#里delegate和event的基礎知識。
我們知道在C#里,event其實就是一種簡化的delegate。所謂的簡化其實是為了更好地解決訂閱和廣播的問題。為什么?我們知道在處理訂閱時,各個listener之間應該時相互不知道對方的存在的,並且相互間互不影響。delegate和event都通過+=和-=來注冊和反注冊listener。而delegate還可以通過=進行賦值。這個=賦值完美地破壞了剛才我們期望的listener之間的相互關系。
如何實現一個event?
我們這里簡單列出一個完整的event實例,然后就這個實例來討論核心問題INotifyPropertyChanged。
1 public class ValueChangedEventArgs : System.EventArgs 2 { 3 public readonly int oldValue; 4 public readonly int newValue; 5 6 public ValueChangedEventArgs(int oldOne, int newOne) 7 { 8 oldValue = oldOne; 9 newValue = newOne; 10 } 11 } 12 13 public class Commodity 14 { 15 string symbol; 16 int volume; 17 18 public event EventHandler<ValueChangedEventArgs> ValueChanged; 19 20 protected void OnValueChanged(ValueChangedEventArgs e) 21 { 22 if (ValueChanged != null) 23 { 24 ValueChanged(this, e); 25 } 26 } 27 28 public int Volume 29 { 30 get { return volume; } 31 set 32 { 33 if (volume == value) { return; } 34 35 int oldValue = volume; 36 volume = value; 37 38 OnValueChanged(new ValueChangedEventArgs(oldValue, volume); 39 } 40 } 41 42 class Test 43 { 44 static void Main() 45 { 46 Commodity c = new Commodity(“600000”); 47 c.Volume = 100; 48 c.ValueChanged += commodity_ValueChanged; 49 c.Volume = 200; 50 } 51 52 static void commodity_ValueChanged(object sender, ValueChangedEventArgs e) 53 { 54 Console.WriteLine(“Volume Changed!”); 55 } 56 }
運行上面的例子我們會發現,c.Volume = 100時執行了OnValueChanged。但是由於ValueChanged這個event為空,OnValueChanged直接返回了。接下來通過給ValueChanged賦上commodity_ValueChanged后繼續執行c.Volume = 200,這時控制台就打印出我們想要的結果了。
現在我們回到對INotifyPropertyChanged的討論上來。
在WPF編程過程中,當我們要實現一個ViewModel時,我們會發現我們通過下面這一行代碼就實現了對PropertyChanged這個event的增刪操作:
public event PropertyChangedEventHandler PropertyChanged;
為什么說上面這一行實現了event的增刪操作?事實上編譯器我們后做了很多事情。參考C# 5.0 in a Nutshell: The Definitive Reference,上述的event語句會被編譯器展開成類似下面的語句:
1 PropertyChangedEventHandler _propertyChanged; 2 public event PropertyChangedEventHandler PropertyChanged 3 { 4 add { _propertyChanged += value; } 5 remove { _propertyChanged -= value; } 6 }
所以,對於event我們就可以簡寫成public event SomeEventHandler SomeEvent。
接下來,通過XAML的data binding就可以將控件中的某個屬性綁定ViewModel的某個屬性上了。比方說Commodity.Volume(這里假定Commodity 實現了INotifyPropertyChanged接口)。
再之后,改變Volume數值,相應控件的綁定屬性就同步更改了。
我在接觸WPF data binding時,最大的疑惑就在這里。PropertyChanged我從來就沒有去顯示的初始化,為什么運行時,PropertyChanged非空?是誰初始化了PropertyChanged?
PropertyChanged是如何被初始化的?
要回答這個問題,不得不說,需要一定的篇幅才能說清楚。
首先我們可以根據現象確定PropertyChanged再運行是是會被初始化的。既然這樣,那么我們可以通過設置斷點來看到底什么時候會被初始化。
如何設置斷點?
我們需要手動將編譯器做的事情自己來做一遍。自己寫event的add和remove。也就是上文看到的那個樣子。
然后再打斷點到add行。運行后我們會看到如下堆棧:

OK!PropertyChanged被初始化了。於此同時我們也發現了一個和PropertyChanged相關聯的類:PropertyChangedEventManager。
好家伙,這個manager類在MSDN上還有介紹!
PropertyChangedEventManager class provides a WeakEventManager implementation so that you can use the "weak event listener" pattern to attach listeners for the PropertyChanged event.
到此,我們算是明白PropertyChanged是被誰自動初始化的了。不過,我覺得這還不夠。既然.Net Framework都開源了,為啥不去看看這個類的源碼是怎么處理也個業務邏輯的呢?
Reference Source我們的好幫手
打開Reference Source主頁,搜索PropertyChangedEventManager,找到StartListening方法:
1 protected override void StartListening(object source) 2 { 3 INotifyPropertyChanged typedSource = (INotifyPropertyChanged)source; 4 typedSource.PropertyChanged += new PropertyChangedEventHandler(OnPropertyChanged); 5 }
不管怎么說,這個實現簡直太直觀了。如果你想看OnPropertyChanged是如何實現的,請自行前往。所以當ViewModel里的屬性改變后,會調用ViewModel.PropertyChanged,繼而調用PropertyChangedEventManager.OnPropertyChanged。PropertyChangedEventManager.OnPropertyChanged會繼而調用WeakEventManager.DeliverEventToList,然后調用ListenerList.DeliverEvent。DeliverEvent會把sender和EventArgs e傳遞給Listener.Handler(sender, e)完成PropertyChanged的整個過程。
總之,自己親自去翻一番源碼,你一定能腦補成功。
關於PropertyChangedEventManager和WeakEventManager是什么東西,請參閱MSDN:Weak Event Patterns。至於更細節的東西,我目前無法提供。我也是新手,暫時還沒有做更深入的研究,后續有時間,會結合代碼再研究一下。
Data Binding的Target是如何處理PropertyChanged的?
我們就拿TextBlock.Text來分析這個問題。
首先TextBlock.Text屬性是一個dependency property。只有dependency property才能執行data binding操作。
廢話不多說,直接上源碼:
1 public static readonly DependencyProperty TextProperty = 2 DependencyProperty.Register( 3 "Text", 4 typeof(string), 5 typeof(TextBlock), 6 new FrameworkPropertyMetadata( 7 string.Empty, 8 FrameworkPropertyMetadataOptions.AffectsMeasure | 9 FrameworkPropertyMetadataOptions.AffectsRender, 10 new PropertyChangedCallback(OnTextChanged), 11 new CoerceValueCallback(CoerceText)));
應該就是加粗的那一行用來處理PropertyChanged事件。
簡單驗證下,在Visual Studio里設置一個斷點:PresentationFramework.dll!System.Windows.Controls.TextBlock.OnTextChanged
我這里看到的堆棧調用是:

通過Visual Studio自帶的Locals窗口,你還可以看到傳入的newText值是多少!
完!
