WPF進階技巧和實戰08-依賴屬性與綁定01

依賴項屬性

定義依賴項屬性

注意:只能為依賴對象(繼承自DependencyObject的類)添加依賴項屬性。WPF中的元素基本上都繼承自DependencyObject類。

  • 靜態欄位
  • 名稱約定(屬性末尾加上Property)
  • Readonly(只能在靜態構造函數中進行設置)
public static readonly DependencyProperty ScalingRatioProperty = 
DependencyProperty.Register("ScalingRatio", 
typeof(double), 
typeof(FundusCanvas), 
new FrameworkPropertyMetadata(1.0));

註冊依賴項屬性

為了確保DependencyProperty對象不被直接實例化,使能使用靜態的DependencyProperty.Register方法創建依賴項屬性實例,而且創建後不能被改變,所以使用只讀屬性。創建過程中需要提供的幾個要素:

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

使用FrameworkPropertyMetadata對象和屬性驗證回調可以更加豐富依賴項屬性,創建依賴項屬性的附加功能。FrameworkPropertyMetadata類的所有屬性:

名稱 說明
AffectsArrange
AffectsMeasure
AffectsParentArrange
AffectsParentMeasure
如果為true,依賴項屬性會影響在布局操作的測量過程和排列過程中如何放置相鄰的元素或者父元素。如果屬性值發生變化,那麼布局容器需要重新執行測量步驟以確定新的布局
AffectsRender 如果為true,依賴項屬性會對元素的繪製方式造成一定的影響,要求重新繪製元素
BindsTwoWayByDefault 如果為true,默認情況下,依賴項屬性將使用雙向數據綁定而不是單向數據綁定。不過,當創建數據綁定時,可以明確指定所需的綁定行為
Inherits 如果為true,就通過元素樹傳播該依賴項屬性值,並且可以被嵌套的元素繼承。例如:Font是可繼承的依賴項屬性,如果在更高層次的元素中為Font屬性設置了值,那麼該屬性值就會被嵌套的元素繼承,除非手動設置來覆蓋繼承來的值
IsAnimationProhibited 如果為true,就不能將依賴項屬性用於動畫
IaNoDataBindable 如果為true,就不能使用綁定表達式設置依賴項屬性
Journal 如果為true,基於頁面的應用程式中,依賴項屬性將被保存到日誌中(瀏覽過的頁面歷史記錄)
SubPropertiesDoNotAffectRender 如果為true,並且對象的某個子屬性(屬性的屬性)發生了變化,WPF將不會重新渲染該對象
DefaultUpdateSourceTrigger 當該屬性用於綁定表達式時,該屬性用於為Binding.UpdateSourceTrigger屬性的默認值。UpdateSourceTrigger屬性決定了數據綁定值在何時應用自身的變化。當創建綁定時,可手動設置UpdateSourceTrigger屬性
DefaultValue 該屬性用於為依賴項屬性設置默認值
CoerceValueCallback 該屬性提供一個回調函數,用於驗證依賴項屬性之前嘗試「糾正」屬性值
PropertyChangedCallback 該屬性提供一個回調函數,當依賴項屬性的值發生變化時調用該函數

添加屬性包裝器

public FundusEditMode EditMode 
{ 
get => (FundusEditMode)GetValue(EditModeProperty); 
set => SetValue(EditModeProperty, value); 
}

當創建屬性封裝器時,應當值包含對SetValue和GetValue方法的調用,不應當添加任何驗證屬性的額外程式碼,引發事件的程式碼等。WPF提供了用於進行這些工作的地方-使用依賴項屬性回調函數。應該通過前面介紹的ValidateValueCallback回調函數進行驗證操作。
依賴項屬性遵循嚴格的優先規則來確定他們的當前值。即使沒有直接設置依賴項屬性,也可能已經有了值(可能是由數據綁定、樣式、或者動畫提供的,也可能是繼承來的),不過,主要直接設置了屬性值,設置的屬性值就會覆蓋所有其他的影響。

可能希望刪除本地值設置,並像從來沒有設置過那樣確定屬性值。顯然不能夠通過設置新值來實現,反而需要使用方法ClearValue()。

使用依賴項屬性

WPF的許多功能都需要使用依賴項屬性,這些功能都是通過每個依賴項屬性都支援的兩個關鍵行為進行工作的———更改通知和動態值識別。

  • 更改通知

當屬性值發生變化時,依賴項屬性不會自動引發事件以通知屬性值發生了變化。相反,他們會觸發受保護的名為OnPropertyChangedCallback()的方法。該方法通過兩個WPF服務(數據綁定和觸發器)傳遞資訊,並調用PropertyChangedCallback回調函數。也就是說,當屬性變化時,如果希望進行相應,有兩種選擇–可使用屬性創建綁定,也可以編寫能夠自動改變其他屬性或者開始動畫的觸發器。但是依賴項屬性沒有提供一種通用的方法觸發一些程式碼,進而對屬性的變化進行相應。

  • 動態值識別

本質上,依賴項屬性依賴於多個屬性提供者,每個提供者都有各自的優先順序。當從屬性檢索值事,WPF屬性系統會通過一系列步驟獲取最終值。首先通過考慮以下因素(優先順序從低到高)來決定基本值。

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

可通過直接應用一個值來覆蓋整個層次,否則屬性值可通過上面5中可用項來確定。

WPF按照上面的5種情況去確定依賴項屬性的基本值。但是基本值未必就是最後從屬性中檢索到的值。這是因為WPF還需要考慮其他幾個可能改變屬性值的提供者:

  1. 確定基本值(按照上述步驟)
  2. 如果屬性是使用表達式設置的,就對表達式進行求值(數據綁定或者資源)
  3. 如果屬性是動畫,就應用動畫
  4. 運行CoerceValueCallback回調函數來修正屬性值

共享依賴項屬性

儘管一些類具有不同的繼承層次,但他們會共享同一依賴項屬性。

public static readonly DependencyProperty FrontContentProperty = 
FlipPanel.FrontContentProperty.AddOwner(typeof(FlipPanel2));

可以使用相同的技術創建自己的自定義類(假定繼承的父類沒有提供屬性,否則直接重寫即可),還可以使用重載的AddOwner方法來提供驗證回調函數以及應用依賴項屬性的新方法的FrameworkPropertyMetadata對象。

附加的依賴項屬性

public static readonly DependencyProperty FrontContent1Property = 
DependencyProperty.RegisterAttached("FrontContent", typeof(object), typeof(FlipPanel2), null);

附加屬性是一種依賴項屬性,由WPF屬性管理器管理,不同之處在於附加屬性被應用到的類並非定義附加屬性的那個類。例如Grid.Row屬性是應用在Grid中定義,但是應用於其他控制項。

屬性驗證

WPF提供了兩種方法類阻止非法值:

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

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

  1. 首先,CoerceValueCallback方法有機會修改提供的值(通常,使提供的值和其他屬性相容),或者返回DependencyProperty.UnsetValue,這會拒絕修改
  2. 接下來激活ValidateValueCallback方法,該方法返回true以接受一個值作為合法值,或者返回false拒絕值。與CoerceValueCallback不同,本方法不能訪問設置屬性的實際對象,這意味著不能檢查其他屬性值。
  3. 最後,如果前兩個階段獲得成功,就會觸發PropertyChangedCallback方法

驗證回調(ValidateValueCallback)

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

對於驗證回調函數有一個限制,他們必須是靜態方法而且無法訪問正在被驗證的對象,所有能夠獲得資訊就是剛剛應用的數值。

public static readonly DependencyProperty CornerRadiusProperty =
    DependencyProperty.Register("CornerRadius", typeof(CornerRadius),
        typeof(FlipPanel), null, new ValidateValueCallback(CornerRadiusValidate));

private static bool CornerRadiusValidate(object value)
{
    CornerRadius cornerRadius = (CornerRadius)value;
    if (cornerRadius.TopLeft == double.NaN ||
        cornerRadius.TopRight == double.NaN ||
        cornerRadius.BottomLeft == double.NaN ||
        cornerRadius.BottomRight == double.NaN)
        return false;
    return true;
}

強制回調(CoerceValueCallback)

通過設置CoerceValueCallback回調函數處理相互關聯的屬性。設置本屬性的時候,需要處理好本屬性和其他屬性之間的關係,根據處理結果返迴響應的值。

public CornerRadius CornerRadius
{
    get => (CornerRadius)GetValue(CornerRadiusProperty);
    set => SetValue(CornerRadiusProperty, value);
}
public static readonly DependencyProperty CornerRadiusProperty =
    DependencyProperty.Register("CornerRadius", typeof(CornerRadius),
        typeof(FlipPanel), new FrameworkPropertyMetadata(new CornerRadius(5, 5, 5, 5),
            FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
            propertyChangedCallback: CornerRadiusChangedCallback,
            coerceValueCallback: CornerRadiusCoerceValueCallback),
        new ValidateValueCallback(CornerRadiusValidate));

private static bool CornerRadiusValidate(object value)
{
    CornerRadius cornerRadius = (CornerRadius)value;
    if (cornerRadius.TopLeft == double.NaN ||
        cornerRadius.TopRight == double.NaN ||
        cornerRadius.BottomLeft == double.NaN ||
        cornerRadius.BottomRight == double.NaN)
        return false;
    return true;
}

private static object CornerRadiusCoerceValueCallback(DependencyObject d, object value)
{
    CornerRadius cornerRadius = (CornerRadius)value;
    if (cornerRadius.TopLeft == double.NaN ||
        cornerRadius.TopRight == double.NaN ||
        cornerRadius.BottomLeft == double.NaN ||
        cornerRadius.BottomRight == double.NaN)
        return new CornerRadius(5, 5, 5, 5);
    return cornerRadius;
}

private static void CornerRadiusChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{

}