Devoluciones de llamada y validación de las propiedades de dependencia

En este tema se describe cómo crear propiedades de dependencia mediante implementaciones personalizadas alternativas de características relacionadas con las propiedades, como la determinación de la validación, las devoluciones de llamada que se invocan cuando cambia el valor efectivo de la propiedad y la invalidación de posibles influencias externas en la determinación del valor. En este tema también se describen los escenarios donde es apropiado expandir los comportamientos predeterminados del sistema de propiedades mediante estas técnicas.

Requisitos previos

En este tema se supone que entiende los escenarios básicos de la implementación de una propiedad de dependencia y cómo se aplican los metadatos a una propiedad de dependencia personalizada. Consulte Propiedades de dependencia personalizadas y Metadatos de las propiedades de dependencia para obtener contexto.

Devoluciones de llamada de validación

Las devoluciones de llamada de validación pueden asignarse a una propiedad de dependencia cuando se registra por primera vez. La devolución de llamada de validación no forma parte de los metadatos de las propiedades; es una entrada directa del método Register. Por lo tanto, una vez creada una devolución de llamada de validación para una propiedad de dependencia, una nueva implementación no puede invalidarla.

public static readonly DependencyProperty CurrentReadingProperty = DependencyProperty.Register(
    "CurrentReading",
    typeof(double),
    typeof(Gauge),
    new FrameworkPropertyMetadata(
        Double.NaN,
        FrameworkPropertyMetadataOptions.AffectsMeasure,
        new PropertyChangedCallback(OnCurrentReadingChanged),
        new CoerceValueCallback(CoerceCurrentReading)
    ),
    new ValidateValueCallback(IsValidReading)
);
public double CurrentReading
{
  get { return (double)GetValue(CurrentReadingProperty); }
  set { SetValue(CurrentReadingProperty, value); }
}
Public Shared ReadOnly CurrentReadingProperty As DependencyProperty =
    DependencyProperty.Register("CurrentReading",
        GetType(Double), GetType(Gauge),
        New FrameworkPropertyMetadata(Double.NaN,
            FrameworkPropertyMetadataOptions.AffectsMeasure,
            New PropertyChangedCallback(AddressOf OnCurrentReadingChanged),
            New CoerceValueCallback(AddressOf CoerceCurrentReading)),
        New ValidateValueCallback(AddressOf IsValidReading))

Public Property CurrentReading() As Double
    Get
        Return CDbl(GetValue(CurrentReadingProperty))
    End Get
    Set(ByVal value As Double)
        SetValue(CurrentReadingProperty, value)
    End Set
End Property

Las devoluciones de llamada se implementan de forma que obtienen un valor de objeto. Devuelven true si el valor proporcionado es válido para la propiedad; en caso contrario, devuelven false. Se supone que la propiedad es del tipo correcto por el tipo registrado con el sistema de propiedades, por lo que la comprobación de tipo en las devoluciones de llamada no se suele llevar a cabo. El sistema de propiedades usa las devoluciones de llamada en distintas operaciones. Se incluyen la inicialización de tipos inicial por valor predeterminado, el cambio mediante programación invocando SetValue o los intentos de invalidación de metadatos con el nuevo valor predeterminado proporcionado. Si cualquiera de estas operaciones invoca la devolución de llamada de validación y devuelve false, se generará una excepción. Los escritores de aplicaciones deben estar preparados para controlar estas excepciones. Un uso común de las devoluciones de llamada de validación es validar los valores de enumeración o restringir los valores de números enteros o dobles cuando la propiedad establece medidas que deben ser cero o un valor superior.

Las devoluciones de llamada de validación están diseñadas específicamente como validadores de clase, no como validadores de instancia. Los parámetros de la devolución de llamada no comunican un determinado elemento DependencyObject en el que se establecen las propiedades que se van a validar. Por lo tanto, las devoluciones de llamada de validación no resultan útiles para aplicar las posibles "dependencias" que podrían influir en un valor de propiedad, donde el valor específico de la instancia de una propiedad depende de factores, tales como los valores específicos de la instancia de otras propiedades o el estado de tiempo de ejecución.

El siguiente es un ejemplo de código para un escenario de validación muy simple: validar que una propiedad que está tipada como la primitiva Double no es PositiveInfinity o NegativeInfinity.

public static bool IsValidReading(object value)
{
    Double v = (Double)value;
    return (!v.Equals(Double.NegativeInfinity) && !v.Equals(Double.PositiveInfinity));
}
Public Shared Function IsValidReading(ByVal value As Object) As Boolean
    Dim v As Double = CType(value, Double)
    Return ((Not v.Equals(Double.NegativeInfinity)) AndAlso
            (Not v.Equals(Double.PositiveInfinity)))
End Function

Devoluciones de llamada de valor de coerción y eventos de propiedad cambiada

Las devoluciones de llamada de valor de coerción pasan la instancia DependencyObject específica para las propiedades, del mismo modo que las implementaciones PropertyChangedCallback que invoca el sistema de propiedades siempre que cambia el valor de una propiedad de dependencia. Con estas dos devoluciones de llamada combinadas, puede crear una serie de propiedades en aquellos elementos donde los cambios en una propiedad forzarán una coerción o reevaluación de otra propiedad.

Un escenario típico para usar la vinculación de propiedades de dependencia es cuando tiene una propiedad controlada por la interfaz de usuario donde el elemento contiene una propiedad para el valor mínimo y otra para el máximo, además de una tercera propiedad para el valor real o actual. Aquí, si el máximo está ajustado de manera que el valor actual supera el nuevo máximo, querrá forzar el valor actual para que no supere el nuevo máximo, además de una relación similar entre los valores mínimo y actual.

El siguiente es un código de ejemplo muy breve para una sola de las tres propiedades de dependencia que ilustran esta relación. En el ejemplo se muestra cómo está registrada la propiedad CurrentReading de un conjunto Min/Max/Current de propiedades *Reading relacionadas. Usa la validación como se muestra en la sección anterior.

public static readonly DependencyProperty CurrentReadingProperty = DependencyProperty.Register(
    "CurrentReading",
    typeof(double),
    typeof(Gauge),
    new FrameworkPropertyMetadata(
        Double.NaN,
        FrameworkPropertyMetadataOptions.AffectsMeasure,
        new PropertyChangedCallback(OnCurrentReadingChanged),
        new CoerceValueCallback(CoerceCurrentReading)
    ),
    new ValidateValueCallback(IsValidReading)
);
public double CurrentReading
{
  get { return (double)GetValue(CurrentReadingProperty); }
  set { SetValue(CurrentReadingProperty, value); }
}
Public Shared ReadOnly CurrentReadingProperty As DependencyProperty =
    DependencyProperty.Register("CurrentReading",
        GetType(Double), GetType(Gauge),
        New FrameworkPropertyMetadata(Double.NaN,
            FrameworkPropertyMetadataOptions.AffectsMeasure,
            New PropertyChangedCallback(AddressOf OnCurrentReadingChanged),
            New CoerceValueCallback(AddressOf CoerceCurrentReading)),
        New ValidateValueCallback(AddressOf IsValidReading))

Public Property CurrentReading() As Double
    Get
        Return CDbl(GetValue(CurrentReadingProperty))
    End Get
    Set(ByVal value As Double)
        SetValue(CurrentReadingProperty, value)
    End Set
End Property

La devolución de llamada de la propiedad cambiada Current se usa para reenviar el cambio a otras propiedades dependientes, mediante la invocación explícita de las devoluciones de llamada del valor de coerción registradas para esas otras propiedades:

private static void OnCurrentReadingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
  d.CoerceValue(MinReadingProperty);
  d.CoerceValue(MaxReadingProperty);
}
Private Shared Sub OnCurrentReadingChanged(ByVal d As DependencyObject, ByVal e As DependencyPropertyChangedEventArgs)
    d.CoerceValue(MinReadingProperty)
    d.CoerceValue(MaxReadingProperty)
End Sub

La devolución de llamada de valor de coerción comprueba los valores de propiedades de las que depende potencialmente la propiedad actual y fuerza el valor actual si es necesario:

private static object CoerceCurrentReading(DependencyObject d, object value)
{
  Gauge g = (Gauge)d;
  double current = (double)value;
  if (current < g.MinReading) current = g.MinReading;
  if (current > g.MaxReading) current = g.MaxReading;
  return current;
}
Private Shared Function CoerceCurrentReading(ByVal d As DependencyObject, ByVal value As Object) As Object
    Dim g As Gauge = CType(d, Gauge)
    Dim current As Double = CDbl(value)
    If current < g.MinReading Then
        current = g.MinReading
    End If
    If current > g.MaxReading Then
        current = g.MaxReading
    End If
    Return current
End Function

Nota:

Los valores predeterminados de las propiedades no se fuerzan. Un valor de propiedad igual al valor predeterminado se puede producir si un valor de propiedad sigue teniendo su valor predeterminado inicial o si se borran otros valores con ClearValue.

Las devoluciones de llamada de valor de coerción y propiedad cambiada forman parte de los metadatos de las propiedades. Por lo tanto, puede cambiar las devoluciones de llamada de una propiedad de dependencia determinada, tal como existe en un tipo que se deriva del tipo que posee la propiedad de dependencia, mediante la invalidación de los metadatos de esa propiedad en el tipo.

Escenarios de coerción y llamada de devolución avanzados

Restricciones y valores deseados

El sistema de propiedades usará las llamadas de devolución CoerceValueCallback para forzar un valor de acuerdo con la lógica declarada, pero un valor forzado de una propiedad establecida localmente conservará un "valor deseado" internamente. Si las restricciones se basan en otros valores de propiedad que pueden cambiar dinámicamente durante el ciclo de vida de la aplicación, las restricciones de coerción también cambian de forma dinámica y la propiedad restringida puede cambiar su valor para que se aproxime al valor deseado lo máximo posible según las nuevas restricciones. El valor se convertirá en el valor deseado si se levantan todas las restricciones. Se pueden introducir algunos escenarios de dependencia complejos en el caso de tener varias propiedades que sean dependientes entre sí de manera circular. Por ejemplo, en el escenario Min/Max/Current, puede elegir que las propiedades Minimum y Maximum las pueda establecer el usuario. En este caso, es posible que deba forzar que Maximum sea siempre mayor que Minimum y viceversa. No obstante, si esa coerción está activa y la propiedad Maximum fuerza la propiedad Minimum, Current no se puede establecer, ya que depende de ambas y está restringida al intervalo entre los valores, que es cero. A continuación, si se ajustan las propiedades Maximum o Minimum, parecerá que Current "sigue" a uno de los valores, ya que el valor deseado de Current sigue estando almacenado e intenta alcanzar el valor deseado a medida que se suavizan las restricciones.

Técnicamente, no hay nada incorrecto en las dependencias complejas, pero pueden suponer un ligero deterioro del rendimiento si requieren grandes cantidades de reevaluaciones y también pueden ser confusas para los usuarios si afectan a la interfaz de usuario directamente. Tenga cuidado con las devoluciones de llamada de propiedad cambiada y valor de coerción, y asegúrese de que la coerción que se está intentando pueda tratarse de la manera más ambigua posible y no sea "sobrerrestrictiva".

Uso de CoerceValue para cancelar cambios de valor

El sistema de propiedades tratará cualquier CoerceValueCallback que devuelva el valor UnsetValue como un caso especial. Este caso especial significa que el sistema de propiedades debería rechazar el cambio de propiedad que produjo el método CoerceValueCallback al que se llama y, en su lugar, debería notificar cualquier valor anterior que tuviera la propiedad. Este mecanismo puede ser útil para comprobar que los cambios en una propiedad iniciados de manera asincrónica siguen siendo válidos para el estado actual del objeto y suprimirlos si no lo son. Otro escenario posible es la posibilidad de suprimir de manera selectiva un valor según el componente de determinación del valor propiedad que sea responsable del valor comunicado. Para ello, puede utilizar el DependencyProperty pasado en la devolución de llamada y el identificador de la propiedad como entrada para el GetValueSource, y luego procesar el ValueSource.

Vea también