Bagikan melalui


Brankas pola konstruktor untuk DependencyObjects (WPF .NET)

Ada prinsip umum dalam pemrograman kode terkelola, sering diberlakukan oleh alat analisis kode, bahwa konstruktor kelas tidak boleh memanggil metode yang dapat diganti. Jika metode yang dapat diambil alih dipanggil oleh konstruktor kelas dasar, dan kelas turunan mengambil alih metode tersebut, maka metode penimpaan di kelas turunan dapat berjalan sebelum konstruktor kelas turunan. Jika konstruktor kelas turunan melakukan inisialisasi kelas, maka metode kelas turunan dapat mengakses anggota kelas yang tidak diinisialisasi. Kelas properti dependensi harus menghindari pengaturan nilai properti dependensi dalam konstruktor kelas untuk menghindari masalah inisialisasi runtime. Artikel ini menjelaskan cara menerapkan DependencyObject konstruktor dengan cara yang menghindari masalah tersebut.

Penting

Dokumentasi Panduan Desktop untuk .NET 7 dan .NET 6 sedang dibangun.

Metode virtual sistem properti dan panggilan balik

Metode virtual properti dependensi dan panggilan balik adalah bagian dari sistem properti Windows Presentation Foundation (WPF) dan memperluas fleksibilitas properti dependensi.

Operasi dasar seperti mengatur nilai properti dependensi menggunakan SetValue akan memanggil OnPropertyChanged peristiwa dan berpotensi beberapa panggilan balik sistem properti WPF.

OnPropertyChanged adalah contoh metode virtual sistem properti WPF yang dapat ditimpa oleh kelas yang memiliki DependencyObject hierarki pewarisannya. Jika Anda menetapkan nilai properti dependensi dalam konstruktor yang dipanggil selama instans kelas properti dependensi kustom Anda, dan kelas yang berasal darinya mengambil alih OnPropertyChanged metode virtual, maka metode kelas OnPropertyChanged turunan akan berjalan sebelum konstruktor kelas turunan apa pun.

PropertyChangedCallback dan CoerceValueCallback adalah contoh panggilan balik sistem properti WPF yang dapat didaftarkan oleh kelas properti dependensi, dan ditimpa oleh kelas yang berasal dari mereka. Jika Anda menetapkan nilai properti dependensi di konstruktor kelas properti dependensi kustom Anda, dan kelas yang berasal darinya mengambil alih salah satu panggilan balik tersebut dalam metadata properti, maka panggilan balik kelas turunan akan berjalan sebelum konstruktor kelas turunan. Masalah ini tidak relevan karena ValidateValueCallback bukan bagian dari metadata properti dan hanya dapat ditentukan oleh kelas pendaftaran.

Untuk informasi selengkapnya tentang panggilan balik properti dependensi, lihat Panggilan balik dan validasi properti dependensi.

Penganalisis.NET

Penganalisis platform kompilator .NET memeriksa kode C# atau Visual Basic Anda untuk masalah kualitas kode dan gaya. Jika Anda memanggil metode yang dapat diganti dalam konstruktor saat aturan penganalisis CA2214 aktif, Anda akan mendapatkan peringatan CA2214: Don't call overridable methods in constructors. Namun, aturan tidak akan menandai metode virtual dan panggilan balik yang dipanggil oleh sistem properti WPF yang mendasarinya ketika nilai properti dependensi diatur dalam konstruktor.

Masalah yang disebabkan oleh kelas turunan

Jika Anda menyegel kelas properti dependensi kustom, atau mengetahui bahwa kelas Anda tidak akan berasal, maka masalah inisialisasi runtime kelas turunan tidak berlaku untuk kelas tersebut. Tetapi, jika Anda membuat kelas properti dependensi yang dapat diwariskan, misalnya jika Anda membuat templat atau kumpulan pustaka kontrol yang dapat diperluas, hindari memanggil metode yang dapat diganti atau mengatur nilai properti dependensi dari konstruktor.

Kode pengujian berikut menunjukkan pola konstruktor yang tidak aman, di mana konstruktor kelas dasar menetapkan nilai properti dependensi sehingga memicu panggilan ke metode virtual dan panggilan balik.

    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

Urutan di mana metode dipanggil dalam pengujian pola konstruktor yang tidak aman adalah:

  1. Konstruktor statis kelas turunan, yang mengambil alih metadata Aquarium properti dependensi untuk mendaftar PropertyChangedCallback dan CoerceValueCallback.

  2. Konstruktor kelas dasar, yang menetapkan nilai properti dependensi baru yang menghasilkan panggilan ke SetValue metode . Panggilan SetValue memicu panggilan balik dan peristiwa dalam urutan berikut:

    1. ValidateValueCallback, yang diimplementasikan di kelas dasar. Panggilan balik ini bukan bagian dari metadata properti dependensi dan tidak dapat diimplementasikan di kelas turunan dengan mengambil alih metadata.

    2. PropertyChangedCallback, yang diimplementasikan di kelas turunan dengan mengambil alih metadata properti dependensi. Panggilan balik ini menyebabkan pengecualian referensi null ketika memanggil metode pada bidang s_temperatureLogkelas yang tidak diinisialisasi .

    3. CoerceValueCallback, yang diimplementasikan di kelas turunan dengan mengambil alih metadata properti dependensi. Panggilan balik ini menyebabkan pengecualian referensi null ketika memanggil metode pada bidang s_temperatureLogkelas yang tidak diinisialisasi .

    4. OnPropertyChanged yang diimplementasikan di kelas turunan dengan mengambil alih metode virtual. Kejadian ini menyebabkan pengecualian referensi null ketika memanggil metode pada bidang s_temperatureLogkelas yang tidak diinisialisasi .

  3. Konstruktor tanpa parameter kelas turunan s_temperatureLog, yang menginisialisasi .

  4. Konstruktor parameter kelas turunan, yang menetapkan nilai properti dependensi baru yang menghasilkan panggilan lain ke metode .SetValue Karena s_temperatureLog sekarang diinisialisasi, panggilan balik dan peristiwa akan berjalan tanpa menyebabkan pengecualian referensi null.

Masalah inisialisasi ini dapat dihindari melalui penggunaan pola konstruktor yang aman.

pola konstruktor Brankas

Masalah inisialisasi kelas turunan yang ditunjukkan dalam kode pengujian dapat diselesaikan dengan cara yang berbeda, termasuk:

  • Hindari menetapkan nilai properti dependensi di konstruktor kelas properti dependensi kustom Anda jika kelas Anda dapat digunakan sebagai kelas dasar. Jika Anda perlu menginisialisasi nilai properti dependensi, pertimbangkan untuk mengatur nilai yang diperlukan sebagai nilai default dalam metadata properti selama pendaftaran properti dependensi atau saat mengambil alih metadata.

  • Menginisialisasi bidang kelas turunan sebelum digunakan. Misalnya, menggunakan salah satu pendekatan ini:

    • Membuat instans dan menetapkan bidang instans dalam satu pernyataan. Dalam contoh sebelumnya, pernyataan List<int> s_temperatureLog = new(); akan menghindari penetapan terlambat.

    • Lakukan penugasan di konstruktor statis kelas turunan, yang berjalan di depan konstruktor kelas dasar apa pun. Dalam contoh sebelumnya, menempatkan pernyataan s_temperatureLog = new List<int>(); penugasan di konstruktor statis kelas turunan akan menghindari penugasan yang terlambat.

    • Gunakan inisialisasi dan instansiasi malas, yang menginisialisasi objek sebagai dan kapan diperlukan. Dalam contoh sebelumnya, membuat instans dan menetapkan s_temperatureLog dengan menggunakan inisialisasi dan instansiasi malas akan menghindari penugasan yang terlambat. Untuk informasi selengkapnya, lihat Inisialisasi malas.

  • Hindari menggunakan variabel kelas yang tidak diinisialisasi dalam panggilan balik dan peristiwa sistem properti WPF.

Baca juga