自訂相依性屬性 (WPF .NET)

Windows Presentation Foundation (WPF) 應用程式開發人員和元件作者可以建立自訂相依性屬性,以擴充其屬性的功能。 與 Common Language Runtime (CLR) 屬性不同,相依性屬性新增樣式、資料繫結、繼承、動畫和預設值的支援。 BackgroundWidthText 是 WPF 類別中現有相依性屬性的範例。 本文說明如何實作自訂相依性屬性,並提供改善效能、可使用性和多功能性的選項。

必要條件

本文假設您具備相依性屬性的基本知識,而且您已閱讀相依性屬性概觀。 若要遵循本文中的範例,且您熟悉 Extensible Application Markup Language (XAML),並知道如何撰寫 WPF 應用程式,則這很有幫助。

相依性屬性的識別碼

相依性屬性是透過 RegisterRegisterReadOnly 呼叫向 WPF 屬性系統登錄的屬性。 方法 Register 會傳回 DependencyProperty 執行個體,這個執行個體會保存相依性屬性的已登錄名稱和特性。 您會將 DependencyProperty 執行個體指派給靜態唯讀欄位,稱為相依性屬性識別碼,依慣例命名為 <property name>Property。 例如,Background 屬性的識別碼欄位一律為 BackgroundProperty

相依性屬性識別碼是用來取得或設定屬性值的支援欄位,而不是使用私密欄位支援屬性的標準模式。 不僅屬性系統會使用識別碼,XAML 處理器也可以使用識別碼,而且您的程式碼 (以及可能的外部程式碼) 可以透過識別碼存取相依性屬性。

相依性屬性只能套用至衍生自 DependencyObject 類型的類別。 大部分的 WPF 類別都支援相依性屬性,因為 DependencyObject 接近 WPF 類別階層的根目錄。 如需相依性屬性的詳細資訊,以及用來描述相依性屬性的術語和慣例,請參閱相依性屬性概觀

相依性屬性包裝函式

未附加屬性的 WPF 相依性屬性是由實作 getset 存取子的 CLR 包裝函式公開。 藉由使用屬性包裝函式,相依性屬性的取用者可以取得或設定相依性屬性值,就像任何其他 CLR 屬性一樣。 getset 存取子會透過 DependencyObject.GetValueDependencyObject.SetValue 呼叫與基礎屬性系統互動,並傳入相依性屬性識別碼做為參數。 相依性屬性的取用者通常不會直接呼叫 GetValueSetValue,但如果您要實作自訂相依性屬性,您會在包裝函式中使用那些方法。

實作相依性屬性的時間

當您在衍生自 DependencyObject 的類別上實作屬性時,您可以使用 DependencyProperty 識別碼來支援屬性,使其成為相依性屬性。 這對建立相依性屬性是否有幫助,視您的情況而定。 雖然使用私密欄位支援屬性適用於某些情況,但如果您想要讓屬性支援下列一或多個 WPF 功能,請考慮實作相依性屬性:

  • 可在樣式中設定的屬性。 如需詳細資訊,請參閱樣式和範本

  • 支援資料繫結的屬性。 如需資料繫結相依性屬性的詳細資訊,請參閱繫結兩個控制項的屬性

  • 可透過動態資源參考設定的屬性。 如需詳細資訊,請參閱 XAML 資源

  • 從元素樹狀結構中父元素自動繼承其值的屬性。 為此,即使您也建立 CLR 存取的屬性包裝函式,您也必須使用 RegisterAttached 進行登錄。 如需詳細資訊,請參閱屬性值繼承

  • 可產生動畫效果的屬性。 如需詳細資訊,請參閱動畫概觀

  • 當屬性值變更時,WPF 屬性系統發出的通知。 變更可能是由於屬性系統、環境、使用者或樣式的動作所造成。 您的屬性可以在屬性中繼資料中指定回呼方法,每次屬性系統判定屬性值變更時,都會叫用這個方法。 相關的概念是屬性值強制型轉。 如需詳細資訊,請參閱相依性屬性回呼和驗證

  • 存取由 WPF 處理序所讀取的相依性屬性中繼資料。 例如,您可以使用屬性中繼資料來:

    • 指定變更的相依性屬性值是否應該讓版面配置系統重新編排元素的視覺效果。

    • 藉由覆寫衍生類別上的中繼資料,設定相依性屬性的預設值。

  • Visual Studio WPF 設計工具支援,例如在 [屬性] 視窗中編輯自訂控制項的屬性。 如需詳細資訊,請參閱控制項製作概觀

在某些情況下,覆寫現有相依性屬性的中繼資料比實作新的相依性屬性更好。 中繼資料覆寫是否實際可行,取決於您的情況,以及該情況與現有 WPF 相依性屬性和類別實作的類似程度。 如需覆寫現有相依性屬性中繼資料的詳細資訊,請參閱相依性屬性中繼資料

建立相依性屬性所使用的檢查清單

請遵循下列步驟來建立相依性屬性。 某些步驟可以在單行程式碼中合併和實作。

  1. (選擇性) 建立相依性屬性中繼資料。

  2. 向屬性系統登錄相依性屬性,並指定屬性名稱、擁有者類型、屬性值類型,以及選擇性的屬性中繼資料。

  3. DependencyProperty 識別碼定義為擁有者類型上的 public static readonly 欄位。 識別碼欄位名稱是附加尾碼 Property 的屬性名稱。

  4. 使用與相依性屬性名稱相同的名稱定義 CLR 包裝函式屬性。 在 CLR 包裝函式中,實作 getset 存取子,以連接支援包裝函式的相依性屬性。

註冊屬性

為了讓屬性成為相依性屬性,您必須向屬性系統登錄它。 若要登錄屬性,請從類別主體內部 (但在任何成員定義之外) 呼叫 Register 方法。 方法 Register 會傳回呼叫屬性系統 API 時將使用的唯一相依性屬性識別碼。 在成員定義之外進行 Register 呼叫的原因,是因為您將傳回值指派給 DependencyProperty 類型的 public static readonly 欄位。 您將在類別中建立的這個欄位是相依性屬性的識別碼。 在下列範例中,Register 的第一個引數將指名相依性屬性 AquariumGraphic

// Register a dependency property with the specified property name,
// property type, owner type, and property metadata. Store the dependency
// property identifier as a public static readonly member of the class.
public static readonly DependencyProperty AquariumGraphicProperty =
    DependencyProperty.Register(
      name: "AquariumGraphic",
      propertyType: typeof(Uri),
      ownerType: typeof(Aquarium),
      typeMetadata: new FrameworkPropertyMetadata(
          defaultValue: new Uri("http://www.contoso.com/aquarium-graphic.jpg"),
          flags: FrameworkPropertyMetadataOptions.AffectsRender,
          propertyChangedCallback: new PropertyChangedCallback(OnUriChanged))
    );
' Register a dependency property with the specified property name,
' property type, owner type, and property metadata. Store the dependency
' property identifier as a public static readonly member of the class.
Public Shared ReadOnly AquariumGraphicProperty As DependencyProperty =
    DependencyProperty.Register(
        name:="AquariumGraphic",
        propertyType:=GetType(Uri),
        ownerType:=GetType(Aquarium),
        typeMetadata:=New FrameworkPropertyMetadata(
            defaultValue:=New Uri("http://www.contoso.com/aquarium-graphic.jpg"),
            flags:=FrameworkPropertyMetadataOptions.AffectsRender,
            propertyChangedCallback:=New PropertyChangedCallback(AddressOf OnUriChanged)))

注意

在類別主體中定義相依性屬性是一般的實作,但也可能在類別靜態建構函式中定義相依性屬性。 如果您需要多行程式碼來初始化相依性屬性,這個方式可能有意義。

相依性屬性命名

為相依性屬性建立的命名慣例對於屬性系統的一般行為而言是必要的。 您要建立的識別碼欄位名稱必須是屬性的登錄名稱,含尾碼 Property

相依性屬性名稱在登錄類別內必須是唯一的。 透過基礎類型繼承的相依性屬性已經登錄,且無法由衍生類型登錄。 不過,您可以藉由將類別新增為相依性屬性的擁有者,使用由不同類型登錄的相依性屬性,即使您的類別並不是從其繼承的類型。 如需將類別新增為擁有者的詳細資訊,請參閱相依性屬性中繼資料

實作屬性包裝函式

依照慣例,包裝函式屬性的名稱必須與呼叫 Register 的第一個參數相同,也就是相依性屬性名稱。 您的包裝函式實作會在 get 存取子中呼叫 GetValue 和在存取子 set 中呼叫 SetValue (用於讀寫屬性)。 下列範例顯示包裝函式,其遵循登錄呼叫和識別碼欄位宣告。 WPF 類別上的所有公用相依性屬性都會使用類似的包裝函式模型。

// Register a dependency property with the specified property name,
// property type, owner type, and property metadata. Store the dependency
// property identifier as a public static readonly member of the class.
public static readonly DependencyProperty AquariumGraphicProperty =
    DependencyProperty.Register(
      name: "AquariumGraphic",
      propertyType: typeof(Uri),
      ownerType: typeof(Aquarium),
      typeMetadata: new FrameworkPropertyMetadata(
          defaultValue: new Uri("http://www.contoso.com/aquarium-graphic.jpg"),
          flags: FrameworkPropertyMetadataOptions.AffectsRender,
          propertyChangedCallback: new PropertyChangedCallback(OnUriChanged))
    );

// Declare a read-write property wrapper.
public Uri AquariumGraphic
{
    get => (Uri)GetValue(AquariumGraphicProperty);
    set => SetValue(AquariumGraphicProperty, value);
}
' Register a dependency property with the specified property name,
' property type, owner type, and property metadata. Store the dependency
' property identifier as a public static readonly member of the class.
Public Shared ReadOnly AquariumGraphicProperty As DependencyProperty =
    DependencyProperty.Register(
        name:="AquariumGraphic",
        propertyType:=GetType(Uri),
        ownerType:=GetType(Aquarium),
        typeMetadata:=New FrameworkPropertyMetadata(
            defaultValue:=New Uri("http://www.contoso.com/aquarium-graphic.jpg"),
            flags:=FrameworkPropertyMetadataOptions.AffectsRender,
            propertyChangedCallback:=New PropertyChangedCallback(AddressOf OnUriChanged)))

' Declare a read-write property wrapper.
Public Property AquariumGraphic As Uri
    Get
        Return CType(GetValue(AquariumGraphicProperty), Uri)
    End Get
    Set
        SetValue(AquariumGraphicProperty, Value)
    End Set
End Property

除了罕見的情況下,您的包裝函式實作應該只包含 GetValueSetValue 程式碼。 如需其背後的原因,請參閱自訂相依性屬性的影響

如果您的屬性未遵循已建立的命名慣例,您可能會遇到下列問題:

  • 樣式和範本的某些層面將無法運作。

  • 大部分的工具和設計工具都使用命名慣例,才能正確序列化 XAML,並在每一屬性層級提供設計工具環境協助。

  • 目前的 WPF XAML 載入器實作會略過整個包裝函式,並依賴命名慣例來處理屬性值。 如需詳細資訊,請參閱 XAML 載入和相依性屬性

相依性屬性中繼資料

當您登錄相依性屬性時,屬性系統會建立中繼資料物件來儲存屬性特性。 方法 Register 的多載可讓您在登錄期間指定屬性中繼資料,例如 Register(String, Type, Type, PropertyMetadata)。 屬性中繼資料的常見用法是為使用相依性屬性的新執行個體套用自訂預設值。 如果您沒有提供屬性中繼資料,則屬性系統會將預設值指派給許多相依性屬性特性。

如果您要在衍生自 FrameworkElement 的類別上建立相依性屬性,您可以使用更特殊化的中繼資料類別 FrameworkPropertyMetadata,而不是其基礎類別 PropertyMetadata。 數個 FrameworkPropertyMetadata 建構函式簽章可讓您指定不同的中繼資料特性組合。 如果您只想要指定預設值,請使用 FrameworkPropertyMetadata(Object) 並將預設值傳遞至 Object 參數。 確定值類型符合 Register 呼叫中指定的 propertyType

某些 FrameworkPropertyMetadata 多載可讓您為屬性指定中繼資料選項旗標。 屬性系統會將這些旗標轉換成離散屬性,而且 WPF 處理序會使用旗標值,例如版面配置引擎。

設定中繼資料旗標

設定中繼資料旗標時,請考慮下列事項:

  • 如果您的屬性值 (或變更的屬性值) 會影響版面配置系統轉譯 UI 元素的方式,請設定下列一或多個旗標:

    • AffectsMeasure,表示屬性值的變更需要 UI 轉譯的變更,特別是物件在其父代內佔用的空間。 例如,為屬性 Width 設定這個中繼資料旗標。

    • AffectsArrange,表示屬性值的變更需要變更 UI 轉譯,特別是物件在其父代中的位置。 一般而言,物件也不會變更大小。 例如,為屬性 Alignment 設定這個中繼資料旗標。

    • AffectsRender,表示發生的變更不會影響版面配置和量值,但仍需要另一種轉譯。 例如,為 Background 屬性或影響元素顏色的任何其他屬性設定這個旗標。

    您也可以使用這些旗標作為屬性系統 (或版面配置) 回呼覆寫實作的輸入。 例如,當執行個體的屬性報告值變更並在中繼資料中設定 AffectsArrange 時,您可以使用 OnPropertyChanged 回呼來呼叫 InvalidateArrange

  • 某些屬性會以其他方式影響其父元素的轉譯特性。 例如,屬性 MinOrphanLines 的變更可以變更流程文件的整體轉譯。 使用 AffectsParentArrangeAffectsParentMeasure 來向您自己的屬性中的父代動作發出訊號。

  • 相依性屬性預設支援資料繫結。 不過,當資料繫結沒有實際發生的情況,或資料繫結效能有問題時 (例如大型物件),您可以使用 IsDataBindingAllowed 來停用資料繫結。

  • 雖然相依性屬性的預設資料繫結模式OneWay,但您可以將特定繫結的繫結模式變更為 TwoWay。 如需詳細資訊,請參閱繫結方向。 身為相依性屬性作者,您甚至可以選擇將雙向繫結設為預設模式。 使用雙向資料繫結的現有相依性屬性範例是 MenuItem.IsSubmenuOpen,其狀態是以其他屬性和方法呼叫為基礎。 IsSubmenuOpen 的情況是以其設定邏輯和 MenuItem 的結合來與預設主題樣式互動。 TextBox.Text 是預設使用雙向繫結的另一個 WPF 相依性屬性。

  • 您可以藉由設定 Inherits 旗標來啟用相依性屬性的屬性繼承。 屬性繼承對於父元素和子元素具有共同屬性的情況非常有用,而且子元素繼承共同屬性的父代值是有意義的。 可繼承屬性的範例是 DataContext,其支援使用資料呈現的主要詳細資料案例的繫結作業。 屬性值繼承可讓您在頁面或應用程式根目錄指定資料內容,這樣就不必為子元素繫結指定資料內容。 儘管繼承的屬性值會覆寫預設值,但可以在任何子元素上本機設定屬性值。 請謹慎使用屬性值繼承,因為其會帶來效能成本。 如需詳細資訊,請參閱屬性值繼承

  • 設定 Journal 旗標,指出瀏覽日誌服務應該偵測或使用您的相依性屬性。 例如,SelectedIndex 屬性會設定 Journal 旗標,以建議應用程式保留選取項目的日誌記錄。

唯讀相依性屬性

您可以定義唯讀的相依性屬性。 典型的情況是儲存內部狀態的相依性屬性。 例如,IsMouseOver 是唯讀的,因為其狀態應該只由滑鼠輸入來決定。 如需詳細資訊,請參閱唯讀相依性屬性

集合類型相依性屬性

集合類型相依性屬性需要考慮額外的實作問題,例如設定參考類型的預設值,以及集合元素的資料繫結支援。 如需詳細資訊,請參閱集合類型相依性屬性

相依性屬性的安全性

一般而言,您會將相依性屬性宣告為公用屬性,並將 DependencyProperty 識別碼欄位宣告為 public static readonly 欄位。 如果您指定更嚴格的存取層級,例如 protected,則仍可透過其識別碼結合屬性系統 API 來存取相依性屬性。 即使是受保護的識別碼欄位也可以透過 WPF 中繼資料報告或值判斷 API (例如 LocalValueEnumerator) 來存取。 如需詳細資訊,請參閱相依性屬性的安全性

若為唯讀相依性屬性,從 RegisterReadOnly 傳回的值是 DependencyPropertyKey,而且通常您不會將 DependencyPropertyKey 設為類別的 public 成員。 因為 WPF 屬性系統不會在程式碼外部傳播 DependencyPropertyKey,因此唯讀相依性屬性的 set 安全性比讀寫相依性屬性更好。

相依性屬性和類別建構函式

Managed 程式碼的程式設計中有個一般原則 (通常由程式碼分析工具強制執行),就是類別建構函式不應該呼叫虛擬方法。 這是因為基礎建構函式可以在衍生類別建構函式初始化期間呼叫,而基礎建構函式所呼叫的虛擬方法可能會在完成衍生類別的初始化之前執行。 當您衍生自已經衍生自 DependencyObject 的類別時,屬性系統本身會在內部呼叫並公開虛擬方法。 這些虛擬方法都屬於 WPF 屬性系統服務。 覆寫方法可讓衍生的類別參與值判斷。 若要避免執行階段初始化可能發生的問題,您不應該在類別的建構函式中設定相依性屬性值,除非您遵循明確的建構函式模式。 如需詳細資訊,請參閱 DependencyObjects 的安全建構函式模式

另請參閱