Freigeben über


Sichere Konstruktormuster für DependencyObjects

Es gibt ein allgemeines Prinzip in der Programmierung von verwaltetem Code, die häufig von Codeanalysetools erzwungen wird, dass Klassenkonstruktoren keine überschreibbaren Methoden aufrufen sollten. 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 abgeleiteten Klassenkonstruktor ausgeführt werden. Wenn der abgeleitete Klassenkonstruktor die Klasseninitialisierung durchführt, greift die abgeleitete Klassenmethode möglicherweise auf nicht initialisierte Klassenmber zu. Abhängigkeitseigenschaftenklassen sollten das Festlegen von Abhängigkeitseigenschaftswerten in einem Klassenkonstruktor vermeiden, um Laufzeitinitialisierungsprobleme zu vermeiden. In diesem Artikel wird beschrieben, wie Konstruktoren so implementiert DependencyObject werden, dass diese Probleme vermieden werden.

Virtuelle Methoden und Rückrufe des Eigenschaftensystems

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

Ein grundlegender Vorgang, wie das Festlegen eines Abhängigkeitseigenschaftswerts mittels SetValue, löst das OnPropertyChanged-Ereignis aus und potenziell mehrere Rückrufe des WPF-Eigenschaftssystems.

OnPropertyChanged ist ein Beispiel für eine virtuelle WPF-Eigenschaftssystemmethode, die von Klassen außer Kraft gesetzt werden kann, wenn diese DependencyObject in ihrer Vererbungshierarchie haben. Wenn Sie einen Abhängigkeitseigenschaftswert in einem Konstruktor festlegen, der während der Instanziierung der benutzerdefinierten Abhängigkeitseigenschaftsklasse aufgerufen wird, und eine von ihr abgeleitete Klasse überschreibt die OnPropertyChanged virtuelle Methode, wird die abgeleitete Klassenmethode OnPropertyChanged vor einem abgeleiteten Klassenkonstruktor ausgeführt.

PropertyChangedCallback und CoerceValueCallback sind Beispiele für WPF-Eigenschaftensystemrückrufe, die von Klassen für Abhängigkeitseigenschaften registriert und von Klassen überschrieben werden können, die von ihnen abgeleitet sind. Wenn Sie einen Abhängigkeitseigenschaftswert im Konstruktor der benutzerdefinierten Abhängigkeitseigenschaftsklasse festlegen und eine Von ihr abgeleitete Klasse einen dieser Rückrufe in Eigenschaftsmetadaten außer Kraft setzt, wird der abgeleitete Klassenrückruf vor einem abgeleiteten Klassenkonstruktor ausgeführt. Dieses Problem ist nicht relevant, ValidateValueCallback da es nicht Teil von Eigenschaftsmetadaten ist und nur von der registrierenden Klasse angegeben werden kann.

Weitere Informationen zu Rückrufen und Validierungen von Abhängigkeitseigenschaften finden Sie unter Dependency property callbacks and validation.

.NET-Analystetools

.NET-Compilerplattformanalysatoren untersuchen Ihren C#- oder Visual Basic-Code auf Codequalitäts- und Stilprobleme. Wenn Sie überschreibbare Methoden in einem Konstruktor aufrufen, wenn die Analyseregel CA2214 aktiv ist, wird die Warnung CA2214: Don't call overridable methods in constructorsangezeigt. Die Regel kennzeichnet jedoch keine virtuellen Methoden und Rückrufe, die vom zugrunde liegenden WPF-Eigenschaftensystem aufgerufen werden, wenn ein Abhängigkeitseigenschaftswert in einem Konstruktor festgelegt wird.

Durch abgeleitete Klassen verursachte Probleme

Wenn Sie die benutzerdefinierte Abhängigkeitseigenschaftsklasse versiegeln oder andernfalls wissen, dass Ihre Klasse nicht abgeleitet wird, gelten abgeleitete Initialisierungsprobleme der Klassenlaufzeit nicht für diese Klasse. Wenn Sie jedoch eine Abhängigkeitseigenschaftsklasse erstellen, die vererbbar ist, z. B. wenn Sie Vorlagen oder einen erweiterbaren Steuerelementbibliothekssatz erstellen, vermeiden Sie das Aufrufen überschreibbarer Methoden oder Festlegen von Abhängigkeitseigenschaftswerten aus einem Konstruktor.

Der folgende Testcode veranschaulicht ein unsicheres Konstruktormuster, bei dem ein Basisklassenkonstruktor einen Abhängigkeitseigenschaftswert festlegt, wodurch Aufrufe virtueller Methoden und Rückrufe ausgelöst werden.

    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 unsicheren Konstruktormustertest aufgerufen werden, lautet:

  1. Abgeleiteter statischer Klassenkonstruktor, der die Metadaten der Abhängigkeitseigenschaft überschreibt, um Aquarium zu registrieren und PropertyChangedCallback sowie CoerceValueCallback zu registrieren.

  2. Basisklassenkonstruktor, der einen neuen Abhängigkeitseigenschaftswert festlegt, der zu einem Aufruf der SetValue Methode führt. Der SetValue Aufruf löst Rückrufe und Ereignisse in der folgenden Reihenfolge aus:

    1. ValidateValueCallback, die in der Basisklasse implementiert wird. Dieser Rückruf ist nicht Teil der Metadaten von Abhängigkeitseigenschaften und kann nicht in der abgeleiteten Klasse implementiert werden, indem Metadaten überschrieben werden.

    2. PropertyChangedCallback, welches in der abgeleiteten Klasse implementiert wird, indem die Metadaten von Abhängigkeitseigenschaften überschrieben werden. Dieser Rückruf verursacht eine Nullverweis-Ausnahme, wenn er eine Methode auf dem nicht initialisierten Klassenfeld s_temperatureLog aufruft.

    3. CoerceValueCallback, welches in der abgeleiteten Klasse implementiert wird, indem die Metadaten von Abhängigkeitseigenschaften überschrieben werden. Dieser Rückruf verursacht eine Nullverweis-Ausnahme, wenn er eine Methode auf dem nicht initialisierten Klassenfeld s_temperatureLog aufruft.

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

  3. Abgeleiteter Klassenparameterloser Konstruktor, der initialisiert s_temperatureLog.

  4. Abgeleiteter Klassenparameterkonstruktor, der einen neuen Abhängigkeitseigenschaftswert festlegt, der zu einem anderen Aufruf der SetValue Methode führt. Da s_temperatureLog jetzt initialisiert ist, werden Callbacks und Ereignisse ausgeführt, ohne dass Nullverweis-Ausnahmen verursacht werden.

Diese Initialisierungsprobleme können durch verwendung sicherer Konstruktormuster vermieden werden.

Sichere Konstruktormuster

Die im Testcode gezeigten abgeleiteten Klasseninitialisierungsprobleme können auf unterschiedliche Weise behoben werden, darunter:

  • Vermeiden Sie das Festlegen eines Abhängigkeitseigenschaftswerts in einem Konstruktor der benutzerdefinierten Abhängigkeitseigenschaftsklasse, wenn Ihre Klasse möglicherweise als Basisklasse verwendet wird. Wenn Sie einen Abhängigkeitseigenschaftswert initialisieren müssen, sollten Sie den erforderlichen Wert als Standardwert in Eigenschaftsmetadaten bei der Registrierung von Abhängigkeitseigenschaften oder beim Überschreiben von Metadaten festlegen.

  • Initialisieren abgeleiteter Klassenfelder vor deren Verwendung. Verwenden Sie z. B. eine der folgenden Ansätze:

    • Instanziieren und Zuweisen von Instanzfeldern in einer einzelnen Anweisung. Im vorherigen Beispiel würde die Anweisung List<int> s_temperatureLog = new(); eine verspätete Zuordnung vermeiden.

    • Führen Sie Zuweisungen im abgeleiteten statischen Klassenkonstruktor durch, der vor einem Basisklassenkonstruktor ausgeführt wird. Im vorherigen Beispiel würde das Platzieren der Zuordnungsanweisung s_temperatureLog = new List<int>(); in den abgeleiteten statischen Klassenkonstruktor eine verspätete Zuordnung vermeiden.

    • Verwenden Sie Lazy-Initialisierung und Instanziierung; dabei werden die Objekte dann initialisiert, wenn sie benötigt werden. Im vorherigen Beispiel könnte die verspätete Zuordnung vermieden werden, indem s_temperatureLog durch faule Initialisierung und Instanziierung instanziiert und zugewiesen wird. Weitere Informationen finden Sie unter Lazy Initialization.

  • Vermeiden Sie die Verwendung nicht initialisierter Klassenvariablen in WPF-Eigenschaftensystemrückrufen und -ereignissen.

Siehe auch