共用方式為


自訂相依性屬性

下面介紹如何使用 C++、C# 或 Visual Basic 為 Windows 執行階段應用定義和實作自己的相依性屬性。 我們列出應用程式開發人員和元件作者可能想要建立自訂相依性屬性的原因。 我們會描述自訂相依性屬性的實作步驟,以及一些可改善相依性屬性效能、可用性或多功能性的最佳做法。

必要條件

我們假設您從現有相依性屬性的使用者的角度瞭解相依性屬性,並且您已閱讀相依性屬性概觀。 若要遵循本主題中的範例,您也應該瞭解 XAML,並瞭解如何使用 C++、C# 或 Visual Basic 撰寫基本 Windows 執行階段應用程式。

什麼是相依性屬性?

若要支援屬性的樣式、資料繫結、動畫和預設值,則應該實作為相依性屬性。 相依性屬性值不會儲存為類別上的欄位,而是由 xaml 框架儲存,並使用金鑰進行參考,當透過呼叫 DependencyProperty.Register 方法向 Windows 執行階段屬性系統註冊屬性時,將擷取該按金鑰。 相依性屬性只能由衍生自 DependencyObject 的類型使用。 但是在類別階層中,DependencyObject 相當高,因此大部分用於 UI 和呈現支援的類別都可以支援相依性屬性。 如需相依性屬性的詳細資訊,以及此文件中描述它們所使用的一些術語和慣例,請參閱相依性屬性概觀

Windows 執行階段中的相依性屬性範例包括:Control.BackgroundFrameworkElement.WidthTextBox.Text 等。

慣例是,類別公開的每個相依性屬性都有一個對應的 DependencyProperty 類型的公共靜態唯讀屬性,該屬性在同一類別上公開,並提供相依性屬性的識別碼。 識別碼的名稱遵循此慣例:相依性屬性的名稱,並將字串「Property」新增至名稱結尾。 例如,Control.Background 屬性的對應 DependencyProperty 識別碼是 Control.BackgroundProperty。 識別碼在註冊相依性屬性時儲存有關相依性屬性的資訊,然後可用於涉及相依性屬性的其他操作,例如呼叫 SetValue

屬性包裝函式

相依性屬性通常具有包裝函式實作。 如果沒有包裝函式,取得或設定屬性的唯一方式就是使用相依性屬性公用程式方法 GetValueSetValue,並將識別碼傳遞至它們做為參數。 對於表面上是屬性的內容而言,這是相當不自然的使用方式。 但是,使用包裝函式時,您的程式碼和參考相依性屬性的任何其他程式碼都可以使用直接的物件屬性語法,而這種語法對於您使用的語言而言是自然的。

如果您自行實作自訂相依性屬性,並想要公開且易於呼叫,請定義屬性包裝函式。 屬性包裝函式也適用於向反映或靜態分析程序報告相依性屬性的基本資訊。 具體而言,包裝函式是您放置 ContentPropertyAttribute 等屬性的位置。

將屬性實作為相依性屬性的時機

每當您在類別上實作公用讀取/寫入屬性時,只要您的類別衍生自 DependencyObject,就可以選擇讓屬性以相依性屬性的形式運作。 有時候,支援有私用欄位的屬性,一般的技巧即已足夠。 將自訂屬性定義為相依性屬性不一定是必要或適當的。 選擇將取決於您想要支援屬性的案例。

當您想要將屬性實作為相依性屬性時,您可以考慮將屬性實作為 Windows 執行階段或 Windows 執行階段應用程式的其中一或多個功能:

  • 透過 Style 設定屬性
  • 充當與 {Binding} 進行資料繫結的有效目標屬性
  • 透過分鏡腳本支援動畫值
  • 報告財產價值何時變更:
    • 屬性系統本身所採取的動作
    • 環境
    • 使用者動作
    • 讀取和寫入樣式

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

定義相依性屬性可以視為一組概念。 這些概念不一定是程序性步驟,因為實作中的單行程式碼可以處理數個概念。 此清單只提供快速概觀。 我們將在本主題稍後更詳細地說明每個概念,我們將示範數種語言的範例程式碼。

  • 向屬性系統註冊屬性名稱 (呼叫 Register),指定擁有者類型和屬性值的類型。
    • Register 有一個需要屬性中繼資料的必要參數。 為此指定 null,或者如果您想要屬性變更行為,或者可以透過呼叫 ClearValue 來恢復依據中繼資料的預設值,請指定 PropertyMetadata 的執行個體。
  • DependencyProperty 識別碼定義為擁有者類型的公共靜態唯讀屬性成員。
  • 定義包裝函式屬性,並遵循您要實作之語言中使用的屬性存取子模型。 包裝函式屬性名稱應該符合您在 Register 中使用的名稱字串。 實作 getset 存取子,藉由呼叫 GetValueSetValue 並將您自己的屬性識別碼當做參數,將包裝函式與其包裝的相依性屬性連接起來。
  • (選擇性) 將 ContentPropertyAttribute 等屬性放在包裝函式上。

注意

如果您要定義自訂附加屬性,您通常會省略包裝函式。 相反,您可以編寫 XAML 處理器可以使用的不同樣式的存取器。 請參閱自訂附加屬性。 

註冊屬性

為了使您的屬性成為相依性屬性,您必須將該屬性註冊到由 Windows 執行階段屬性系統維護的屬性存放區中。 若要註冊屬性,您可以呼叫 Register 方法。

對於 Microsoft .NET 語言 (C# 和 Microsoft Visual Basic),您可以在類別的主體內 (在類別內部,但在任何成員定義之外) 呼叫 Register。 識別碼是由 Register 方法呼叫所提供,做為傳回值。 Register 呼叫通常是做為靜態建構函式,或做為 DependencyProperty 類型之公用靜態唯讀屬性初始化的一部分,做為類別的一部分。 此屬性會公開您的相依性屬性的識別碼。 以下是 Register 呼叫的範例。

注意

將相依性屬性註冊為識別碼屬性定義的一部分是典型的實作,但您也可以在類別靜態構造函式中註冊相依性屬性。 如果您需要多行程式碼來初始化相依性屬性,這個方式可能有意義。

針對 C++/CX,您可以選擇如何在標頭和程式碼檔案之間分割實作。 典型的分割是將識別碼本身宣告為標題中的公共靜態屬性,並使用 get 實作但不設定 setget 實作參考一個私人欄位,它是一個未初始化的 DependencyProperty 執行個體。 您也可以宣告包裝函式和包裝函式的 getset 實作。 在此情況下,標題包含一些最少的實作。 如果包裝函式需要 Windows 執行階段屬性,標題中的屬性也一樣。 將 Register 呼叫放在程式碼檔案中,該函式只會在第一次初始化應用程式時才會執行。 使用 Register 的傳回值來填入您在標題中宣告的靜態但未初始化的識別碼,您一開始會在實作檔案的根範圍設定為 nullptr

public static readonly DependencyProperty LabelProperty = DependencyProperty.Register(
  nameof(Label),
  typeof(String),
  typeof(ImageWithLabelControl),
  new PropertyMetadata(null)
);
Public Shared ReadOnly LabelProperty As DependencyProperty = 
    DependencyProperty.Register("Label", 
      GetType(String), 
      GetType(ImageWithLabelControl), 
      New PropertyMetadata(Nothing))
// ImageWithLabelControl.idl
namespace ImageWithLabelControlApp
{
    runtimeclass ImageWithLabelControl : Windows.UI.Xaml.Controls.Control
    {
        ImageWithLabelControl();
        static Windows.UI.Xaml.DependencyProperty LabelProperty{ get; };
        String Label;
    }
}

// ImageWithLabelControl.h
...
struct ImageWithLabelControl : ImageWithLabelControlT<ImageWithLabelControl>
{
...
public:
    static Windows::UI::Xaml::DependencyProperty LabelProperty()
    {
        return m_labelProperty;
    }

private:
    static Windows::UI::Xaml::DependencyProperty m_labelProperty;
...
};

// ImageWithLabelControl.cpp
...
Windows::UI::Xaml::DependencyProperty ImageWithLabelControl::m_labelProperty =
    Windows::UI::Xaml::DependencyProperty::Register(
        L"Label",
        winrt::xaml_typename<winrt::hstring>(),
        winrt::xaml_typename<ImageWithLabelControlApp::ImageWithLabelControl>(),
        Windows::UI::Xaml::PropertyMetadata{ nullptr }
);
...
//.h file
//using namespace Windows::UI::Xaml::Controls;
//using namespace Windows::UI::Xaml::Interop;
//using namespace Windows::UI::Xaml;
//using namespace Platform;

public ref class ImageWithLabelControl sealed : public Control
{
private:
    static DependencyProperty^ _LabelProperty;
...
public:
    static void RegisterDependencyProperties();
    static property DependencyProperty^ LabelProperty
    {
        DependencyProperty^ get() {return _LabelProperty;}
    }
...
};

//.cpp file
using namespace Windows::UI::Xaml;
using namespace Windows::UI::Xaml.Interop;

DependencyProperty^ ImageWithLabelControl::_LabelProperty = nullptr;

// This function is called from the App constructor in App.xaml.cpp
// to register the properties
void ImageWithLabelControl::RegisterDependencyProperties()
{ 
    if (_LabelProperty == nullptr)
    { 
        _LabelProperty = DependencyProperty::Register(
          "Label", Platform::String::typeid, ImageWithLabelControl::typeid, nullptr);
    } 
}

注意

針對 C++/CX 程式碼,您擁有私人欄位和顯示 DependencyProperty 的公用唯讀屬性的原因,是讓其他使用相依性屬性的呼叫端也可以使用需要公用識別碼的屬性系統公用程式 API。 如果您將識別碼保留為私人,則人員無法使用這些公用程式 API。 此類 API 和案例的範例包括選擇 GetValueSetValueClearValueGetAnimationBaseValueSetBindingSetter.Property。 您無法為此使用公用欄位,因為 Windows 執行階段中繼資料規則不允許公用欄位。

相依性屬性命名慣例

相依性屬性有命名慣例;除特殊情況外,在所有情況下都遵循慣例。 相依性屬性本身有一個基本名稱 (上例中的「Label」),做為 Register 的第一個參數提供。 名稱在每個註冊類型內必須是唯一的,而且唯一性需求也適用於任何繼承的成員。 透過基底類型繼承的相依性屬性視為登錄類型的一部分,已繼承屬性的名稱無法再次登錄。

警告

儘管您在此處提供的名稱可以是在您選擇的語言程式設計中有效的任何字串識別碼,但您通常也希望能夠在 XAML 中設定相依性屬性。 若要在 XAML 中設定,您選擇的屬性名稱必須是有效的 XAML 名稱。 如需詳細資訊,請參閱 XAML 概觀

當您建立識別碼屬性時,請將屬性的名稱與後置詞「Property」(例如「LabelProperty」) 合併。 此屬性是相依性屬性的識別碼,它用做您在自己的屬性包裝函式中進行的 SetValueGetValue 呼叫的輸入。 它也被屬性系統和其他 XAML 處理器 (例如 {x:Bind}) 使用

實作包裝函式

您的屬性包裝函式應在 get 實作中呼叫 GetValue,並在 set 實作中呼叫 SetValue

警告

在除特殊情況外的所有情況下,您的包裝函式實作應僅執行 GetValueSetValue 操作。 否則,當屬性是透過 XAML 設定,而不是透過程式碼設定時,您會收到不同的行為。 為了提高效率,XAML 剖析器在設定相依性屬性時繞過包裝函式;並透過 SetValue 與備份存放區對話。

public String Label
{
    get { return (String)GetValue(LabelProperty); }
    set { SetValue(LabelProperty, value); }
}
Public Property Label() As String
    Get
        Return DirectCast(GetValue(LabelProperty), String) 
    End Get 
    Set(ByVal value As String)
        SetValue(LabelProperty, value)
    End Set
End Property
// ImageWithLabelControl.h
...
winrt::hstring Label()
{
    return winrt::unbox_value<winrt::hstring>(GetValue(m_labelProperty));
}

void Label(winrt::hstring const& value)
{
    SetValue(m_labelProperty, winrt::box_value(value));
}
...
//using namespace Platform;
public:
...
  property String^ Label
  {
    String^ get() {
      return (String^)GetValue(LabelProperty);
    }
    void set(String^ value) {
      SetValue(LabelProperty, value);
    }
  }

自訂相依性屬性的屬性中繼資料

將屬性中繼資料指派給相依性屬性時,屬性擁有者類型或其子類別的每個執行個體都會將相同的中繼資料套用至該屬性。 在屬性中繼資料中,您可以指定兩種行為:

  • 屬性系統指派給屬性所有案例的預設值。
  • 每當偵測到屬性值變更時,就會在屬性系統中自動叫用的靜態回呼方法。

使用屬性中繼資料呼叫 Register

在前面呼叫 DependencyProperty.Register 的範例中,我們為 propertyMetadata 參數傳遞了 null 值。 若要讓相依性屬性提供預設值或使用屬性變更的回呼,您必須定義 PropertyMetadata 執行個體,以提供其中一或兩項功能。

一般而言,您會在 DependencyProperty.Register 的參數內,提供 PropertyMetadata 做為內嵌建立的執行個體。

注意

如果您要定義 CreateDefaultValueCallback 實作,您必須使用公用程式方法 PropertyMetadata.Create,而不是呼叫 PropertyMetadata 建構函式來定義 PropertyMetadata 執行個體。

下一個範例會參考具有 PropertyChangedCallback 值的 PropertyMetadata 執行個體,藉此修改先前顯示的 DependencyProperty.Register 範例。 本節稍後會顯示「OnLabelChanged」回呼的實作。

public static readonly DependencyProperty LabelProperty = DependencyProperty.Register(
  nameof(Label),
  typeof(String),
  typeof(ImageWithLabelControl),
  new PropertyMetadata(null,new PropertyChangedCallback(OnLabelChanged))
);
Public Shared ReadOnly LabelProperty As DependencyProperty =
    DependencyProperty.Register("Label",
      GetType(String),
      GetType(ImageWithLabelControl),
      New PropertyMetadata(
        Nothing, new PropertyChangedCallback(AddressOf OnLabelChanged)))
// ImageWithLabelControl.cpp
...
Windows::UI::Xaml::DependencyProperty ImageWithLabelControl::m_labelProperty =
    Windows::UI::Xaml::DependencyProperty::Register(
        L"Label",
        winrt::xaml_typename<winrt::hstring>(),
        winrt::xaml_typename<ImageWithLabelControlApp::ImageWithLabelControl>(),
        Windows::UI::Xaml::PropertyMetadata{ nullptr, Windows::UI::Xaml::PropertyChangedCallback{ &ImageWithLabelControl::OnLabelChanged } }
);
...
DependencyProperty^ ImageWithLabelControl::_LabelProperty =
    DependencyProperty::Register("Label",
    Platform::String::typeid,
    ImageWithLabelControl::typeid,
    ref new PropertyMetadata(nullptr,
      ref new PropertyChangedCallback(&ImageWithLabelControl::OnLabelChanged))
    );

預設值

您可以指定相依性屬性的預設值,讓屬性在未設定時一律傳回特定的預設值。 這個值可能不同於該屬性類型的固有預設值。

如果未指定預設值,相依性屬性的預設值為 null 做為參考類型,或是值類型或語言基本類型的預設值 (例如,整數為 0 或字串的空字串)。 建立預設值的主要原因是當您在屬性上呼叫 ClearValue 時,會還原此值。 根據每個屬性建立預設值可能比在建構函式中建立預設值更方便,特別是針對值類型。 不過,針對參考類型,請確定建立預設值不會建立非預期的單一模式。 有關詳細資訊,請參閱本主題後面的最佳做法

// ImageWithLabelControl.cpp
...
Windows::UI::Xaml::DependencyProperty ImageWithLabelControl::m_labelProperty =
    Windows::UI::Xaml::DependencyProperty::Register(
        L"Label",
        winrt::xaml_typename<winrt::hstring>(),
        winrt::xaml_typename<ImageWithLabelControlApp::ImageWithLabelControl>(),
        Windows::UI::Xaml::PropertyMetadata{ winrt::box_value(L"default label"), Windows::UI::Xaml::PropertyChangedCallback{ &ImageWithLabelControl::OnLabelChanged } }
);
...

注意

請勿向 UnsetValue 的預設值註冊。 如果您這樣做,它會混淆屬性取用者,而且會在屬性系統中產生非預期的結果。

CreateDefaultValueCallback

在某些情況下,您會為多個 UI 執行緒上所使用的物件定義相依性屬性。 如果您要定義多個應用程式所使用的資料物件,或您在多個應用程式中使用的控制項,則可能是這種情況。 您可以藉由提供 CreateDefaultValueCallback 實作,而不是繫結至註冊屬性的執行緒,來啟用物件在不同 UI 執行緒之間的交換。 基本上,CreateDefaultValueCallback 會定義預設值的處理站。 CreateDefaultValueCallback 所傳回的值一律與使用物件的目前 UI CreateDefaultValueCallback 執行緒相關聯。

若要定義指定 CreateDefaultValueCallback 的中繼資料,您必須呼叫 PropertyMetadata.Create 以傳回中繼資料執行個體;PropertyMetadata 建構函式沒有包含 CreateDefaultValueCallback 參數的簽章。

CreateDefaultValueCallback 的一般實作模式是建立新的 DependencyObject 類別、將 DependencyObject 的每個屬性的特定屬性值設定為預期的預設值,然後透過 CreateDefaultValueCallback 方法的傳回值,將新類別當做 Object 參考傳回。

屬性變更回呼方法

您可以定義屬性變更回呼方法,以定義屬性與其他相依性屬性的互動,或每當屬性變更時更新物件的內部屬性或狀態。 如果叫用回呼,則屬性系統已判斷有有效的屬性值變更。 因為回呼方法是靜態的,所以回呼的 d 參數很重要,因為它會告訴您類別的哪個執行個體已報告變更。 一般實作會使用事件資料的 NewValue 屬性,並以某種方式處理該值,通常是藉由對傳遞為 d 的物件執行一些其他變更。 屬性變更的其他回應是拒絕 NewValue 所報告的值、還原 OldValue,或將值設定為套用至 NewValue 的程式設計條件約束。

下一個範例會顯示 PropertyChangedCallback 實作。 它會實作您在先前 Register 範例中所參考的方法,做為 PropertyMetadata 建構引數的一部分。 此回呼解決的案例是該類別還有一個名為「HasLabelValue」的計算唯讀屬性 (未顯示實作)。 每當重新評估「Label」屬性時,就會叫用這個回呼方法,而且回呼可讓相依計算值與相依性屬性的變更保持同步。

private static void OnLabelChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
    ImageWithLabelControl iwlc = d as ImageWithLabelControl; //null checks omitted
    String s = e.NewValue as String; //null checks omitted
    if (s == String.Empty)
    {
        iwlc.HasLabelValue = false;
    } else {
        iwlc.HasLabelValue = true;
    }
}
    Private Shared Sub OnLabelChanged(d As DependencyObject, e As DependencyPropertyChangedEventArgs)
        Dim iwlc As ImageWithLabelControl = CType(d, ImageWithLabelControl) ' null checks omitted
        Dim s As String = CType(e.NewValue,String) ' null checks omitted
        If s Is String.Empty Then
            iwlc.HasLabelValue = False
        Else
            iwlc.HasLabelValue = True
        End If
    End Sub
void ImageWithLabelControl::OnLabelChanged(Windows::UI::Xaml::DependencyObject const& d, Windows::UI::Xaml::DependencyPropertyChangedEventArgs const& e)
{
    auto iwlc{ d.as<ImageWithLabelControlApp::ImageWithLabelControl>() };
    auto s{ winrt::unbox_value<winrt::hstring>(e.NewValue()) };
    iwlc.HasLabelValue(s.size() != 0);
}
static void OnLabelChanged(DependencyObject^ d, DependencyPropertyChangedEventArgs^ e)
{
    ImageWithLabelControl^ iwlc = (ImageWithLabelControl^)d;
    Platform::String^ s = (Platform::String^)(e->NewValue);
    if (s->IsEmpty()) {
        iwlc->HasLabelValue=false;
    }
}

結構與列舉的屬性變更行為

如果 DependencyProperty 的類型是列舉或結構,即使結構的內部值或列舉值未變更,仍可能會叫用回呼。 這與系統基本類型不同,例如只有在值變更時才會叫用它的字串。 這是 Box 和 unbox 作業對內部完成之這些值的副作用。 如果您有屬性的 PropertyChangedCallback 方法,其中您的值是列舉或結構,您必須自行轉換值,並使用現在轉換值可用的多載比較運算子來比較 OldValueNewValue。 或者,如果沒有這類運算子可用 (可能是自訂結構的情況),您可能需要比較個別值。 如果結果是值未變更,您通常會選擇不執行任何動作。

private static void OnVisibilityValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
    if ((Visibility)e.NewValue != (Visibility)e.OldValue)
    {
        //value really changed, invoke your changed logic here
    } // else this was invoked because of boxing, do nothing
}
Private Shared Sub OnVisibilityValueChanged(d As DependencyObject, e As DependencyPropertyChangedEventArgs)
    If CType(e.NewValue,Visibility) != CType(e.OldValue,Visibility) Then
        '  value really changed, invoke your changed logic here
    End If
    '  else this was invoked because of boxing, do nothing
End Sub
static void OnVisibilityValueChanged(Windows::UI::Xaml::DependencyObject const& d, Windows::UI::Xaml::DependencyPropertyChangedEventArgs const& e)
{
    auto oldVisibility{ winrt::unbox_value<Windows::UI::Xaml::Visibility>(e.OldValue()) };
    auto newVisibility{ winrt::unbox_value<Windows::UI::Xaml::Visibility>(e.NewValue()) };

    if (newVisibility != oldVisibility)
    {
        // The value really changed; invoke your property-changed logic here.
    }
    // Otherwise, OnVisibilityValueChanged was invoked because of boxing; do nothing.
}
static void OnVisibilityValueChanged(DependencyObject^ d, DependencyPropertyChangedEventArgs^ e)
{
    if ((Visibility)e->NewValue != (Visibility)e->OldValue)
    {
        //value really changed, invoke your changed logic here
    } 
    // else this was invoked because of boxing, do nothing
    }
}

最佳作法

當您定義自訂相依性屬性時,請記住下列考慮做為最佳做法。

DependencyObject 和執行緒

所有 DependencyObject 執行個體,都必須建立在與 Windows 執行階段應用程式所顯示目前視窗相關聯的 UI 執行緒上。 雖然必須在主要 UI 執行緒上建立每個 DependencyObject,但是呼叫 Dispatcher,可以使用來自其他執行緒的調度員參考來存取物件。

DependencyObject 的執行緒層面是相關的,因為它通常表示只有在 UI 執行緒上執行的程式碼可以變更或甚至讀取相依性屬性的值。 在正確使用非同步模式和背景工作執行緒的典型 UI 程式碼中,通常可以避免執行緒問題。 通常,如果您定義自己的 DependencyObject 類型並嘗試將它們用於資料來源或 DependencyObject 不一定合適的其他案例,則通常只會遇到與 DependencyObject 相關的執行緒問題。

避免非預期的單一

如果您要宣告採用參考類型的相依性屬性,而且呼叫該參考類型的建構函式做為建立 PropertyMetadata 程式碼的一部分,就可能發生非預期的單一。 所發生的情況是,相依性屬性的所有用法都只共用 PropertyMetadata 的一個執行個體,因此嘗試共用您所建構的單一參考類型。 您透過相依性屬性設定之該實值類型的任何子屬性,然後以您可能未預期的方式傳播至其他物件。

如果您想要非 Null 值,您可以使用類別建構函式來設定參考類型相依性屬性的初始值,但請注意,基於相依性屬性概觀的目的,這會被視為本機值。 如果您的類別支援範本,可能更適合使用此用途的範本。 另一個避免單一模式,但仍提供實用的預設值的另一種方式,是在參考類型上公開靜態屬性,該參考類型會為該類別的值提供適當的預設值。

集合類型相依性屬性

集合類型相依性屬性要考慮一些其他的實作問題。

Windows 執行階段 API 中,集合類型相依性屬性相對罕見。 在大部分情況下,您可以使用項目是 DependencyObject 子類別的集合,但集合屬性本身會實作為傳統的 CLR 或 C++ 屬性。 這是因為集合不一定適合涉及相依性屬性的一些典型案例。 例如:

  • 您通常不會建立集合的動畫效果。
  • 您通常不會使用樣式或範本預先填入集合中的項目。
  • 雖然繫結至集合是主要案例,但集合不需要是繫結來源的相依性屬性。 對於繫結目標,使用 ItemsControlDataTemplate 的子類別來支援集合項目,或使用檢視模型模式比較常見。 有關繫結到集合和從集合繫結的詳細資訊,請參閱深入瞭解資料繫結
  • 集合變更的通知會透過 INotifyPropertyChangedINotifyCollectionChanged 等介面,或從 ObservableCollection<T> 衍生集合類型來改善解決。

不過,集合類型相依性屬性的案例確實存在。 接下來三節提供如何實作集合類型相依性屬性的一些指引。

初始化集合

當您建立相依性屬性時,可以藉由相依性屬性中繼資料建立預設值。 但請小心不要使用單一靜態集合做為預設值。 相反,必須特意將集合值設定為唯一 (執行個體) 集合,做為集合屬性的擁有者類別的類別構造函式邏輯的一部分。

// WARNING - DO NOT DO THIS
public static readonly DependencyProperty ItemsProperty = DependencyProperty.Register(
  nameof(Items),
  typeof(IList<object>),
  typeof(ImageWithLabelControl),
  new PropertyMetadata(new List<object>())
);

// DO THIS Instead
public static readonly DependencyProperty ItemsProperty = DependencyProperty.Register(
  nameof(Items),
  typeof(IList<object>),
  typeof(ImageWithLabelControl),
  new PropertyMetadata(null)
);

public ImageWithLabelControl()
{
    // Need to initialize in constructor instead
    Items = new List<object>();
}

DependencyProperty 及其 PropertyMetadata 的預設值是 DependencyProperty 靜態定義的一部分。 藉由提供預設集合 (或其他實例化) 值做為預設值,它會在類別的所有實例之間共用,而不是每個類別都有自己的集合,就像通常想要的一樣。

變更通知

將集合定義為相依性屬性並不會藉由叫用「PropertyChanged」回呼方法的屬性系統,自動為集合中的項目提供變更通知。 如果您想要集合或集合項目的通知,例如,針對資料繫結案例,請實作 INotifyPropertyChangedINotifyCollectionChanged 介面。 如需詳細資訊,請參閱深入瞭解資料繫結

相依性屬性安全性考量

將相依性屬性宣告為公共屬性。 將相依性屬性識別碼宣告為公用靜態唯讀成員。 即使您嘗試宣告語言允許的其他存取層級 (例如 protected),也一律可以透過識別碼結合屬性系統 API 來存取相依性屬性。 將相依性屬性識別碼宣告為內部或私用將無法運作,因為屬性系統無法正常運作。

包裝函式屬性實際上只是為了方便,可以透過呼叫 GetValueSetValue 來繞過套用於包裝函式的安全機制。 因此,請保持包裝函式屬性公開;否則,您只會讓合法呼叫者更難使用您的財產,而不會提供任何真正的安全性優勢。

Windows 執行階段不提供將自訂相依性屬性註冊為唯讀的方法。

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

類別建構函式不應該呼叫虛擬方法的一般準則。 這是因為可以呼叫建構函式來完成衍生類別建構函式的基底類別初始化,並且當正在建構的物件執行個體尚未完全初始化時,可能會發生透過建構函式進入虛擬方法的情況。 當您從任何已從 DependencyObject 衍生的類別衍生時,請記住屬性系統本身在內部呼叫並公開虛擬方法做為其服務的一部分。 若要避免執行階段初始化的潛在問題,請勿在類別的建構函式中設定相依性屬性值。

註冊 C++/CX 應用程式的相依性屬性

在 C++/CX 中註冊屬性的實作比 C# 更棘手,兩者都是因為分割成標題和實作檔案,也因為實作檔根範圍的初始化是錯誤的做法。 (Visual C++ 元件延伸模組 (C++/CX) 將靜態初始化程式程式碼從根範圍直接放入 DllMain,而 C# 編譯器將靜態初始化程式指派給類別,進而避免 DllMain 載入鎖定問題。) 此處的最佳做法是宣告協助程式函式,該函式會針對類別執行所有相依性屬性註冊,每個類別各一個函式。 然後,針對應用程式取用的每個自訂類別,您必須參考您想要使用之每個自訂類別所公開的協助程式註冊函式。 在 InitializeComponent 之前,做為應用程式建構函式 (App::App()) 的一部分呼叫每個幫助程式註冊函數一次。 該建構函式只會在第一次真正參考應用程式時執行,例如,如果暫停的應用程式繼續執行,就不會再次執行。 另外,如前面的 C++ 註冊範例所示,每個 Register 呼叫周圍的 nullptr 檢查很重要:它確保函數的呼叫者不能註冊該屬性兩次。 第二個註冊呼叫可能會因為屬性名稱重複而造成您的應用程式在沒有這類檢查的情況下當機。 如果您查看範例的 C++/CX 版本的程式碼,您可以在 XAML 使用者和自訂控制項範例中看到此實作模式。