WPF高級教程(六)依賴項屬性

概述

依賴項屬性是屬性的一種全新的實現。通過對原有屬性的升級,依賴項能夠實現數據綁定,動畫或者WPF的其他進階功能。通過對於依賴項屬性的封裝,使得依賴項屬性的使用與普通屬性一樣,這樣既兼容了老的使用方法,又把WPF的新特性帶到了普通的WPF程序中。

功能

  • 每個依賴項屬性都支持 更改通知和動態值識別,這也是依賴項屬性的特點和基礎

實現

定義依賴項屬性

  • 只能爲依賴對象(繼承自DependencyObject的類)添加依賴項屬性,通過第二節的類圖我們知道,幾乎所有的WPF元素都繼承自DependencyObject
  • 定義的依賴項屬性必須是靜態的,這樣才能通過類直接拿到屬性而不需要實例化
  • 約定屬性的名稱必須以Property結尾(約定而非必須)
public static readonly DependencyProperty VerticalStretchSizeProperty;

註冊依賴項屬性

  • 註冊必須在屬性的靜態構造函數中進行以保證在使用屬性之前屬性已經被註冊。
  • DependencyProperty不能被實例化,它的構造函數時私有的,只能通過Register()方法來實例化
  • DependencyProperty是隻讀的,能保證它不能在創建後被修改
    public class FormatedText
    {
        static FormatedText() // 注意是在靜態的構造函數中註冊
        {
            // 首先要創建 FrameworkPropertyMetadata對象
            // 這個對象說明了我們需要通過依賴項屬性使用什麼服務
            PropertyMetadata VerticalStretchMeta = new PropertyMetadata(0.0, (DependencyObject d, DependencyPropertyChangedEventArgs e) =>
            {
                (d as FormatedText).FlushTheText();
            });
            // 調用Register註冊
            // 參數: 屬性名 VerticalStretchSize
            // 參數: 屬性的數據類型 double
            // 參數: 誰擁有該屬性 FormatedText
            // 參數: 可選的FrameworkPropertyMetadata對象
            // 參數: ValidateValueCallback 可選的回調函數,用於驗證屬性
            VerticalStretchSizeProperty = DependencyProperty.Register("VerticalStretchSize", typeof(double), typeof(FormatedText), VerticalStretchMeta); 
        }
    }
    
  • FrameworkPropertyMetadata對象可以用於給依賴項屬性設置默認值,調整默認的綁定行爲(單向雙向),調整是否允許綁定,還能在屬性改變之前糾正屬性值,能監聽到屬性的變化,下面的圖列出了FrameworkPropertyMetadata對象能做的事情
    在這裏插入圖片描述
    在這裏插入圖片描述

添加屬性包裝器

通過將依賴項屬性包裝成傳統的屬性將會讓依賴項屬性能像普通屬性一樣被使用,這也是創建依賴項屬性必須的一步。

public double VerticalStretchSize
{
    get
    {
        return (double)GetValue(VerticalStretchSizeProperty);
    }
    set
    {
        SetValue(VerticalStretchSizeProperty, value);
    }
}
  • 注意不能在這一步進行輸入的驗證,數據的處理,如果想驗證屬性或者引發事件,在這裏的get set中進行處理是不能達到目的的,原因簡單來說就是WPF會繞過屬性包裝器直接訪問GetValue SetValue方法,比如在運行時解析編譯過的Xaml的時候
  • 如果需要進行輸入的驗證,需要在第二步,DependencyProperty.ValidateValueCallback中進行驗證,即註冊時候的最後一個可選參數
  • 如果需要進行事件的觸發,需要在FrameworkPropertyMetadata.PropertyChangedCallback中進行,即註冊的時候倒數第二個參數
  • 如果要進行值的修正,需要在FrameworkPropertyMetadata.CoerceValueCallback中修正

清除依賴項屬性

清除依賴項屬性用於將依賴項屬性重置爲從來沒有設置過的狀態

myControl.ClearValue(FrameworkElement.VerticalStretchSizeProperty)

更改通知

我們知道依賴項屬性支持更改通知,那我們如何監聽這種通知呢,方法有兩種:

  1. 創建一個綁定
  2. 編寫觸發器

對於WinForm中,我們想要通知,就需要引發一個事件,依賴項屬性同樣支持在屬性改變的時候引發事件,即在FrameworkPropertyMetadata.PropertyChangedCallback中進行,事實上WPF中已經有這樣的實現了,比如TextBox的TextChanged事件,需要注意的是,這種使用對於性能有額外的消耗,所以這種實現方式並不是通用的,更像是對於之前WinForm使用方式的兼容和WPF綁定機制的補充。

動態值識別

動態值試別聽起來很難理解,實際意思很簡單,依賴項屬性究竟值是多少是由一系列值決定的,換句話說,依賴項屬性的值不是保存在某一塊內存中,而是由許多因素決定的,要確定這個值是多少,只需要循着優先級查找即可,優先級是:

  1. 默認值
  2. 繼承來的值,可能由父控件提供
  3. 來自主題樣式的值
  4. 來自項目樣式的值
  5. 本地值(即手動設置的值)

優先級高的覆蓋優先級低的(即上圖中的5覆蓋1),有點像CSS的屬性覆蓋。這樣做的優點十分明顯,WPF不需要額外的內存開銷去保存這些屬性值,需要設置屬性的時候只需要去相應的地方獲取值,而這些值本就是以各種形式存在於內存中的,這在屬性很多的情況下可以節約大筆的內存消耗。

實際上,WPF支持表達式賦值,支持轉換器等,使得WPF確定屬性值比我們上面描述的更復雜,有更多需要考慮的因素,實際上WPF確定屬性值的順序是:

  1. 由上面描述的流程確定一個基本值
  2. 如果有設置值的表達式,應該對錶達式進行求值(數據綁定和資源都屬於表達式)
  3. 屬性是動畫,應用動畫
  4. 運行CoerceValueCallback修正值

共享的依賴項屬性

我們注意到一個很有趣的事情,依賴項屬性的定義都是靜態的,而我們使用的屬性,是通過屬性包裝器包裝過的非靜態的屬性,這樣就讓不同的控件的屬性值不會互相影響,但是根據上面的屬性值確定的流程來看,如果不進行任何形式的賦值,屬性的值最終由默認值來確定,默認值會由依賴屬性在註冊的時候確定,而依賴屬性是靜態的,一個類在定義的時候可以訪問到別的類的靜態屬性,也就可以訪問到別的類的依賴屬性,進一步,也就可以使用別人的依賴屬性定義來構造自己的依賴屬性,這樣的使用也確實存在。

很多跟文字相關的控件都擁有FontFamily這個依賴屬性,我們只需定義一次,其他的需要使用這個依賴屬性的類都可以拿來用,甚至不受繼承關係的限制,要實現共享依賴屬性需要下面的語法

// 使用下面的代碼替換依賴屬性的註冊,可以省去設置默認值,默認行爲等代碼
public class TextBlock
{
        public static readonly DependencyProperty FontFamilyProperty =
            TextElement.FontFamilyProperty.AddOwner(typeof(TextBlock), new UIPropertyMetadata(null));
}

我們接着思考,如果這樣設置了共享之後,我們進行任何屬性的設置,是不是兩個共享的屬性最終使用默認值的時候使用的是同一塊內存呢?答案是肯定的,這就導致了樣式中如果自動設置了TextBlock.FontFamily屬性,樣式也會影響Control.FontFamily屬性。

附加屬性

我們之前講過附加屬性,比如Grid.Row屬性。任何控件都可以使用,甚至不在Grid中的控件也可以正常使用,我們可以認爲,附加屬性,就是全局的依賴項屬性,任何控件都可以使用。定義這樣的附加屬性,需要使用RegisterAttached方法進行註冊。

注意:

  • 需要使用RegisterAttached方法進行註冊
  • 因爲它是全局的,不是某一個類的屬性,所以不需要屬性包裝器
  • 沒有屬性包裝器,也需要實現Get Set方法,因爲是全局的,顯然 Get Set方法都應該是靜態的
// 註冊附加屬性
Grid.RowProperty = DependencyProperty.RegisterAttached("Row", typeof(int), typeof(Grid), metadata, new ValidateValueCallback(Grid.IsIntValueNotNegative))
// 實現設置和獲取屬性值
public static int GetRow(UIElement element)
{
    if(element == null)
    {
        throw new ArguementNullException(...);
    }
    return (int)element.GetValue(Grid.RowProperty);
}
public static void SetRow(UIElement element, int value)
{
    if(element == null)
    {
        throw new ArguementNullException(...);
    }
    element.SetValue(Grid.RowProperty, value);
}
// 將元素放置到Grid的第二行
Grid.SetRow(txtElement, 1);
// 嘗試不通過Grid的Set來直接放置元素
txtElement.SetValue(Grid.RowProperty, 1);

上面不通過Set直接設置值的方法其實給了我們一些啓示,在修改一個依賴屬性的值的時候,我們可以通過他的屬性包裝來設置這個值,同樣,我們也可以直接調用屬性包裝器中調用的SetValue(VerticalStretchSizeProperty, value);這樣的方法來改變依賴項屬性的值。這也就印證了我們之前說的,不要在屬性包裝器的get set中嘗試截獲值,而是要在回調中處理屬性值的變化。那麼下面我們就詳細講講屬性驗證和強制回調。

屬性驗證

屬性生效之前我們有多少機會能夠驗證這個值是否合法呢,答案是兩個:

  • ValidateValueCallback 這個回調可以接受或者拒絕新值。用於捕獲違反約束的明顯錯誤,它是Register方法的一個參數,是回調函數。與下面的回調相比,在這個回調中我們不能檢查其他屬性值,原因是我們訪問不到擁有這個屬性的實際對象。
// 注意最後一個參數
MarginProperty = DependencyProperty.Register("Margin", typeof(Thickness), typeof(FrameworkElement), metadata, new ValidateValueCallback(FrameworkElement.IsMarginValid));
// 最後一個參數Callback中傳遞的參數必須是一個靜態的方法
// 回調回來的參數值只有當前設置值,且不能訪問其他的屬性值
// 我們可以發現,這裏能驗證的值多是數據格式問題或者不能爲負數這種明顯錯誤
private static bool IsMarginValid(object value)
{
    Thickness thickness1 = (Thickness) value;
    return thickness1.IsValid(true, false, true, false);
}
  • CoerceValueCallback(Coerce強制) 該回調函數用於修正值。常用於兩個依賴屬性相互影響的情況(例如A B兩個依賴屬性的和爲100),它作爲metadata中的一個參數註冊進依賴屬性中
FrameworkPropertyMetadata metadata = new FrameworkPropertyMetadata();
matadata.CoerceValueCallback = new CoerceValueCallback(CoerceMaximum);

DependencyProperty.Register("Maximum", typeof(double), typeof(RangeBase), metadata);

// 可以看到這個回調函數能夠拿到參數d,就是這個依賴項屬性的實例,自然可以獲取這個依賴項屬性中的所有的值
private static object CoerceMaximum(DependencyObject d, object value)
{
    RangeBase base1 = (RangeBase)d;
    if((double)value < base1.Minimum)
    {
        return base1.Minimum;
    }
    return value;
}

那麼這兩個方法的順序是怎麼樣的呢?

  1. CoerceValueCallback有機會修正傳來的屬性值
  2. ValidateValueCallback生效,返回true接受值,返回false拒絕值。
  3. 觸發PropertyChangedCallback,在這個裏面可以引發自定義事件來新增自定義的監聽邏輯

多個值互動

我們想一下滾動條的邏輯:

  • 滾動條擁有最大值和最小值
  • 最大值不能小於最小值,最小值不能大於最大值,Value的設置必須在最大值和最小值中間

我們看一下WPF如何處理這樣的邏輯:

  1. 設置Maxmum的強制回調
    private static object CoerceMaximum(DependencyObject d, object value)
    {
        RangeBase base1 = (RangeBase)d;
        if((double)value < base1.Minimum)
        {
            return base1.Minimum;
        }
        return value;
    }
    
  2. 監聽Minimum的屬性變化並且觸發Maxmum和Value的強制回調
    // metadata中可以設置PropertyChangedCallback
    private static void OnMinimumChanged(Dependency d, DependenctPropertyChangedEventArgs e)
    {
        RangeBase base1 = (RangeBase) d;
        base1.CoerceValue(RangeBase.MaximumProperty);
        base1.CoerceValue(RangeBase.ValueProperty);
    }
    
  3. 在最大值變化的時候觸發Value的強制回調

這樣優先權是 Mini > Max > Value

這裏我們需要注意一個很有意思的事情,一旦我們設置了Value的值,這個值會被記錄下來,如果不符合大小限制,將會使用最大值或者最小值,但是一旦我們在程序中將最大值最小值改變使得Value符合範圍,則Value的值就會生效,這也符合我們的正常思維。

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