Partager via


Modèles de constructeur sécurisés pour DependencyObjects (WPF .NET)

Il existe un principe général dans la programmation de code managé, souvent appliqué par les outils d’analyse du code, que les constructeurs de classe ne doivent pas appeler des méthodes substituables. Si une méthode substituable est appelée par un constructeur de classe de base et qu’une classe dérivée remplace cette méthode, la méthode override de la classe dérivée peut s’exécuter avant le constructeur de classe dérivée. Si le constructeur de classe dérivée effectue une initialisation de classe, la méthode de classe dérivée peut accéder aux membres de classe non initialisés. Les classes de propriétés de dépendance doivent éviter de définir des valeurs de propriété de dépendance dans un constructeur de classe pour éviter les problèmes d’initialisation du runtime. Cet article explique comment implémenter DependencyObject des constructeurs d’une manière qui évite ces problèmes.

Méthodes virtuelles et rappels du système de propriétés

Les méthodes virtuelles de propriété de dépendance et les rappels font partie du système de propriétés WPF (Windows Presentation Foundation) et développent la polyvalence des propriétés de dépendance.

Une opération de base telle que la définition d’une valeur de propriété de dépendance à l’aide SetValue de l’événement appelle l’événement OnPropertyChanged et peut-être plusieurs rappels de système de propriétés WPF.

OnPropertyChanged est un exemple de méthode virtuelle de système de propriétés WPF qui peut être substituée par des classes qui ont DependencyObject dans leur hiérarchie d’héritage. Si vous définissez une valeur de propriété de dépendance dans un constructeur appelé lors de l’instanciation de votre classe de propriété de dépendance personnalisée et qu’une classe dérivée de celle-ci remplace la OnPropertyChanged méthode virtuelle, la méthode de classe dérivée s’exécute avant tout constructeur de classe OnPropertyChanged dérivé.

PropertyChangedCallback et CoerceValueCallback sont des exemples de rappels de système de propriétés WPF qui peuvent être inscrits par des classes de propriétés de dépendance et substitués par des classes qui dérivent de ces derniers. Si vous définissez une valeur de propriété de dépendance dans le constructeur de votre classe de propriété de dépendance personnalisée et une classe qui dérive de celle-ci remplace l’un de ces rappels dans les métadonnées de propriété, le rappel de classe dérivé s’exécute avant tout constructeur de classe dérivé. Ce problème n’est pas pertinent car ValidateValueCallback il ne fait pas partie des métadonnées de propriété et ne peut être spécifié que par la classe d’inscription.

Pour plus d’informations sur les rappels de propriétés de dépendance, consultez Rappels et validation des propriétés de dépendance.

Analyseurs .NET

Les analyseurs de plateforme du compilateur .NET inspectent votre code C# ou Visual Basic pour connaître les problèmes de qualité et de style du code. Si vous appelez des méthodes substituables dans un constructeur lorsque la règle d’analyseur CA2214 est active, vous recevez l’avertissement CA2214: Don't call overridable methods in constructors. Toutefois, la règle ne signale pas les méthodes virtuelles et les rappels qui sont appelés par le système de propriétés WPF sous-jacent lorsqu’une valeur de propriété de dépendance est définie dans un constructeur.

Problèmes causés par les classes dérivées

Si vous sealez votre classe de propriété de dépendance personnalisée ou si vous savez que votre classe ne sera pas dérivée, les problèmes d’initialisation du runtime de classe dérivé ne s’appliquent pas à cette classe. Toutefois, si vous créez une classe de propriété de dépendance qui hérite, par exemple si vous créez des modèles ou un jeu de bibliothèques de contrôles extensible, évitez d’appeler des méthodes substituables ou de définir des valeurs de propriété de dépendance à partir d’un constructeur.

Le code de test suivant illustre un modèle de constructeur non sécurisé, où un constructeur de classe de base définit une valeur de propriété de dépendance, ce qui déclenche des appels aux méthodes virtuelles et aux rappels.

    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

L’ordre dans lequel les méthodes sont appelées dans le test de modèle de constructeur non sécurisé est :

  1. Constructeur statique de classe dérivée, qui remplace les métadonnées de propriété de dépendance de Aquarium registre PropertyChangedCallback et CoerceValueCallback.

  2. Constructeur de classe de base, qui définit une nouvelle valeur de propriété de dépendance résultant d’un appel à la SetValue méthode. L’appel SetValue déclenche des rappels et des événements dans l’ordre suivant :

    1. ValidateValueCallback, qui est implémenté dans la classe de base. Ce rappel ne fait pas partie des métadonnées de propriété de dépendance et ne peut pas être implémenté dans la classe dérivée en substituant les métadonnées.

    2. PropertyChangedCallback, qui est implémenté dans la classe dérivée en substituant les métadonnées de propriété de dépendance. Ce rappel provoque une exception de référence Null lorsqu’elle appelle une méthode sur le champ s_temperatureLogde classe non initialisé.

    3. CoerceValueCallback, qui est implémenté dans la classe dérivée en substituant les métadonnées de propriété de dépendance. Ce rappel provoque une exception de référence Null lorsqu’elle appelle une méthode sur le champ s_temperatureLogde classe non initialisé.

    4. OnPropertyChanged événement, qui est implémenté dans la classe dérivée en remplaçant la méthode virtuelle. Cet événement provoque une exception de référence Null lorsqu’il appelle une méthode sur le champ s_temperatureLogde classe non initialisé .

  3. Constructeur sans paramètre de classe dérivé, qui initialise s_temperatureLog.

  4. Constructeur de paramètre de classe dérivé, qui définit une nouvelle valeur de propriété de dépendance, ce qui entraîne un autre appel à la SetValue méthode. Étant donné qu’il s_temperatureLog est maintenant initialisé, les rappels et les événements s’exécutent sans provoquer d’exceptions de référence Null.

Ces problèmes d’initialisation sont évitables par l’utilisation de modèles de constructeur sécurisés.

Modèles de constructeur sécurisés

Les problèmes d’initialisation de classe dérivée démontrés dans le code de test peuvent être résolus de différentes façons, notamment :

  • Évitez de définir une valeur de propriété de dépendance dans un constructeur de votre classe de propriété de dépendance personnalisée si votre classe peut être utilisée comme classe de base. Si vous devez initialiser une valeur de propriété de dépendance, envisagez de définir la valeur requise comme valeur par défaut dans les métadonnées de propriété pendant l’inscription de propriété de dépendance ou lors de la substitution de métadonnées.

  • Initialisez les champs de classe dérivés avant leur utilisation. Par exemple, à l’aide de l’une de ces approches :

    • Instanciez et affectez des champs d’instance dans une seule instruction. Dans l’exemple précédent, l’instruction List<int> s_temperatureLog = new(); évite l’affectation tardive.

    • Effectuez une affectation dans le constructeur statique de classe dérivée, qui s’exécute devant n’importe quel constructeur de classe de base. Dans l’exemple précédent, le fait de placer l’instruction s_temperatureLog = new List<int>(); d’affectation dans le constructeur statique de classe dérivée évite l’affectation tardive.

    • Utilisez l’initialisation différée et l’instanciation, qui initialise les objets comme et quand ils sont nécessaires. Dans l’exemple précédent, l’instanciation s_temperatureLog et l’affectation à l’aide de l’initialisation différée et de l’instanciation évitent l’affectation tardive. Pour plus d’informations, consultez Initialisation différée.

  • Évitez d’utiliser des variables de classe non initialisées dans les rappels et événements du système de propriétés WPF.

Voir aussi