Dela via


Säkra konstruktormönster för DependencyObjects (WPF .NET)

Det finns en allmän princip i hanterad kodprogrammering, som ofta framtvingas av kodanalysverktyg, att klasskonstruktorer inte ska anropa åsidosättbara metoder. Om en åsidosättningsbar metod anropas av en basklasskonstruktor och en härledd klass åsidosätter den metoden, kan åsidosättningsmetoden i den härledda klassen köras före konstruktorn för den härledda klassen. Om konstruktorn för den härledda klassen utför klassinitiering kan metoden härledd klass komma åt onitialiserade klassmedlemmar. Beroendeegenskapsklasser bör undvika att ange värden för beroendeegenskaper i en klasskonstruktor för att undvika problem med körningsinitiering. I den här artikeln beskrivs hur du implementerar DependencyObject konstruktorer på ett sätt som undviker dessa problem.

Egenskapssystemets virtuella metoder och återanrop

Virtuella metoder för beroendeegenskap och återanrop ingår i WPF-egenskapssystemet (Windows Presentation Foundation) och utökar beroendeegenskapernas mångsidighet.

En grundläggande åtgärd som att ange ett beroendeegenskapsvärde med hjälp av SetValue anropar händelsen OnPropertyChanged och eventuellt flera WPF-egenskapssystemåteranrop.

OnPropertyChanged är ett exempel på en virtuell WPF-egenskapssystemmetod som kan åsidosättas av klasser som har DependencyObject i arvshierarkin. Om du anger ett beroendeegenskapsvärde i en konstruktor som anropas under instansiering av din anpassade beroendeegenskapsklass och en klass som härleds från den åsidosätter den OnPropertyChanged virtuella metoden, körs metoden härledd klass OnPropertyChanged före någon härledd klasskonstruktor.

PropertyChangedCallback och CoerceValueCallback är exempel på wpf-egenskapssystemåteranrop som kan registreras av beroendeegenskapsklasser och åsidosättas av klasser som härleds från dem. Om du anger ett beroendeegenskapsvärde i konstruktorn för din anpassade beroendeegenskapsklass, och en klass som härleds från den åsidosätter en av dessa återanrop i egenskapsmetadata, körs återanropet för den härledda klassen före någon härledd klasskonstruktor. Det här problemet är inte relevant för ValidateValueCallback eftersom det inte ingår i egenskapsmetadata och endast kan anges av registreringsklassen.

Mer information om återanrop och verifiering av beroendeegenskap finns i .

.NET-analysverktyg

.NET-kompilatorplattformsanalyserare inspekterar din C#- eller Visual Basic-kod för problem med kodkvalitet och format. Om du anropar åsidosättande metoder i en konstruktor när analysregeln CA2214 är aktiv får du varningen CA2214: Don't call overridable methods in constructors. Men regeln flaggar inte virtuella metoder och återanrop som anropas av det underliggande WPF-egenskapssystemet när ett beroendeegenskapsvärde anges i en konstruktor.

Problem som orsakas av härledda klasser

Om du förseglar din anpassade beroendeegenskapklass, eller på annat sätt vet att din klass inte kommer att härledas ifrån, så gäller inte initieringsproblem som uppstår vid körning för härledda klasser för den klassen. Men om du skapar en beroendeegenskapsklass som kan ärva, till exempel om du skapar mallar eller en expanderbar kontrollbiblioteksuppsättning, bör du undvika att anropa åsidosättbara metoder eller ange värden för beroendeegenskap från en konstruktor.

Följande testkod visar ett osäkert konstruktormönster, där en basklasskonstruktor anger ett beroendeegenskapsvärde vilket utlöser anrop till virtuella metoder och återanrop.

    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

I vilken ordning metoderna anropas i ett test av det osäkra konstruktormönstret är:

  1. Den statiska konstruktorn för den härledda klassen, som ersätter metadata för beroendeegenskaper för Aquarium för att registrera PropertyChangedCallback och CoerceValueCallback.

  2. Basklasskonstruktor, som anger ett nytt beroendeegenskapsvärde som resulterar i ett anrop till metoden SetValue. Det SetValue samtalet utlöser återanrop och händelser i följande ordning:

    1. ValidateValueCallback, som implementeras i basklassen. Det här återanropet är inte en del av beroendeegenskapsmetadata och kan inte implementeras i den härledda klassen genom att åsidosätta metadata.

    2. PropertyChangedCallback, som implementeras i den härledda klassen genom att åsidosätta metadata för beroendeegenskap. Det här återanropet orsakar ett null-referensundantag när det anropar en metod i det uninitialiserade klassfältet s_temperatureLog.

    3. CoerceValueCallback, som implementeras i den härledda klassen genom att åsidosätta metadata för beroendeegenskap. Det här återanropet orsakar ett null-referensundantag när det anropar en metod i det uninitialiserade klassfältet s_temperatureLog.

    4. OnPropertyChanged händelse som implementeras i den härledda klassen genom att åsidosätta den virtuella metoden. Den här händelsen orsakar ett null-referensfel när den anropar en metod i det uninitialiserade klassfältet s_temperatureLog.

  3. Parameterlös konstruktor för härledd klass, som initierar s_temperatureLog.

  4. Härledd klassparameterkonstruktor, som anger ett nytt beroendeegenskapsvärde som resulterar i ett annat anrop till metoden SetValue. Eftersom s_temperatureLog nu har initierats körs återanrop och händelser utan att orsaka null-referensundantag.

Dessa initieringsproblem kan undvikas med hjälp av säkra konstruktormönster.

Säkra konstruktormönster

De problem med initiering av härledda klasser som visas i testkoden kan lösas på olika sätt, inklusive:

  • Undvik att ange ett beroendeegenskapsvärde i en konstruktor för din anpassade beroendeegenskapsklass om klassen kan användas som basklass. Om du behöver initiera ett beroendeegenskapsvärde bör du överväga att ange det nödvändiga värdet som standardvärde i egenskapsmetadata under registrering av beroendeegenskap eller när du åsidosättar metadata.

  • Initiera härledda klassfält innan de används. Du kan till exempel använda någon av följande metoder:

    • Instansiera och tilldela instansfält i en enda instruktion. I föregående exempel skulle instruktionen List<int> s_temperatureLog = new(); undvika sen tilldelning.

    • Utför tilldelning i den statiska konstruktorn för den härledda klassen, som körs före alla basklasskonstruktorer. I föregående exempel skulle sena tilldelningar undvikas om tilldelningssatsen s_temperatureLog = new List<int>(); placeras i den statiska konstruktorn för den härledda klassen.

    • Använd fördröjd initiering och instansiering, vilket initierar objekt allteftersom de behövs. I föregående exempel skulle instansiering och tilldelning av s_temperatureLog med hjälp av lat initiering och instansiering undvika sen tilldelning. För mer information, se lat initiering.

  • Undvik att använda uninitialiserade klassvariabler i WPF-egenskapssystemets återanrop och händelser.

Se även