依賴項屬性是標准.NET屬性的全新實現——具有大量新增價值。在WPF的核心特性(如動畫、數據綁定以及樣式)中需要嵌入依賴項屬性。WPF元素提供的大多數屬性都是依賴項屬性。到目前位置所見到的所有示例都用到了依賴項屬性,但你可能還沒有意識到這一點。這是因為依賴項屬性的用法和普通屬性的是相同的。
然而,依賴項屬性並非普通屬性。可能樂意認為依賴項屬性是添加了一套WPF功能的常規屬性(采用典型的.NET方式進行定義)。從概念上講,依賴項屬性確實以這種方式工作,但它們的背后的實現方式並非如此。原因十分簡單:出於性能的考慮。如果WPF設計者只是在.NET屬性系統上添加額外功能,就需要為編寫代碼創建一個復雜龐大的層次。如果不承受這一額外的負擔,普通屬性就不能支持這些依賴項屬性的所有功能。
依賴項屬性是專門針對WPF創建的。但WPF庫中的依賴項屬性都使用普通的.NET屬性過程(property procedure)進行了封裝。這樣便可以通過常規方式使用它們,即使使用它們的代碼不理解WPF依賴項屬性系統也同樣如此。用舊技術封裝新技術看起來有些奇怪,但這正是WPF能夠改變基礎組成部分(如屬性),而不會擾亂.NET領域中其他部分的原因。
一、定義依賴項屬性
相對於創建依賴項屬性,大多數情況下只是使用它們。但是,仍然有許多原因需要創建自己的依賴項屬性。顯然,如果正在設計自定義的WPF元素,它們肯定是關鍵部分。然而,當希望為原本不支持數據綁定、動畫或其他WPF功能的部分代碼添加這些功能時,也需要創建依賴項屬性。創建依賴項屬性並不難,但需要使用一些特殊語法。這與創建普通的.NET屬性完全不同。
第一步是定義表示屬性的對象,它是DependencyProperty類的實例。屬性信息應該始終保持可用,甚至可能需要在多個類之間共享這些信息(在WPF元素中這是十分普遍的)。因此,必須將DependencyProperty對象為與其相關聯的類的靜態字段。
例如,FrameworkElement類定義了Margin屬性,所有元素都共享該屬性。Margin屬性是依賴項屬性。這意味着,在FrameworkElement類中需要使用類似下面的代碼來定義Margin屬性:
public class FrameworkElement:UIElement.... { public static readonly DependencyProperty MarginProperty .... }
根據約定,定義依賴項屬性的字段的名稱是在普遍屬性的末尾處加上單詞"Property"。根據這種命名方式,可從實際屬性的名稱中區分出依賴項屬性的定義。字段的定義使用了readonly關鍵字,這意味着只能在FrameworkElement類的靜態構造函數中對其進行設置,這就是接下來將完成的任務。
二、注冊依賴項屬性
定義DependencyProperty對象只是第一步而已。為了使用依賴項屬性,還需要使用WPF注冊創建的依賴項屬性。這一步驟需要在任何使用屬性的代碼之前完成,因此必須在與其關聯的類的靜態構造函數中進行。
WPF確保DependencyProperty對象不能被直接實例化,因為DependencyProperty類沒有公有的構造函數。相反,只能使用靜態的DependencyProperty.Register()方法創建DependencyProperty實例。WPF還確保在創建DependencyProperty對象后不能改變該對象,因為所有DependencyProperty成員都是只讀的。它們的值必須作為Register()方法的參數來提供。
下面的代碼顯示了如何創建DependencyProperty對象。在此,FrameworkElement類使用靜態函數來初始化MarginProperty:
static FrameworkElement(){ FrameworkPropertyMetadata metadata=new FrameworkPropertyMetadata(new Thickness(),FrameworkPropertyMetadataOptions.AffectsMeasure); MarginProperty=DependencyProperty.Register("Margin",typeof(Thickness),typeof(FrameworkElement),metadata,new ValidateValueCallback(FrameworkElement.IsMarginValid)); .... }
注冊依賴項屬性需要經歷兩個步驟。首先創建FrameworkPropertyMetadata對象,該對象指示希望通過依賴項屬性使用什么服務(如支持數據庫綁定、動畫以及日志)。接下來通過調用DependencyProperty.Register()靜態方法注冊屬性。在這一步驟中,你負責提供以下幾個要素:
- 屬性名(在該例中為Margin)
- 屬性使用的數據類型(在該例中為Thickness結構)
- 擁有該屬性的類型(在該例中為FrameworkElement類)
- 一個具有附加屬性設置的FrameworkPropertyMetadata對象,該要素是可選的。
- 一個用於驗證屬性的回調函數,該要素是可選的
前三個要素都很直觀。FrameworkPropeMetadata對象和屬性驗證回調函數更有趣一些。
使用FrameworkPropertyMetadata對象配置創建的依賴項屬性的附加功能。FrameworkPropertyMetadata類的大多數屬性是簡單的Boolean標記,通過設置這些屬性來翻轉某項功能(每個Boolean標記的默認值為false)。只有少數幾個是指向用於執行特定任務的自定義方法的回調函數,其中一個是FrameworPropertyMetadata.Defaultvalue,用於設置在第一次初始化屬性時WPF將要應用的默認值。下表列出了FrameworkPropertyMetadata類的所有屬性。
名稱 | 說明 |
AffectsArrange、AffectsMeasure、AffectsParentArrange和AffectsParentMeasure | 如果為true,依賴項屬性會影響在布局操作的測量過程和排列過程中如何放置相鄰的元素或父元素。例如,Margin依賴項屬性將AffectsMeasure屬性設置為true,表面如果一個元素的邊距發生變化,那么布局容器需要重新執行測量步驟以確定元素新的布局。 |
AffectsRender | 如果為true,依賴項屬性會對元素的繪制方式造成一定的影響,要求重新繪制元素。 |
BindsTwoWayByDefault | 如果為true,默認情況下,依賴項屬性將使用雙向數據綁定而不是單向數據綁定。不過,當創建數據綁定時,可以明確指定所需的綁定行為。 |
Inherits | 如果為true,就通過元素樹傳播該依賴項屬性值,並且可以被嵌套的元素繼承。例如,Font屬性是可繼承的依賴項屬性——如果在更高層次的元素中為Font屬性設置了值,那么該屬性值就會被嵌套的元素繼承(除非使用自己的字體設置明確地覆蓋繼承而來的值) |
IsAnimationProhibited | 如果為true,就不能將依賴項屬性用於動畫 |
IsNotDataBindale | 如果為true,就不能使用綁定表達式設置依賴項屬性 |
Journal | 如果為true,在基於頁面的應用程序中,依賴項屬性將被保存到日志(瀏覽過的頁面的歷史記錄)中 |
SubPropertiesDoNotAffectRender | 如果為true,並且對象的某個子屬性(屬性的屬性)發生了變化,WPF將不會重新渲染該對象 |
DefaultUpdateSourceTrigger | 當該屬性用於綁定表達式時,該屬性用於為Binding.UpdateSourceTrigger屬性設置默認值。UpdateSourceTrigger屬性決定了數據綁定值在何時應用自身的變化。當創建綁定時,可以手動設置UpdateSourceTrigger屬性。 |
DefaultValue | 該屬性用於為依賴項屬性設置默認值 |
CoerceValueCallback | 該屬性提供了一個回調函數,用於在驗證依賴項屬性之前嘗試“糾正”屬性值 |
PropertyChangedCallback | 該屬性提供了一個回調函數,當依賴項屬性的值變化時調用該回調函數 |
三、添加屬性包裝器
創建依賴項屬性的最后一個步驟就是使用傳統的.NET屬性封裝WPF依賴項屬性。但典型的屬性過程是檢索或設置某個私有字段的值,而WPF屬性的屬性過程是使用在DependencyObject基類中定義的GetValue()和SetValue()方法。下面列舉一個示例:
public Thickness Margin { get{ return (Thickness)GetValue(MarginProperty);} set{ SetValue(MarginProperty,value);} }
當創建屬性封裝器時,應當只包含對SetValue()和GetValue()方法的調用,如上面的示例所示。不應當添加任何驗證屬性值和額外代碼、引發事件的代碼等。這是因為WPF中的其他功能可能會忽略屬性封裝器,並直接調用SetValue()和GetValue()方法(一個例子是,在運行時解析編譯過的XAML文件)。SetValue()和GetValue()方法都是公有的。
現在已經擁有了一個功能完備的依賴項屬性,可以使用屬性封裝器像設置其他任何.NET屬性那樣設置該依賴項屬性了:
myElement.Margin=new Thickness(5);
還有一個額外的細節。依賴項屬性遵循嚴格的優先規則來確定他們的當前值。即使你沒有直接設置依賴項屬性,它也可能已經有了數值——該數值可能是由數據綁定、樣式或動畫提供的,也可能是通過元素樹繼承來的。不過,只要直接設置了屬性值,設置的屬性值就會覆蓋所有其他的影響。
以后,可能希望刪除本地值設置,並像從來沒有設置過那樣確定屬性值。顯然,這不能通過設置一個新值來實現。反而需要使用另外一個繼承自DependencyObject類的方法:ClearValue()。下面是該方法的用法:
myElement.ClearValue(FrameworkElement.MarginProperty);
四、WPF使用依賴項屬性的方式
WPF的許多功能都需要使用依賴項屬性。但是,所有這些功能都是通過每個依賴項屬性都支持的兩個關鍵行為進行工作的——更改通知和動態值識別。
可能與你所期望的相反,當屬性值發生變化時,依賴項屬性不會自動引發事件以通知屬性值發生了變化。相反,他們會觸發受保護的名為OnPropertyChangedCallback()的方法。該方法通過兩個WPF服務(數據綁定和觸發器)傳遞信息,並調用PropertyChangedCallback()回調函數(如果已經定義了該函數)。
換句話說,當屬性變化時,如果希望進行響應,有兩種選擇——可以使用屬性值創建綁定,也可以編寫能夠自動改變其他屬性或開始動畫的觸發器。但依賴項屬性沒有提供一種通用的方法以觸發一些代碼,從而對屬性的變化進行響應。
對於依賴項屬性工作很重要的第二個功能就是動態值識別。這意味着當從依賴項屬性檢索值時,WPF需要考慮多個功能。
依賴項屬性因該行為得名——本質上,依賴項屬性依賴於多個屬性提供者,每個提供者都有各自的優先級。當從屬性檢索值時,WPF屬性系統會通過一系列步驟獲取最終值。首先通過考慮以下因素(按優先級從低到高的順序排列)來決定基本值(base value):
(1)默認值(由FrameworkPropertyMetadata對象設置的值)。
(2)繼承而來的值(假設設置了FrameworkPropertyMetadata.Inherits標志,並為包含層次中的某個元素提供了值)。
(3)來自主題樣式的值。
(4)來自項目樣式的值。
(5)本地值(使用代碼或XAML直接為對象設置的值)。
如上面的列表所示,可通過直接應用一個值來覆蓋整個層次。如果不這么做,屬性值可由上面列表中的下一個可用項確定。
WPF按照上面的列表確定依賴項屬性的基本值。但基本值未必就是最后從屬性中檢索的值。這是因為WPF還需要考慮其他幾個可能改變屬性值得提供者。
下面列出WPF決定屬性值得四步驟過程:
(1)確定基本值
(2)如果屬性是使用表達式設置的,就對表達式進行求值。當前,WPF支持兩類表達式:數據綁定和資源。
(3)如果屬性是動畫的目標,就應用動畫。
(4)運動CoerceValueCallback回調函數來修正屬性值。
本質上,依賴項屬性被硬編碼連接到一小部分WPF服務中。如果並非用於這個基礎結構,這些功能就會無謂地增加復雜性並帶來沉重負擔。
五、共享的依賴項屬性
盡管一些類具有不同的繼承層次,但他們回共享同一依賴項屬性。例如,TextBlock.FontFamily屬性和Control.FontFamily屬性指向同一個靜態的依賴項屬性,該屬性實際上是在TextElement類中定義的TextElement.FontFamilyProperty依賴項屬性。TextElement類的靜態構造函數注冊該函數,而TextBlock類和Control類的靜態構造函數只是通過調用DependencyProperty.AddOwner()方法重用該屬性:
TextBlock.FontFamilyProperty=TextElement.FontFamilyProperty.AddOwner(typeof(TextBlock));
可以使用相同的基礎來創建自己的自定義類(假定在所繼承的父類中還沒有提供屬性,否則直接重用即可)。還可以使用重載的AddOwner()方法來提供驗證回調函數以及僅應用於依賴項屬性用法的新FrameworkPropertyMetadata對象。
在WPF中重用依賴項屬性可得到一些奇異的效果,最有名得是樣式。例如,如果使用樣式自動設置TextBlock.FontFamily屬性,樣式也會影響Control.FontFamily屬性,因為在后台這兩個類使用同一個依賴項屬性。
六、附加的依賴項屬性
附加屬性是一種依賴項屬性,由WPF屬性系統管理。不同之處在於附加屬性被應用到得類並非定義附加屬性的那個類。
例如,Grid類定義了Row和Column附加屬性,這兩個屬性被用於設置Grid面板包含的元素,以指明這些元素應被放到哪個單元格中。類似地,DockPanel類定義了Dock附加屬性,而Canvas類定義了Left、Right、Top和Bottom附加屬性。
為了定義附加屬性,需要使用RegisterAttached()方法,而不是使用Register()方法。下面列舉了一個注冊Grid.Row屬性的例子:
FrameworkPropertyMetadata metadata=new FrameworkPropertyMetadata(0,new PropertyChangedCallback(Grid.OnCellAttachedPropertyChanged)); Grid.RowProperty=DependencyProperty.RegisterAttached("Row",typeof(int),typeof(Grid),metadata,new ValidateValueCallback(Grid.IsIntValueNotNegative));
與普遍的依賴項屬性一樣,可提供FrameworkPropertyMetadata對象和ValidateValueCallback回調函數。
當創建附加屬性時,不必定義.NET屬性封裝器。這是因為附加屬性可以被用於任何依賴對象。例如,Grid.Row屬性可能被用於Grid對象(如果在Grid控件中嵌套了另一個Grid控件),也可能被用於其他元素。實際上,Grid.Row屬性甚至可以被用於並不位於Grid控件中的元素——甚至在元素樹中根本就不存在Grid對象。
不是使用.NET屬性封裝器,反而附加屬性需要調用兩個靜態方法來設置和獲取屬性值,這兩個方法使用了為人熟知的SetValue()和GetValue()方法(繼承自DependencyObject類)。這兩個靜態方法應當命名為SetPropertyName()和GetPropertyName()。
下面是實現Grid.Row附加屬性的靜態方法:
public static int GetRow(UIElement element) { if(element==null) { throw new ArgumentNullException(...); } return (int)element.GetValue(Grid.RowProperty); } public static void SetRow(UIElement element,int value) { if(element==null) { throw new ArgumentNullException(...); } element.SetValue(Grid.RowProperty,value); }
下面的示例使用代碼將元素放到Grid控件中的第一行:
Grid.SetRow(txtElement,0);
也可直接調用SetValue()或GetValue()方法,從而繞過這兩個靜態方法:
txtElement.SetValue(Grid.RowProperty,0);
顯然,使用SetValue()方法設置附加屬性的過程不符合一般人的思維習慣。盡管XAML不允許,但可在代碼中使用重載版本的SetValue()方法,為任何依賴項屬性附加一個值,即使該屬性沒有被定義為附加屬性也同樣如此。例如,下面的代碼是完全合法的:
ComboBox comboBox=new ComboBox(); .... comboBox.SetValue(PasswordBox.PasswordCharProperty,"*");
這里為ComboBox對象設置了PasswordBox.PasswordChar屬性值,盡管PasswordBox.PasswordCharProperty屬性被注冊為普通的依賴項屬性而不是附加屬性。該操作不會改變ComboBox的工作方式——畢竟,ComboBox的內部代碼不會去查找它並不知道的屬性的值——但在你自己的代碼中可以對PasswordChar值進行操作。
盡管很少使用,但該技巧提供了WPF屬性系統內部工作方式的更多細節,還演示了其非凡的可擴展性。它還表明,盡管使用不同的方法注冊附加屬性和常規的依賴項屬性,但對於WPF而言它們沒有實質性區別。唯一的區別是XAML解析器是否允許。除非將屬性注冊為附加屬性,否則在標記的其他元素中無法設置。