依存関係プロパティのコールバックと検証 (WPF .NET)
この記事では、依存関係プロパティを定義し、依存関係プロパティのコールバックを実装する方法について説明します。 コールバックは、値の検証、値の強制、およびプロパティ値が変更されたときに必要なその他のロジックをサポートします。
前提 条件
この記事では、依存関係プロパティの基本的な知識と、依存関係プロパティの概要
値検証コールバック
値の検証コールバックは、プロパティ システムによって適用される前に、新しい依存関係プロパティ値が有効かどうかを確認する方法を提供します。 このコールバックは、値が検証条件を満たしていない場合に例外を発生させます。
検証値コールバックは、プロパティの登録中に依存関係プロパティに 1 回だけ割り当てることができます。 依存関係プロパティを登録するときに、Register(String, Type, Type, PropertyMetadata, ValidateValueCallback) メソッドに ValidateValueCallback 参照を渡すオプションがあります。 値の検証コールバックはプロパティ メタデータの一部ではなく、オーバーライドできません。
依存関係プロパティの有効な値は、その適用された値です。 有効な値は、複数のプロパティ ベースの入力 存在する場合に プロパティ値の優先順位によって決定されます。 依存関係プロパティに対して検証値コールバックが登録されている場合、プロパティ システムは値の変更時に検証値コールバックを呼び出し、新しい値をオブジェクトとして渡します。 コールバック内で、値オブジェクトをプロパティ システムに登録されている型にキャストし直し、検証ロジックを実行できます。 コールバックは、値がプロパティに対して有効な場合は true
を返します。それ以外の場合は false
。
検証値コールバックが false
を返した場合、例外が発生し、新しい値は適用されません。 これらの例外を処理するには、アプリケーション ライターを準備する必要があります。 値検証コールバックの一般的な用途は、列挙値の検証、または制限がある測定値を表すときの数値の制約です。 検証値コールバックは、次のようなさまざまなシナリオでプロパティ システムによって呼び出されます。
- オブジェクトの初期化。作成時に既定値が適用されます。
- SetValueへのプログラムによる呼び出し。
- 新しい既定値を指定するメタデータオーバーライド。
値の検証コールバックには、新しい値が設定されている DependencyObject インスタンスを指定するパラメーターがありません。 DependencyObject
のすべてのインスタンスは同じ検証値コールバックを共有するため、インスタンス固有のシナリオを検証するために使用することはできません。 詳細については、ValidateValueCallbackを参照してください。
次の例は、Doubleとして入力されたプロパティが PositiveInfinity または NegativeInfinityに設定されないようにする方法を示しています。
public class Gauge1 : Control
{
public Gauge1() : base() { }
// Register a dependency property with the specified property name,
// property type, owner type, property metadata, and callbacks.
public static readonly DependencyProperty CurrentReadingProperty =
DependencyProperty.Register(
name: "CurrentReading",
propertyType: typeof(double),
ownerType: typeof(Gauge1),
typeMetadata: new FrameworkPropertyMetadata(
defaultValue: double.NaN,
flags: FrameworkPropertyMetadataOptions.AffectsMeasure),
validateValueCallback: new ValidateValueCallback(IsValidReading));
// CLR wrapper with get/set accessors.
public double CurrentReading
{
get => (double)GetValue(CurrentReadingProperty);
set => SetValue(CurrentReadingProperty, value);
}
// Validate-value callback.
public static bool IsValidReading(object value)
{
double val = (double)value;
return !val.Equals(double.NegativeInfinity) &&
!val.Equals(double.PositiveInfinity);
}
}
Public Class Gauge1
Inherits Control
Public Sub New()
MyBase.New()
End Sub
Public Shared ReadOnly CurrentReadingProperty As DependencyProperty =
DependencyProperty.Register(
name:="CurrentReading",
propertyType:=GetType(Double),
ownerType:=GetType(Gauge1),
typeMetadata:=New FrameworkPropertyMetadata(
defaultValue:=Double.NaN,
flags:=FrameworkPropertyMetadataOptions.AffectsMeasure),
validateValueCallback:=New ValidateValueCallback(AddressOf IsValidReading))
Public Property CurrentReading As Double
Get
Return GetValue(CurrentReadingProperty)
End Get
Set(value As Double)
SetValue(CurrentReadingProperty, value)
End Set
End Property
Public Shared Function IsValidReading(value As Object) As Boolean
Dim val As Double = value
Return Not val.Equals(Double.NegativeInfinity) AndAlso
Not val.Equals(Double.PositiveInfinity)
End Function
End Class
public static void TestValidationBehavior()
{
Gauge1 gauge = new();
Debug.WriteLine($"Test value validation scenario:");
// Set allowed value.
gauge.CurrentReading = 5;
Debug.WriteLine($"Current reading: {gauge.CurrentReading}");
try
{
// Set disallowed value.
gauge.CurrentReading = double.PositiveInfinity;
}
catch (ArgumentException e)
{
Debug.WriteLine($"Exception thrown by ValidateValueCallback: {e.Message}");
}
Debug.WriteLine($"Current reading: {gauge.CurrentReading}");
// Current reading: 5
// Exception thrown by ValidateValueCallback: '∞' is not a valid value for property 'CurrentReading'.
// Current reading: 5
}
Public Shared Sub TestValidationBehavior()
Dim gauge As New Gauge1()
Debug.WriteLine($"Test value validation scenario:")
' Set allowed value.
gauge.CurrentReading = 5
Debug.WriteLine($"Current reading: {gauge.CurrentReading}")
Try
' Set disallowed value.
gauge.CurrentReading = Double.PositiveInfinity
Catch e As ArgumentException
Debug.WriteLine($"Exception thrown by ValidateValueCallback: {e.Message}")
End Try
Debug.WriteLine($"Current reading: {gauge.CurrentReading}")
' Current reading: 5
' Exception thrown by ValidateValueCallback: '∞' is not a valid value for property 'CurrentReading'.
' Current reading 5
End Sub
プロパティ変更コールバック
プロパティ変更コールバックは、依存関係プロパティの有効な値が変更されたときに通知します。
プロパティ変更コールバックは、依存関係プロパティメタデータの一部です。 依存関係プロパティを定義するクラスから派生した場合、またはクラスを依存関係プロパティの所有者として追加する場合は、メタデータをオーバーライドできます。 メタデータをオーバーライドする場合は、新しい PropertyChangedCallback 参照を指定するオプションがあります。 プロパティ値が変更されたときに必要なロジックを実行するには、プロパティ変更コールバックを使用します。
検証値コールバックとは異なり、プロパティ変更コールバックには、新しい値が設定されている DependencyObject インスタンスを指定するパラメーターがあります。 次の例では、プロパティ変更コールバックで DependencyObject
インスタンス参照を使用して強制値コールバックをトリガーする方法を示します。
値強制コールバック
強制値コールバックは、依存関係プロパティの有効な値が変更されようとしているときに通知を受け取る方法を提供し、新しい値を適用する前に調整できるようにします。 プロパティ システムによってトリガーされるだけでなく、コードから強制値コールバックを呼び出すことができます。
強制値コールバックは、依存関係プロパティのメタデータの一部です。 依存関係プロパティを定義するクラスから派生した場合、またはクラスを依存関係プロパティの所有者として追加する場合は、メタデータをオーバーライドできます。 メタデータをオーバーライドする場合は、新しい CoerceValueCallbackへの参照を指定するオプションがあります。 強制値コールバックを使用して新しい値を評価し、必要に応じて強制します。 強制型指定が発生した場合、コールバックは強制値を返し、それ以外の場合は変更されていない新しい値を返します。
プロパティ変更コールバックと同様に、強制値コールバックには、新しい値が設定される DependencyObject インスタンスを指定するパラメーターがあります。 次の例では、強制値コールバックが DependencyObject
インスタンス参照を使用してプロパティ値を強制する方法を示します。
手記
既定のプロパティ値を強制することはできません。 依存関係プロパティの既定値は、オブジェクトの初期化時、または ClearValueを使用して他の値をクリアするときに設定されます。
値強制コールバックとプロパティ変更コールバックの組み合わせ
強制値コールバックとプロパティ変更コールバックを組み合わせて使用することで、要素のプロパティ間の依存関係を作成できます。 たとえば、あるプロパティの変更によって、別の依存関係プロパティでの強制強制または再評価が行われます。 次の例は、一般的なシナリオを示しています。それぞれ UI 要素の現在の値、最小値、最大値を格納する 3 つの依存関係プロパティです。 最大値が現在の値より小さいように変更された場合、現在の値は新しい最大値に設定されます。 また、最小値が現在の値より大きいように変更された場合、現在の値は新しい最小値に設定されます。 この例では、現在の値の PropertyChangedCallback は、最小値と最大値に対して CoerceValueCallback を明示的に呼び出します。
public class Gauge2 : Control
{
public Gauge2() : base() { }
// Register a dependency property with the specified property name,
// property type, owner type, property metadata, and callbacks.
public static readonly DependencyProperty CurrentReadingProperty =
DependencyProperty.Register(
name: "CurrentReading",
propertyType: typeof(double),
ownerType: typeof(Gauge2),
typeMetadata: new FrameworkPropertyMetadata(
defaultValue: double.NaN,
flags: FrameworkPropertyMetadataOptions.AffectsMeasure,
propertyChangedCallback: new PropertyChangedCallback(OnCurrentReadingChanged),
coerceValueCallback: new CoerceValueCallback(CoerceCurrentReading)
),
validateValueCallback: new ValidateValueCallback(IsValidReading)
);
// CLR wrapper with get/set accessors.
public double CurrentReading
{
get => (double)GetValue(CurrentReadingProperty);
set => SetValue(CurrentReadingProperty, value);
}
// Validate-value callback.
public static bool IsValidReading(object value)
{
double val = (double)value;
return !val.Equals(double.NegativeInfinity) && !val.Equals(double.PositiveInfinity);
}
// Property-changed callback.
private static void OnCurrentReadingChanged(DependencyObject depObj, DependencyPropertyChangedEventArgs e)
{
depObj.CoerceValue(MinReadingProperty);
depObj.CoerceValue(MaxReadingProperty);
}
// Coerce-value callback.
private static object CoerceCurrentReading(DependencyObject depObj, object value)
{
Gauge2 gauge = (Gauge2)depObj;
double currentVal = (double)value;
currentVal = currentVal < gauge.MinReading ? gauge.MinReading : currentVal;
currentVal = currentVal > gauge.MaxReading ? gauge.MaxReading : currentVal;
return currentVal;
}
// Register a dependency property with the specified property name,
// property type, owner type, property metadata, and callbacks.
public static readonly DependencyProperty MaxReadingProperty = DependencyProperty.Register(
name: "MaxReading",
propertyType: typeof(double),
ownerType: typeof(Gauge2),
typeMetadata: new FrameworkPropertyMetadata(
defaultValue: double.NaN,
flags: FrameworkPropertyMetadataOptions.AffectsMeasure,
propertyChangedCallback: new PropertyChangedCallback(OnMaxReadingChanged),
coerceValueCallback: new CoerceValueCallback(CoerceMaxReading)
),
validateValueCallback: new ValidateValueCallback(IsValidReading)
);
// CLR wrapper with get/set accessors.
public double MaxReading
{
get => (double)GetValue(MaxReadingProperty);
set => SetValue(MaxReadingProperty, value);
}
// Property-changed callback.
private static void OnMaxReadingChanged(DependencyObject depObj, DependencyPropertyChangedEventArgs e)
{
depObj.CoerceValue(MinReadingProperty);
depObj.CoerceValue(CurrentReadingProperty);
}
// Coerce-value callback.
private static object CoerceMaxReading(DependencyObject depObj, object value)
{
Gauge2 gauge = (Gauge2)depObj;
double maxVal = (double)value;
return maxVal < gauge.MinReading ? gauge.MinReading : maxVal;
}
// Register a dependency property with the specified property name,
// property type, owner type, property metadata, and callbacks.
public static readonly DependencyProperty MinReadingProperty = DependencyProperty.Register(
name: "MinReading",
propertyType: typeof(double),
ownerType: typeof(Gauge2),
typeMetadata: new FrameworkPropertyMetadata(
defaultValue: double.NaN,
flags: FrameworkPropertyMetadataOptions.AffectsMeasure,
propertyChangedCallback: new PropertyChangedCallback(OnMinReadingChanged),
coerceValueCallback: new CoerceValueCallback(CoerceMinReading)
),
validateValueCallback: new ValidateValueCallback(IsValidReading));
// CLR wrapper with get/set accessors.
public double MinReading
{
get => (double)GetValue(MinReadingProperty);
set => SetValue(MinReadingProperty, value);
}
// Property-changed callback.
private static void OnMinReadingChanged(DependencyObject depObj, DependencyPropertyChangedEventArgs e)
{
depObj.CoerceValue(MaxReadingProperty);
depObj.CoerceValue(CurrentReadingProperty);
}
// Coerce-value callback.
private static object CoerceMinReading(DependencyObject depObj, object value)
{
Gauge2 gauge = (Gauge2)depObj;
double minVal = (double)value;
return minVal > gauge.MaxReading ? gauge.MaxReading : minVal;
}
}
Public Class Gauge2
Inherits Control
Public Sub New()
MyBase.New()
End Sub
' Register a dependency property with the specified property name,
' property type, owner type, property metadata, And callbacks.
Public Shared ReadOnly CurrentReadingProperty As DependencyProperty =
DependencyProperty.Register(
name:="CurrentReading",
propertyType:=GetType(Double),
ownerType:=GetType(Gauge2),
typeMetadata:=New FrameworkPropertyMetadata(
defaultValue:=Double.NaN,
flags:=FrameworkPropertyMetadataOptions.AffectsMeasure,
propertyChangedCallback:=New PropertyChangedCallback(AddressOf OnCurrentReadingChanged),
coerceValueCallback:=New CoerceValueCallback(AddressOf CoerceCurrentReading)),
validateValueCallback:=New ValidateValueCallback(AddressOf IsValidReading))
' CLR wrapper with get/set accessors.
Public Property CurrentReading As Double
Get
Return GetValue(CurrentReadingProperty)
End Get
Set(value As Double)
SetValue(CurrentReadingProperty, value)
End Set
End Property
' Validate-value callback.
Public Shared Function IsValidReading(value As Object) As Boolean
Dim val As Double = value
Return Not val.Equals(Double.NegativeInfinity) AndAlso Not val.Equals(Double.PositiveInfinity)
End Function
' Property-changed callback.
Private Shared Sub OnCurrentReadingChanged(depObj As DependencyObject, e As DependencyPropertyChangedEventArgs)
depObj.CoerceValue(MinReadingProperty)
depObj.CoerceValue(MaxReadingProperty)
End Sub
' Coerce-value callback.
Private Shared Function CoerceCurrentReading(depObj As DependencyObject, value As Object) As Object
Dim gauge As Gauge2 = CType(depObj, Gauge2)
Dim currentVal As Double = value
currentVal = If(currentVal < gauge.MinReading, gauge.MinReading, currentVal)
currentVal = If(currentVal > gauge.MaxReading, gauge.MaxReading, currentVal)
Return currentVal
End Function
Public Shared ReadOnly MaxReadingProperty As DependencyProperty =
DependencyProperty.Register(
name:="MaxReading",
propertyType:=GetType(Double),
ownerType:=GetType(Gauge2),
typeMetadata:=New FrameworkPropertyMetadata(
defaultValue:=Double.NaN,
flags:=FrameworkPropertyMetadataOptions.AffectsMeasure,
propertyChangedCallback:=New PropertyChangedCallback(AddressOf OnMaxReadingChanged),
coerceValueCallback:=New CoerceValueCallback(AddressOf CoerceMaxReading)),
validateValueCallback:=New ValidateValueCallback(AddressOf IsValidReading))
' CLR wrapper with get/set accessors.
Public Property MaxReading As Double
Get
Return GetValue(MaxReadingProperty)
End Get
Set(value As Double)
SetValue(MaxReadingProperty, value)
End Set
End Property
' Property-changed callback.
Private Shared Sub OnMaxReadingChanged(depObj As DependencyObject, e As DependencyPropertyChangedEventArgs)
depObj.CoerceValue(MinReadingProperty)
depObj.CoerceValue(CurrentReadingProperty)
End Sub
' Coerce-value callback.
Private Shared Function CoerceMaxReading(depObj As DependencyObject, value As Object) As Object
Dim gauge As Gauge2 = CType(depObj, Gauge2)
Dim maxVal As Double = value
Return If(maxVal < gauge.MinReading, gauge.MinReading, maxVal)
End Function
' Register a dependency property with the specified property name,
' property type, owner type, property metadata, And callbacks.
Public Shared ReadOnly MinReadingProperty As DependencyProperty =
DependencyProperty.Register(
name:="MinReading",
propertyType:=GetType(Double),
ownerType:=GetType(Gauge2),
typeMetadata:=New FrameworkPropertyMetadata(
defaultValue:=Double.NaN,
flags:=FrameworkPropertyMetadataOptions.AffectsMeasure,
propertyChangedCallback:=New PropertyChangedCallback(AddressOf OnMinReadingChanged),
coerceValueCallback:=New CoerceValueCallback(AddressOf CoerceMinReading)),
validateValueCallback:=New ValidateValueCallback(AddressOf IsValidReading))
' CLR wrapper with get/set accessors.
Public Property MinReading As Double
Get
Return GetValue(MinReadingProperty)
End Get
Set(value As Double)
SetValue(MinReadingProperty, value)
End Set
End Property
' Property-changed callback.
Private Shared Sub OnMinReadingChanged(depObj As DependencyObject, e As DependencyPropertyChangedEventArgs)
depObj.CoerceValue(MaxReadingProperty)
depObj.CoerceValue(CurrentReadingProperty)
End Sub
' Coerce-value callback.
Private Shared Function CoerceMinReading(depObj As DependencyObject, value As Object) As Object
Dim gauge As Gauge2 = CType(depObj, Gauge2)
Dim minVal As Double = value
Return If(minVal > gauge.MaxReading, gauge.MaxReading, minVal)
End Function
End Class
高度なコールバック シナリオ
制約と必要な値
強制によって依存関係プロパティのローカルに設定された値が変更された場合、変更されていないローカル設定値は、
public static void TestCoercionBehavior()
{
Gauge2 gauge = new()
{
// Set initial values.
MinReading = 0,
MaxReading = 10,
CurrentReading = 5
};
Debug.WriteLine($"Test current/min/max values scenario:");
// Current reading is not coerced.
Debug.WriteLine($"Current reading: " +
$"{gauge.CurrentReading} (min: {gauge.MinReading}, max: {gauge.MaxReading})");
// Current reading is coerced to max value.
gauge.MaxReading = 3;
Debug.WriteLine($"Current reading: " +
$"{gauge.CurrentReading} (min: {gauge.MinReading}, max: {gauge.MaxReading})");
// Current reading is coerced, but tracking back to the desired value.
gauge.MaxReading = 4;
Debug.WriteLine($"Current reading: " +
$"{gauge.CurrentReading} (min: {gauge.MinReading}, max: {gauge.MaxReading})");
// Current reading reverts to the desired value.
gauge.MaxReading = 10;
Debug.WriteLine($"Current reading: " +
$"{gauge.CurrentReading} (min: {gauge.MinReading}, max: {gauge.MaxReading})");
// Current reading remains at the desired value.
gauge.MinReading = 5;
gauge.MaxReading = 5;
Debug.WriteLine($"Current reading: " +
$"{gauge.CurrentReading} (min: {gauge.MinReading}, max: {gauge.MaxReading})");
// Current reading: 5 (min=0, max=10)
// Current reading: 3 (min=0, max=3)
// Current reading: 4 (min=0, max=4)
// Current reading: 5 (min=0, max=10)
// Current reading: 5 (min=5, max=5)
}
Public Shared Sub TestCoercionBehavior()
' Set initial values.
Dim gauge As New Gauge2 With {
.MinReading = 0,
.MaxReading = 10,
.CurrentReading = 5
}
Debug.WriteLine($"Test current/min/max values scenario:")
' Current reading is not coerced.
Debug.WriteLine($"Current reading: " &
$"{gauge.CurrentReading} (min={gauge.MinReading}, max={gauge.MaxReading})")
' Current reading is coerced to max value.
gauge.MaxReading = 3
Debug.WriteLine($"Current reading: " &
$"{gauge.CurrentReading} (min={gauge.MinReading}, max={gauge.MaxReading})")
' Current reading is coerced, but tracking back to the desired value.
gauge.MaxReading = 4
Debug.WriteLine($"Current reading: " &
$"{gauge.CurrentReading} (min={gauge.MinReading}, max={gauge.MaxReading})")
' Current reading reverts to the desired value.
gauge.MaxReading = 10
Debug.WriteLine($"Current reading: " &
$"{gauge.CurrentReading} (min={gauge.MinReading}, max={gauge.MaxReading})")
' Current reading remains at the desired value.
gauge.MinReading = 5
gauge.MaxReading = 5
Debug.WriteLine($"Current reading: " &
$"{gauge.CurrentReading} (min={gauge.MinReading}, max={gauge.MaxReading})")
' Current reading: 5 (min=0, max=10)
' Current reading: 3 (min=0, max=3)
' Current reading: 4 (min=0, max=4)
' Current reading: 5 (min=0, max=10)
' Current reading: 5 (min=5, max=5)
End Sub
相互に依存する複数のプロパティが循環的に存在する場合、かなり複雑な依存関係のシナリオが発生する可能性があります。 技術的には、複雑な依存関係に問題はありません。ただし、多数の再評価によってパフォーマンスが低下する可能性があります。 また、UI で公開されている複雑な依存関係によって、ユーザーが混乱する可能性があります。 PropertyChangedCallback と CoerceValueCallback を可能な限り明確に扱い、過度に制約しないでください。
値の変更を取り消す
CoerceValueCallbackから UnsetValue を返すことで、プロパティ値の変更を拒否できます。 このメカニズムは、プロパティ値の変更が非同期的に開始されるが、適用されている場合に現在のオブジェクトの状態に対して有効ではなくなった場合に便利です。 別のシナリオとして、発生元に基づいて値の変更を選択的に抑制する場合があります。 次の例では、CoerceValueCallback
は GetValueSource メソッドを呼び出します。このメソッドは、新しい値のソースを識別する BaseValueSource 列挙体を持つ ValueSource 構造体を返します。
// Coerce-value callback.
private static object CoerceCurrentReading(DependencyObject depObj, object value)
{
// Get value source.
ValueSource valueSource =
DependencyPropertyHelper.GetValueSource(depObj, CurrentReadingProperty);
// Reject any property value change that's a locally set value.
return valueSource.BaseValueSource == BaseValueSource.Local ?
DependencyProperty.UnsetValue : value;
}
' Coerce-value callback.
Private Shared Function CoerceCurrentReading(depObj As DependencyObject, value As Object) As Object
' Get value source.
Dim valueSource As ValueSource =
DependencyPropertyHelper.GetValueSource(depObj, CurrentReadingProperty)
' Reject any property value that's a locally set value.
Return If(valueSource.BaseValueSource = BaseValueSource.Local, DependencyProperty.UnsetValue, value)
End Function
関連項目
.NET Desktop feedback