自訂相依性屬性

本主題會說明 Windows Presentation Foundation (WPF) 應用程式開發人員和元件作者可能想要建立自訂相依性屬性的原因,並說明實作步驟以及某些可以改善屬性的效能、可用性或多功能的實作選項。

必要條件

本主題假設您已從 WPF 類別的現有相依性屬性消費者角度了解相依性屬性,並已閱讀相依性屬性概觀主題。 為了遵循本主題中的範例,您也應該了解 XAML 並知道如何撰寫 WPF 應用程式。

什麼是相依性屬性?

您可將原可能成為通用語言執行平台 (CLR) 屬性的項目實作為相依性屬性,以支援樣式、資料繫結、繼承、動畫和預設值。 相依性屬性是透過呼叫 Register 方法 (或 RegisterReadOnly) 從 WPF 屬性系統註冊的屬性,且由 DependencyProperty 識別碼欄位支援。 相依性屬性僅為 DependencyObject 類型所用,但 DependencyObject 在 WPF 類別階層中的位階相當高,所以 WPF 中可用的大部分類別都支援相依性屬性。 如需相依性屬性的詳細資訊,以及此 SDK 中描述它們所使用的一些術語和慣例,請參閱相依性屬性概觀

相依性屬性範例

在 WPF 類別上實作的相依性屬性範例包括 Background 屬性、 Width 屬性,以及 Text 屬性。 類別公開的每個相依性屬性都會在同一類別上公開 DependencyProperty 類型的對應公用靜態欄位。 這是相依性屬性的識別碼。 識別碼依慣例命名︰相依性屬性的名稱後綴字串 Property。 例如, Background 屬性的對應 DependencyProperty 識別碼欄位為 BackgroundProperty。 識別碼會在相依性屬性登錄時儲存其相關資訊,稍後再用於與相依性屬性有關的其他作業,例如呼叫 SetValue

相依性屬性概觀所述,因為實作 "wrapper",所以 WPF 的所有相依性屬性也都是 CLR 屬性 (最緊密的屬性除外)。 因此,在程式碼中,您可以像使用其他 CLR 屬性一樣呼叫定義包裝函式的 CLR 存取子,來取得或設定相依性屬性。 身為已建立相依性屬性的取用者,您通常不會使用 DependencyObject 方法、GetValueSetValue,這些是基礎屬性系統的連線點。 相反地,CLR 屬性的現有實作已經呼叫 GetValueSetValue,並在屬性的 getset 包裝函式實作中適當地使用識別碼欄位 。 如果您要自行實作自訂的相依性屬性,則會以類似的方式定義包裝函式。

您應於何時實作相依性屬性?

當您實作類別的屬性時,只要您的類別衍生自 DependencyObject,就可以選擇倒轉有 DependencyProperty 識別碼的屬性,讓它成為相依性屬性。 讓您的屬性成為相依性屬性並非絕對必要或合適,視案例需求而定。 有時候,支援有私用欄位的屬性,一般的技巧即已足夠。 但只要您希望屬性支援下列一或多項 WPF 功能,就應該將屬性實作為相依性屬性:

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

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

  • 您希望屬性可使用動態資源參考來設定。 如需詳細資訊,請參閱 XAML 資源

  • 您想要自動繼承項目樹狀結構父項目的屬性值。 在此情況下,即使您也為 CLR 存取建立屬性包裝函式,請仍使用 RegisterAttached 方法註冊。 如需詳細資訊,請參閱屬性值繼承

  • 您希望屬性可製成動畫。 如需詳細資訊,請參閱 動畫概觀

  • 當屬性系統、環境或使用者所採取的動作,或讀取和使用樣式變更了先前的屬性值時,您希望屬性系統能夠回報。 使用屬性中繼資料,您的屬性可以指定每次屬性系統判定屬性值變更時都會叫用回呼方法。 相關的概念是屬性值強制型轉。 如需詳細資訊,請參閱相依性屬性回呼和驗證

  • 您想要使用 WPF 程序也使用的已建立中繼資料慣例,例如報告變更屬性值是否應該需要配置系統重新撰寫項目的視覺效果。 或者您想要能夠使用中繼資料覆寫,以便衍生類別可以變更中繼資料型的特性,例如預設值。

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

當您檢查這些案例時,您也應該考慮是否能夠以覆寫現有相依性屬性中繼資料的方式完成您的案例,而不是實作全新的屬性。 中繼資料覆寫是否實際可行,取決於您的案例,以及該案例與現有 WPF 相依性屬性和類別實作的類似程度。 如需覆寫現有屬性中繼資料的詳細資訊,請參閱相依性屬性中繼資料

定義相依性屬性所使用的檢查清單

定義相依性屬性包含四個不同的概念。 這些概念不一定得是嚴苛的程序步驟,因為其中有些最後會結合為實作中的單一段程式碼︰

  • (選擇性) 建立相依性屬性的屬性中繼資料。

  • 向屬性系統登錄屬性名稱,指定擁有者類型和屬性值類型。 如經使用,也指定屬性中繼資料。

  • DependencyProperty 標識碼定義為擁有者類型的 public static readonly 欄位。

  • 定義名稱與相依性屬性名稱符合的 CLR "wrapper" 屬性。 實作 CLR "wrapper" 屬性的 getset 存取子,以連接支援它的相依性屬性。

向屬性系統登錄屬性

為使屬性成為相依性屬性,您必須將該屬性登錄到屬性系統維護的資料表中,並給它唯一識別碼用為後續屬性系統作業的限定詞。 這些作業可能是內部作業,或您自己程式碼呼叫的屬性系統 API。 為登錄屬性,您要在類別的主體內呼叫 Register 方法 (位在類別內,但在任何成員定義外)。 識別碼欄位也會由 Register 方法呼叫提供,做為傳回值。 Register 呼叫之所以在其他成員定義之外完成,是因為您使用此傳回值指派及建立類型 DependencyPropertypublic static readonly 欄位,當成類別的一部分。 此欄位會變成您相依性屬性的識別碼。

public static readonly DependencyProperty AquariumGraphicProperty = DependencyProperty.Register(
  "AquariumGraphic",
  typeof(Uri),
  typeof(AquariumObject),
  new FrameworkPropertyMetadata(null,
      FrameworkPropertyMetadataOptions.AffectsRender,
      new PropertyChangedCallback(OnUriChanged)
  )
);
Public Shared ReadOnly AquariumGraphicProperty As DependencyProperty = DependencyProperty.Register("AquariumGraphic", GetType(Uri), GetType(AquariumObject), New FrameworkPropertyMetadata(Nothing, FrameworkPropertyMetadataOptions.AffectsRender, New PropertyChangedCallback(AddressOf OnUriChanged)))

相依性屬性命名慣例

相依性屬性有已建立的命名慣例,除非例外情況,否則必須遵循。

相依性屬性本身有個基本名稱,如本例中的 "AquariumGraphic",會指定為 Register 的第一個參數。 該名稱在每個登錄類型中都必須是唯一的。 透過基底類型繼承的相依性屬性視為登錄類型的一部分,已繼承屬性的名稱無法再次登錄。 不過,即使不能繼承該相依性屬性,還有一種技巧可將類別新增為相依性屬性的擁有者;如需詳細資訊,請參閱相依性屬性中繼資料

當您建立識別碼欄位時,請以登錄屬性時所用名稱命名此欄位,再加上尾碼 Property。 此欄位是您的相依性屬性識別碼,稍後會用為您在包裝函式中進行之 SetValueGetValue呼叫的輸入,執行者為存取屬性的任何其他程式碼、您自己的程式碼、您允許的任何外部程式碼存取,也可能是 XAML 處理器。

注意

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

實作 "wrapper"

您的包裝函式實作應該在 get 實作中呼叫 GetValue ,並在 set 實作中呼叫 SetValue (原始的註冊呼叫和欄位也會在這裡顯示,以便清楚可見)。

除了例外情況之外,您的包裝函式實作應該只會分別執行 GetValueSetValue 動作。 相關原因討論請參閱 XAML 載入和相依性屬性主題。

WPF 類別中提供的所有現有公用相依性屬性都使用這個簡單的包裝函式實作模型,相依性屬性運作方式最複雜的部分或為固有的屬性系統行為,或為透過其他概念予以實行,例如透過屬性中繼資料的強制型轉或屬性變更回呼。


public static readonly DependencyProperty AquariumGraphicProperty = DependencyProperty.Register(
  "AquariumGraphic",
  typeof(Uri),
  typeof(AquariumObject),
  new FrameworkPropertyMetadata(null,
      FrameworkPropertyMetadataOptions.AffectsRender,
      new PropertyChangedCallback(OnUriChanged)
  )
);
public Uri AquariumGraphic
{
  get { return (Uri)GetValue(AquariumGraphicProperty); }
  set { SetValue(AquariumGraphicProperty, value); }
}

Public Shared ReadOnly AquariumGraphicProperty As DependencyProperty = DependencyProperty.Register("AquariumGraphic", GetType(Uri), GetType(AquariumObject), New FrameworkPropertyMetadata(Nothing, FrameworkPropertyMetadataOptions.AffectsRender, New PropertyChangedCallback(AddressOf OnUriChanged)))
Public Property AquariumGraphic() As Uri
    Get
        Return CType(GetValue(AquariumGraphicProperty), Uri)
    End Get
    Set(ByVal value As Uri)
        SetValue(AquariumGraphicProperty, value)
    End Set
End Property

再次,依照慣例,包裝函式屬性的名稱必須和登錄屬性時 Register 呼叫的第一個參數所選擇和指定的名稱一樣。 如果您的屬性不遵循慣例,不一定會停用所有可能的用途,但您會遇到幾個值得注意的問題︰

  • 樣式和範本的某些方面不起作用。

  • 大部分的工具和設計工具必須依賴命名慣例,才能正確序列化 XAML,或依屬性層級提供設計工具環境協助。

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

新相依性屬性的屬性中繼資料

當您登錄相依性屬性時,登錄會透過屬性系統建立儲存屬性特性的中繼資料物件。 如果屬性使用 Register 的簡易特徵標記登錄,許多這些特性都會有設定好的預設值。 Register 的其他特徵標記可讓您在登錄屬性時指定想要的中繼資料。 相依性屬性最常指定的中繼資料,是套用在新執行個體的預設值,而新執行個體使用該屬性。

如果您要建立存在於 FrameworkElement 衍生類別上的相依性屬性,您可以使用更特製化的 FrameworkPropertyMetadata 中繼資料類別,而不是基礎 PropertyMetadata 類別。 FrameworkPropertyMetadata 類別的建構函式有幾項特徵標記,您可在其中指定各種中繼資料特性的組合。 如果只想要指定預設值,請使用採用類型 Object 單一參數的特徵標記。 將該物件參數傳遞為您屬性的特定類型預設值 (提供的預設值必須是您在 Register 呼叫中提供為 propertyType 參數的類型)。

您也可以針對 FrameworkPropertyMetadata 指定屬性的中繼資料選項旗標。 這些旗標在登錄後會轉換成屬性中繼資料中的個別屬性,用以與版面配置引擎等其他處理序溝通特定條件。

設定適當的中繼資料旗標

  • 如果您的屬性 (或其值變更) 會影響使用者介面 (UI),特別是影響版面配置系統在頁面中的大小或轉譯元素的方式,請設定下列一或多個旗標:AffectsMeasureAffectsArrangeAffectsRender

    • AffectsMeasure 指出要變更此表示需要變更 UI 轉譯,此轉譯包含的物件可能需要增加或減少父代的空間。 例如,"Width" 屬性應該設定此旗標。

    • AffectsArrange 指出要變更此屬性需要變更 UI 轉譯,此轉譯一般不需要變更專用的空間,但會指出空間內的定位已變更。 例如,"Alignment" 屬性應該設定此旗標。

    • AffectsRender 指出已發生的其他一些變更不會影響版面配置和測量,但確實需要另一種轉譯。 例如可變更現有項目色彩的 "Background" 等屬性。

    • 這些旗標在您自己的屬性系統或配置回呼覆寫實作中,通常用為中繼資料的通訊協定。 例如,您可能會有 OnPropertyChanged 回撥,如果執行個體的任何屬性報告值變更,且在其中繼資料中有 AffectsArrange 作為 true,其將會呼叫 InvalidateArrange

  • 某些屬性會影響包含父項目的轉譯特性,超過前文所述之所需大小的變更。 例如非固定格式文件模型中使用的 MinOrphanLines,該屬性的變更可以變更包含段落之非固定格式文件的整體轉譯。 使用 AffectsParentArrangeAffectsParentMeasure 來識別您自己的屬性中的類似案例。

  • 相依性屬性預設支援資料繫結。 對於沒有任何實際案例可進行資料繫結的情況,或者大型物件的資料繫結效能認定有問題的情況,您可以故意停用資料繫結。

  • 相依性屬性的資料繫結 Mode 預設為 OneWay。 繫結一律可依繫結執行個體變更為 TwoWay。如需詳細資料,請參閱指定繫結的方向。 但身為相依性屬性的作者,您可以選擇讓屬性預設使用 TwoWay 繫結模式。 現有相依性屬性的範例是 MenuItem.IsSubmenuOpen; 此屬性的案例是 IsSubmenuOpen 設定邏輯,以及結合 MenuItem 與預設主題樣式互動。 IsSubmenuOpen 屬性邏輯以原生方式使用資料繫結,讓屬性的狀態與其他狀態屬性及方法呼叫保持一致。 預設繫結 TwoWay 的另一個範例屬性為 TextBox.Text

  • 您也可以設定 Inherits 旗標,在自訂的相依性屬性中啟用屬性繼承。 屬性繼承對父項目和子項目有共同屬性的案例很有用,而且對子項目將特定屬性值設定為和父項目設定的值一樣,才有意義。 DataContext 是可繼承屬性的範例,在繫結作業中可啟用重要的主從式案例以呈現資料。 藉由讓 DataContext 可繼承,任何子元素也會繼承該資料內容。 因為屬性值繼承的緣故,您可以指定位在網頁或應用程式根目錄中的資料內容,不需要重新指定即可繫結所有可能的子項目。 DataContext 也是示範繼承覆寫預設值的好例子,但它在任何特定子項目上皆可一律在本機設定。如需詳細資訊,請參閱使用含階層式資料的主從式模式。 屬性值繼承確實有可能的效能成本,因此應謹慎使用。如需詳細資訊,請參閱屬性值繼承

  • 設定 Journal 旗標,指出瀏覽日誌服務應該偵測還是使用您的相依性屬性。 例如 SelectedIndex 屬性,瀏覽日誌記錄時,在選取範圍控制項中的任何選取項目都應該保存。

唯讀相依性屬性

您可以定義唯讀的相依性屬性。 但您為何可能將屬性定義為唯讀的案例有點不同,和向屬性系統登錄它們並公開識別碼的程序一樣。 如需詳細資訊,請參閱唯讀相依性屬性

集合類型的相依性屬性

集合類型相依性屬性要考慮一些其他的實作問題。 如需詳細資訊,請參閱集合類型相依性屬性

相依性屬性安全性考量

相依性屬性應該宣告為公用屬性。 相依性屬性識別碼欄位應該宣告為公用靜態欄位。 即使您嘗試宣告其他存取層級 (例如受保護的),仍一律可以透過與屬性系統 API 結合的識別項存取相依性屬性。 即使受保護的識別碼欄位也可能為可存取,因為中繼資料報表或值判斷 API 屬於屬性系統,例如 LocalValueEnumerator。 如需詳細資訊,請參閱相依性屬性的安全性

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

Managed 程式碼程式設計中有項一般原則 (通常由 FxCop 等程式碼分析工具強制執行),類別建構函式不應該呼叫虛擬方法。 這是因為建構函式可以呼叫為衍生類別建構函式的基底初始化,而透過建構函式進入虛擬方法,可能會發生在建構中的物件執行個體尚未完全初始化的狀態。 當您衍生任何衍生自 DependencyObject 的類別時,您應該知道屬性系統本身會在內部呼叫及公開虛擬方法。 這些虛擬方法都屬於 WPF 屬性系統服務。 覆寫方法可讓衍生的類別參與值判斷。 若要避免執行階段初始化可能發生的問題,您不應該在類別的建構函式中設定相依性屬性值,除非您遵循非常明確的建構函式模式。 如需詳細資訊,請參閱 DependencyObject 的安全建構函式模式

另請參閱