一站式WPF--依賴屬性(DependencyProperty)一
Windows Presentation Foundation (WPF) 提供了一組服務,這些服務可用于擴展公共語言運行時 (CLR) 屬性的功能,這些服務通常統稱為 WPF 屬性系統。由 WPF 屬性系統支持的屬性稱為依賴項屬性。
這段是MSDN上對依賴屬性(DependencyProperty)的描述。主要介紹了兩個方面,WPF中提供了可用于擴展CLR屬性的服務;被這個服務支持的屬性稱為依賴屬性。
單看描述,云里霧里的,了解一個知識,首先要知道它產生的背景和為什么要有它,那么WPF引入依賴屬性是為了解決什么問題呢?
從屬性說起
屬性是我們很熟悉的,封裝類的字段,表示類的狀態,編譯后被轉化為get_,set_方法,可以被類或結構等使用。 一個常見的屬性如下:
1: public class NormalObject
2: {
3: private string _unUsedField;
4:
5: private string _name;
6: public string Name
7: {
8: get
9: {
10: return _name;
11: }
12: set
13: {
14: _name = value;
15: }
16: }
17: }
在面向對象的世界里,屬性大量存在,比如Button,就大約定義了70-80個屬性來描述其狀態。那么屬性的不足又在哪里呢?
當然,所謂的不足,要針對具體環境來說。拿Button來講,它的繼承樹是Button->ButtonBase->ContentControl->Control->FrameworkElement->UIElement->Visual->DependencyObject->…
每次繼承,父類的私有字段都被繼承下來。當然,這個繼承是有意思的,不過以Button來說,大多數屬性并沒有被修改,仍然保持著父類定義時的默認值。通常情況,在整個Button對象的生命周期里,也只有少部分屬性被修改,大多數屬性一直保持著初始值。每個字段,都需要占用4K等不等的內存,這里,就出現了期望可以優化的地方:
- 因繼承而帶來的對象膨脹。每次繼承,父類的字段都被繼承,這樣,繼承樹的低端對象不可避免的膨脹。
- 大多數字段并沒有被修改,一直保持著構造時的默認值,可否把這些字段從對象中剝離開來,減少對象的體積。
依賴屬性的原型
根據前面提出的需求,依賴屬性就應運而生了。一個簡單的依賴屬性的原型如下:
DependencyProperty:
1: public class DependencyProperty
2: {
3: internal static Dictionary<object, DependencyProperty> RegisteredDps = new Dictionary<object, DependencyProperty>();
4: internal string Name;
5: internal object Value;
6: internal object HashCode;
7:
8: private DependencyProperty(string name, Type propertyName, Type ownerType, object defaultValue)
9: {
10: this.Name = name;
11: this.Value = defaultValue;
12: this.HashCode = name.GetHashCode() ^ ownerType.GetHashCode();
13: }
14:
15: public static DependencyProperty Register(string name, Type propertyType, Type ownerType, object defaultValue)
16: {
17: DependencyProperty dp = new DependencyProperty(name, propertyType, ownerType, defaultValue);
18: RegisteredDps.Add(dp.HashCode, dp);
19: return dp;
20: }
21: }
22:
DependencyObject:
1: public class DependencyObject
2: {
3: private string _unUsedField;
4:
5: public static readonly DependencyProperty NameProperty =
6: DependencyProperty.Register("Name", typeof(string), typeof(DependencyObject), string.Empty);
7:
8: public object GetValue(DependencyProperty dp)
9: {
10: return DependencyProperty.RegisteredDps[dp.HashCode].Value;
11: }
12:
13: public void SetValue(DependencyProperty dp, object value)
14: {
15: DependencyProperty.RegisteredDps[dp.HashCode].Value = value;
16: }
17:
18: public string Name
19: {
20: get
21: {
22: return (string)GetValue(NameProperty);
23: }
24: set
25: {
26: SetValue(NameProperty, value);
27: }
28: }
29: }
30:
這里,首先定義了依賴屬性DependencyProperty,它里面存儲前面我們提到希望抽出來的字段。DP內部維護了一個全局的Map用來儲存所有的DP,對外暴露了一個Register方法用來注冊新的DP。當然,為了保證在Map中鍵值唯一,注冊時需要根據傳入的名字和注冊類的的HashCode取異或來生成Key。這里最關鍵的就是最后一個參數,設置了這個DP的默認值。
然后定義了DependencyObject來使用DP。首先使用DependencyProperty.Register方法注冊了一個新的DP(NameProperty),然后提供了GetValue和SetValue兩個方法來操作DP。最后,類似前面例子中的NormalObject,同樣定義了一個屬性Name,和NormalObject的區別是,實際的值不是用字段來保存在DependencyObject中的,而是保存在NameProperty這個DP中,通過GetValue和SetValue來完成屬性的賦值取值操作。
當然,作為一個例子,為了簡潔,很多情況沒有考慮,現在來測試一下是否解決了前面的問題。
新建兩個對象,NormalObject和DependencyObject,在VS下打開SOS查看:
.load sos
extension C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\sos.dll loaded
!DumpHeap -stat -type NormalObject
total 1 objects
Statistics:
MT Count TotalSize Class Name
009e30d0 1 16 DPDemonstration.NormalObject
Total 1 objects
!DumpHeap -stat -type DependencyObject
total 1 objects
Statistics:
MT Count TotalSize Class Name
009e31a0 1 12 DPDemonstration.DependencyObject
Total 1 objects
這里在對象中分別建立了一個_unUsedField的字段,.Net的GC要求對象的最小Size為12字節。如果對象的Size不足12字節,則會自動補齊。默認的Object對象占用8字節,Syncblk(4字節)以及TypeHandle(4字節),為了演示方便,加入了一個_unUsedField(4字節)來補齊。
這里,DependencyObject相比NormalObject,減少了_name的儲存空間4字節。
再進一步
萬里長征第一步,這個想法可以解決我們希望的問題,這個做法還不能讓人接受。在這個實現中,所有DependencyObject共用一個DP,這個可以理解,但修改一個對象的屬性后,所有對象的屬性相當于都被修改了,這個就太可笑了。
所以對象屬性一旦被修改,這個還是要維護在自己當中的,修改一下前面的DependencyObject,引入一個有效(Effective)的概念。
改進的DependencyObject,加入了_effectiveValues:
1: public class DependencyObject
2: {
3: private List<EffectiveValueEntry> _effectiveValues = new List<EffectiveValueEntry>();
4:
5: public static readonly DependencyProperty NameProperty =
6: DependencyProperty.Register("Name", typeof(string), typeof(DependencyObject), string.Empty);
7:
8: public object GetValue(DependencyProperty dp)
9: {
10: EffectiveValueEntry effectiveValue = _effectiveValues.FirstOrDefault((i) => i.PropertyIndex == dp.Index);
11: if (effectiveValue.PropertyIndex != 0)
12: {
13: return effectiveValue.Value;
14: }
15: else
16: {
17: return DependencyProperty.RegisteredDps[dp.HashCode].Value;
18: }
19: }
20:
21: public void SetValue(DependencyProperty dp, object value)
22: {
23: EffectiveValueEntry effectiveValue = _effectiveValues.FirstOrDefault((i) => i.PropertyIndex == dp.Index);
24: if (effectiveValue.PropertyIndex != 0)
25: {
26: effectiveValue.Value = value;
27: }
28: else
29: {
30: effectiveValue = new EffectiveValueEntry() { PropertyIndex = dp.Index, Value = value };
31: _effectiveValues.Add(effectiveValue);
32: }
33: }
34:
35: public string Name
36: {
37: get
38: {
39: return (string)GetValue(NameProperty);
40: }
41: set
42: {
43: SetValue(NameProperty, value);
44: }
45: }
46: }
新引進的EffectiveValueEntry:
1: internal struct EffectiveValueEntry
2: {
3: internal int PropertyIndex { get; set; }
4:
5: internal object Value { get; set; }
6: }
改進的DependencyProperty,加入了ProperyIndex:
1: public class DependencyProperty
2: {
3: private static int globalIndex = 0;
4: internal static Dictionary<object, DependencyProperty> RegisteredDps = new Dictionary<object, DependencyProperty>();
5: internal string Name;
6: internal object Value;
7: internal int Index;
8: internal object HashCode;
9:
10: private DependencyProperty(string name, Type propertyName, Type ownerType, object defaultValue)
11: {
12: this.Name = name;
13: this.Value = defaultValue;
14: this.HashCode = name.GetHashCode() ^ ownerType.GetHashCode();
15: }
16:
17: public static DependencyProperty Register(string name, Type propertyType, Type ownerType, object defaultValue)
18: {
19: DependencyProperty dp = new DependencyProperty(name, propertyType, ownerType, defaultValue);
20: globalIndex++;
21: dp.Index = globalIndex;
22: RegisteredDps.Add(dp.HashCode, dp);
23: return dp;
24: }
25: }
在DependencyObject加入了一個_effectiveValues,就是把所有修改過的DP都保存在EffectiveValueEntry里,這樣,就可以達到只保存修改的屬性,未修改過的屬性仍然讀取DP的默認值,優化了屬性的儲存。
更進一步的發展
到目前為止,從屬性到依賴屬性的改造一切順利。但隨著實際的使用,又一個問題暴露出來了。使用繼承,子類可以重寫父類的字段,換句話說,這個默認值應該是可以子類化的。那么怎么處理,子類重新注冊一個DP,傳入新的默認值?
當然,不會實現的這么丑陋。同一個DP,要想支持不同的默認值,那么內部就要維護一個對應不同DependencyObjectType的一個List,可以根據傳入的DependencyObject的類型來讀取它對應的默認值。
DP內需要維護一個自描述的List,按照微軟的命名規則,添加新的類型屬性元數據(PropertyMetadata):
1: public class PropertyMetadata
2: {
3: public Type Type { get; set; }
4: public object Value { get; set; }
5:
6: public PropertyMetadata(object defaultValue)
7: {
8: this.Value = defaultValue;
9: }
10: }
對應修改DependencyProperty
1: public class DependencyProperty
2: {
3: private static int globalIndex = 0;
4: internal static Dictionary<object, DependencyProperty> RegisteredDps = new Dictionary<object, DependencyProperty>();
5: internal string Name;
6: internal object Value;
7: internal int Index;
8: internal object HashCode;
9: private List<PropertyMetadata> _metadataMap = new List<PropertyMetadata>();
10: private PropertyMetadata _defaultMetadata;
11:
12: private DependencyProperty(string name, Type propertyName, Type ownerType, object defaultValue)
13: {
14: this.Name = name;
15: this.Value = defaultValue;
16: this.HashCode = name.GetHashCode() ^ ownerType.GetHashCode();
17:
18: PropertyMetadata metadata = new PropertyMetadata(defaultValue) { Type = ownerType };
19: _metadataMap.Add(metadata);
20: _defaultMetadata = metadata;
21: }
22:
23: public static DependencyProperty Register(string name, Type propertyType, Type ownerType, object defaultValue)
24: {
25: DependencyProperty dp = new DependencyProperty(name, propertyType, ownerType, defaultValue);
26: globalIndex++;
27: dp.Index = globalIndex;
28: RegisteredDps.Add(dp.HashCode, dp);
29: return dp;
30: }
31:
32: public void OverrideMetadata(Type forType, PropertyMetadata metadata)
33: {
34: metadata.Type = forType;
35: _metadataMap.Add(metadata);
36: }
37:
38: public PropertyMetadata GetMetadata(Type type)
39: {
40: PropertyMetadata medatata = _metadataMap.FirstOrDefault((i) => i.Type == type) ??
41: _metadataMap.FirstOrDefault((i) => type.IsSubclassOf(i.Type));
42: if (medatata == null)
43: {
44: medatata = _defaultMetadata;
45: }
46: return medatata;
47: }
48: }
修改DenpendencyObject中的GetValue并更改_effectiveValues,為了簡潔去掉了NameProperty以及SetValue.
1: public class DependencyObject
2: {
3: private List<EffectiveValueEntry> _effectiveValues = new List<EffectiveValueEntry>();
4:
5: public object GetValue(DependencyProperty dp)
6: {
7: EffectiveValueEntry effectiveValue = _effectiveValues.FirstOrDefault((i) => i.PropertyIndex == dp.Index);
8: if (effectiveValue.PropertyIndex != 0)
9: {
10: return effectiveValue.Value;
11: }
12: else
13: {
14: PropertyMetadata metadata;
15: metadata = DependencyProperty.RegisteredDps[dp.HashCode].GetMetadata(this.GetType());
16: return metadata.Value;
17: }
18: }
19: }
這樣,就可以定義一個SubDependencyObject,調用OverrideMedata向DP的_metadataMap中加入新的Metadata。
1: public class SubDependencyObject : DependencyObject
2: {
3: static SubDependencyObject()
4: {
5: NameProperty.OverrideMetadata(typeof(SubDependencyObject), new PropertyMetadata("SubName"));
6: }
7: }
創建一個DependencyObject以及SubDependencyObject,可以發現,Name的值已經被改為”SubName”了。當然,實際DP中對Metadata的操作比較繁瑣,當子類調用OverrideMetadata時會涉及到Merge操作,把新的Metadata與父類的合二為一。并且在GetMetadata中,要取得自己或者是與它最近的父類的Metadata,為了可以獲得最近的父類,WPF引入了一個DependencyObjectType的類,在構造時傳入BaseType=this.base.GetType(),這里為了簡單,忽略不計。
WPF對依賴屬性的擴展
前面的例子里,依據優化儲存的思想,我們打造了一個DependencyProperty。當然,有了這樣一門利器,不好好打磨打磨真是對不起它,WPF在這個基礎上對DP進行了擴展,使其更加的強大。
對通常的CLR屬性來說,在Set中加入一些邏輯判斷是很正常的,當然也可以在Set中發出一些事件或者更改其他一些屬性。那么依賴屬性,它對此又有什么支持呢?
順水推舟,WPF在DP的PropertyMedata中加入了PropertyChangedCallback以及CoerceValueCallback等。這些Delegate可以在構造PropertyMetadata時傳入,在SetValue過程中,會取得對應的PropertyMetadata,然后回調PropertyChangedCallback。這個PropertyMetadata可以在構建DP時傳入,也可以在子類調用OverrideMetadata時傳入,這就保證了同一個DP不同的DependencyObject可以有不同的應用。WPF對此進行了很多擴展,定義了一套屬性賦值的規則,包括計算(calculate)、限制(Coerce)、驗證(Validate)等等。
當然,這些擴展說開了會很多,WPF對此也進行了精巧的設計,這也就是我們開篇提到的WPF提供了一組服務,用于擴展CLR屬性。
多屬性值
發展都是由需求來推動的,在WPF的實現過程中,又產生了這樣一個需要:
WPF是原生支持動畫的,一個DP屬性,比如Button的Width,你可以加入動畫使他在1秒內由100變為200,在動畫結束后,又希望它能恢復原來的屬性值。同理,你可以在XAML表達式中對屬性進行賦值,當表達式失效時同樣期望他恢復成原來的屬性值。這個需求來自于,對同一個屬性的賦值可能發生在不同的場合,當對象狀態改變時屬性也要發生相應的變化,這里就產生了兩個需要:
- 屬性對外暴露一個值,但內部可以存放多個值,根據狀態(條件)的改變來確定當前值。
- 這些狀態(條件)要定義優先級,根據優先級來判斷當前應取哪個值。
同一個屬性有多個值,這個對CLR屬性來說有些難為它了。但是對DP來說卻很簡單,本來DP的值就是保存在我們定義的EffectiveValueEntry中的,以前是保存一個Value,現在定義多個值就可以了。
1: internal struct EffectiveValueEntry
2: {
3: private object _value;
4:
5: internal int PropertyIndex { get; set; }
6:
7: internal object Value
8: {
9: get
10: {
11: return _value;
12: }
13: set
14: {
15: _value = value;
16: }
17: }
18:
19: internal ModifiedValue ModifiedValue
20: {
21: get
22: {
23: if (this._value != null)
24: {
25: return (this._value as ModifiedValue);
26: }
27: return null;
28: }
29: }
30: }
對應的ModifiedValue:
1: internal class ModifiedValue
2: {
3: internal object AnimatedValue { get; set; }
4: internal object BaseValue { get; set; }
5: internal object CoercedValue { get; set; }
6: internal object ExpressionValue { get; set; }
7: }
當屬性沒有被修改過,ModifiedValue為空,當修改過后,ModifiedValue被賦值。這里EffectiveValueEntry定義了很多方法如SetExpressionValue(object value), SetAnimatedValue(object value)等來向ModifiedValue中寫入對應值;并且EffectiveValueEntry提供了IsAnimated,IsExpression等屬性來表示當前的狀態。當然,這個賦值的操作比較復雜,這個優先級分兩大類:一 ModifiedValue中各屬性的優先級;二 對于ExpressionValue來說,它又有自己的優先級,Local>Style>Template…這里就不詳細解釋了。
依賴屬性的優點
回過頭來,總結一下依賴屬性的優點:
- 優化了屬性的儲存,減少了不必要的內存使用。
- 加入了屬性變化通知,限制、驗證等,
- 可以儲存多個值,配合Expression以及Animation等,打造出更靈活的使用方式。
總結
借助于依賴屬性,WPF提供了強大的屬性系統,可以支持數據綁定、樣式、動畫、附加屬性等功能。這篇文章主要是簡略的實現了一個從屬性到依賴屬性的發展過程,當然,具體和WPF的實現還有偏差,希望朋友們都能抓住這個主要的脈絡,更好的去玩轉它。
除了依賴屬性的實現,還有一些很重要的部分,比如借助于依賴屬性提出的附加屬性,以及如何利用依賴屬性來更好的設計實現程序,使用依賴屬性有哪些要注意的地方。呵呵,那就,下篇吧。
作者:周永恒
出處:http://www.rzrgm.cn/Zhouyongh
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利。

浙公網安備 33010602011771號