保管庫 DependencyObjects 的建構函式模式 (WPF .NET)

Managed 程式碼程式設計有一個一般準則,通常是由程式碼分析工具強制執行,類別建構函式不應該呼叫可覆寫的方法。 如果基類建構函式呼叫可覆寫的方法,而衍生類別會覆寫該方法,則衍生類別中的 override 方法可以在衍生類別建構函式之前執行。 如果衍生類別建構函式執行類別初始化,則衍生類別方法可能會存取未初始化的類別成員。 相依性屬性類別應該避免在類別建構函式中設定相依性屬性值,以避免執行時間初始化問題。 本文說明如何以避免這些問題的方式實 DependencyObject 作建構函式。

重要

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

屬性系統虛擬方法和回呼

相依性屬性虛擬方法和回呼是 Windows Presentation Foundation (WPF) 屬性系統的一部分,並擴充相依性屬性的多功能性。

使用 SetValue 設定相依性屬性值等基本作業會叫用 事件,並可能叫 OnPropertyChanged 用數個 WPF 屬性系統回呼。

OnPropertyChanged 是 WPF 屬性系統虛擬方法的範例,可由繼承階層中具有 DependencyObject 的類別覆寫。 如果您在自訂相依性屬性類別具現化期間呼叫的建構函式中設定相依性屬性值,而衍生自它的類別會覆寫 OnPropertyChanged 虛擬方法,則衍生類別方法會在任何衍生類別 OnPropertyChanged 建構函式之前執行。

PropertyChangedCallbackCoerceValueCallback 是 WPF 屬性系統回呼的範例,可由相依性屬性類別註冊,並由衍生自它們的類別覆寫。 如果您在自訂相依性屬性類別的建構函式中設定相依性屬性值,而衍生自它的類別會覆寫屬性中繼資料中的其中一個回呼,則衍生類別回呼會在任何衍生類別建構函式之前執行。 此問題與 無關 ValidateValueCallback ,因為它不是屬性中繼資料的一部分,而且只能由註冊類別指定。

如需相依性屬性回呼的詳細資訊,請參閱 相依性屬性回呼和驗證

.NET 分析器

.NET 編譯器平臺分析器會檢查您的 C# 或 Visual Basic 程式碼是否有程式碼品質和樣式問題。 如果您在分析器規則 CA2214 作用中時,在建構函式中呼叫可覆寫的方法,您將會收到警告 CA2214: Don't call overridable methods in constructors 。 但是,規則不會在建構函式中設定相依性屬性值時,為基礎 WPF 屬性系統叫用的虛擬方法和回呼加上旗標。

衍生類別所造成的問題

如果您 密封 您的自訂相依性屬性類別,或知道類別不會衍生自,則衍生類別執行時間初始化問題不適用於該類別。 但是,如果您建立可繼承的相依性屬性類別,例如,如果您要建立範本或可展開的控制程式庫集,請避免從建構函式呼叫可覆寫的方法或設定相依性屬性值。

下列測試程式碼示範不安全的建構函式模式,其中基類建構函式會設定相依性屬性值,進而觸發對虛擬方法和回呼的呼叫。

    private static void TestUnsafeConstructorPattern()
    {
        //Aquarium aquarium = new();
        //Debug.WriteLine($"Aquarium temperature (C): {aquarium.TempCelcius}");

        // Instantiate and set tropical aquarium temperature.
        TropicalAquarium tropicalAquarium = new(tempCelcius: 25);
        Debug.WriteLine($"Tropical aquarium temperature (C): " +
            $"{tropicalAquarium.TempCelcius}");

        /* Test output:
        Derived class static constructor running.
        Base class ValidateValueCallback running.
        Base class ValidateValueCallback running.
        Base class ValidateValueCallback running.
        Base class parameterless constructor running.
        Base class ValidateValueCallback running.
        Derived class CoerceValueCallback running.
        Derived class CoerceValueCallback: null reference exception.
        Derived class OnPropertyChanged event running.
        Derived class OnPropertyChanged event: null reference exception.
        Derived class PropertyChangedCallback running.
        Derived class PropertyChangedCallback: null reference exception.
        Aquarium temperature (C): 20
        Derived class parameterless constructor running.
        Derived class parameter constructor running.
        Base class ValidateValueCallback running.
        Derived class CoerceValueCallback running.
        Derived class OnPropertyChanged event running.
        Derived class PropertyChangedCallback running.
        Tropical aquarium temperature (C): 25
        */
    }
}

public class Aquarium : DependencyObject
{
    // Register a dependency property with the specified property name,
    // property type, owner type, property metadata with default value,
    // and validate-value callback.
    public static readonly DependencyProperty TempCelciusProperty =
        DependencyProperty.Register(
            name: "TempCelcius",
            propertyType: typeof(int),
            ownerType: typeof(Aquarium),
            typeMetadata: new PropertyMetadata(defaultValue: 0),
            validateValueCallback: 
                new ValidateValueCallback(ValidateValueCallback));

    // Parameterless constructor.
    public Aquarium()
    {
        Debug.WriteLine("Base class parameterless constructor running.");

        // Set typical aquarium temperature.
        TempCelcius = 20;

        Debug.WriteLine($"Aquarium temperature (C): {TempCelcius}");
    }

    // Declare public read-write accessors.
    public int TempCelcius
    {
        get => (int)GetValue(TempCelciusProperty);
        set => SetValue(TempCelciusProperty, value);
    }

    // Validate-value callback.
    public static bool ValidateValueCallback(object value)
    {
        Debug.WriteLine("Base class ValidateValueCallback running.");
        double val = (int)value;
        return val >= 0;
    }
}

public class TropicalAquarium : Aquarium
{
    // Class field.
    private static List<int> s_temperatureLog;

    // Static constructor.
    static TropicalAquarium()
    {
        Debug.WriteLine("Derived class static constructor running.");

        // Create a new metadata instance with callbacks specified.
        PropertyMetadata newPropertyMetadata = new(
            defaultValue: 0,
            propertyChangedCallback: new PropertyChangedCallback(PropertyChangedCallback),
            coerceValueCallback: new CoerceValueCallback(CoerceValueCallback));

        // Call OverrideMetadata on the dependency property identifier.
        TempCelciusProperty.OverrideMetadata(
            forType: typeof(TropicalAquarium),
            typeMetadata: newPropertyMetadata);
    }

    // Parameterless constructor.
    public TropicalAquarium()
    {
        Debug.WriteLine("Derived class parameterless constructor running.");
        s_temperatureLog = new List<int>();
    }

    // Parameter constructor.
    public TropicalAquarium(int tempCelcius) : this()
    {
        Debug.WriteLine("Derived class parameter constructor running.");
        TempCelcius = tempCelcius;
        s_temperatureLog.Add(tempCelcius);
    }

    // Property-changed callback.
    private static void PropertyChangedCallback(DependencyObject depObj, 
        DependencyPropertyChangedEventArgs e)
    {
        Debug.WriteLine("Derived class PropertyChangedCallback running.");
        try
        {
            s_temperatureLog.Add((int)e.NewValue);
        }
        catch (NullReferenceException)
        {
            Debug.WriteLine("Derived class PropertyChangedCallback: null reference exception.");
        }
    }

    // Coerce-value callback.
    private static object CoerceValueCallback(DependencyObject depObj, object value)
    {
        Debug.WriteLine("Derived class CoerceValueCallback running.");
        try
        {
            s_temperatureLog.Add((int)value);
        }
        catch (NullReferenceException)
        {
            Debug.WriteLine("Derived class CoerceValueCallback: null reference exception.");
        }
        return value;
    }

    // OnPropertyChanged event.
    protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
    {
        Debug.WriteLine("Derived class OnPropertyChanged event running.");
        try
        {
            s_temperatureLog.Add((int)e.NewValue);
        }
        catch (NullReferenceException)
        {
            Debug.WriteLine("Derived class OnPropertyChanged event: null reference exception.");
        }

        // Mandatory call to base implementation.
        base.OnPropertyChanged(e);
    }
}
    Private Shared Sub TestUnsafeConstructorPattern()
        'Aquarium aquarium = new Aquarium();
        'Debug.WriteLine($"Aquarium temperature (C): {aquarium.TempCelcius}");

        ' Instantiate And set tropical aquarium temperature.
        Dim tropicalAquarium As New TropicalAquarium(tempCelc:=25)
        Debug.WriteLine($"Tropical aquarium temperature (C): 
            {tropicalAquarium.TempCelcius}")

        ' Test output:
        ' Derived class static constructor running.
        ' Base class ValidateValueCallback running.
        ' Base class ValidateValueCallback running.
        ' Base class ValidateValueCallback running.
        ' Base class parameterless constructor running.
        ' Base class ValidateValueCallback running.
        ' Derived class CoerceValueCallback running.
        ' Derived class CoerceValueCallback: null reference exception.
        ' Derived class OnPropertyChanged event running.
        ' Derived class OnPropertyChanged event: null reference exception.
        ' Derived class PropertyChangedCallback running.
        ' Derived class PropertyChangedCallback: null reference exception.
        ' Aquarium temperature(C):  20
        ' Derived class parameterless constructor running.
        ' Derived class parameter constructor running.
        ' Base class ValidateValueCallback running.
        ' Derived class CoerceValueCallback running.
        ' Derived class OnPropertyChanged event running.
        ' Derived class PropertyChangedCallback running.
        ' Tropical Aquarium temperature (C): 25

    End Sub
End Class

Public Class Aquarium
    Inherits DependencyObject

    'Register a dependency property with the specified property name,
    ' property type, owner type, property metadata with default value,
    ' and validate-value callback.
    Public Shared ReadOnly TempCelciusProperty As DependencyProperty =
        DependencyProperty.Register(
        name:="TempCelcius",
        propertyType:=GetType(Integer),
        ownerType:=GetType(Aquarium),
        typeMetadata:=New PropertyMetadata(defaultValue:=0),
        validateValueCallback:=
            New ValidateValueCallback(AddressOf ValidateValueCallback))

    ' Parameterless constructor.
    Public Sub New()
        Debug.WriteLine("Base class parameterless constructor running.")

        ' Set typical aquarium temperature.
        TempCelcius = 20

        Debug.WriteLine($"Aquarium temperature (C): {TempCelcius}")
    End Sub

    ' Declare public read-write accessors.
    Public Property TempCelcius As Integer
        Get
            Return GetValue(TempCelciusProperty)
        End Get
        Set(value As Integer)
            SetValue(TempCelciusProperty, value)
        End Set
    End Property

    ' Validate-value callback.
    Public Shared Function ValidateValueCallback(value As Object) As Boolean
        Debug.WriteLine("Base class ValidateValueCallback running.")
        Dim val As Double = CInt(value)
        Return val >= 0
    End Function

End Class

Public Class TropicalAquarium
    Inherits Aquarium

    ' Class field.
    Private Shared s_temperatureLog As List(Of Integer)

    ' Static constructor.
    Shared Sub New()
        Debug.WriteLine("Derived class static constructor running.")

        ' Create a new metadata instance with callbacks specified.
        Dim newPropertyMetadata As New PropertyMetadata(
                defaultValue:=0,
                propertyChangedCallback:=
                    New PropertyChangedCallback(AddressOf PropertyChangedCallback),
                coerceValueCallback:=
                    New CoerceValueCallback(AddressOf CoerceValueCallback))

        ' Call OverrideMetadata on the dependency property identifier.
        TempCelciusProperty.OverrideMetadata(
                forType:=GetType(TropicalAquarium),
                typeMetadata:=newPropertyMetadata)
    End Sub

    ' Parameterless constructor.
    Public Sub New()
        Debug.WriteLine("Derived class parameterless constructor running.")
        s_temperatureLog = New List(Of Integer)()
    End Sub

    ' Parameter constructor.
    Public Sub New(tempCelc As Integer)
        Me.New()
        Debug.WriteLine("Derived class parameter constructor running.")
        TempCelcius = tempCelc
        s_temperatureLog.Add(TempCelcius)
    End Sub

    ' Property-changed callback.
    Private Shared Sub PropertyChangedCallback(depObj As DependencyObject,
        e As DependencyPropertyChangedEventArgs)
        Debug.WriteLine("Derived class PropertyChangedCallback running.")

        Try
            s_temperatureLog.Add(e.NewValue)
        Catch ex As NullReferenceException
            Debug.WriteLine("Derived class PropertyChangedCallback: null reference exception.")
        End Try
    End Sub

    ' Coerce-value callback.
    Private Shared Function CoerceValueCallback(depObj As DependencyObject, value As Object) As Object
        Debug.WriteLine("Derived class CoerceValueCallback running.")

        Try
            s_temperatureLog.Add(value)
        Catch ex As NullReferenceException
            Debug.WriteLine("Derived class CoerceValueCallback: null reference exception.")
        End Try

        Return value
    End Function

    ' OnPropertyChanged event.
    Protected Overrides Sub OnPropertyChanged(e As DependencyPropertyChangedEventArgs)
        Debug.WriteLine("Derived class OnPropertyChanged event running.")

        Try
            s_temperatureLog.Add(e.NewValue)
        Catch ex As NullReferenceException
            Debug.WriteLine("Derived class OnPropertyChanged event: null reference exception.")
        End Try

        ' Mandatory call to base implementation.
        MyBase.OnPropertyChanged(e)
    End Sub

End Class

在不安全的建構函式模式測試中呼叫方法的順序如下:

  1. 衍生類別靜態建構函式,其會覆寫 的相依性屬性中繼資料 Aquarium ,以註冊 PropertyChangedCallbackCoerceValueCallback

  2. 基類建構函式會設定新的相依性屬性值,導致呼叫 SetValue 方法。 呼叫 SetValue 會依下列順序觸發回呼和事件:

    1. ValidateValueCallback,這是在基類中實作。 此回呼不是相依性屬性中繼資料的一部分,無法透過覆寫中繼資料在衍生類別中實作。

    2. PropertyChangedCallback,這是藉由覆寫相依性屬性中繼資料,在衍生類別中實作。 這個回呼會在未初始化類別欄位 s_temperatureLog 上呼叫 方法時,造成 Null 參考例外狀況。

    3. CoerceValueCallback,這是藉由覆寫相依性屬性中繼資料,在衍生類別中實作。 這個回呼會在未初始化類別欄位 s_temperatureLog 上呼叫 方法時,造成 Null 參考例外狀況。

    4. OnPropertyChanged 事件,這是藉由覆寫虛擬方法,在衍生類別中實作。 當這個事件在未初始化的類別欄位 s_temperatureLog 上呼叫方法時,會導致 Null 參考例外狀況。

  3. 衍生類別無參數建構函式,其會 s_temperatureLog 初始化 。

  4. 衍生類別參數建構函式,這個建構函式會設定新的相依性屬性值,導致方法的另一個呼叫 SetValue 。 由於 s_temperatureLog 現在已初始化,因此會執行回呼和事件,而不會造成 Null 參考例外狀況。

這些初始化問題可透過使用安全建構函式模式來避免。

保管庫建構函式模式

測試程式碼中示範的衍生類別初始化問題可以透過不同方式解決,包括:

  • 如果您的類別可能用作基類,請避免在自訂相依性屬性類別的建構函式中設定相依性屬性值。 如果您需要初始化相依性屬性值,請考慮在相依性屬性註冊期間或覆寫中繼資料時,將必要值設定為屬性中繼資料中的預設值。

  • 使用之前,請先初始化衍生類別欄位。 例如,使用下列任一方法:

    • 在單一語句中具現化和指派實例欄位。 在上一個範例中,語句 List<int> s_temperatureLog = new(); 會避免延遲指派。

    • 在衍生類別靜態建構函式中執行指派,它會在任何基類建構函式之前執行。 在上述範例中,將指派語句 s_temperatureLog = new List<int>(); 放在衍生類別靜態建構函式中,可避免晚期指派。

    • 使用延遲初始化和具現化,以在需要時將物件初始化為 和 。 在上一個範例中,使用延遲初始化和具現化來具現化和指派 s_temperatureLog 會避免延遲指派。 如需詳細資訊,請參閱 延遲初始化

  • 避免在 WPF 屬性系統回呼和事件中使用未初始化的類別變數。

另請參閱