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尋值邏輯

當你調用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屬性作爲例子。

容器類在疊加子控件的時候,需要考慮哪個控件放置在最上面,那個放在下面。

image

那麼容器類怎麼知道子控件的疊放順序呢?最不動腦子的設計就是爲所有的控件都添加一個ZIndex的屬性,屬性的值代表疊放的順序。但這樣的後果就是,如果我這個控件不參與佈局,那多這個屬性就會顯得很浪費。所以比較理想的設計是,需要用到這個屬性的時候就有這個屬性,不需要的時候就沒有這個屬性的負擔。附加屬性的出現就是爲了解決這樣的問題。一旦控件需要某個屬性的時候,我們可以把這個屬性附加到這個控件類上。

註冊附加屬性和依賴屬性差不多,只不過函數名爲RegisterAttached。這個就不多說了。

什麼時候應該用到依賴屬性

既然依賴屬性那麼高效,而且那麼強大,那麼我們是不是應該保持使用依賴屬性的習慣呢?事實上,任何好處都是有代價的。Silverlight的依賴屬性在訪問效率上並不如直接訪問成員變量那麼高效。因此,對於那些比較簡單而訪問頻率又非常高的屬性,建議還是使用傳統的CLR屬性去實現。

 

暫時想到這麼多了,以後有新的認識再補充補充。

轉自:http://www.gridsum.com/gsdnblogs/post/2010/03/29/silverlight25e7259a258425e425be259d25e825b5259625e525b1259e25e6258025a725e425b8258e25e92599258425e5258a25a025e525b1259e25e6258025a7.aspx

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章