Compartir a través de


Patrones de constructor seguros para objetos DependencyObject (WPF .NET)

Hay un principio general en la programación de código administrado, a menudo aplicado por las herramientas de análisis de código, y es que los constructores de clase no deben llamar a métodos reemplazables. Si un constructor de clase base llama a un método reemplazable y una clase derivada invalida ese método, el método de invalidación de la clase derivada se puede ejecutar antes que el constructor de clase derivada. Si el constructor de clase derivada realiza la inicialización de clase, el método de clase derivada podría tener acceso a miembros de clase no inicializados. Las clases de propiedad de dependencia deben evitar establecer valores de propiedad de dependencia en un constructor de clase para evitar problemas de inicialización en tiempo de ejecución. En este artículo se describe cómo implementar constructores DependencyObject de una manera que evite esos problemas.

Métodos virtuales y devoluciones de llamada del sistema de propiedades

Los métodos virtuales y las devoluciones de llamada de las propiedades de dependencia forman parte del sistema de propiedades de Windows Presentation Foundation (WPF) y amplían la versatilidad de las propiedades de dependencia.

Una operación básica como establecer un valor de propiedad de dependencia mediante SetValue invocará el evento OnPropertyChanged y, potencialmente, varias devoluciones de llamada del sistema de propiedades de WPF.

OnPropertyChanged es un ejemplo de un método virtual del sistema de propiedades de WPF que pueden invalidar las clases que tienen DependencyObject en su jerarquía de herencia. Si establece un valor de propiedad de dependencia en un constructor al que se llama durante la creación de instancias de la clase de propiedad de dependencia personalizada y una clase derivada de ella invalida el método virtual OnPropertyChanged, el método OnPropertyChanged de clase derivada se ejecutará antes de cualquier constructor de clase derivada.

PropertyChangedCallback y CoerceValueCallback son ejemplos de devoluciones de llamada del sistema de propiedades de WPF que se pueden registrar mediante clases de propiedad de dependencia y ser invalidadas por clases que derivan de ellas. Si establece un valor de propiedad de dependencia en el constructor de la clase de propiedad de dependencia personalizada y una clase que deriva de ella invalida una de esas devoluciones de llamada en los metadatos de propiedad, la devolución de llamada de clase derivada se ejecutará antes de cualquier constructor de clase derivada. Este problema no es relevante para ValidateValueCallback, ya que no forma parte de los metadatos de propiedad y solo se puede especificar mediante la clase de registro.

Para obtener más información sobre las devoluciones de llamada de propiedades de dependencia, consulte Devoluciones de llamada y validación de las propiedades de dependencia.

Analizadores de .NET

Los analizadores de .NET Compiler Platform inspeccionan el código de C# o Visual Basic para supervisar la calidad e identificar problemas de estilo. Si llama a métodos reemplazables en un constructor cuando la regla de analizador CA2214 está activa, recibirá la advertencia CA2214: Don't call overridable methods in constructors. Sin embargo, la regla no marcará métodos virtuales ni devoluciones de llamada invocadas por el sistema de propiedades WPF subyacente cuando se establezca un valor de propiedad de dependencia en un constructor.

Problemas causados por clases derivadas

Si sella la clase de propiedad de dependencia personalizada o si sabe que la clase no se derivará de ella, los problemas de inicialización en tiempo de ejecución de la clase derivada no se aplicarán a esa clase. Sin embargo, si crea una clase de propiedad de dependencia que se puede heredar, por ejemplo, si está creando plantillas o un conjunto de bibliotecas de controles ampliables, evite llamar a métodos reemplazables o establecer valores de propiedad de dependencia desde un constructor.

El código de prueba siguiente muestra un patrón de constructor no seguro, donde un constructor de clase base establece un valor de propiedad de dependencia, lo que desencadena llamadas a métodos virtuales y devoluciones de llamada.

    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

El orden en que se llama a los métodos en la prueba del patrón de constructor no seguro es:

  1. Constructor estático de clase derivada, que invalida los metadatos de propiedad de dependencia de Aquarium para registrar PropertyChangedCallback y CoerceValueCallback.

  2. Constructor de clase base, que establece un nuevo valor de propiedad de dependencia que da como resultado una llamada al método SetValue. La llamada SetValue desencadena devoluciones de llamada y eventos en el orden siguiente:

    1. ValidateValueCallback, que se implementa en la clase base. Esta devolución de llamada no forma parte de los metadatos de propiedad de dependencia y no se puede implementar en la clase derivada mediante invalidación de los metadatos.

    2. PropertyChangedCallback, que se implementa en la clase derivada mediante invalidación de los metadatos de propiedad de dependencia. Esta devolución de llamada produce una excepción de referencia nula cuando llama a un método en el campo de clase no inicializado s_temperatureLog.

    3. CoerceValueCallback, que se implementa en la clase derivada mediante invalidación de los metadatos de propiedad de dependencia. Esta devolución de llamada produce una excepción de referencia nula cuando llama a un método en el campo de clase no inicializado s_temperatureLog.

    4. El evento OnPropertyChanged, que se implementa en la clase derivada mediante invalidación del método virtual. Este evento inicia una excepción de referencia nula cuando llama a un método en el campo de clase no inicializado s_temperatureLog.

  3. Constructor sin parámetros de clase derivada, que inicializa s_temperatureLog.

  4. Constructor con parámetros de clase base, que establece un nuevo valor de propiedad de dependencia que da como resultado una llamada al método SetValue. Puesto que s_temperatureLog ahora está inicializado, las devoluciones de llamada y los eventos se ejecutarán sin generar excepciones de referencia nulas.

Estos problemas de inicialización se pueden evitar mediante el uso de patrones de constructor seguros.

Patrones de constructor seguros

Los problemas de inicialización de la clase derivada que se muestran en el código de prueba se pueden resolver de maneras diferentes, entre las que se incluyen:

  • Evitar establecer un valor de propiedad de dependencia en un constructor de la clase de propiedad de dependencia personalizada si la clase puede usarse como clase base. En caso de necesitar inicializar un valor de propiedad de dependencia, considerar la posibilidad de establecer el valor necesario como el valor predeterminado en los metadatos de propiedad durante el registro de propiedades de dependencia o al invalidar los metadatos.

  • Inicializar los campos de clase derivada antes de su uso. Por ejemplo, mediante cualquiera de estos enfoques:

    • Crear instancias y asignar campos de instancia en una sola instrucción. En el ejemplo anterior la instrucción List<int> s_temperatureLog = new(); evitaría la asignación tardía.

    • Realizar la asignación en el constructor estático de clase derivada, que se ejecuta por antes que cualquier constructor de clase base. En el ejemplo anterior colocar la instrucción s_temperatureLog = new List<int>(); de asignación en el constructor estático de clase derivada evitaría la asignación tardía.

    • Usar la inicialización diferida y la creación de instancias, que inicializa los objetos como y cuando son necesarios. En el ejemplo anterior la creación de instancias y la asignación de s_temperatureLog mediante la inicialización diferida y la creación de instancias evitarían la asignación tardía. Para obtener más información, vea Inicialización diferida.

  • Evitar usar variables de clase no inicializadas en devoluciones de llamada y eventos del sistema de propiedades de WPF.

Vea también