DependencyObject の安全なコンストラクター パターン (WPF .NET)

マネージド コードのプログラミングには、クラス コンストラクターでオーバーライド可能なメソッドを呼び出さないという一般的な原則があり、多くの場合、コード分析ツールで強制されます。 オーバーライド可能なメソッドが基底クラスのコンストラクターによって呼び出され、派生クラスでそのメソッドがオーバーライドされる場合、派生クラスのコンストラクターの前に、派生クラスのオーバーライド メソッドを実行できます。 派生クラスのコンストラクターでクラスの初期化が実行される場合、派生クラスのメソッドで、初期化されていないクラス メンバーにアクセスする可能性があります。 依存関係プロパティ クラスでは、実行時の初期化の問題を回避するために、クラスのコンストラクターで依存関係プロパティ値を設定しないようにする必要があります。 この記事では、それらの問題を回避して DependencyObject のコンストラクターを実装する方法について説明します。

重要

.NET 7 と .NET 6 用のデスクトップ ガイド ドキュメントは作成中です。

プロパティ システムの仮想メソッドとコールバック

依存関係プロパティの仮想メソッドとコールバックは、Windows Presentation Foundation (WPF) プロパティ システムの一部であり、依存関係プロパティの汎用性が拡張されます。

SetValue を使用した依存関係プロパティ値の設定などの基本的な操作では、OnPropertyChanged イベントが呼び出され、いくつかの WPF プロパティ システムのコールバックが呼び出されることもあります。

OnPropertyChanged は、継承階層に DependencyObject を持つクラスによってオーバーライドできる WPF プロパティ システムの仮想メソッドの例です。 カスタム依存関係プロパティ クラスのインスタンス化中に呼び出されるコンストラクターで依存関係プロパティ値を設定し、そこから派生したクラスで OnPropertyChanged 仮想メソッドがオーバーライドされる場合、派生クラスの OnPropertyChanged メソッドは派生クラスのコンストラクターの前に実行されます。

PropertyChangedCallbackCoerceValueCallback は、依存関係プロパティ クラスによって登録され、そこから派生するクラスによってオーバーライドできる WPF プロパティ システムのコールバックの例です。 カスタム依存関係プロパティ クラスのコンストラクターで依存関係プロパティ値を設定し、そこから派生するクラスでプロパティ メタデータ内のそれらのコールバックのいずれかがオーバーライドされる場合、派生クラスのコールバックは派生クラスのコンストラクターの前に実行されます。 ValidateValueCallback はプロパティのメタデータの一部ではなく、登録するクラスでのみ指定できるため、この問題は関係ありません。

依存関係プロパティのコールバックの詳細については、「依存関係プロパティのコールバックと検証」を参照してください。

.NET アナライザー

.NET コンパイラ プラットフォーム アナライザーでは、お使いの C# または Visual Basic コードについて、コードの品質やスタイルに関する問題を検査できます。 アナライザー規則 CA2214 がアクティブなときにコンストラクターでオーバーライド可能なメソッドを呼び出すと、CA2214: Don't call overridable methods in constructors という警告が表示されます。 ただし、依存関係プロパティの値がコンストラクターで設定される場合、この規則では、基になる WPF プロパティ システムによって呼び出される仮想メソッドとコールバックにフラグが設定されません。

派生クラスが原因で発生する問題

カスタム依存関係プロパティのクラスをシールした場合、またはクラスが派生したものではないことがわかっている場合、そのクラスには派生クラスの実行時初期化の問題は当てはまりません。 ただし、継承可能な依存関係プロパティ クラスを作成する場合 (たとえば、テンプレートや展開可能なコントロール ライブラリ セットを作成する場合)、オーバーライド可能なメソッドを呼び出したり、コンストラクターから依存関係プロパティ値を設定したりすることは避けてください。

次のテスト コードは、安全ではないコンストラクター パターンを示しています。このパターンでは、基底クラスのコンストラクターで依存関係プロパティ値が設定されるため、仮想メソッドとコールバックの呼び出しがトリガーされます。

    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

安全ではないコンストラクター パターンのテストでメソッドが呼び出される順序は次のとおりです。

  1. 派生クラスの静的コンストラクター: PropertyChangedCallbackCoerceValueCallback を登録するために Aquarium の依存関係プロパティのメタデータがオーバーライドされます。

  2. 基底クラスのコンストラクター: SetValue メソッドの呼び出しの結果として新しい依存関係プロパティ値が設定されます。 SetValue の呼び出しでは、コールバックとイベントが次の順序でトリガーされます。

    1. ValidateValueCallback: 基底クラスに実装されます。 このコールバックは依存関係プロパティのメタデータの一部ではなく、メタデータをオーバーライドして派生クラスに実装することはできません。

    2. PropertyChangedCallback: 依存関係プロパティのメタデータをオーバーライドすることによって派生クラスに実装されます。 初期化されていないクラス フィールド s_temperatureLog に対してメソッドを呼び出すと、このコールバックによって null 参照例外が発生します。

    3. CoerceValueCallback: 依存関係プロパティのメタデータをオーバーライドすることによって派生クラスに実装されます。 初期化されていないクラス フィールド s_temperatureLog に対してメソッドを呼び出すと、このコールバックによって null 参照例外が発生します。

    4. OnPropertyChanged イベント: 仮想メソッドをオーバーライドすることによって派生クラスに実装されます。 初期化されていないクラス フィールド s_temperatureLog に対してメソッドを呼び出すと、このイベントによって null 参照例外が発生します。

  3. 派生クラスのパラメーターなしのコンストラクター: s_temperatureLog が初期化されます。

  4. 派生クラスのパラメーター コンストラクター: SetValue メソッドの別の呼び出しの結果として新しい依存関係プロパティ値が設定されます。 s_temperatureLog が初期化されているため、コールバックとイベントは null 参照例外が発生することなく実行されます。

これらの初期化の問題は、安全なコンストラクター パターンを使用して回避できます。

安全なコンストラクター パターン

テスト コードで示されている派生クラスの初期化の問題は、次のようなさまざまな方法で解決できます。

  • クラスが基底クラスとして使用される可能性がある場合は、カスタム依存関係プロパティ クラスのコンストラクターで依存関係プロパティ値を設定しないようにします。 依存関係プロパティ値を初期化する必要がある場合は、依存関係プロパティの登録時またはメタデータのオーバーライド時に、必要な値をプロパティ メタデータの既定値として設定することを検討してください。

  • 派生クラスのフィールドを使用する前に初期化します。 たとえば、次のいずれかの方法を使用します。

    • 1 つのステートメントでインスタンスのフィールドをインスタンス化して割り当てます。 前の例では、ステートメント List<int> s_temperatureLog = new(); によって遅延割り当てが回避されています。

    • 派生クラスの静的コンストラクターで割り当てを実行します。このコンストラクターは、すべての基底クラスのコンストラクターの前に実行されます。 前の例では、派生クラスの静的コンストラクターに割り当てステートメント s_temperatureLog = new List<int>(); を記述すると、遅延割り当てが回避されます。

    • 遅延初期化とインスタンス化を使用します。これにより、必要になったときにオブジェクトが初期化されます。 前の例では、遅延初期化とインスタンス化を使用して s_temperatureLog のインスタンス化と割り当てを行うと、遅延割り当てが回避されます。 詳細については、「限定的な初期化」を参照してください。

  • WPF プロパティ システムのコールバックとイベントで初期化されていないクラス変数を使用しないようにします。

参照