Cassaforte modelli di costruttore per DependencyObjects (WPF .NET)

Esiste un principio generale nella programmazione del codice gestito, spesso applicato dagli strumenti di analisi del codice, che i costruttori di classi non devono chiamare metodi sottoponibili a override. Se un metodo sottoponibile a override viene chiamato da un costruttore della classe base e una classe derivata esegue l'override di tale metodo, il metodo di override nella classe derivata può essere eseguito prima del costruttore della classe derivata. Se il costruttore della classe derivata esegue l'inizializzazione della classe, il metodo della classe derivata potrebbe accedere ai membri della classe non inizializzati. Le classi di proprietà di dipendenza devono evitare di impostare i valori delle proprietà di dipendenza in un costruttore di classe per evitare problemi di inizializzazione di runtime. Questo articolo descrive come implementare i DependencyObject costruttori in modo da evitare tali problemi.

Importante

La documentazione della Guida desktop per .NET 6 e .NET 5 (incluso .NET Core 3.1) è in fase di costruzione.

Metodi virtuali e callback del sistema di proprietà

I metodi virtuali e i callback delle proprietà di dipendenza fanno parte del sistema di proprietà Windows Presentation Foundation (WPF) ed espandono la versatilità delle proprietà di dipendenza.

Un'operazione di base come l'impostazione di un valore della proprietà di dipendenza tramite SetValueOnPropertyChanged richiama l'evento e potenzialmente diversi callback del sistema di proprietà WPF.

OnPropertyChanged è un esempio di metodo virtuale del sistema di proprietà WPF che può essere sottoposto a override dalle classi che hanno nella DependencyObject gerarchia di ereditarietà. Se si imposta un valore della proprietà di dipendenza in un costruttore chiamato durante la creazione di un'istanza della classe della proprietà di dipendenza personalizzata e una classe derivata da essa esegue l'override OnPropertyChanged del metodo virtuale, OnPropertyChanged il metodo della classe derivata verrà eseguito prima di qualsiasi costruttore della classe derivata.

PropertyChangedCallback e CoerceValueCallback sono esempi di callback del sistema di proprietà WPF che possono essere registrati dalle classi di proprietà di dipendenza ed sottoposti a override dalle classi che derivano da esse. Se si imposta un valore della proprietà di dipendenza nel costruttore della classe della proprietà di dipendenza personalizzata e una classe che deriva da esso esegue l'override di uno di questi callback nei metadati della proprietà, il callback della classe derivata verrà eseguito prima di qualsiasi costruttore della classe derivata. Questo problema non è rilevante per perché ValidateValueCallback non fa parte dei metadati delle proprietà e può essere specificato solo dalla classe di registrazione.

Per altre informazioni sui callback delle proprietà di dipendenza, vedere Callback e convalida delle proprietà di dipendenza.

Analizzatori .NET

Gli analizzatori della piattaforma del compilatore .NET esaminano il codice C# o Visual Basic per verificare la qualità del codice e i problemi di stile. Se si chiamano metodi sottoponibili a override in un costruttore quando la regola dell'analizzatore CA2214 è attiva, verrà visualizzato l'avviso CA2214: Don't call overridable methods in constructors. Tuttavia, la regola non contrassegna i metodi virtuali e i callback richiamati dal sistema di proprietà WPF sottostante quando un valore della proprietà di dipendenza viene impostato in un costruttore.

Problemi causati dalle classi derivate

Se si blocca la classe della proprietà di dipendenza personalizzata o si sa che la classe non verrà derivata, i problemi di inizializzazione del runtime della classe derivata non si applicano a tale classe. Tuttavia, se si crea una classe di proprietà di dipendenza ereditabile, ad esempio se si creano modelli o un set di librerie di controlli espandibili, evitare di chiamare metodi sottoponibili a override o di impostare i valori delle proprietà di dipendenza da un costruttore.

Il codice di test seguente illustra un modello di costruttore unsafe, in cui un costruttore della classe base imposta un valore della proprietà di dipendenza attivando così chiamate a metodi virtuali e callback.

    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

L'ordine in cui i metodi vengono chiamati nel test del modello di costruttore non sicuro è:

  1. Costruttore statico della classe derivata, che esegue l'override dei metadati della proprietà di dipendenza di Aquarium per registrare PropertyChangedCallback e CoerceValueCallback.

  2. Costruttore della classe base, che imposta un nuovo valore della proprietà di dipendenza con conseguente chiamata al SetValue metodo . La SetValue chiamata attiva callback ed eventi nell'ordine seguente:

    1. ValidateValueCallback, implementato nella classe di base. Questo callback non fa parte dei metadati della proprietà di dipendenza e non può essere implementato nella classe derivata eseguendo l'override dei metadati.

    2. PropertyChangedCallback, implementato nella classe derivata eseguendo l'override dei metadati della proprietà di dipendenza. Questo callback genera un'eccezione di riferimento Null quando chiama un metodo sul campo della classe non inizializzata s_temperatureLog.

    3. CoerceValueCallback, implementato nella classe derivata eseguendo l'override dei metadati della proprietà di dipendenza. Questo callback genera un'eccezione di riferimento Null quando chiama un metodo sul campo della classe non inizializzata s_temperatureLog.

    4. OnPropertyChanged , implementato nella classe derivata eseguendo l'override del metodo virtuale . Questo evento genera un'eccezione di riferimento Null quando chiama un metodo sul campo della classe non inizializzata s_temperatureLog.

  3. Costruttore senza parametri della classe derivata, che inizializza s_temperatureLog.

  4. Costruttore di parametri della classe derivata, che imposta un nuovo valore della proprietà di dipendenza, determinando un'altra chiamata al SetValue metodo . Poiché s_temperatureLog è ora inizializzato, i callback e gli eventi verranno eseguiti senza causare eccezioni di riferimento Null.

Questi problemi di inizializzazione sono evitabili tramite l'uso di modelli di costruttore sicuri.

Cassaforte di costruttore

I problemi di inizializzazione delle classi derivate illustrati nel codice di test possono essere risolti in modi diversi, tra cui:

  • Evitare di impostare un valore della proprietà di dipendenza in un costruttore della classe della proprietà di dipendenza personalizzata se la classe può essere usata come classe di base. Se è necessario inizializzare un valore della proprietà di dipendenza, è consigliabile impostare il valore richiesto come valore predefinito nei metadati della proprietà durante la registrazione della proprietà di dipendenza o quando si esegue l'override dei metadati.

  • Inizializzare i campi della classe derivata prima dell'uso. Ad esempio, usando uno di questi approcci:

    • Creare un'istanza e assegnare campi di istanza in una singola istruzione. Nell'esempio precedente l'istruzione evita l'assegnazione List<int> s_temperatureLog = new(); tardiva.

    • Eseguire l'assegnazione nel costruttore statico della classe derivata, che viene eseguito prima di qualsiasi costruttore della classe base. Nell'esempio precedente l'inserimento dell'istruzione di s_temperatureLog = new List<int>(); assegnazione nel costruttore statico della classe derivata evita l'assegnazione tardiva.

    • Usare l'inizializzazione differita e la creazione di istanze, che inizializzano gli oggetti come e quando sono necessari. Nell'esempio precedente la creazione di un'istanza e l'assegnazione tramite s_temperatureLog l'inizializzazione differita e la creazione di istanze eviterebbe l'assegnazione tardiva. Per altre informazioni, vedere Inizializzazione differita.

  • Evitare di usare variabili di classe non inizializzate nei callback e negli eventi del sistema di proprietà WPF.

Vedi anche