次の方法で共有


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

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

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

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

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

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

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

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

.NET アナライザー

.NET コンパイラ プラットフォーム アナライザーは、C# または Visual Basic コードでコードの品質とスタイルの問題を検査します。 CA2214 がアクティブなアナライザー ルール コンストラクターでオーバーライド可能なメソッドを呼び出すと、警告 が表示されます。 ただし、依存関係プロパティの値がコンストラクターで設定されている場合、基になる 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. 派生クラスの静的コンストラクター。Aquarium の依存関係プロパティ メタデータをオーバーライドして、PropertyChangedCallbackCoerceValueCallbackを登録します。

  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(); 遅延割り当てを回避できます。

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

    • 遅延初期化とインスタンス化を使用します。これは、オブジェクトを必要なときに初期化します。 前の例では、遅延初期化とインスタンス化を使用して s_temperatureLog をインスタンス化して割り当てると、遅い割り当てを避けられます。 詳細については、「遅延初期化を参照してください。

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

関連項目