自訂相依性屬性

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

必要條件

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

什麼是相依性屬性?

您可以藉由將它實作為相依性屬性,以啟用否則為 Common Language Runtime (CLR) 屬性的功能,以支援樣式、資料系結、繼承、動畫和預設值。 相依性屬性是透過呼叫 Register 方法 (或 RegisterReadOnly ) 向 WPF 屬性系統註冊的屬性,且由 DependencyProperty 識別碼欄位支援。 相依性屬性只能由 DependencyObject 類型使用,但在 DependencyObject WPF 類別階層中相當高,因此 WPF 中可用的大多數類別都可以支援相依性屬性。 如需相依性屬性的詳細資訊,以及用於在此 SDK 中描述這些屬性的一些術語和慣例,請參閱 相依性屬性概觀

相依性屬性範例

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

如相依性屬性概觀 中所述 ,WPF 中的所有相依性屬性(但大部分附加屬性除外)也是 CLR 屬性,因為「包裝函式」實作。 因此,從程式碼中,您可以呼叫 CLR 存取子來取得或設定相依性屬性,這些存取子會以使用其他 CLR 屬性的相同方式定義包裝函式。 身為已建立相依性屬性的取用者,您通常不會使用 DependencyObjectGetValueSetValue ,也就是基礎屬性系統的連接點。 相反地,CLR 屬性的現有實作已經呼叫 GetValueSetValue 屬性的 getset 包裝函式實作,並適當地使用識別碼欄位。 如果您要自行實作自訂的相依性屬性,則會以類似的方式定義包裝函式。

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

當您在類別上實作屬性時,只要類別衍生自 DependencyObject ,就可以選擇使用 DependencyProperty 識別碼來備份屬性,因此使其成為相依性屬性。 讓您的屬性成為相依性屬性並非絕對必要或合適,視案例需求而定。 有時候,支援有私用欄位的屬性,一般的技巧即已足夠。 不過,每當您想要屬性支援下列一或多個 WPF 功能時,您應該實作屬性作為相依性屬性:

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

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

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

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

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

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

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

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

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

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

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

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

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

  • DependencyProperty將識別碼定義為 publicstaticreadonly 擁有者類型上的欄位。

  • 定義 CLR 「wrapper」 屬性,其名稱符合相依性屬性的名稱。 實作 CLR 「wrapper」 屬性的 getset 存取子,以與備份它的相依性屬性連接。

向屬性系統登錄屬性

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

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 ,並在 SetValue 實作中 set 呼叫 (原始註冊呼叫和 欄位也會在這裡顯示,以便清楚起見)。

除了例外狀況之外,包裝函式實作也應該分別執行 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 呼,如果實例的任何屬性報告值變更,並在其中繼資料中一樣 trueAffectsArrange 您可能會呼叫 InvalidateArrange

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

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

  • 根據預設,相依性屬性的資料系結 Mode 預設為 OneWay 。 您一律可以將系結變更為 TwoWay 每個系結實例;如需詳細資訊,請參閱 指定系結的方向 。 但是,身為相依性屬性作者,您可以選擇預設讓屬性使用 TwoWay 系結模式。 現有相依性屬性的範例是 MenuItem.IsSubmenuOpen ;這個屬性的案例是 IsSubmenuOpen 設定邏輯和與預設主題樣式互動的 MenuItem 組合。 屬性邏輯會 IsSubmenuOpen 以原生方式使用資料系結,根據其他狀態屬性和方法呼叫來維護屬性的狀態。 依預設系結 TwoWay 的另一個範例屬性是 TextBox.Text

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

  • Journal設定 旗標,指出巡覽日誌服務是否應該偵測或使用您的相依性屬性。 例如 屬性 SelectedIndex ;選取控制項中選取的任何專案都應該在巡覽日誌歷程記錄時保存。

唯讀相依性屬性

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

集合類型的相依性屬性

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

相依性屬性安全性考量

相依性屬性應該宣告為公用屬性。 相依性屬性識別碼欄位應該宣告為公用靜態欄位。 即使您嘗試宣告其他存取層級(例如 protected),相依性屬性一律可以透過識別碼與屬性系統 API 搭配存取。 即使是受保護的識別碼欄位,也可能因為元資料包告或值判斷 API 屬於屬性系統的一部分而可供存取,例如 LocalValueEnumerator 。 如需詳細資訊,請參閱相依性屬性的安全性

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

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

另請參閱