前言
接觸WPF有一段時間了,之前雖然也經常使用,但是對於DependencyProperty一直處於一知半解的狀態。今天花了整整一下午將這個概念梳理了一下,自覺對這個概念有了較為清晰的認識,之前很多很混沌的概念和理解也變得比較清晰,因此想把那些問題和不解的解決過程都清晰地還原展示出來,期望對那些也在學習WPF的朋友有所幫助。
這里還要說句題外話,在博客園上有很多非常出色的介紹WPF的文章,為什么我還要去寫這個呢?一方面對我個人而言是總結歸納,另一方面,也是最重要的一點,我一直認為最適合教授解答某個問題的人是剛理解這個問題的那些人,而不是有很豐富經驗的人,因為這個人剛剛經歷了從不理解到理解的思維過程,那些困擾他,讓他欲罷不能苦惱萬分的關鍵問題(key point,很多時候是無法理解某個問題的關鍵)的思維過程還非常新鮮。這些經驗有時候才是最珍貴的,因為人的思維方式都是大同小異的,在對同一個比較困難的問題的理解上很多人的思維路徑基本都是一樣的,如果能循着自己思維慣性向前推進將一個個難點消除,這樣理解的程度和學習的效果肯定遠比被動接受一連串概念和知識要強的多。那些經驗很豐富的大師因為對這個問題已經有了很深刻的理解,那些我們看來很難理解的地方對他們而言已經變得如常識本能一般,他認為理所當然的東西往往在新手看來其實非常費解,所以他們更傾向於將他們所知道的知識瀑布式地寫下來,新手看完后除了依稀記得幾個概念,對於理解過程仍是一頭霧水。說了這么多廢話是希望那些對這個概念仍有不解的朋友們能耐心看完本文,我會盡力帶您和我一起解決這塊難啃的骨頭。
DP的存儲方式
Dependency Property(下文簡稱為DP) 是WPF的基礎,WPF的很多非常關鍵的特性都依賴於DP,比如DataBinding,Animation,Style等。和普通的.net屬性相比(clr property),DP有如下特點:
- 一個屬性可以有多個值(local,default,animation等),而且可以根據優先級確定具體的值。也就是是多數據源支持(multiple providers)。同時DP內置了對新值的驗證,如果不符合要求可以取消更改(如slider的value如果超過了上下界就可以取消更改)
- 改變通知(DP的改變會自動通知界面更新,clr屬性若要實現該功能需要實現INotifyPropertyChange接口並處罰propertychange事件通知界面更新)
- 屬性繼承(沿着邏輯樹繼承)
我們先來看看DP的基本用法
public static readonly DependencyProperty myProperty = DependencyProperty.Register("my", typeof (string), typeof (UserControl1),new PropertyMetadata("default")); public string my { get { return (string) GetValue(myProperty); } set { SetValue(myProperty, value); } }
這是在visualstudio中鍵入dependencyproperty時幫我們自動生成的代碼,從中我們可以看到dp是靜態屬性,而根據我們的使用經驗,我們知道每個實例的DP的值是不同的(不然我們也不可能在XAML里使用DP來定義每個窗體實例的諸如寬度,顏色等屬性了),也就是說按照我們理解DP應該是個實例屬性才對,那問題到底出在哪呢?理解這個問題也是理解DP的關鍵。我們不妨先來猜測一下WPF是如何實現的,很容易想到使用DP的每個實例應該都有一個數據結構用來存儲DP真正的值,靜態的DP屬性用來存儲DP的默認值(在之前的代碼中是”default”),當然這只是我們的初步設想,到底是不是這樣呢?讓我們去WPF的代碼里一窺究竟吧。
我們先來看一下DependencyObject(下文簡稱DO)的代碼(只有繼承自該類的類才能使用DP),我們在DO中發現了如下代碼:
在屬性定義中有一個數組:
private EffectiveValueEntry[] _effectiveValues; EffectiveValueEntry結構的代碼: Internal struct EffectiveValueEntry { Internal int PropertyIndex{get;set;} Internal object Value{get;set;} }
這個數組是不是就是用來保存我們的具體DP值的數據結構呢?我們要去DO的GetValue和SetValue看一下:
public object GetValue(DependencyProperty dp) { this.VerifyAccess(); if (dp == null) throw new ArgumentNullException("dp"); else return this.GetValueEntry(this.LookupEntry(dp.GlobalIndex), dp, (PropertyMetadata) null, RequestFlags.FullyResolved).Value; }
我們可以看到getvalue方法里面根據傳進來的DP實例得到DP的index,然后再用getvalueentry方法在effectiveValueEntry數組里查找這個DO實例里有沒有存儲這個DP的值,如果沒有則返回DP的默認值,如果有,說明這個DO實例的這個DP屬性值修改過,則返回EffectiveValueEntry數組里保存的DP值。
public void SetValue(DependencyProperty dp, object value) { this.VerifyAccess(); PropertyMetadata metadata = this.SetupPropertyChange(dp); this.SetValueCommon(dp, value, metadata, false, false, OperationType.Unknown, false); }
我們可以看到setvalue方法根據傳入的DP實例的meta信息調用setvaluecommon方法進行一系列諸如安全,操作類型等的檢查后,在setvaluecommon的最后執行了如下代碼:
int num = (int) this.UpdateEffectiveValue(entryIndex1, dp, metadata, oldEntry, ref newEntry, coerceWithDeferredReference, coerceWithCurrentValue, operationType);
更新了effectiveValueEntry數組里的的dp的值。
好了看完上述過程,我們對DP的值存儲有了一定的了解,然而離DP的真面目還有一點距離,因為前文里看似簡單的描述中其實遺漏了一個關鍵的信息:
我們平時在XAML里使用DP時,是采用類似Width=”**”的方式,這個Width是怎么轉化成我們setvalue方法傳進來的dp實例的呢?難道是根據width字符串反射到DO的DP屬性字段?
要解決上述疑問則要去DP的代碼里一窺究竟了,首先要看的當然是DP的register方法,DP的register方法是一個重載的方法,最后都調用了如下方法:
public static DependencyProperty Register(string name, Type propertyType, Type ownerType, PropertyMetadata typeMetadata, ValidateValueCallback validateValueCallback) { DependencyProperty.RegisterParameterValidation(name, propertyType, ownerType); PropertyMetadata defaultMetadata = (PropertyMetadata) null; if (typeMetadata != null && typeMetadata.DefaultValueWasSet()) defaultMetadata = new PropertyMetadata(typeMetadata.DefaultValue); DependencyProperty dependencyProperty = DependencyProperty.RegisterCommon(name, propertyType, ownerType, defaultMetadata, validateValueCallback); if (typeMetadata != null) dependencyProperty.OverrideMetadata(ownerType, typeMetadata); return dependencyProperty; }
我們可以看到首先調用registerparametervalidation檢查了一下幾個參數是否為空,然后調用registercommon方法去注冊這個DP,這個方法是DP注冊的關鍵,因此我把這個方法的代碼也列了出來:
private static DependencyProperty RegisterCommon(string name, Type propertyType, Type ownerType, PropertyMetadata defaultMetadata, ValidateValueCallback validateValueCallback) { DependencyProperty.FromNameKey fromNameKey = new DependencyProperty.FromNameKey(name, ownerType); lock (DependencyProperty.Synchronized) { if (DependencyProperty.PropertyFromName.Contains((object) fromNameKey)) throw new ArgumentException(MS.Internal.WindowsBase.SR.Get("PropertyAlreadyRegistered", (object) name, (object) ownerType.Name)); } if (defaultMetadata == null) { defaultMetadata = DependencyProperty.AutoGeneratePropertyMetadata(propertyType, validateValueCallback, name, ownerType); } else { if (!defaultMetadata.DefaultValueWasSet()) defaultMetadata.DefaultValue = DependencyProperty.AutoGenerateDefaultValue(propertyType); DependencyProperty.ValidateMetadataDefaultValue(defaultMetadata, propertyType, name, validateValueCallback); } DependencyProperty dp = new DependencyProperty(name, propertyType, ownerType, defaultMetadata, validateValueCallback); defaultMetadata.Seal(dp, (Type) null); if (defaultMetadata.IsInherited) dp._packedData |= DependencyProperty.Flags.IsPotentiallyInherited; if (defaultMetadata.UsingDefaultValueFactory) dp._packedData |= DependencyProperty.Flags.IsPotentiallyUsingDefaultValueFactory; lock (DependencyProperty.Synchronized) DependencyProperty.PropertyFromName[(object) fromNameKey] = (object) dp; if (TraceDependencyProperty.IsEnabled) TraceDependencyProperty.TraceActivityItem(TraceDependencyProperty.Register, (object) dp, (object) dp.OwnerType); return dp; }
其中FromNameKey是DP里定義的一個類,代碼如下:
private class FromNameKey { private string _name; private Type _ownerType; private int _hashCode; public FromNameKey(string name, Type ownerType) { this._name = name; this._ownerType = ownerType; this._hashCode = this._name.GetHashCode() ^ this._ownerType.GetHashCode(); } public override int GetHashCode() { return this._hashCode; } } }
我們可以看到registercommon方法大致做了以下幾件事:
根據propertyname和ownertype構造的fromnamekey來在DP的PropertyFromName靜態屬性(一個由DP實例組成的哈希表)中查找DP實例。我們來看一下fromnamekey類的gethashcode方法
this._hashCode = this._name.GetHashCode() ^ this._ownerType.GetHashCode();
fromnamekey的哈希值是根據propertyname和ownertype的哈希值求異或得到的,也就是說一個WPF程序啟動后,這個程序中就有一個全局變量(DependencyProperty.PropertyFromName)保存了系統中所有已注冊的DP。這樣我們之前那個“如何根據XAML中的屬性名稱來尋找DP”的問題就有答案了,系統根據屬性名(PropertyName)和使用該屬性的實體類(ownertype)生成的hashcode在DependencyProperty.PropertyFromName中查找到DP實例,再根據DP的globalindex(全局索引,每新注冊一個DP加1)去在DO中調用getvalue方法獲取具體的值。
回答完上面那個問題,我們繼續看registercommon方法,如果根據fromnamekey計算出來的hash值在該哈希表中已存在元素,則說明該DP已被注冊過(由此我們也可以看到WPF是根據PropertyName和Ownertype來唯一標識一個DP的),方法就會拋出一個提示信息為PropertyAlreadyRegistered的異常。如果該DP未注冊,則實例化一個DP,將meta信息封裝到DP的_packedData屬性中,同時將該DP存到PropertyFromName這個哈希表中。
至此之前的兩個問題都已經得到解決了,我們也弄清楚了DP到底是如何存儲的了。這里我們簡單做個總結:
- DO的EffectiveValueEntry數組保存了這個DO實例中的所有被修改過的DP的值。
- DP的PropertyFromName哈希表保存了系統中所有已注冊DP的實例以及根據這個DP的propertyname和ownertype計算出來的hash值。這樣所有DP的默認值和一些meta信息(比如值改變回調函數,是否繼承上級值等一些信息)就能夠很好的保存和查詢。
- 每個DP實例都有一個globalindex,這個globalindex初始值為0,每次注冊一個DP就加1,在DO中查找DP的具體值時就是利用這個index來查詢的
- 在xaml中以如下方式查找DP的具體值:
根據DP的屬性名(就是DP注冊方法的第一個參數)和該DP所有者類型(ownertype)計算出來的hash值來在DP的靜態變量PropertyFromName中查找DP實例,然后根據DP實例的globalindex查找具體的值(見第3點)
這里要多說一句,我們常常看到在xaml中用的Width對應的就是WidthProperty
Color對應的就是ColorProperty,這些都是WPF里的Convention,但並不代表WPF查找DP時只是簡單的在屬性后面加上”property“字符串而已。
說了這么多,現在要說說DP這種存儲方式相對.net普通屬性的好處。拿WPF里的Button來說,這個類有100多個屬性(很多是從父類繼承過來的),對於大部分Button實例來說,這其中很多屬性都不會用到或者只需用默認值,因此如果都采用普通clr屬性的話,每個Button就要浪費很多內存空間。而采用DP的話那些不怎么要用的值或者不怎么更改的值就都存在DP里面,而DP是靜態屬性,是一個類所共有的,因此也就極大地節省了內存空間。
當然DP所帶來的好處絕非只有節省內存而已,筆者本來計划將DP主要的一些特性都在一篇博客里寫完,但是為了盡量把問題說清楚寫着寫着就發現只寫了存儲方式就已經寫了這么多。因此關於DP的其他特性會留待下文分解,最后希望看完本文的你對DP的存儲方式有了清晰的了解,如有不清楚的地方歡迎留言溝通交流,謝謝!