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

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

重要

.NET 7 和 .NET 6 的桌面指南檔正在建置中。

必要條件

本文假設您具備相依性屬性的基本知識,而且您已閱讀 相依性屬性概觀 。 若要遵循本文中的範例,如果您熟悉可延伸的應用程式標記語言(XAML),並知道如何撰寫 WPF 應用程式,它很有説明。

相依性屬性識別碼

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

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

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

相依性屬性包裝函式

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

實作相依性屬性的時機

當您在衍生自 DependencyObject 的類別上實作屬性時,您可以使用識別碼來備份屬性 DependencyProperty ,使其成為相依性屬性。 建立相依性屬性是否有説明,取決於您的案例。 雖然使用私用欄位支援屬性適用于某些案例,但如果您想要讓屬性支援下列一或多個 WPF 功能,請考慮實作相依性屬性:

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

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

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

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

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

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

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

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

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

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

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

建立相依性屬性的檢查清單

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

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

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

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

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

註冊 屬性

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

// 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 相同,也就是相依性屬性名稱。 您的包裝函式實作會在 存取子和 SetValue 存取子中 set 呼叫 GetValueget (用於讀寫屬性)。 下列範例顯示包裝函式,其遵循註冊呼叫和識別碼欄位宣告。 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 參數。 確定實值型別符合 propertyType 呼叫中指定的 Register

某些 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 ,而且您通常不會成為 DependencyPropertyKeypublic 類別的成員。 因為 WPF 屬性系統不會傳播 DependencyPropertyKey 程式碼外部,因此唯讀相依性屬性的安全性比讀寫相依性屬性更好 set

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

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

另請參閱