Udostępnij za pośrednictwem


Bezpieczne wzorce konstruktorów dla obiektów DependencyObjects (WPF .NET)

Istnieje ogólna zasada programowania kodu zarządzanego, często wymuszana przez narzędzia do analizy kodu, która konstruktory klas nie powinny wywoływać metod zastępowalnych. Jeśli przesłonięta metoda jest wywoływana przez konstruktor klasy bazowej, a klasa pochodna zastępuje tę metodę, przesłonięcia metody w klasie pochodnej mogą być uruchamiane przed konstruktorem klasy pochodnej. Jeśli konstruktor klasy pochodnej wykonuje inicjowanie klasy, metoda klasy pochodnej może uzyskać dostęp do niezainicjowanych składowych klasy. Klasy właściwości zależności powinny unikać ustawiania wartości właściwości zależności w konstruktorze klasy, aby uniknąć problemów z inicjowaniem środowiska uruchomieniowego. W tym artykule opisano sposób implementowania DependencyObject konstruktorów w sposób, który pozwala uniknąć tych problemów.

Metody wirtualne i wywołania zwrotne systemu właściwości

Metody wirtualne właściwości zależności i wywołania zwrotne są częścią systemu właściwości Windows Presentation Foundation (WPF) i rozszerzają wszechstronność właściwości zależności.

Podstawowa operacja, na przykład ustawienie wartości właściwości zależności przy użyciu metody SetValue , spowoduje wywołanie OnPropertyChanged zdarzenia i potencjalnie kilka wywołań zwrotnych systemu właściwości WPF.

OnPropertyChanged to przykład metody wirtualnej systemu właściwości WPF, która może zostać zastąpiona przez klasy, które mają DependencyObject w hierarchii dziedziczenia. Jeśli ustawisz wartość właściwości zależności w konstruktorze, który jest wywoływany podczas tworzenia wystąpienia niestandardowej klasy właściwości zależności, a klasa pochodząca z niej zastępuje OnPropertyChanged metodę wirtualną, metoda klasy OnPropertyChanged pochodnej zostanie uruchomiona przed dowolnym konstruktorem klasy pochodnej.

PropertyChangedCallbackCoerceValueCallback i są przykładami wywołań zwrotnych systemu właściwości WPF, które mogą być zarejestrowane przez klasy właściwości zależności i przesłaniane przez klasy, które pochodzą z nich. Jeśli ustawisz wartość właściwości zależności w konstruktorze niestandardowej klasy właściwości zależności, a klasa, która pochodzi z niej zastąpi jedną z tych wywołań zwrotnych w metadanych właściwości, wywołanie zwrotne klasy pochodnej zostanie uruchomione przed dowolnym konstruktorem klasy pochodnej. Ten problem nie jest istotny, ValidateValueCallback ponieważ nie jest częścią metadanych właściwości i można go określić tylko przez klasę rejestrującą.

Aby uzyskać więcej informacji na temat wywołań zwrotnych właściwości zależności, zobacz Wywołania zwrotne właściwości zależności i walidacja.

Analizatory .NET

Analizatory platformy kompilatora .NET sprawdzają kod C# lub Visual Basic pod kątem problemów z jakością kodu i stylem. Jeśli wywołasz metody zastępowalne w konstruktorze, gdy reguła analizatora CA2214 jest aktywna, zostanie wyświetlone ostrzeżenie CA2214: Don't call overridable methods in constructors. Jednak reguła nie będzie flagował metod wirtualnych i wywołań zwrotnych wywoływanych przez bazowy system właściwości WPF, gdy wartość właściwości zależności jest ustawiona w konstruktorze.

Problemy spowodowane przez klasy pochodne

Jeśli przypieczętujesz niestandardową klasę właściwości zależności lub w inny sposób wiesz, że klasa nie będzie pochodzić z klasy, problemy z inicjowaniem środowiska uruchomieniowego klasy pochodnej nie mają zastosowania do tej klasy. Jeśli jednak tworzysz klasę właściwości zależności, która jest dziedziczona, na przykład jeśli tworzysz szablony lub rozszerzalny zestaw biblioteki kontrolek, unikaj wywoływania metod zastępowalnych lub ustawiania wartości właściwości zależności z konstruktora.

Poniższy kod testowy demonstruje niebezpieczny wzorzec konstruktora, w którym konstruktor klasy bazowej ustawia wartość właściwości zależności, wyzwalając wywołania metod wirtualnych i wywołań zwrotnych.

    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

Kolejność wywoływana metod w niebezpiecznym teście wzorca konstruktora to:

  1. Konstruktor statyczny klasy pochodnej, który zastępuje metadane właściwości zależności funkcji do rejestrowania Aquarium PropertyChangedCallback i CoerceValueCallback.

  2. Konstruktor klasy bazowej, który ustawia nową wartość właściwości zależności, co powoduje wywołanie SetValue metody . Wywołanie SetValue wyzwala wywołanie zwrotne i zdarzenia w następującej kolejności:

    1. ValidateValueCallback, który jest implementowany w klasie bazowej. To wywołanie zwrotne nie jest częścią metadanych właściwości zależności i nie można ich zaimplementować w klasie pochodnej przez zastąpienie metadanych.

    2. PropertyChangedCallback, który jest implementowany w klasie pochodnej przez zastąpienie metadanych właściwości zależności. To wywołanie zwrotne powoduje wyjątek odwołania o wartości null, gdy wywołuje metodę w niezainicjowanym polu s_temperatureLogklasy .

    3. CoerceValueCallback, który jest implementowany w klasie pochodnej przez zastąpienie metadanych właściwości zależności. To wywołanie zwrotne powoduje wyjątek odwołania o wartości null, gdy wywołuje metodę w niezainicjowanym polu s_temperatureLogklasy .

    4. OnPropertyChanged zdarzenie implementowane w klasie pochodnej przez zastąpienie metody wirtualnej. To zdarzenie powoduje wyjątek odwołania o wartości null, gdy wywołuje metodę w niezainicjowanym polu s_temperatureLogklasy .

  3. Konstruktor bez parametrów klasy pochodnej, który inicjuje s_temperatureLog.

  4. Konstruktor parametru klasy pochodnej, który ustawia nową wartość właściwości zależności, co powoduje inne wywołanie SetValue metody . Ponieważ s_temperatureLog jest teraz inicjowane, wywołania zwrotne i zdarzenia będą uruchamiane bez powodowania wyjątków odwołania o wartości null.

Te problemy z inicjowaniem można uniknąć dzięki użyciu bezpiecznych wzorców konstruktorów.

Bezpieczne wzorce konstruktorów

Problemy z inicjowaniem klasy pochodnej przedstawione w kodzie testowym można rozwiązać na różne sposoby, w tym:

  • Unikaj ustawiania wartości właściwości zależności w konstruktorze niestandardowej klasy właściwości zależności, jeśli klasa może być używana jako klasa bazowa. Jeśli musisz zainicjować wartość właściwości zależności, rozważ ustawienie wymaganej wartości jako wartości domyślnej w metadanych właściwości podczas rejestracji właściwości zależności lub podczas zastępowania metadanych.

  • Zainicjuj pola klas pochodnych przed ich użyciem. Na przykład przy użyciu dowolnego z następujących podejść:

    • Utwórz wystąpienie i przypisz pola wystąpienia w jednej instrukcji. W poprzednim przykładzie instrukcja List<int> s_temperatureLog = new(); unikała późnego przypisania.

    • Wykonaj przypisanie w konstruktorze statycznym klasy pochodnej, który jest uruchamiany przed dowolnym konstruktorem klasy bazowej. W poprzednim przykładzie umieszczenie instrukcji s_temperatureLog = new List<int>(); przypisania w konstruktorze statycznym klasy pochodnej pozwoli uniknąć opóźnionego przypisania.

    • Użyj inicjowania z opóźnieniem i utworzenia wystąpienia, co inicjuje obiekty jako i wtedy, gdy są potrzebne. W poprzednim przykładzie utworzenie wystąpienia i przypisanie przy użyciu leniwej inicjalizacji i utworzenia wystąpienia pozwoli uniknąć późnego przypisania s_temperatureLog . Aby uzyskać więcej informacji, zobacz Inicjowanie z opóźnieniem.

  • Unikaj używania niezainicjowanych zmiennych klasy w wywołaniach zwrotnych i zdarzeniach systemu właściwości WPF.

Zobacz też