DependencyObject 的安全构造函数模式 (WPF .NET)

托管代码编程(通常通过代码分析工具强制执行)的一般原则是:类构造函数不应调用可替代方法。 如果基类构造函数调用了可替代方法,并且派生类替代了该方法,则派生类中的替代方法可以在派生类构造函数之前运行。 如果派生类构造函数执行类初始化,则派生类方法可能会访问未初始化的类成员。 为避免运行时初始化问题,依赖属性类应避免在类构造函数中设置依赖属性值。 本文介绍如何以避免这些问题的方式实现 DependencyObject 构造函数。

属性系统虚拟方法和回叫

依赖属性虚拟方法和回叫是 Windows Presentation Foundation (WPF) 属性系统的一部分,扩展了依赖属性的多功能性。

使用 SetValue 设置依赖项属性值等基本操作将调用 OnPropertyChanged 事件和可能的几个 WPF 属性系统回叫。

OnPropertyChanged 是 WPF 属性系统虚拟方法的一个示例,它可以被继承层次结构中具有 DependencyObject 的类替代。 如果在自定义依赖属性类的实例化期间调用的构造函数中设置依赖属性值,并且从它派生的类替代 OnPropertyChanged 虚拟方法,则派生类 OnPropertyChanged 方法将在任何派生类构造函数之前运行。

PropertyChangedCallbackCoerceValueCallback 是 WPF 属性系统回叫的示例,它们可以由依赖属性类注册,并由派生自它们的类替代。 如果在自定义依赖属性类的构造函数中设置依赖属性值,并且从它派生的类替代属性元数据中的这些回叫之一,则派生类回叫将在任何派生类构造函数之前运行。 此问题与 ValidateValueCallback 不相关,因为它不是属性元数据的一部分,只能由注册类指定。

有关依赖属性回叫的详细信息,请参阅依赖属性回叫和验证

.NET 分析器

.NET Compiler Platform 分析器会检查 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 上的方法时会导致空引用异常。

    3. CoerceValueCallback,它通过替代依赖属性元数据在派生类中实现。 此回叫在调用未初始化的类字段 s_temperatureLog 上的方法时会导致空引用异常。

    4. OnPropertyChanged 事件,它通过替代虚拟方法在派生类中实现。 此事件在调用未初始化的类字段 s_temperatureLog 上的方法时会导致空引用异常。

  3. 派生类无参数构造函数,该构造函数初始化 s_temperatureLog

  4. 派生类参数构造函数,用于设置新的依赖属性值,从而再次调用 SetValue 方法。 由于 s_temperatureLog 现在已初始化,因此回叫和事件将运行,而不会导致空引用异常。

使用安全构造函数模式可以避免这些初始化问题。

安全构造函数模式

可通过不同的方式解决测试代码中演示的派生类初始化问题,包括:

  • 如果类可能用作基类,请避免在自定义依赖属性类的构造函数中设置依赖属性值。 如果需要初始化依赖属性值,请考虑在依赖属性注册期间或替代元数据时将所需值设置为属性元数据中的默认值。

  • 在使用派生类字段之前先对其进行初始化。 例如,使用以下任一方法:

    • 在单个语句中实例化和分配实例字段。 在前面的示例中,List<int> s_temperatureLog = new(); 语句将避免延迟赋值。

    • 在派生类静态构造函数中执行赋值,该构造函数在任意基类构造函数之前运行。 在前面的示例中,将赋值语句 s_temperatureLog = new List<int>(); 放入派生类静态构造函数将避免延迟赋值。

    • 使用迟缓初始化和实例化,在需要时初始化对象。 在前面的示例中,使用迟缓初始化和实例化来实例化和分配 s_temperatureLog 将避免延迟分配。 若要了解详细信息,请参阅迟缓初始化

  • 避免在 WPF 属性系统回叫和事件中使用未初始化的类变量。

另请参阅