Vzory bezpečného konstruktoru pro DependencyObjects (WPF .NET)

Ve spravovaném programování kódu existuje obecný princip, který často vynucuje nástroje pro analýzu kódu, že konstruktory tříd by neměly volat přepisovatelné metody. Pokud je přepisovatelná metoda volána konstruktorem základní třídy a odvozená třída přepíše tuto metodu, pak přepsání metody v odvozené třídě lze spustit před konstruktor odvozené třídy. Pokud konstruktor odvozené třídy provádí inicializaci třídy, pak odvozená třída metoda může přistupovat k neinicializovaným členům třídy. Třídy vlastností závislostí by se měly vyhnout nastavení hodnot vlastností závislostí v konstruktoru třídy, aby nedocházelo k problémům s inicializací modulu runtime. Tento článek popisuje, jak implementovat DependencyObject konstruktory způsobem, který zabraňuje těmto problémům.

Virtuální metody a zpětná volání systému vlastností

Virtuální metody a zpětná volání závislostí jsou součástí systému vlastností WINDOWS Presentation Foundation (WPF) a rozšiřují všestrannost vlastností závislostí.

Základní operace, jako je nastavení hodnoty vlastnosti závislosti pomocí, SetValue vyvolá OnPropertyChanged událost a potenciálně několik zpětných volání systému vlastností WPF.

OnPropertyChanged je příkladem virtuální metody systému vlastností WPF, která může být přepsána třídami, které mají DependencyObject v hierarchii dědičnosti. Pokud nastavíte hodnotu vlastnosti závislosti v konstruktoru, který je volána během vytváření instance vlastní třídy závislostí třídy a třída odvozená z ní přepíše OnPropertyChanged virtuální metodu, pak odvozená metoda třídy OnPropertyChanged se spustí před jakýmkoli konstruktorem odvozené třídy.

PropertyChangedCallback a CoerceValueCallback jsou příklady zpětného volání systému vlastností WPF, které mohou být registrovány pomocí tříd závislostí a přepsány třídami, které jsou odvozeny z nich. Pokud nastavíte hodnotu vlastnosti závislosti v konstruktoru vlastní třídy vlastnosti závislosti a třída, která je odvozena z ní, přepíše jeden z těchto zpětných volání v metadatech vlastností, pak odvozené zpětné volání třídy se spustí před libovolným konstruktorem odvozené třídy. Tento problém není relevantní, ValidateValueCallback protože není součástí metadat vlastností a je možné ho zadat pouze registrací třídy.

Další informace o zpětných voláních vlastností závislostí naleznete v tématu Zpětné volání vlastností závislostí a ověřování.

Analyzátory .NET

Analyzátory platformy kompilátoru .NET kontrolují problémy s kvalitou a stylem kódu jazyka C# nebo Visual Basic. Pokud voláte přepisovatelné metody v konstruktoru, když je pravidlo analyzátoru CA2214 aktivní, zobrazí se upozornění CA2214: Don't call overridable methods in constructors. Pravidlo ale nebude označovat virtuální metody a zpětná volání, která jsou vyvolána základním systémem vlastností WPF, když je hodnota vlastnosti závislosti nastavena v konstruktoru.

Problémy způsobené odvozenými třídami

Pokud zapečetíte vlastní třídu vlastností závislostí nebo jinak víte, že vaše třída nebude odvozena, pak se na tuto třídu nevztahují problémy inicializace modulu runtime odvozené třídy. Pokud ale vytvoříte třídu vlastností závislostí, která je zděděná, například pokud vytváříte šablony nebo sadu rozšiřitelné knihovny ovládacích prvků, vyhněte se volání přepisovatelných metod nebo nastavení hodnot vlastností závislostí z konstruktoru.

Následující testovací kód ukazuje nebezpečný vzor konstruktoru, kde konstruktor základní třídy nastaví hodnotu vlastnosti závislosti, čímž aktivuje volání virtuálních metod a zpětné volání.

    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

Pořadí, ve kterém se metody volají v nebezpečném testu vzoru konstruktoru, je:

  1. Odvozený statický konstruktor třídy, který přepisuje metadata Aquarium vlastnosti závislosti pro registraci PropertyChangedCallback a CoerceValueCallback.

  2. Konstruktor základní třídy, který nastaví novou hodnotu vlastnosti závislosti, která vede k SetValue volání metody. Volání SetValue aktivuje zpětná volání a události v následujícím pořadí:

    1. ValidateValueCallback, která je implementována v základní třídě. Toto zpětné volání není součástí metadat vlastností závislostí a nelze je implementovat do odvozené třídy přepsáním metadat.

    2. PropertyChangedCallback, která je implementována v odvozené třídě přepsáním metadat vlastností závislostí. Toto zpětné volání způsobí výjimku nulového odkazu při volání metody v poli s_temperatureLogneinicializované třídy .

    3. CoerceValueCallback, která je implementována v odvozené třídě přepsáním metadat vlastností závislostí. Toto zpětné volání způsobí výjimku nulového odkazu při volání metody v poli s_temperatureLogneinicializované třídy .

    4. OnPropertyChanged událost, která je implementována v odvozené třídě přepsáním virtuální metody. Tato událost způsobí výjimku nulového odkazu, když volá metodu v neinicializovaném poli s_temperatureLogtřídy .

  3. Odvozený konstruktor bez parametrů třídy, který inicializuje s_temperatureLog.

  4. Konstruktor parametrů odvozené třídy, který nastaví novou hodnotu vlastnosti závislosti, což vede k jinému SetValue volání metody. Vzhledem k tomu s_temperatureLog , že je nyní inicializována, budou zpětná volání a události spuštěny, aniž by to způsobilo výjimky odkazu na hodnotu null.

Tyto problémy s inicializací se dají vyhnout použitím vzorů bezpečného konstruktoru.

Vzory bezpečného konstruktoru

Problémy inicializace odvozené třídy demonstrované v testovacím kódu je možné vyřešit různými způsoby, mezi které patří:

  • Vyhněte se nastavení hodnoty vlastnosti závislosti v konstruktoru vlastní třídy vlastností závislostí, pokud vaše třída může být použita jako základní třída. Pokud potřebujete inicializovat hodnotu vlastnosti závislosti, zvažte nastavení požadované hodnoty jako výchozí hodnoty v metadatech vlastností během registrace vlastnosti závislosti nebo při přepsání metadat.

  • Inicializovat odvozená pole třídy před jejich použitím. Například použití některého z těchto přístupů:

    • Vytvořte instanci a přiřaďte pole instance v jednom příkazu. V předchozím příkladu by se příkaz List<int> s_temperatureLog = new(); vyhnul pozdnímu přiřazení.

    • Proveďte přiřazení v statickém konstruktoru odvozené třídy, který běží před libovolným konstruktorem základní třídy. V předchozím příkladu by vložení příkazu s_temperatureLog = new List<int>(); přiřazení do statického konstruktoru odvozené třídy zabránilo pozdnímu přiřazení.

    • Použijte opožděnou inicializaci a instanci, která inicializuje objekty jako a kdy jsou potřeba. V předchozím příkladu by vytvoření instance a přiřazení s_temperatureLog pomocí opožděné inicializace a vytváření instancí zabránilo pozdnímu přiřazení. Další informace naleznete v tématu Opožděná inicializace.

  • Nepoužívejte neinicializované proměnné třídy v zpětných voláních a událostech systému vlastností WPF.

Viz také