Silverlight的依賴屬性與附加屬性
好久沒寫Silverlight了,依賴屬性(Dependency Property)和附加屬性(Attached Property)這兩個算是很基礎的知識都不是很記得了。寫一寫,當做一下筆記吧。
CLR屬性 與 依賴屬性
CLR屬性我們非常熟悉了,在DotNet編程中隨處可見。最簡單最常見的屬性訪問器就是直接操縱類的私有成員,如下:
public class Person { private String _name; public string Name { get { return _name; } set { _name = value ; } } }
C#3.0對這種常見的寫法提供了“自動屬性”這一特性,方便了偶等這些懶惰的碼農。
public class Person { public string Name { get; set; } }
這兩種寫法是等價的,都是 需要設立一個實例級的私有變量作為屬性訪問器的持久存儲 。這對于我們非UI應用來說沒什么。因為第一,我們一般不會創建太多類實例;第二,一個類的屬性通常不會很多,加幾個私有變量不會增加系統負擔。但是這兩個理由對于UI應用程序來說恰恰不成立。
在很多UI應用中,我們經常會創建很多類實例,成千上萬個實例在UI系統中是很普遍的事情。同時,UI類通常會包含大量的屬性供設計人員使用,例如背景顏色,前景顏色,字體,邊距等等,這些屬性在絕大多數情況下會保持默認值,如果為每個實例都建立這么多的私有變量來存儲UI屬性的值,勢必會造成極大的浪費,對系統負擔的開銷也是不小。
鑒于以上提到的問題,設計一個高效的屬性存儲系統對于UI應用程序的開發是非常重要的。因此Silverlight引入了“依賴屬性(DependencyProperty)”。
采用鍵值對替代成員變量作為屬性內部存儲
傳統CLR屬性,一個屬性對應一個私有變量,UI元素的屬性那么多,創建過多的私有變量不是一件簡單的事情,況且大多數屬性只會用到默認值。因此Silverlight在每個類實例中使用一個字典型的成員變量來存放那些用戶顯式設置的屬性(稱為Local Value本地值),沒有設置的屬性就不存。那屬性的默認值存放在哪?既然各個實例的默認值都一樣(不然也不叫默認值了),那么直接存放到靜態成員變量( 依賴屬性的靜態成員變量,而不是注冊依賴屬性的類的成員變量 )上就行了。這也就大大提高了存儲的效率。
在實現上,Silverlight中所有的UI元素都繼承自DependencyObject,這個類封裝了對依賴屬性的存儲以及訪問等操作。
注冊依賴屬性
既然依賴屬性采用鍵值對這樣的哈希結構進行存儲,那么要獲取不同屬性的值,我們就必須使用不同的哈希鍵,否則就會讀取到其他屬性的值了。因此,當我們在向Silverlight屬性系統注冊依賴屬性的時候,Silverlight會返回一個唯一的屬性標識對象,類型為DependencyProperty。我們以后就通過這個唯一標識對象去訪問依賴屬性的值。
由于這個唯一標識符是所有類實例都公用并且不會被修改的,因此我們通常將其保存到一個static readonly的成員變量中。
DependencyProperty類提供了兩個方法,一個是Register方法,用于注冊依賴屬性;另外一個是RegisterAttached,用于注冊附加屬性,這個后面再講。
public static DependencyProperty Register( string name, Type propertyType, Type ownerType, PropertyMetadata typeMetadata )
Register方法的簽名由幾部分組成,Name參數指明了依賴屬性使用的名稱,這個名字很重要, 在定義控件Style和Template的時候,Setter的Property屬性填入的值就是注冊依賴屬性時使用的名稱 ;propertyType指明了依賴屬性實際的類型,ownerType指明了是哪個類注冊了此依賴屬性,最后typeMetadata存放了一些依賴屬性的元信息,包括依賴屬性使用的默認值,還有屬性值發生變更時的通知函數。
屬性的存取
和CLR屬性不同,依賴屬性不是直接對私有變量的操縱,而是通過GetValue和SetValue的方法來操作屬性值的。
下面的代碼演示了為Ball控件設置一個Center的依賴屬性,并且在程序中讀取和修改此屬性的過程:
public class Ball : Control { public static readonly DependencyProperty CenterProperty = DependencyProperty.Register( "Center" , typeof (Point), typeof (Ball), null ); } public class BallApp { public void RollBall(Ball ball) { Point curCenter = (Point)ball.GetValue(Ball.CenterProperty); curCenter.X++; // 注意對值類型對象操作完畢之后一定要調用SetValue修改才能生效 ball.SetValue(Ball.CenterProperty, curCenter); } }
由于上述對依賴屬性的操作經常需要涉及到類型的轉換,比較麻煩,而傳統CLR屬性用起來和直接操縱普通變量一樣方便,因此通常在設計依賴屬性的時候,都會使用CLR屬性將其包裝起來,我們稱之為增強型的CLR屬性。
public class Ball : Control { public static readonly DependencyProperty CenterProperty = DependencyProperty.Register( "Center" , typeof (Point), typeof (Ball), null ); public Point Center { get { return (Point)GetValue(CenterProperty); } set { SetValue(CenterProperty, value ); } } }
按照約定,依賴屬性的名稱通常是相應CLR屬性名稱后面加上個“Property”字符串。
事實上,使用CLR包裝依賴屬性并不只是為了方便( http://msdn.microsoft.com/en-us/library/cc221408%28VS.95%29.aspx#back_dependency_properties ),很多依賴于CLR屬性作為基礎的工具或者子系統并不能直接訪問依賴屬性,而只能通過CLR屬性去間接訪問依賴屬性。例如上面的例子中,假設我們并沒有設置一個Center的CLR屬性,那么以下的Xaml將會編譯失敗,因為Xaml解析器無法知道Ball類有一個Center的依賴屬性(在Style中設置Center屬性值就可以編譯成功,因為Style是動態查找屬性的)。
< Ball Center ="2" />
依賴屬性的尋值邏輯和值變更通知
上面提到的只是依賴屬性相比CLR屬性在存儲效率的不同,實際上,依賴屬性還有其他實用的特性。
尋值邏輯
CLR屬性在獲取值的時候是直接讀取成員變量值返回的,而依賴屬性在使用的時候是通過GetValue函數的調用來獲取屬性的值。實際上,GetValue內部做的事情可不止是簡單的讀取字典里頭存放的值。他還有尋值邏輯。如下圖所示:
當你調用GetValue去讀取一個依賴屬性的值的時候,Silverlight的屬性系統會首先從動畫系統中查找當前是否有作用在此依賴屬性上的動畫,如果有,則返回此動畫值。從這里也可以看出,依賴屬性是Silverlight實現動畫機制的基礎。 注意,如果動畫已經停止了,并且沒有設置FillBehavior=HoldEnd的話,那么Silverlight就不會返回此動畫值。
如果讀不到動畫值,那么Silverlight就會嘗試讀取本地值。本地值有幾種類型,一種是用戶通過代碼或者Xaml直接設定的值。一種是通過資源綁定得到的值,最后一種是通過數據綁定得到的值。這些都被視為本地值。
< StackPanel x:Name ="LayoutRoot" > < StackPanel.Resources > < System:String x:Key ="TextBlockResource" > 資源數據綁定文本 </ System:String > </ StackPanel.Resources > < TextBlock Text ="{Binding Source={StaticResource TextBlockResource}}" /> < TextBlock x:Name ="DataBindingElement" Text ="{Binding ElementName}" /> </ StackPanel >
如果還是讀取不到,那么就繼續嘗試讀取控件模板和樣式中設置的值。
如果所有這些值都讀取失敗,那么Silverlight屬性系統就會返回該依賴屬性的默認值。當我們注冊依賴屬性的時候,可以傳入一個PropertyMetaData對象,這個對象包含了此依賴屬性的默認值和值變更通知回調函數。如果注冊的時候沒有傳入默認值,則對于引用類型的依賴屬性,返回null,對于字符串,返回String.Empty,對于值類型,則返回一個以默認值初始化的實例。
這里需要對集合類型特別注意,由于通過PropertyMetaData傳入的默認值是所有類實例共享的,因此,一定要在類構造函數中顯式傳入集合的實例 。
public class GameRoom : Control { public List<Ball> Balls { get { return (List<Ball>)GetValue(BallsProperty); } set { SetValue(BallsProperty, value ); } } public static readonly DependencyProperty BallsProperty = DependencyProperty.Register( "Balls" , typeof (List<Ball>), typeof (GameRoom), null ); public GameRoom() { Balls = new List<Ball>(); } }
可能正是因為Silverlight的依賴屬性在獲取值的時候需要從多個地方去讀取值,而不是像CLR屬性一樣,直接從成員變量中讀取值,所以才被稱之為“依賴”屬性吧。
值變更通知
屬性值的變更通知我們并不陌生。我們在DotNet中實現的時候,一般是讓類實現INotifyPropertyChanged接口。在UI系統中,值變更通知是經常需要用到的。數據源一旦變更,所有相應的UI元素的值都要相應的做出調整。Silverlight的依賴屬性對此有內置的支持。只要你在綁定時使用依賴屬性,那么當依賴屬性值發生變更的時候,所有綁定的地方的值都會同步更新。而且,依賴屬性也提供了一個值變更通知函數(在注冊依賴屬性時通過PropertyMetaData傳入),你可以自定義一個函數來控制值變更時需要執行的操作。
public class Ball : Control { public static readonly DependencyProperty CenterProperty = DependencyProperty.Register( "Center" , typeof (Point), typeof (Ball), new PropertyMetadata(OnCenterChanged)); public Point Center { get { return (Point)GetValue(CenterProperty); } set { SetValue(CenterProperty, value ); } } private static void OnCenterChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { Ball ball = d as Ball; // 獲取新的球心 Point newCenter = (Point)e.NewValue; // ... } }
Silverlight的附加屬性(Attached Property)——全局的依賴屬性
剛才提到的依賴屬性和CLR屬性一樣都是服務于某一個類的。只不過將屬性改造得存儲上更有效率,使用上更加強大。在Silverlight中還有一種特殊的依賴屬性,這種依賴屬性并不只是服務于某個特定的類,而是服務于全局,這就是附加屬性。從名字上也可以看出來,附加屬性是在某個類里面注冊,然后可以被其他類所使用。
什么情況下需要使用附加屬性呢?舉Canvas類的ZIndex屬性作為例子。
容器類在疊加子控件的時候,需要考慮哪個控件放置在最上面,那個放在下面。
那么容器類怎么知道子控件的疊放順序呢?最不動腦子的設計就是為所有的控件都添加一個ZIndex的屬性,屬性的值代表疊放的順序。但這樣的后果就是,如果我這個控件不參與布局,那多這個屬性就會顯得很浪費。所以比較理想的設計是,需要用到這個屬性的時候就有這個屬性,不需要的時候就沒有這個屬性的負擔。附加屬性的出現就是為了解決這樣的問題。一旦控件需要某個屬性的時候,我們可以把這個屬性附加到這個控件類上。
注冊附加屬性和依賴屬性差不多,只不過函數名為RegisterAttached。這個就不多說了。
什么時候應該用到依賴屬性
既然依賴屬性那么高效,而且那么強大,那么我們是不是應該保持使用依賴屬性的習慣呢?事實上,任何好處都是有代價的。Silverlight的依賴屬性在訪問效率上并不如直接訪問成員變量那么高效。因此,對于那些比較簡單而訪問頻率又非常高的屬性,建議還是使用傳統的CLR屬性去實現。
暫時想到這么多了,以后有新的認識再補充補充。
—— Kevin Yang
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
