Padrões de construtor seguros para DependencyObjects (WPF .NET)

Há um princípio geral na programação de código gerenciado, muitas vezes imposto por ferramentas de análise de código, que os construtores de classe não devem chamar métodos substituíveis. Se um método substituível for chamado por um construtor de classe base e uma classe derivada substituir esse método, o método de substituição na classe derivada poderá ser executado antes do construtor de classe derivada. Se o construtor de classe derivada executar a inicialização de classe, o método de classe derivada poderá acessar membros de classe não inicializados. As classes de propriedade de dependência devem evitar a definição de valores de propriedade de dependência em um construtor de classe para evitar problemas de inicialização de tempo de execução. Este artigo descreve como implementar DependencyObject construtores de uma forma que evita esses problemas.

Importante

A documentação do Guia da Área de Trabalho para .NET 7 e .NET 6 está em construção.

Métodos virtuais e retornos de chamada do sistema de propriedades

Os métodos virtuais de propriedade de dependência e os retornos de chamada fazem parte do sistema de propriedades do Windows Presentation Foundation (WPF) e expandem a versatilidade das propriedades de dependência.

Uma operação básica, como definir um valor de propriedade de dependência usando SetValue invocará o evento e, potencialmente, OnPropertyChanged vários retornos de chamada do sistema de propriedades WPF.

OnPropertyChanged é um exemplo de um método virtual do sistema de propriedades WPF que pode ser substituído por classes que têm DependencyObject em sua hierarquia de herança. Se você definir um valor de propriedade de dependência em um construtor que é chamado durante a instanciação de sua classe de propriedade de dependência personalizada e uma classe derivada dela substitui o OnPropertyChanged método virtual, o método de classe derivada será executado antes de qualquer construtor de classe OnPropertyChanged derivada.

PropertyChangedCallback e são exemplos de retornos de chamada do sistema de propriedades WPF que podem ser registrados por classes de propriedade de dependência e CoerceValueCallback substituídos por classes que derivam deles. Se você definir um valor de propriedade de dependência no construtor de sua classe de propriedade de dependência personalizada e uma classe derivada dela substituir um desses retornos de chamada em metadados de propriedade, o retorno de chamada de classe derivado será executado antes de qualquer construtor de classe derivada. Esse problema não é relevante ValidateValueCallback , pois não faz parte dos metadados da propriedade e só pode ser especificado pela classe de registro.

Para obter mais informações sobre retornos de chamada de propriedade de dependência, consulte Retornos de chamada e validação de propriedade de dependência.

Analisadores de .NET

Os analisadores de plataforma do compilador .NET inspecionam seu código C# ou Visual Basic em busca de problemas de qualidade e estilo de código. Se você chamar métodos substituíveis em um construtor quando a regra do analisador CA2214 estiver ativa, você receberá o aviso CA2214: Don't call overridable methods in constructors. Mas, a regra não sinalizará métodos virtuais e retornos de chamada que são invocados pelo sistema de propriedades WPF subjacente quando um valor de propriedade de dependência é definido em um construtor.

Problemas causados por classes derivadas

Se você selar sua classe de propriedade de dependência personalizada ou souber que sua classe não será derivada, os problemas de inicialização de tempo de execução de classe derivada não se aplicarão a essa classe. Mas, se você criar uma classe de propriedade de dependência que seja herdável, por exemplo, se estiver criando modelos ou um conjunto de bibliotecas de controle expansível, evite chamar métodos substituíveis ou definir valores de propriedade de dependência de um construtor.

O código de teste a seguir demonstra um padrão de construtor não seguro, onde um construtor de classe base define um valor de propriedade de dependência, disparando chamadas para métodos virtuais e retornos de chamada.

    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

A ordem em que os métodos são chamados no teste de padrão do construtor inseguro é:

  1. Construtor estático de classe derivada, que substitui os metadados da propriedade de dependência de Aquarium registrar PropertyChangedCallback e CoerceValueCallback.

  2. Construtor de classe base, que define um novo valor de propriedade de dependência resultando em uma chamada para o SetValue método. A SetValue chamada dispara retornos de chamada e eventos na seguinte ordem:

    1. ValidateValueCallback, que é implementado na classe base. Esse retorno de chamada não faz parte dos metadados da propriedade de dependência e não pode ser implementado na classe derivada substituindo metadados.

    2. PropertyChangedCallback, que é implementado na classe derivada substituindo metadados de propriedade de dependência. Esse retorno de chamada causa uma exceção de referência nula quando chama um método no campo s_temperatureLogde classe não inicializado .

    3. CoerceValueCallback, que é implementado na classe derivada substituindo metadados de propriedade de dependência. Esse retorno de chamada causa uma exceção de referência nula quando chama um método no campo s_temperatureLogde classe não inicializado .

    4. OnPropertyChanged , que é implementado na classe derivada substituindo o método virtual. Esse evento causa uma exceção de referência nula quando chama um método no campo s_temperatureLogde classe não inicializado .

  3. Construtor sem parâmetros de classe derivada, que inicializa s_temperatureLog.

  4. Construtor de parâmetro de classe derivada, que define um novo valor de propriedade de dependência, resultando em outra chamada para o SetValue método. Como s_temperatureLog agora é inicializado, retornos de chamada e eventos serão executados sem causar exceções de referência nulas.

Esses problemas de inicialização são evitáveis por meio do uso de padrões de construtores seguros.

Padrões de construtor seguros

Os problemas de inicialização de classe derivada demonstrados no código de teste podem ser resolvidos de diferentes maneiras, incluindo:

  • Evite definir um valor de propriedade de dependência em um construtor de sua classe de propriedade de dependência personalizada se sua classe puder ser usada como uma classe base. Se você precisar inicializar um valor de propriedade de dependência, considere definir o valor necessário como o valor padrão nos metadados de propriedade durante o registro de propriedade de dependência ou ao substituir metadados.

  • Inicialize campos de classe derivados antes de seu uso. Por exemplo, usando qualquer uma destas abordagens:

    • Instancie e atribua campos de instância em uma única instrução. No exemplo anterior, a instrução List<int> s_temperatureLog = new(); evitaria a atribuição tardia.

    • Execute a atribuição no construtor estático de classe derivada, que é executado à frente de qualquer construtor de classe base. No exemplo anterior, colocar a instrução s_temperatureLog = new List<int>(); assignment no construtor estático de classe derivada evitaria a atribuição tardia.

    • Use inicialização e instanciação lentas, que inicializam objetos conforme e quando forem necessários. No exemplo anterior, instanciar e atribuir s_temperatureLog usando inicialização e instanciação lentas evitaria a atribuição tardia. Para obter mais informações, consulte Inicialização lenta.

  • Evite usar variáveis de classe não inicializadas em eventos e retornos de chamada do sistema de propriedades WPF.

Confira também