DependencyObjects 的安全建構函式模式 (WPF .NET)
受控程式碼程式設計有一個一般準則,通常是由程式碼分析工具強制執行,類別建構函式不應該呼叫可覆寫的方法。 如果基礎建構函式呼叫可覆寫的方法,而衍生類別會覆寫該方法,則衍生類別中的 override 方法可以在衍生類別建構函式之前執行。 如果衍生類別建構函式執行類別初始化,則衍生類別方法可能會存取未初始化的類別成員。 相依性屬性類別應該避免在類別建構函式中設定相依性屬性值,以避免執行階段初始化問題。 本文說明如何以避免這些問題的方式實作 DependencyObject 建構函式。
屬性系統虛擬方法和回呼
相依性屬性虛擬方法和回呼是 Windows Presentation Foundation (WPF) 屬性系統的一部分,並擴充相依性屬性的多功能性。
使用 SetValue 設定相依性屬性值等基本作業會叫用 OnPropertyChanged 事件,並可能叫用數個 WPF 屬性系統回呼。
OnPropertyChanged
是 WPF 屬性系統虛擬方法的範例,可由繼承階層中具有 DependencyObject 的類別覆寫。 如果您在自訂相依性屬性類別具現化期間呼叫的建構函式中設定相依性屬性值,而衍生自其類別會覆寫 OnPropertyChanged
虛擬方法,則衍生類別 OnPropertyChanged
方法會在任何衍生類別建構函式之前執行。
PropertyChangedCallback 和 CoerceValueCallback 是 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
在不安全的建構函式模式測試中呼叫方法的順序如下:
衍生類別靜態建構函式,其會覆寫
Aquarium
的相依性屬性中繼資料,以登錄 PropertyChangedCallback 和 CoerceValueCallback。基礎類別建構函式會設定新的相依性屬性值,導致呼叫 SetValue 方法。 呼叫
SetValue
會依下列順序觸發回呼和事件:ValidateValueCallback,這是在基礎類別中實作。 此回呼不是相依性屬性中繼資料的一部分,無法透過覆寫中繼資料在衍生類別中實作。
PropertyChangedCallback
,這是藉由覆寫相依性屬性中繼資料,在衍生類別中實作。 這個回呼會在未初始化類別欄位s_temperatureLog
上呼叫方法時,造成 Null 參考例外狀況。CoerceValueCallback
,這是藉由覆寫相依性屬性中繼資料,在衍生類別中實作。 這個回呼會在未初始化類別欄位s_temperatureLog
上呼叫方法時,造成 Null 參考例外狀況。OnPropertyChanged 事件,這是藉由覆寫虛擬方法,在衍生類別中實作。 當這個事件在未初始化的類別欄位
s_temperatureLog
上呼叫方法時,會導致 Null 參考例外狀況。
衍生類別無參數建構函式,其會初始化
s_temperatureLog
。衍生類別參數建構函式,這個建構函式會設定新的相依性屬性值,導致另一個呼叫
SetValue
方法。 由於s_temperatureLog
現在已初始化,因此會執行回呼和事件,而不會造成 Null 參考例外狀況。
這些初始化問題可透過使用安全建構函式模式來避免。
安全的建構函式模式
測試程式碼中示範的衍生類別初始化問題可以透過不同方式解決,包括:
如果您的類別可能用作基礎類別,請避免在自訂相依性屬性類別的建構函式中設定相依性屬性值。 如果您需要初始化相依性屬性值,請考慮在相依性屬性登錄期間或覆寫中繼資料時,將必要值設定為屬性中繼資料中的預設值。
使用之前,請先初始化衍生類別欄位。 例如,使用下列任一方法:
在單一陳述式中具現化和指派執行個體欄位。 在上一個範例中,陳述式
List<int> s_temperatureLog = new();
會避免稍後指派。在衍生類別靜態建構函式中執行指派,它會在任何基礎建構函式之前執行。 在上述範例中,將指派陳述式
s_temperatureLog = new List<int>();
放在衍生類別靜態建構函式中,可避免稍後指派。使用延遲初始設定和具現化,以在需要時將物件初始化。 在上一個範例中,使用延遲初始設定和具現化來具現化和指派
s_temperatureLog
會避免稍後指派。 如需詳細資訊,請參閱延遲初始設定。
避免在 WPF 屬性系統回呼和事件中使用未初始化的類別變數。