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 sostituibili. Se un metodo sottoponibile a override viene chiamato da un costruttore di 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 classi per evitare problemi di inizializzazione di runtime. Questo articolo descrive come implementare DependencyObject costruttori in modo da evitare tali problemi.

Importante

La documentazione di Desktop Guide per .NET 7 e .NET 6 è 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, ad esempio l'impostazione di un valore della proprietà di dipendenza tramite SetValue richiamerà l'evento OnPropertyChanged 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 da classi che hanno DependencyObject nella gerarchia di ereditarietà. Se si imposta un valore della proprietà di dipendenza in un costruttore chiamato durante la creazione di istanze della classe di proprietà di dipendenza personalizzata e una classe derivata esegue l'override del OnPropertyChanged metodo virtuale, il metodo della classe OnPropertyChanged derivata verrà eseguito prima di qualsiasi costruttore di classe derivata.

PropertyChangedCallback e CoerceValueCallback sono esempi di callback del sistema di proprietà WPF che possono essere registrati dalle classi di proprietà di dipendenza e sottoposti a override da classi che derivano da essi. Se si imposta un valore della proprietà di dipendenza nel costruttore della classe di proprietà di dipendenza personalizzata e una classe che ne deriva esegue l'override di uno di questi callback nei metadati della proprietà, il callback della classe derivata verrà eseguito prima di qualsiasi costruttore di classe derivata. Questo problema non è rilevante perché ValidateValueCallback non fa parte dei metadati della 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 controllano il codice C# o Visual Basic per individuare problemi di qualità e stile del codice. Se si chiamano metodi sostituibili 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 contrassegnerà 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 chiude la classe di 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 espandibile, evitare di chiamare metodi sottoponibili a override o 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 quindi 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 unsafe è:

  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 delle 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 delle proprietà di dipendenza. Questo callback causa un'eccezione di riferimento Null quando chiama un metodo nel campo s_temperatureLogdella classe non inizializzato .

    3. CoerceValueCallback, implementato nella classe derivata eseguendo l'override dei metadati delle proprietà di dipendenza. Questo callback causa un'eccezione di riferimento Null quando chiama un metodo nel campo s_temperatureLogdella classe non inizializzato .

    4. OnPropertyChanged evento, implementato nella classe derivata eseguendo l'override del metodo virtuale. Questo evento genera un'eccezione di riferimento Null quando chiama un metodo nel campo s_temperatureLogdella classe non inizializzato .

  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, generando 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.

modelli di costruttore Cassaforte

I problemi di inizializzazione della classe derivata 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 di proprietà di dipendenza personalizzata se la classe potrebbe essere usata come classe di base. Se è necessario inizializzare un valore della proprietà di dipendenza, è consigliabile impostare il valore obbligatorio come valore predefinito nei metadati delle proprietà durante la registrazione delle 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 List<int> s_temperatureLog = new(); eviterebbe l'assegnazione 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 s_temperatureLog = new List<int>(); di assegnazione nel costruttore statico della classe derivata evita l'assegnazione tardiva.

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

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

Vedi anche