Zpětná volání a ověřování vlastností závislostí (WPF .NET)
Tento článek popisuje, jak definovat vlastnost závislosti a implementovat zpětná volání vlastností závislostí. Zpětná volání podporují ověřování hodnot, převod hodnot a další logiku, která je nutná při změně hodnoty vlastnosti.
Požadavky
V článku se předpokládá základní znalost vlastností závislostí a že jste si přečetli přehled vlastností závislostí. Pokud chcete postupovat podle příkladů v tomto článku, pomůže vám to, pokud znáte jazyk XAML (Extensible Application Markup Language) a víte, jak psát aplikace WPF.
Zpětná volání pro ověření hodnoty
Zpětné volání typu Validate-value poskytují způsob, jak zkontrolovat, jestli je nová hodnota vlastnosti závislosti platná před použitím systému vlastností. Toto zpětné volání vyvolá výjimku, pokud hodnota nesplňuje ověřovací kritéria.
Zpětné volání typu Validate-value lze při registraci vlastnosti vlastnosti závislosti přiřadit pouze jednou. Při registraci vlastnosti závislosti máte možnost předat ValidateValueCallback odkaz na metodu Register(String, Type, Type, PropertyMetadata, ValidateValueCallback) . Zpětná volání typu Validate-value nejsou součástí metadat vlastností a nelze je přepsat.
Platná hodnota vlastnosti závislosti je její použitá hodnota. Efektivní hodnota je určena prostřednictvím priority hodnoty vlastnosti, pokud existuje více vstupů založených na vlastnostech. Pokud je zpětné volání typu validate-value registrováno pro vlastnost závislosti, systém vlastností vyvolá zpětné volání ověření-hodnota při změně hodnoty a předá novou hodnotu jako objekt. V rámci zpětného volání můžete přetypovat objekt hodnoty zpět na typ zaregistrovaný v systému vlastností a pak na něj spustit ověřovací logiku. Zpětné volání vrátí true
, pokud je hodnota platná pro vlastnost, jinak false
.
Pokud zpětné volání typu validate-value vrátí false
, vyvolá se výjimka a nová hodnota se nepoužije. Zapisovače aplikací musí být připravené na zpracování těchto výjimek. Běžným použitím zpětných volání ověřovacích hodnot je ověřování hodnot výčtu nebo omezení číselných hodnot, pokud představují hodnoty, které mají omezení. Zpětné volání pro ověření hodnoty je vyvoláno systémem vlastností v různých scénářích, mezi které patří:
- Inicializace objektů, která použije výchozí hodnotu při vytváření.
- Programová volání .SetValue
- Metadata přepisují, které určují novou výchozí hodnotu.
Zpětné volání typu Validate-value nemají parametr, který určuje DependencyObject instanci, na které je nastavena nová hodnota. Všechny instance DependencyObject
sdílené složky mají stejné zpětné volání typu validate-value, takže ho nelze použít k ověření scénářů specifických pro instanci. Další informace najdete na webu ValidateValueCallback.
Následující příklad ukazuje, jak zabránit vlastnosti, typ jako Double, být nastaven na PositiveInfinity nebo 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
Zpětné volání změněné vlastností
Zpětné volání změněné vlastností vás upozorní, když se změní efektivní hodnota vlastnosti závislosti.
Zpětná volání změněná vlastnostmi jsou součástí metadat vlastností závislostí. Pokud odvozujete z třídy, která definuje vlastnost závislosti, nebo přidáte třídu jako vlastníka vlastnosti závislosti, můžete přepsat metadata. Při přepsání metadat máte možnost zadat nový PropertyChangedCallback odkaz. Zpětné volání změněné vlastností použijte ke spuštění logiky, která je nutná při změně hodnoty vlastnosti.
Na rozdíl od zpětných volání typu validate-value mají zpětné volání změněné vlastností parametr, který určuje DependencyObject instanci, na které je nastavena nová hodnota. Další příklad ukazuje, jak může zpětné volání změněné vlastností použít DependencyObject
odkaz na instanci k aktivaci zpětného volání coerce-hodnota.
Zpětná volání coerce-hodnota
Zpětné volání coerce-hodnota poskytují způsob, jak získat oznámení, když efektivní hodnota vlastnosti závislosti se bude měnit, takže můžete upravit novou hodnotu před jejím použití. Kromě toho, že se aktivuje systémem vlastností, můžete z kódu vyvolat zpětné volání zpětného volání s hodnotou coerce.
Zpětná volání coerce-hodnota jsou součástí metadat vlastností závislostí. Pokud odvozujete z třídy, která definuje vlastnost závislosti, nebo přidáte třídu jako vlastníka vlastnosti závislosti, můžete přepsat metadata. Při přepsání metadat máte možnost poskytnout odkaz na nový CoerceValueCallback. Pomocí zpětného volání coerce-hodnota vyhodnoťte nové hodnoty a v případě potřeby je vynutíte. Zpětné volání vrátí hodnotu v případě, že došlo k převodu, v opačném případě vrátí nealterovanou novou hodnotu.
Podobně jako zpětné volání změněné vlastnostmi mají zpětné volání zpětného volání coerce-value parametr, který určuje DependencyObject instanci, pro kterou je nová hodnota nastavena. Další příklad ukazuje, jak zpětné volání coerce-value může použít DependencyObject
odkaz instance na hodnoty vlastnosti coerce.
Poznámka:
Výchozí hodnoty vlastností nelze přetěžovat. Vlastnost závislosti má výchozí hodnotu nastavenou na inicializaci objektu nebo když vymažete jiné hodnoty pomocí ClearValue.
Zpětná volání se změněnou hodnotou a vlastnostmi v kombinaci
Závislosti mezi vlastnostmi v elementu můžete vytvořit pomocí zpětného volání zpětného volání v coerce-hodnota a změněné vlastnosti zpětné volání v kombinaci. Například změny v jedné vlastnosti vynucení nebo opětovné vyhodnocení v jiné vlastnosti závislosti. Následující příklad ukazuje běžný scénář: tři vlastnosti závislosti, které ukládají aktuální hodnotu, minimální hodnotu a maximální hodnotu prvku uživatelského rozhraní. Pokud se maximální hodnota změní tak, aby byla menší než aktuální hodnota, nastaví se aktuální hodnota na novou maximální hodnotu. A pokud se minimální hodnota změní tak, aby byla větší než aktuální hodnota, aktuální hodnota se nastaví na novou minimální hodnotu. V příkladu PropertyChangedCallback aktuální hodnota explicitně vyvolá CoerceValueCallback pro minimální a maximální hodnoty.
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
Pokročilé scénáře zpětného volání
Omezení a požadované hodnoty
Pokud se místně nastavená hodnota vlastnosti závislosti změní prostřednictvím převodu, zachová se nezměněná místně nastavená hodnota jako požadovaná hodnota. Pokud je převod založen na jiných hodnotách vlastností, systém vlastností bude dynamicky přehodnotit převod pokaždé, když se tyto ostatní hodnoty změní. V rámci omezení převodu použije systém vlastností hodnotu, která je nejblíže požadované hodnotě. Pokud se podmínka převodu už nepoužije, systém vlastností obnoví požadovanou hodnotu – za předpokladu, že není aktivní žádná hodnota vyšší priority . Následující příklad testuje převod v aktuální hodnotě, minimální hodnotě a maximální hodnotě scénáře.
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
K poměrně složitým scénářům závislostí může dojít v případě, že máte více vlastností závislých na sobě cyklicky. Technicky vzato není nic špatného u složitých závislostí – s tím rozdílem, že velký počet opakovaných vyhodnocení může snížit výkon. Složité závislosti, které jsou vystaveny v uživatelském rozhraní, mohou také zmást uživatele. Zacházejte s PropertyChangedCallback co nejjekvněji a CoerceValueCallback nepřetěžujte omezení.
Zrušení změn hodnot
Vrácením UnsetValue z objektu CoerceValueCallbackmůžete odmítnout změnu hodnoty vlastnosti. Tento mechanismus je užitečný, pokud je změna hodnoty vlastnosti inicializována asynchronně, ale když je použita, již není platná pro aktuální stav objektu. Dalším scénářem může být selektivní potlačení změny hodnoty na základě místa, odkud pochází. V následujícím příkladu CoerceValueCallback
volá metodu GetValueSource , která vrací ValueSource strukturu s výčtem BaseValueSource , který identifikuje zdroj nové hodnoty.
// 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
Viz také
.NET Desktop feedback