【筆記】【WPF編程寶典】 第04章 依賴項屬性

目錄

4.1 理解依賴項屬性

4.1.1 定義依賴項屬性

4.1.2 註冊依賴項屬性

4.1.3 添加屬性包裝器

4.1.4 WPF 使用依賴項屬性的方式

4.1.5 共享依賴項屬性

4.1.6 附加依賴項屬性

4.2 屬性驗證

4.2.1 驗證回調

4.2.2 強制回調


4.1 理解依賴項屬性

依賴項屬性是標準.NET屬性的全新實現——具有大量新增價值。在WPF的核心特性(如動畫、數據綁定以及樣式)中需要嵌入依賴項屬性。WPF元素提供的大多數屬性都是依賴項屬性

依賴項屬性是專門針對WPF創建的。但WPF庫中的依賴項屬性都是使用普通的.NET屬性過程進行了封裝。這樣便可以通過常規方式使用它們,即使用它們的代碼不理解WPF依賴項屬性系統也同樣如此。

4.1.1 定義依賴項屬性

只能爲依賴對象(繼承自DependencyObject的類)添加依賴項屬性。

第一步是定義表示屬性的對象,他是DependencyProperty類的實例。屬性信息應該始終保持可用,甚至可能需要多個類之間共享這些信息。因此,必須將DependencyProperty對象定義爲與其相關聯的類的靜態字段。

例如,FrameworkElement類定義了Margin屬性,所有元素都共享該屬性。這意味着,在FrameworkElement類中需要使用類是下面的代碼來定義Margin屬性:

public class FrameworkElement:UIElement,...
{
    public static readonly DependencyProperty MarginProperty;
    ...
}

根據約定,定義依賴項屬性的字段的名稱是在普通屬性的末尾加上單詞“Property”。根據這種命名方式,可從實際屬性的名稱中區分出依賴項屬性的定義。字段的定義使用了readonly關鍵字,這意味着只能在FrameworkElement類的靜態構造函數中對其進行設置

4.1.2 註冊依賴項屬性

爲了使用依賴項屬性,還需註冊創建的依賴項屬性。這一步驟需要在任何使用屬性的代碼之前完成,因此必須在與其關聯的類的靜態構造函數中進行

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 CalidateCalueCallback(FrameworkElement.IsMarginValid);
    ...
}

註冊依賴項屬性需要經歷兩個步驟。首先創建FrameworkPropertyMetadata對象,該對象指示希望通過依賴項屬性使用什麼服務(如,支持數據綁定、動畫以及日誌)。接下來通過調用DependencyProperty.Register()靜態方法註冊屬性。

  • 屬性名稱
  • 屬性使用的數據類型
  • 擁有該屬性的類型
  • 一個具有附加屬性設置的FrameworkPropertyMetadata對象(可選)
  • 一個用於驗證屬性的回調函數(可選)

FrameworkPropertyMetadata對象配置依賴項屬性的附加功能。FrameworkPropertyMetadata類大多數屬性是簡單的bool標誌,通過設置這些屬性來翻轉某項功能(默認值均爲false)。只有燒出幾個是指向用於執行特定任務的自定義方法的回調函數,其中一個是FrameworkPropertyMetadata.DefaultValue,用於設置在第一次初始化屬性時WPF講演應用的默認值。

FrameworkPropertyMetadata類的屬性
名稱 說明

AffectsArrange

AffectsMeasure

AffectsParentArrange

AffectsParentMeasure

如果爲true,依賴項屬性會影響在佈局操作的測量過程和排列過程中如何放置相鄰的元素或父元素。

例如,Margin依賴項屬性將AffectsMeasure屬性設置爲true,表明如果一個元素的邊距發生變化,那麼佈局容器需要重新執行測量步驟以確定元素新的佈局

AffectsRender 如果爲true,依賴項屬性會對元素的繪製方式造成一定的影響,要求重新繪製元素
BindsTwoWaysByDefault 如果爲true,默認情況下,依賴項屬性使用雙向數據綁定而不是單向數據綁定。不過,當創建數據綁定時,可以明確指定所需的綁定行爲
Inherits

如果爲true,就通過元素樹傳播該依賴項屬性值,並且可以被嵌套的元素繼承。

例如,Font屬性時可繼承的依賴項屬性——如果在更高層次中的元素中爲Font屬性設置了值,那麼該屬性值就會被嵌套的元素繼承(除非使用自己的字體設置明確地覆蓋繼承而來的值)

IsAnimationProhibited 如果爲true,就不能將依賴項屬性用於動畫
IsNotDataBindable 如果爲true,就不能使用綁定表示設置依賴項屬性
Journal 如果爲true,在基於頁面的應用程序中,依賴項屬性將被保存到日誌
SubPropertiesDoNotAffectRender 如果爲true,並且對象的某個子屬性發生了變化,WPF將不會重新渲染該對象
DefaultUpdateSourceTrigger 當該屬性用於綁定表達式時,該屬性用於爲Binding.UpdateSourceTrigger屬性設置默認值。UpdateSourceTrigger屬性決定了數據綁定值在合適應用自身的變化。當創建綁定時,可以手動設置UpdateSourceTrigger屬性。
DefaultValue 該屬性用於爲依賴項屬性設置默認值
DefaultValueCallback 該屬性提供了一個回調函數,用於在驗證依賴項屬性之前嘗試"糾正"屬性值
PropertyChangedCallback 該屬性提供了一個回調函數,當依賴項屬性的值發生變化時調用該回調函數

4.1.3 添加屬性包裝器

創建依賴項屬性的最後一個步驟是使用傳統的.NET屬性封裝WPF依賴項屬性。但典型的屬性過程是檢索或設置某個私有字段的值,而WPF屬性的屬性過程是使用在DependencyObject基類中定義的GetValue()和SetValue()方法。

public Thickness Margin
{
    set {SetValue(MarginProperty, value);}
    set {return (Thickness)GetValue(MarginProperty);}
}

當創建屬性封裝器時,應當只包含對SetValue()和GetValue()方法的調用。不應添加熱河驗證屬性值的額外代碼、引發事件的代碼等。這是因爲WPF中的其他功能可能會忽略屬性封裝器,並直接調用SetValue()和Get Value()方法。

屬性封裝器不是驗證數據或引發事件的正確位置。WPF的確提供了使用依賴項屬性回調函數。

驗證操作:DependencyProperty.ValidateValueCallback

事件操作:FrameworkPropertyMetadata.PropertyChangedCallback

可能在某些時候希望刪除本地值設置,並像從來沒有設置過那樣確定屬性值。顯然,這不能通過設置一個新值來實現。反而需要使用另一個繼承自DependencyObject類的方法ClearValue().

myElement.ClearValue(FrameworkElement.MafinProperty);

4.1.4 WPF 使用依賴項屬性的方式

WPF的許多功能都需要使用依賴項屬性。但是,所有這些功能都是通過每個依賴項屬性都支持的兩個關鍵行爲進行工作的——更改通知動態值識別。當屬性之發生變化時,依賴項屬性不會自動引發事件已通知屬性值發生變化。相反,它們會觸發受保護的名爲OnPropertyChangedCallback()方法。該方法通過兩個WPF服務(數據綁定和觸發器)出點信息,並調用PropertyChangedCallback回電函數。

換句話說,當屬性變化時,如果希望盡心給相應,有兩種選擇——可以使用屬性值創建綁定,也可編寫能夠自動改變其他屬性或開始動的觸發器。但依賴項屬性沒有提供已中通用的方法以觸發一些代碼,從而對屬性的變化進行響應。

當從屬性檢索值時,WPF屬性系統會通過一些列步驟獲取最終值。首先,通過考慮以下因素(按優先級從低到高排列)來決定基本值。

  1. 默認值(由FrameworkPropertyMetadata對象設置的值)
  2. 繼承而來的值(假設設置了FrameworkPropertyMetadata.Inherits標誌,併爲包含層次中的某個元素提供了值)
  3. 來自主題樣式的值
  4. 來自項目樣式的值
  5. 本地值(使用diamagnetic或XAML直接爲對象設置的值)

如上面的列表所示,可通過直接應用一個值來覆蓋整個層次。如果不這麼左,屬性值可有上面列表中的下一個可用項確定。

WPF按照上面的列表確定依賴項的基本值。但基本值未必就是最後從屬性中檢索到的值。這是因爲WPF還需要考慮其他幾個可能改變屬性值的提供者。下面列出WPF決定屬性值的四步驟:

  1. 確定基本值
  2. 如果實行時使用表達式設置的,就對錶達式進行求值。當前,WPF支持兩類表達式:數據綁定和資源
  3. 如果屬性時動畫的目標,就應用動畫
  4. 運行CoerceValueCallback回調函數來修正屬性值

4.1.5 共享依賴項屬性

儘管一些類具有不同的繼承層次,但它們會共享同一依賴項屬性。例如,TextBlock.FontFamily屬性和Control.FontFamily屬性指向同一靜態的依賴項屬性,該屬性實際上在TextElement類中定義的TextElement.FontFamilyProperty依賴項屬性。TextElement類的靜態構造函數註冊該屬性,而TextBlock類和Control類的靜態構造函數只是通過DependencyProperty.AddOwner()方法重用該屬性:

TextBlock.FontFamilyProperty = TextElement.FontFamilyProperty.AddOwner(typeof(TextBlock));

在WPF中重用依賴項屬性可得到一些奇異的效果,最有名的是樣式。例如,如果使用樣式設置TextBlock.FontFamily屬性,樣式也會影響Control.FontFamily屬性,因爲在後臺這兩個類使用同一個依賴項屬性。

4.1.6 附加依賴項屬性

附加屬性是一種依賴項屬性。爲了定義附加屬性,需要使用RegisterAttached()方法。

var 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屬性封裝器,反而附加屬性需要調用兩個經他i方法來設置和獲取屬性值。

public static int GetRow(UIElement element)
{
    return (int)elementGetValue(element);
}

public static void SetRow(UIElement element, int value)
{
    element.SetValue(Grid.RowProperty, value);
}

4.2 屬性驗證

在定義任何類型的屬性時,都需要面對錯誤設置屬性的可能性。對於傳統的.NET屬性,可嘗試在屬性設置器中保活這類問題。但對於依賴項屬性而言,這種方式不合適。

作爲代替,WPF提供了兩種方法來組織非法值:

  • ValidateValueCallback:該回調函數可接受或拒絕新值。通常,該回調函數用於捕獲違反屬性約束的明顯錯誤。可作爲DependencyProperty.Register()方法的一個參數提供該回調函數。
  • CoerceValueCallback:該回調函數可將新值修該爲更能接受的值。該回調函數通常用於處理爲相同兌現共設置的依賴項屬性值相互衝突的問題。這些本身可能是合法的,但當同時應用時它們是不相容的。爲了使用這個回調函數,當創建FrameworkPropertyMetadata對象時,作爲構造函數的一個參數提供該回調函數。

下面時應用程序試圖設置依賴項屬性時,所有這些內容的作用過程:

  1. 首先,CoerceValueCallback方法有機會修改提供的值,或者返回DependencyProperty.UnsetValue,這會拒絕修改。
  2. 接下來激活ValidateValueCallback方法。該方法返回true以接受一個值作爲合法值,或者返回false拒絕值。與CoerceValueCallback方法不同,ValidateValueCallback方法不能訪問設置屬性的實際對象,這意味着不能檢查其他屬性值。
  3. 最後,如果前兩個階段都獲得成功,就會觸發PropertyChangedCallback方法。此時,如果希望爲其他類提供通知,可以引發更改事件。

4.2.1 驗證回調

MarginProperty = DependencyProperty.Register("Margin",
                                             typeof(Thickness), 
                                             typeof(FrameworkElement),
                                             metadata, 
                                             new CalidateCalueCallback(FrameworkElement.IsMarginValid);

可使用這個回調函數加強驗證,驗證通常應該被添加到屬性過程的設置部分。提供的回調函數必須指向一個接受對象參數並返回Boolean值的方法。返回true以接受對象時合法的,返回false拒絕對象。

對於驗證回調函數有一個限制:它們必須是靜態方法而且無權訪問正在被驗證的對象。所有 能夠獲得的信息只有芳芳應用的數值。儘管這樣更便於重用屬性,但可能無法創建考慮其他屬性的驗證過程。典型的例子是具有Maximum和Minimum屬性的元素。顯然,爲Maximum屬性設置的值不能小於爲Minimum屬性設置的值。但是,不能使用驗證會帶哦函數來實施這一邏輯,因爲一次只能訪問一個屬性。

4.2.2 強制回調

var metadata = new FrameworkPropertyMetadata();
metadata.CoerceValueCallback = new CoerceValueCallback(CoerceMaximum);

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

可以通過CoerceValueBackcall回調函數處理相互關聯的屬性。例如,ScrollBar控件提供了Maximum、Minimum和Value屬性,這些屬性都繼承自RangeBase類。保持對這些屬性進行調整的一種方法是使用屬性強制。

例如,當設置Maximum屬性時,必須使用強制以確保不能小於Minimum屬性的值。

private static object CoerceMaximum(DependencyObject d, object value)
{
    var base1 = (RangeBase)d;
    if(((double) value) < base1.Minimum)
    {
        return base1.Minimum;
    }
    return value;
}

換句話說,如果應用於Maximum屬性的值小於Minimum屬性值,就用Minimum屬性的值設置Maximum屬性。注意,CoerceValueBackcall傳遞兩個參數——準備使用的數值和該數值將要應用到的對象。

當設置Value屬性時,會發生類似的強制過程。對Value屬性進行強制,確保不會超出由Minimum和Maximum屬性定義的範圍:

internal static object ConstrainToRange(DependencyObject d, object value)
{
    var newValue = (double) value;
    var base1    = (RangeBase)d;

    if(newValue < base1.Minimum) return base1.Minimum;
    if(newValue > base1.Maximum) return base1.Maximum;
    return newValue;
}

Minimum屬性根本不使用值強制。相反,一旦值發生變化,就觸發PropertyChangedCallback,然後通過手動觸發Maximum和Value屬性的強制過程,使它們適應Minimum屬性值的變化:

private static object OnMinimumChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    var base1    = (RangeBase)d;
    ...

    base1.CoerceValue(RangeBase.MaximumProperty);
    base1.CoerceValue(RangeBase.ValueProperty);
}

類似地,一旦設置或強制Maximum屬性值,那麼也會手動強制Value屬性以適應Maximum屬性值的變化:

private static object OnMaximumChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    var base1    = (RangeBase)d;
    ...

    base1.CoerceValue(RangeBase.ValueProperty);
}

 

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