Devoluciones de llamada y validación de propiedades de dependencia (WPF .NET)
En este artículo se describe cómo definir una propiedad de dependencia e implementar devoluciones de llamada de propiedades de dependencia. Las devoluciones de llamada admiten la validación de valores, la coerción de valores y otra lógica necesaria cuando cambia un valor de propiedad.
En el artículo se da por supuesto un conocimiento básico de las propiedades de dependencia y que ha leído Información general sobre las propiedades de dependencia. Para seguir los ejemplos de este artículo, resultará útil que esté familiarizado con el lenguaje XAML y que sepa cómo escribir aplicaciones WPF.
Las devoluciones de llamada de validación de valor proporcionan una manera de comprobar si un nuevo valor de propiedad de dependencia es válido antes de que el sistema de propiedades lo aplique. Esta devolución de llamada genera una excepción si el valor no cumple los criterios de validación.
Las devoluciones de llamada de validación de valor solo se pueden asignar a una propiedad de dependencia una vez, durante el registro de propiedades. Al registrar una propiedad de dependencia, tiene la opción de pasar una referencia ValidateValueCallback al método Register(String, Type, Type, PropertyMetadata, ValidateValueCallback). Las devoluciones de llamada de validación de valor no forman parte de los metadatos de la propiedad y no se pueden invalidar.
El valor efectivo de una propiedad de dependencia es su valor aplicado. El valor efectivo se determina mediante la precedencia de valores de propiedad cuando existen varias entradas basadas en propiedades. Si se registra una devolución de llamada de validación de valor para una propiedad de dependencia, el sistema de propiedades invocará su devolución de llamada de validación de valor al cambiar el valor, y pasará el nuevo valor como un objeto. Dentro de la devolución de llamada, puede convertir el objeto de valor al tipo registrado con el sistema de propiedades y, a continuación, ejecutar la lógica de validación en él. La devolución de llamada devuelve true
si el valor es válido para la propiedad; de lo contrario, devuelve false
.
Si una devolución de llamada de validación de valor devuelve false
, se genera una excepción y no se aplica el nuevo valor. Los escritores de aplicaciones deben estar preparados para controlar estas excepciones. Un uso común de las devoluciones de llamada de validación de valor es validar los valores de enumeración o restringir los valores numéricos cuando representan medidas que tienen límites. El sistema de propiedades invoca las devoluciones de llamada de validación de valor en diferentes escenarios, entre los que se incluyen:
- Inicialización de objetos, que aplica un valor predeterminado en el momento de la creación.
- Llamadas mediante programación a SetValue.
- Invalidaciones de metadatos que especifican un nuevo valor predeterminado.
Las devoluciones de llamada de validación de valor no tienen un parámetro que especifique la instancia DependencyObject en la que se establece el nuevo valor. Todas las instancias de un objeto DependencyObject
comparten la misma devolución de llamada de validación de valor, por lo que no se puede usar para validar escenarios específicos de la instancia. Para obtener más información, vea ValidateValueCallback.
En el ejemplo siguiente se muestra cómo evitar que una propiedad, con tipo Double, se establezca en PositiveInfinity o 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
Las devoluciones de llamada de cambio de propiedad le envían una notificación cuando cambia el valor efectivo de una propiedad de dependencia.
Las devoluciones de llamada de cambio de propiedad forman parte de los metadatos de la propiedad de dependencia. Si deriva de una clase que define una propiedad de dependencia o agrega la clase como propietario de una propiedad de dependencia, puede invalidar los metadatos. Al invalidar los metadatos, tiene la opción de proporcionar una nueva referencia PropertyChangedCallback. Use una devolución de llamada de cambio de propiedad para ejecutar la lógica necesaria cuando cambia un valor de propiedad.
A diferencia de las devoluciones de llamada de validación de valor, las devoluciones de llamada de cambio de propiedad tienen un parámetro que especifica la instancia DependencyObject en la que se establece el nuevo valor. En el ejemplo siguiente se muestra cómo una devolución de llamada de cambio de propiedad puede usar la referencia de instancia DependencyObject
para desencadenar devoluciones de llamada de coerción de valor.
Las devoluciones de llamada de coerción de valor proporcionan una manera de recibir notificaciones cuando el valor efectivo de una propiedad de dependencia está a punto de cambiar, de modo que pueda ajustar el nuevo valor antes de aplicarlo. Además de ser desencadenadas por el sistema de propiedades, puede invocar devoluciones de llamada de coerción de valor desde el código.
Las devoluciones de llamada de coerción de valor forman parte de los metadatos de la propiedad de dependencia. Si deriva de una clase que define una propiedad de dependencia o agrega la clase como propietario de una propiedad de dependencia, puede invalidar los metadatos. Al invalidar los metadatos, tiene la opción de proporcionar una referencia a un nuevo elemento CoerceValueCallback. Use una devolución de llamada de coerción de valor para evaluar nuevos valores y coercerlos cuando sea necesario. La devolución de llamada devuelve el valor coercido si se ha producido la coerción; de lo contrario, devuelve el nuevo valor sin modificar.
De forma parecida a las devoluciones de llamada de cambio de propiedad, las devoluciones de llamada de coerción de valor tienen un parámetro que especifica la instancia DependencyObject en la que se establece el nuevo valor. En el ejemplo siguiente se muestra cómo una devolución de llamada de coerción de valor puede usar una referencia de instancia DependencyObject
para coercer valores de propiedad.
Nota
Los valores de propiedad predeterminados no se pueden coercer. Una propiedad de dependencia tiene su valor predeterminado establecido en la inicialización de objetos o cuando se borran otros valores mediante ClearValue.
Puede crear dependencias entre las propiedades de un elemento mediante el uso combinado de devoluciones de llamada de coerción de valor y devoluciones de llamada de cambio de propiedad. Por ejemplo, los cambios en una propiedad fuerzan la coerción o la reevaluación en otra propiedad de dependencia. En el ejemplo siguiente se muestra un escenario común: tres propiedades de dependencia que almacenan respectivamente el valor actual, el valor mínimo y el valor máximo de un elemento de la interfaz de usuario. Si el valor máximo cambia de modo que sea menor que el valor actual, el valor actual se establece en el nuevo valor máximo. Y, si el valor mínimo cambia de modo que sea mayor que el valor actual, el valor actual se establece en el nuevo valor mínimo. En el ejemplo, el elemento PropertyChangedCallback del valor actual invoca explícitamente el elemento CoerceValueCallback de los valores mínimo y máximo.
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
Si se cambia un valor establecido localmente de una propiedad de dependencia mediante la coerción, el valor establecido localmente sin modificar se conserva como el valor deseado. Si la coerción se basa en otros valores de propiedad, el sistema de propiedades volverá a evaluar dinámicamente la coerción cada vez que cambien esos otros valores. Dentro de las restricciones de la coerción, el sistema de propiedades aplicará el valor más cercano al valor deseado. Si la condición de coerción ya no se aplica, el sistema de propiedades restaurará el valor deseado, suponiendo que no haya ningún valor de precedencia superior activo. En el ejemplo siguiente, se prueba la coerción en el escenario de valor actual, valor mínimo y valor máximo.
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
Se pueden producir escenarios de dependencia bastante complejos cuando hay varias propiedades que dependen unas de otras de forma circular. Técnicamente, no hay nada malo en las dependencias complejas, excepto que un gran número de reevaluaciones puede reducir el rendimiento. Además, las dependencias complejas que se exponen en la interfaz de usuario pueden confundir a los usuarios. Trate PropertyChangedCallback y CoerceValueCallback con la mínima ambigüedad posible y no aplique demasiadas restricciones.
Al devolver UnsetValue de CoerceValueCallback, puede rechazar un cambio de un valor de propiedad. Este mecanismo es útil cuando se inicia un cambio de valor de propiedad de forma asincrónica, pero cuando se aplica ya no es válido para el estado de objeto actual. Otro escenario podría ser suprimir selectivamente un cambio de valor en función de dónde se originó. En el ejemplo siguiente, CoerceValueCallback
llama al método GetValueSource, que devuelve una estructura ValueSource con una enumeración BaseValueSource que identifica el origen del nuevo valor.
// 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
Comentarios de .NET Desktop feedback
.NET Desktop feedback es un proyecto de código abierto. Seleccione un vínculo para proporcionar comentarios: