Megosztás a következőn keresztül:


Biztonságos konstruktorminták a DependencyObject-ekhez

A felügyelt kódprogramozásban van egy általános alapelv, amelyet gyakran kódelemzési eszközök kényszerítenek ki, az osztálykonstruktoroknak nem szabad felülírható metódusokat hívniuk. Ha egy felülbírálható metódust egy alaposztály-konstruktor hív meg, és egy származtatott osztály felülbírálja ezt a metódust, akkor a származtatott osztály felülbírálási metódusa futtatható a származtatott osztálykonstruktor előtt. Ha a származtatott osztálykonstruktor osztályinicializálást hajt végre, akkor a származtatott osztálymetódus hozzáférhet az nem inicializált osztálytagokhoz. A függőségi tulajdonságosztályok ne állítsanak be függőségi tulajdonságértékeket egy osztálykonstruktorban a futtatókörnyezet inicializálási problémáinak elkerülése érdekében. Ez a cikk bemutatja, hogyan implementálhat DependencyObject konstruktorokat oly módon, hogy elkerülje ezeket a problémákat.

Tulajdonságrendszerbeli virtuális metódusok és visszahívások

A függőségi tulajdonság virtuális módszerei és visszahívásai a Windows Presentation Foundation (WPF) tulajdonságrendszer részét képezik, és kibővítik a függőségi tulajdonságok sokoldalúságát.

Egy alapművelet, mint például egy függőségi tulajdonság értékének beállítása a SetValue segítségével, meghívja a OnPropertyChanged eseményt, és potenciálisan több WPF tulajdonságrendszer visszahívást is.

OnPropertyChanged Egy példa egy WPF tulajdonságrendszer virtuális metódusára, amelyet felül lehet bírálni az öröklési hierarchiában lévő DependencyObject osztályok. Ha olyan függőségi tulajdonságértéket állít be egy konstruktorban, amelyet az egyéni függőségi tulajdonságosztály példányosítása során hív meg, és az abból származtatott osztály felülírja a OnPropertyChanged virtuális metódust, akkor a származtatott osztálymetódus OnPropertyChanged minden származtatott osztálykonstruktor előtt fut.

PropertyChangedCallback és CoerceValueCallback példák a WPF tulajdonságrendszer-visszahívásokra, amelyeket a függőségi tulajdonságot definiáló osztályok regisztrálhatnak, és amelyeket a belőlük származó osztályok felülbírálhatják. Ha beállít egy függőségi tulajdonságértéket az egyéni függőségi tulajdonságosztály konstruktorában, és az abból származtatott osztály felülbírálja a tulajdonság metaadatainak egyik visszahívását, akkor a származtatott osztályvisszahívás minden származtatott osztálykonstruktor előtt lefut. Ez a probléma nem releváns ValidateValueCallback számára, mivel nem része a tulajdonság metaadatainak, és csak a regisztráló osztály adhatja meg.

A függőségi tulajdonság visszahívásáról további információt a Függőség tulajdonság visszahívásai és érvényesítése című témakörben talál.

.NET-elemzők

A .NET fordítóplatform-elemzői a kódminőséggel és a stílussal kapcsolatos problémák esetén ellenőrzik a C# vagy a Visual Basic-kódot. Ha egy konstruktorban felülírható metódusokat hív meg, amikor a CA2214 elemzőszabály aktív, a figyelmeztetést CA2214: Don't call overridable methods in constructorsfogja kapni. A szabály azonban nem jelöli meg a mögöttes WPF-tulajdonságrendszer által meghívott virtuális metódusokat és visszahívásokat, ha egy függőségi tulajdonság értéke konstruktorban van beállítva.

Származtatott osztályok által okozott problémák

Ha lezárja az egyéni függőségi tulajdonságosztályt, vagy más módon tudja, hogy az osztály nem származik belőle, akkor a származtatott osztály futásidejű inicializálási problémái nem vonatkoznak az adott osztályra. Ha azonban örökölhető függőségi tulajdonságosztályt hoz létre, például sablonokat vagy bővíthető vezérlőtárkészletet hoz létre, kerülje a felülírható metódusok meghívását vagy a függőségi tulajdonságértékek konstruktortól való beállítását.

Az alábbi tesztkód egy nem biztonságos konstruktormintát mutat be, amelyben egy alaposztály-konstruktor beállít egy függőségi tulajdonságértéket, így hívásokat indít el virtuális metódusokhoz és visszahívásokhoz.

    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

A metódusok meghívásának sorrendje a nem biztonságos konstruktorminta-tesztben a következő:

  1. Származtatott osztály statikus konstruktora, amely felülbírálja a Aquarium függőségi tulajdonság metadatát a PropertyChangedCallback és CoerceValueCallback regisztrálására.

  2. Alaposztály-konstruktor, amely beállít egy új függőségi tulajdonságértéket, amely meghívja a SetValue metódust. A SetValue hívás a következő sorrendben indítja el a visszahívásokat és eseményeket:

    1. ValidateValueCallback, amely az alaposztályban van implementálva. Ez a visszahívás nem része a függőségi tulajdonság metaadatainak, és a metaadatok felülírásával nem implementálható a származtatott osztályban.

    2. PropertyChangedCallback, amelyet a származtatott osztályban a függőségi tulajdonság metaadatainak felülírásával valósít meg. Ez a visszahívás nullhivatkozási kivételt okoz, amikor metódust hív meg a nem inicializált osztálymezőben s_temperatureLog.

    3. CoerceValueCallback, amelyet a származtatott osztályban a függőségi tulajdonság metaadatainak felülírásával valósít meg. Ez a visszahívás nullhivatkozási kivételt okoz, amikor metódust hív meg a nem inicializált osztálymezőben s_temperatureLog.

    4. OnPropertyChanged eseményt, amely a származtatott osztályban a virtuális metódus felülírásával implementálható. Ez az esemény nullhivatkozási kivételt okoz, amikor metódust hív meg a nem inicializált osztálymezőben s_temperatureLog.

  3. Származtatott osztály paraméter nélküli konstruktor, amely inicializálja s_temperatureLog.

  4. Származtatott osztályparaméter-konstruktor, amely beállít egy új függőségi tulajdonságértéket, amely a metódus újabb hívását SetValue eredményezi. Mivel s_temperatureLog most inicializálva van, a visszahívások és az események nullhivatkozási kivételek nélkül fognak futni.

Ezek az inicializálási problémák biztonságos konstruktorminták használatával elkerülhetők.

Biztonságos konstruktorminták

A tesztkódban bemutatott származtatott osztály inicializálási problémái különböző módon oldhatók meg, például:

  • Ne adjon meg függőségi tulajdonságértéket az egyéni függőségi tulajdonságosztály konstruktorában, ha az osztály alaposztályként használható. Ha inicializálnia kell egy függőségi tulajdonságértéket, fontolja meg a szükséges érték alapértelmezett értékként való beállítását a tulajdonság metaadataiban a függőségi tulajdonság regisztrálása vagy a metaadatok felülírása során.

  • Származtatott osztálymezők inicializálása használat előtt. Például az alábbi módszerek bármelyikét használhatja:

    • A példánymezők létrehozása és hozzárendelése egyetlen utasításban. Az előző példában az utasítás List<int> s_temperatureLog = new(); elkerülné a késői hozzárendelést.

    • Végezze el a hozzárendelést a származtatott osztály statikus konstruktorában, amely minden alaposztály-konstruktor előtt fut. Az előző példában a hozzárendelési utasítás s_temperatureLog = new List<int>(); származtatott osztály statikus konstruktorba helyezése elkerülné a késői hozzárendelést.

    • Használjon késleltetett inicializálást és példányosítást, amely az objektumokat csak akkor inicializálja, amikor valóban szükség van rájuk. Az előző példában a s_temperatureLog lusta inicializálással való példányosítása és hozzárendelése elkerülheti a késői hozzárendelést. További információ: Lazy inicializálás.

  • Ne használjon nem inicializált osztályváltozókat a WPF tulajdonságrendszerbeli visszahívásokban és eseményekben.

Lásd még