Delen via


Veilige constructorpatronen voor DependencyObjects

Er is een algemeen principe in het programmeren van beheerde code, vaak afgedwongen door hulpprogramma's voor codeanalyse, die klasseconstructors geen overschrijfbare methoden mogen aanroepen. Als een overschrijfbare methode wordt aangeroepen door een basisklasseconstructor en een afgeleide klasse deze methode overschrijft, kan de onderdrukkingsmethode in de afgeleide klasse worden uitgevoerd vóór de afgeleide klasseconstructor. Als de afgeleide klasseconstructor klasse-initialisatie uitvoert, heeft de afgeleide klassemethode mogelijk toegang tot niet-geïnitialiseerde klasseleden. Afhankelijkheidseigenschapsklassen moeten voorkomen dat afhankelijkheidseigenschapswaarden in een klasseconstructor worden ingesteld om runtime-initialisatieproblemen te voorkomen. In dit artikel wordt beschreven hoe u DependencyObject constructors implementeert op een manier die deze problemen voorkomt.

Virtuele methoden en callbacks van het eigenschappensysteem

Virtuele methoden en callbacks voor afhankelijkheden maken deel uit van het WPF-eigenschappensysteem (Windows Presentation Foundation) en breiden de veelzijdigheid van afhankelijkheidseigenschappen uit.

Een eenvoudige bewerking, zoals het instellen van een waarde van een afhankelijkheidseigenschap met behulp van SetValue, roept de OnPropertyChanged gebeurtenis en mogelijk verschillende terugbelacties van het WPF-eigenschappensysteem aan.

OnPropertyChanged is een voorbeeld van een virtuele WPF-eigenschapssysteemmethode die kan worden overschreven door klassen met DependencyObject in hun overnamehiërarchie. Als u een afhankelijkheidseigenschapswaarde instelt in een constructor die wordt aangeroepen tijdens het instantiëeren van de eigenschapsklasse voor aangepaste afhankelijkheden en een klasse die ervan is afgeleid, de OnPropertyChanged virtuele methode overschrijft, wordt de afgeleide klasse OnPropertyChanged methode uitgevoerd voordat een afgeleide klasseconstructor wordt uitgevoerd.

PropertyChangedCallback en CoerceValueCallback zijn voorbeelden van callbacks van WPF-eigenschappensystemen die kunnen worden geregistreerd door afhankelijke eigenschapsklassen en worden overschreven door klassen die daarvan afstammen. Als u een afhankelijkheidseigenschap gebruikt binnen de constructor van uw aangepaste afhankelijkheidsklasse, en een klasse die daarvan afgeleid is een van die callbacks in de metagegevens van een eigenschap overschrijft, wordt de callback van de afgeleide klasse uitgevoerd voordat enige constructor van de afgeleide klasse wordt aangeroepen. Dit probleem is niet relevant voor ValidateValueCallback omdat het geen deel uitmaakt van metagegevens van eigenschappen en alleen kan worden opgegeven door de registratieklasse.

Zie callbacks van afhankelijkheidseigenschappen en validatievoor meer informatie over callbacks van afhankelijkheidseigenschappen.

.NET Analyzers

.NET Compiler-platformanalyses inspecteren uw C# of Visual Basic-code voor problemen met codekwaliteit en -stijl. Als u overschrijfbare methoden aanroept in een constructor wanneer de analyseregel CA2214 actief is, krijgt u de waarschuwing CA2214: Don't call overridable methods in constructors. Maar de regel markeert geen virtuele methoden en callbacks die worden aangeroepen door het onderliggende WPF-eigenschappensysteem wanneer een waarde van een afhankelijkheidseigenschap is ingesteld in een constructor.

Problemen veroorzaakt door afgeleide klassen

Als u afdicht de eigenschapsklasse van uw aangepaste afhankelijkheid, of als u op een andere manier weet dat uw klasse niet wordt afgeleid, zijn er problemen met initialisatie van afgeleide klasseruntimes niet van toepassing op die klasse. Maar als u een eigenschapsklasse voor afhankelijkheden maakt die kan worden overgenomen, bijvoorbeeld als u sjablonen of een uitbreidbare besturingsbibliotheekset maakt, vermijdt u het aanroepen van overschrijfbare methoden of het instellen van afhankelijkheidseigenschapswaarden van een constructor.

De volgende testcode demonstreert een onveilig constructorpatroon, waarbij een basisklasseconstructor een waarde voor afhankelijkheidseigenschappen instelt, waardoor aanroepen naar virtuele methoden en callbacks worden geactiveerd.

    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

De volgorde waarin methoden worden aangeroepen in de onveilige constructorpatroontest is:

  1. De statische constructor van de afgeleide klasse die de metadata van de afhankelijkheidseigenschap van Aquarium overschrijft om PropertyChangedCallback en CoerceValueCallbackte registreren.

  2. Basisklasseconstructor, waarmee een nieuwe afhankelijkheidseigenschapswaarde wordt ingesteld, wat resulteert in een aanroep naar de SetValue methode. De SetValue aanroep activeert callbacks en gebeurtenissen in de volgende volgorde:

    1. ValidateValueCallback, die wordt geïmplementeerd in de basisklasse. Deze callback maakt geen deel uit van metagegevens van afhankelijkheidseigenschappen en kan niet worden geïmplementeerd in de afgeleide klasse door metagegevens te overschrijven.

    2. PropertyChangedCallback, die in de afgeleide klasse wordt geïmplementeerd door metagegevens van afhankelijke eigenschappen te overschrijven. Deze callback veroorzaakt een null-referentie-exceptie wanneer er een methode wordt aangeroepen op het niet-geïnitialiseerde klasseveld s_temperatureLog.

    3. CoerceValueCallback, die in de afgeleide klasse wordt geïmplementeerd door metagegevens van afhankelijke eigenschappen te overschrijven. Deze callback veroorzaakt een null-referentie-exceptie wanneer er een methode wordt aangeroepen op het niet-geïnitialiseerde klasseveld s_temperatureLog.

    4. OnPropertyChanged gebeurtenis, die in de afgeleide klasse wordt geïmplementeerd door de virtuele methode te overschrijven. Deze gebeurtenis veroorzaakt een null-referentie-exceptie wanneer een methode wordt aangeroepen op het niet-geïnitialiseerde veld van de klasse s_temperatureLog.

  3. De parameterloze constructor van de afgeleide klasse, die s_temperatureLoginitialiseert.

  4. Afgeleide klasseparameterconstructor, waarmee een nieuwe eigenschapswaarde voor afhankelijkheden wordt ingesteld, wat resulteert in een andere aanroep van de SetValue methode. Aangezien s_temperatureLog nu is geïnitialiseerd, worden callbacks en gebeurtenissen uitgevoerd zonder dat er null-verwijzingsuitzonderingen optreden.

Deze initialisatieproblemen kunnen worden vermeden door gebruik te maken van veilige constructorpatronen.

Veilige bouwerpatronen

De problemen met initialisatie van afgeleide klassen die in de testcode worden gedemonstreerd, kunnen op verschillende manieren worden opgelost, waaronder:

  • Vermijd het instellen van een afhankelijkheidseigenschapswaarde in een constructor van uw aangepaste afhankelijkheidseigenschapsklasse als uw klasse kan worden gebruikt als basisklasse. Als u een afhankelijkheidseigenschapswaarde wilt initialiseren, kunt u overwegen de vereiste waarde in te stellen als de standaardwaarde in metagegevens van eigenschappen tijdens de registratie van afhankelijkheidseigenschappen of bij het overschrijven van metagegevens.

  • Initialiseer afgeleide klassevelden voordat ze worden gebruikt. U kunt bijvoorbeeld een van deze methoden gebruiken:

    • Instantievelden instantiëren en toewijzen in één instructie. In het vorige voorbeeld zou de instructie List<int> s_temperatureLog = new(); een late toewijzing voorkomen.

    • Voer de toewijzing uit in de statische constructor van de afgeleide klasse, die voorafgaat aan een basisklasseconstructor. In het vorige voorbeeld zou het plaatsen van de toewijzingsinstructie s_temperatureLog = new List<int>(); in de statische constructor van de afgeleide klasse voorkomen dat de toewijzing te laat wordt toegewezen.

    • Gebruik luie initialisatie en instantiëring, waarmee objecten worden geïnitialiseerd als en wanneer ze nodig zijn. In het vorige voorbeeld zou het instantiëren en toewijzen van s_temperatureLog met behulp van luie initialisatie en instantiëring late toewijzing voorkomen. Voor meer informatie, zie luie initialisatie.

  • Vermijd het gebruik van niet-geïnitialiseerde klassevariabelen in callbacks en gebeurtenissen van het WPF-eigenschappensysteem.

Zie ook