DependencyObjects の安全なコンストラクター パターン (WPF .NET)
マネージド コード プログラミングには一般的な原則があり、多くの場合、コード分析ツールによって適用されます。クラス コンストラクターはオーバーライド可能なメソッドを呼び出すべきではありません。 オーバーライド可能なメソッドが基底クラスコンストラクターによって呼び出され、派生クラスがそのメソッドをオーバーライドする場合、派生クラスのオーバーライド メソッドは派生クラス コンストラクターの前に実行できます。 派生クラス コンストラクターがクラスの初期化を実行する場合、派生クラス メソッドは初期化されていないクラス メンバーにアクセスする可能性があります。 依存関係プロパティ クラスは、ランタイム初期化の問題を回避するために、クラス コンストラクターに依存関係プロパティ値を設定しないようにする必要があります。 この記事では、これらの問題を回避する方法 DependencyObject コンストラクターを実装する方法について説明します。
プロパティ システムの仮想メソッドとコールバック
依存関係プロパティの仮想メソッドとコールバックは、Windows Presentation Foundation (WPF) プロパティ システムの一部であり、依存関係プロパティの多様性を拡張します。
SetValue を使用して依存関係プロパティ値を設定するなどの基本的な操作では、OnPropertyChanged イベントと、場合によってはいくつかの WPF プロパティ システム コールバックが呼び出されます。
OnPropertyChanged
は、継承階層内に DependencyObject クラスによってオーバーライドできる WPF プロパティ システムの仮想メソッドの例です。 カスタム依存関係プロパティ クラスのインスタンス化中に呼び出されるコンストラクターで依存関係プロパティ値を設定し、そこから派生したクラスが OnPropertyChanged
仮想メソッドをオーバーライドする場合、派生クラス OnPropertyChanged
メソッドは派生クラス コンストラクターの前に実行されます。
PropertyChangedCallback と CoerceValueCallback は、依存関係プロパティ クラスによって登録され、そこから派生するクラスによってオーバーライドできる WPF プロパティ システム コールバックの例です。 カスタム依存関係プロパティ クラスのコンストラクターで依存関係プロパティ値を設定し、そこから派生するクラスがプロパティ メタデータ内のコールバックの 1 つをオーバーライドする場合、派生クラス コールバックは派生クラス コンストラクターの前に実行されます。 この問題は、プロパティ メタデータの一部ではなく、登録クラスでのみ指定できるため、ValidateValueCallback には関係ありません。
依存関係プロパティのコールバックの詳細については、「依存関係プロパティのコールバックと検証
.NET アナライザー
.NET コンパイラ プラットフォーム アナライザーは、C# または Visual Basic コードでコードの品質とスタイルの問題を検査します。 CA2214 がアクティブなアナライザー ルール
派生クラスによって発生する問題
カスタム依存関係プロパティ クラス シール
次のテスト コードは、基底クラスのコンストラクターが依存関係プロパティ値を設定し、仮想メソッドとコールバックの呼び出しをトリガーする、安全でないコンストラクター パターンを示しています。
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
アンセーフ コンストラクター パターン テストでメソッドが呼び出される順序は次のとおりです。
派生クラスの静的コンストラクター。
Aquarium
の依存関係プロパティ メタデータをオーバーライドして、PropertyChangedCallback と CoerceValueCallbackを登録します。基底クラス コンストラクター。新しい依存関係プロパティ値を設定し、その結果、SetValue メソッドが呼び出されます。
SetValue
呼び出しは、コールバックとイベントを次の順序でトリガーします。ValidateValueCallback。これは基底クラスで実装されます。 このコールバックは依存関係プロパティメタデータの一部ではなく、メタデータをオーバーライドして派生クラスに実装することはできません。
PropertyChangedCallback
。依存関係プロパティのメタデータをオーバーライドすることによって派生クラスに実装されます。 このコールバックは、初期化されていないクラス フィールドs_temperatureLog
でメソッドを呼び出すと、null 参照例外を引き起こします。CoerceValueCallback
。依存関係プロパティのメタデータをオーバーライドすることによって派生クラスに実装されます。 このコールバックは、初期化されていないクラス フィールドs_temperatureLog
でメソッドを呼び出すと、null 参照例外を引き起こします。OnPropertyChanged イベントです。これは、仮想メソッドをオーバーライドすることによって派生クラスに実装されます。 このイベントは、初期化されていないクラス フィールド
s_temperatureLog
でメソッドを呼び出すと、null 参照例外が発生します。
s_temperatureLog
を初期化する、派生クラスのパラメーターなしのコンストラクター。派生クラス パラメーター コンストラクター。新しい依存関係プロパティ値を設定し、
SetValue
メソッドを別の呼び出しにします。s_temperatureLog
が初期化されたので、コールバックとイベントは null 参照例外を発生させることなく実行されます。
これらの初期化の問題は、安全なコンストラクター パターンを使用して回避できます。
安全なコンストラクター パターン
テスト コードで示されている派生クラスの初期化の問題は、次のようなさまざまな方法で解決できます。
クラスが基底クラスとして使用される可能性がある場合は、カスタム依存関係プロパティ クラスのコンストラクターで依存関係プロパティ値を設定しないでください。 依存関係プロパティの値を初期化する必要がある場合は、依存関係プロパティの登録時またはメタデータのオーバーライド時に、必要な値をプロパティ メタデータの既定値として設定することを検討してください。
使用する前に、派生クラス フィールドを初期化します。 たとえば、次のいずれかの方法を使用します。
1 つのステートメントでインスタンス フィールドをインスタンス化して割り当てます。 前の例では、ステートメント
List<int> s_temperatureLog = new();
遅延割り当てを回避できます。派生クラスの静的コンストラクターで割り当てを実行します。このコンストラクターは、任意の基底クラス コンストラクターの前で実行されます。 前の例では、派生クラスの静的コンストラクターに assignment ステートメント
s_temperatureLog = new List<int>();
を配置すると、遅延割り当てが回避されます。遅延初期化とインスタンス化を使用します。これは、オブジェクトを必要なときに初期化します。 前の例では、遅延初期化とインスタンス化を使用して
s_temperatureLog
をインスタンス化して割り当てると、遅い割り当てを避けられます。 詳細については、「遅延初期化を参照してください。
WPF プロパティ システムのコールバックとイベントで初期化されていないクラス変数を使用しないでください。
関連項目
.NET Desktop feedback