Freigeben über


Sichere Konstruktormuster für DependencyObjects (WPF .NET)

Es ist ein allgemeines Prinzip für das Programmieren von verwaltetem Code (oft durch Codeanalysetools erzwungen), dass Klassenkonstruktoren keine überschreibbaren Methoden aufrufen dürfen. Wenn eine überschreibbare Methode von einem Basisklassenkonstruktor aufgerufen wird und eine abgeleitete Klasse diese Methode überschreibt, kann die Überschreibungsmethode in der abgeleiteten Klasse vor dem Konstruktor der abgeleiteten Klasse ausgeführt werden. Wenn der Konstruktor der abgeleiteten Klasse die Initialisierung der Klasse durchführt, kann die Methode der abgeleiteten Klasse auf nicht initialisierte Klassenmember zugreifen. Klassen mit Abhängigkeitseigenschaften sollten das Festlegen von Werten für Abhängigkeitseigenschaften in einem Klassenkonstruktor vermeiden, um Probleme bei der Laufzeitinitialisierung zu vermeiden. In diesem Artikel wird beschrieben, wie Sie DependencyObject-Konstruktoren so implementieren, dass diese Probleme vermieden werden.

Wichtig

Der Desktopleitfaden zu .NET 7 und .NET 6 ist in Bearbeitung.

Virtuelle Methoden und Rückrufe des Eigenschaftensystems

Virtuelle Methoden und Rückrufe von Abhängigkeitseigenschaften sind Teil des Eigenschaftssystems von Windows Presentation Foundation (WPF) und erweitern die Vielseitigkeit von Abhängigkeitseigenschaften.

Ein einfacher Vorgang wie das Festlegen des Werts einer Abhängigkeitseigenschaft mit SetValue ruft das OnPropertyChanged-Ereignis und möglicherweise mehrere Rückrufe an das WPF-Eigenschaftensystem auf.

OnPropertyChanged ist ein Beispiel einer virtuellen Methode des WPF-Eigenschaftensystems, die von Klassen überschrieben werden kann, die DependencyObject in ihrer Vererbungshierarchie enthalten. Wenn Sie den Wert einer Abhängigkeitseigenschaft in einem Konstruktor festlegen, der während der Instanziierung Ihrer benutzerdefinierten Abhängigkeitseigenschaftsklasse aufgerufen wird, und eine davon abgeleitete Klasse die virtuelle OnPropertyChanged-Methode überschreibt, wird die Methode der abgeleiteten OnPropertyChanged-Klasse vor dem Konstruktor der abgeleiteten Klasse ausgeführt.

PropertyChangedCallback und CoerceValueCallback sind Beispiele für Rückrufe des WPF-Eigenschaftensystems, die von abhängigen Eigenschaftsklassen registriert und von Klassen, die von ihnen abgeleitet sind, überschrieben werden können. Wenn Sie den Wert einer Abhängigkeitseigenschaft im Konstruktor Ihrer benutzerdefinierten Abhängigkeitseigenschaftsklasse festlegen und eine davon abgeleitete Klasse einen dieser Rückrufe in den Eigenschaftsmetadaten überschreibt, wird der Rückruf der abgeleiteten Klasse vor jedem Konstruktor der abgeleiteten Klasse ausgeführt. Dieses Problem ist für ValidateValueCallback nicht von Belang, da es nicht Teil der Eigenschaftsmetadaten ist und nur von der registrierenden Klasse angegeben werden kann.

Weitere Informationen zu Rückrufen von Abhängigkeitseigenschaften finden Sie unter Rückrufe und Validierung von Abhängigkeitseigenschaften.

.NET-Analystetools

Die Analysetools für die .NET-Compilerplattform untersuchen Ihren C#- oder Visual Basic-Code auf Probleme bei Codequalität und -stil. Wenn Sie in einem Konstruktor überschreibbare Methoden aufrufen, während die Analysetoolregel CA2214 aktiv ist, erhalten Sie die Warnung CA2214: Don't call overridable methods in constructors. Allerdings werden virtuelle Methoden und Rückrufe, die vom zugrunde liegenden WPF-Eigenschaftensystem aufgerufen werden, wenn der Wert einer Abhängigkeitseigenschaft in einem Konstruktor festgelegt wird, von der Regel nicht gekennzeichnet.

Durch abgeleitete Klassen verursachte Probleme

Wenn Sie Ihre benutzerdefinierte Abhängigkeitseigenschaftsklasse versiegeln oder anderweitig wissen, dass Ihre Klasse nicht abgeleitet wird, gelten die Probleme bei der Laufzeitinitialisierung abgeleiteter Klassen nicht für diese Klasse. Wenn Sie jedoch eine Abhängigkeitseigenschaftsklasse erstellen, die vererbbar ist, z. B. wenn Sie Vorlagen oder einen erweiterbaren Bibliothekssatz für ein Steuerelement erstellen, vermeiden Sie den Aufruf überschreibbarer Methoden oder das Festlegen von Abhängigkeitseigenschaftswerten über einen Konstruktor.

Der folgende Testcode veranschaulicht ein unsicheres Konstruktormuster, bei dem ein Basisklassenkonstruktor den Wert einer Abhängigkeitseigenschaft festlegt und damit Aufrufe von virtuellen Methoden und Rückrufen auslöst.

    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

Die Reihenfolge, in der Methoden im Test auf unsichere Konstruktormuster aufgerufen werden, ist wie folgt:

  1. Statischer Konstruktor der abgeleiteten Klasse, der die Metadaten der Abhängigkeitseigenschaft von Aquarium überschreibt, um PropertyChangedCallback und CoerceValueCallback zu registrieren.

  2. Basisklassenkonstruktor, der einen neuen Wert für eine Abhängigkeitseigenschaft festlegt, was zu einem Aufruf der SetValue-Methode führt. Der Aufruf von SetValue löst Rückrufe und Ereignisse in der folgenden Reihenfolge aus:

    1. ValidateValueCallback, der in der Basisklasse implementiert ist. Dieser Rückruf ist nicht Teil der Metadaten der Abhängigkeitseigenschaft und kann in der abgeleiteten Klasse nicht durch Überschreiben der Metadaten implementiert werden.

    2. PropertyChangedCallback, der in der abgeleiteten Klasse implementiert wird, indem die Metadaten der Abhängigkeitseigenschaft überschrieben werden. Dieser Rückruf verursacht eine NULL-Verweisausnahme, wenn eine Methode für das nicht initialisierte Klassenfeld s_temperatureLog aufgerufen wird.

    3. CoerceValueCallback, der in der abgeleiteten Klasse implementiert wird, indem die Metadaten der Abhängigkeitseigenschaft überschrieben werden. Dieser Rückruf verursacht eine NULL-Verweisausnahme, wenn eine Methode für das nicht initialisierte Klassenfeld s_temperatureLog aufgerufen wird.

    4. OnPropertyChanged, der in der abgeleiteten Klasse implementiert wird, indem die virtuelle Methode überschrieben wird. Dieses Ereignis verursacht eine NULL-Verweisausnahme, wenn es eine Methode für das nicht initialisierte Klassenfeld s_temperatureLog aufruft.

  3. Parameterloser Konstruktor der abgeleiteten Klasse, der s_temperatureLog initialisiert.

  4. Parameterkonstruktor der abgeleiteten Klasse, der einen neuen Wert für eine Abhängigkeitseigenschaft festlegt, was zu einem weiteren Aufruf der SetValue-Methode führt. Da s_temperatureLog jetzt initialisiert ist, werden Rückrufe und Ereignisse ausgeführt, ohne dass es zu NULL-Verweisausnahmen kommt.

Diese Initialisierungsprobleme lassen sich durch die Verwendung sicherer Konstruktormuster vermeiden.

Sichere Konstruktormuster

Die Initialisierungsprobleme abgeleiteter Klassen, die im Testcode demonstriert werden, können auf verschiedene Weise behoben werden, u. a. so:

  • Vermeiden Sie es, den Wert einer Abhängigkeitseigenschaft in einem Konstruktor Ihrer benutzerdefinierten Abhängigkeitseigenschaftsklasse festzulegen, wenn Ihre Klasse als Basisklasse verwendet werden könnte. Wenn Sie den Wert einer Abhängigkeitseigenschaft initialisieren müssen, sollten Sie den erforderlichen Wert als Standardwert in den Metadaten der Eigenschaft während der Registrierung der Abhängigkeitseigenschaft oder beim Überschreiben der Metadaten festlegen.

  • Initialisieren Sie Felder abgeleiteter Klassen vor deren Verwendung. Befolgen Sie beispielsweise einen der folgenden Ansätze:

    • Instanziieren und weisen Sie Instanzfelder in einer einzelnen Anweisung zu. Im vorherigen Beispiel wird mit der List<int> s_temperatureLog = new();-Anweisung eine späte Zuweisung vermieden.

    • Führen Sie die Zuweisung im statischen Konstruktor der abgeleiteten Klasse durch, der vor dem Konstruktor der Basisklasse ausgeführt wird. Wenn Sie im vorherigen Beispiel die s_temperatureLog = new List<int>();-Anweisung in den statischen Konstruktor der abgeleiteten Klasse einfügen, wird eine späte Zuweisung vermieden.

    • Verwenden Sie die verzögerte Initialisierung und Instanziierung, bei der Objekte je nach Bedarf initialisiert werden. Im vorherigen Beispiel lässt sich durch das Instanziieren und Zuweisen von s_temperatureLog mittels verzögerter Initialisierung und Instanziierung eine späte Zuweisung vermeiden. Weitere Informationen finden Sie unter Verzögerte Initialisierung.

  • Vermeiden Sie die Verwendung nicht initialisierter Klassenvariablen in Rückrufen und Ereignissen des WPF-Eigenschaftensystems.

Weitere Informationen