2010-07-24 17 views
9

Estoy escribiendo un control real NumericUpDown/Spinner como un ejercicio para aprender la creación de control personalizado. Tengo la mayor parte del comportamiento que estoy buscando, incluida la coerción adecuada. Sin embargo, una de mis pruebas ha revelado un defecto.¿Por qué mi enlace de datos ve el valor real en lugar del valor coercionado?

Mi control tiene 3 propiedades de dependencia: Value, MaximumValue y MinimumValue. Utilizo la coerción para asegurar que Value permanezca entre el mínimo y el máximo, inclusive. Por ejemplo:

// In NumericUpDown.cs 

public static readonly DependencyProperty ValueProperty = 
    DependencyProperty.Register("Value", typeof(int), typeof(NumericUpDown), 
    new FrameworkPropertyMetadata(0, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault | FrameworkPropertyMetadataOptions.Journal, HandleValueChanged, HandleCoerceValue)); 

[Localizability(LocalizationCategory.Text)] 
public int Value 
{ 
    get { return (int)this.GetValue(ValueProperty); } 
    set { this.SetCurrentValue(ValueProperty, value); } 
} 

private static object HandleCoerceValue(DependencyObject d, object baseValue) 
{ 
    NumericUpDown o = (NumericUpDown)d; 
    var v = (int)baseValue; 

    if (v < o.MinimumValue) v = o.MinimumValue; 
    if (v > o.MaximumValue) v = o.MaximumValue; 

    return v; 
} 

Mi prueba es solo para garantizar que el enlace de datos funciona como yo esperaba. He creado un defecto WPF uso de las ventanas y la tiré en el siguiente XAML:

<Window x:Class="WpfApplication.MainWindow" x:Name="This" 
     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
     xmlns:nud="clr-namespace:WpfCustomControlLibrary;assembly=WpfCustomControlLibrary" 
     Title="MainWindow" Height="350" Width="525"> 
    <Grid> 
     <Grid.RowDefinitions> 
      <RowDefinition /> 
      <RowDefinition /> 
     </Grid.RowDefinitions> 
     <nud:NumericUpDown Value="{Binding ElementName=This, Path=NumberValue}"/> 
     <TextBox Grid.Row="1" Text="{Binding ElementName=This, Path=NumberValue, Mode=OneWay}" /> 
    </Grid> 
</Window> 

con muy simple código subyacente:

public partial class MainWindow : Window 
{ 
    public int NumberValue 
    { 
     get { return (int)GetValue(NumberValueProperty); } 
     set { SetCurrentValue(NumberValueProperty, value); } 
    } 

    // Using a DependencyProperty as the backing store for NumberValue. This enables animation, styling, binding, etc... 
    public static readonly DependencyProperty NumberValueProperty = 
     DependencyProperty.Register("NumberValue", typeof(int), typeof(MainWindow), new UIPropertyMetadata(0));  

    public MainWindow() 
    { 
     InitializeComponent(); 
    } 
} 

(estoy omitiendo el XAML para la presentación del control)

Ahora si ejecuto esto veo el valor del NumericUpDown reflejado apropiadamente en el cuadro de texto, pero si escribo un valor fuera de rango, el valor fuera de rango se muestra en el cuadro de texto de prueba mientras que el NumericUpDown muestra el valor correcto.

¿Es así como se supone que actúan los valores de coerción? Es bueno que se forzara en la interfaz de usuario, pero también esperaba que el valor forzado se ejecutara a través de la unión de datos.

+0

Parece que la encuadernación no es compatible con el val. ¿Has probado el modo diff en TextBox? –

Respuesta

9

Guau, eso es sorprendente. Cuando establece un valor en una propiedad de dependencia, las expresiones de enlace se actualizan antes de que se ejecute la coerción de valor.

Si observa DependencyObject.SetValueCommon en Reflector, puede ver la llamada a Expression.SetValue a la mitad del método. La llamada a UpdateEffectiveValue que invocará su CoerceValueCallback está al final, después de que el enlace ya se haya actualizado.

Puede ver esto en las clases de framework también. A partir de una nueva aplicación de WPF, añada el siguiente código XAML:

<StackPanel> 
    <Slider Name="Slider" Minimum="10" Maximum="20" Value="{Binding Value, 
     RelativeSource={RelativeSource AncestorType=Window}}"/> 
    <Button Click="SetInvalid_Click">Set Invalid</Button> 
</StackPanel> 

y el siguiente código:

private void SetInvalid_Click(object sender, RoutedEventArgs e) 
{ 
    var before = this.Value; 
    var sliderBefore = Slider.Value; 
    Slider.Value = -1; 
    var after = this.Value; 
    var sliderAfter = Slider.Value; 
    MessageBox.Show(string.Format("Value changed from {0} to {1}; " + 
     "Slider changed from {2} to {3}", 
     before, after, sliderBefore, sliderAfter)); 
} 

public int Value { get; set; } 

Si arrastra el deslizador y luego clic en el botón, obtendrá un mensaje como "Valor cambió de 11 a -1; el control deslizante cambió de 11 a 10 ".

+1

Interesante, por lo que este es el comportamiento esperado. Supongo que mi siguiente paso es descubrir cómo proporcionar una propiedad enlazable 'ActualValue' que refleje lo que realmente se muestra en NumericUpDown. ¿Conoces una manera mejor que simplemente reevaluar la coerción en un controlador de propiedad modificado y establecer 'ActualValue' allí? –

+0

Creo que mereces más de un voto positivo por la respuesta. –

-1

Está forzando v que es un tipo de valor int y como tal. Por lo tanto, se almacena en la pila. No está de ninguna manera conectado a baseValue. Entonces, cambiar v no cambiará a baseValue.

La misma lógica se aplica a baseValue. Se pasa por valor (no por referencia) por lo que cambiarlo no cambiará el parámetro real.

v se devuelve y se utiliza claramente para actualizar la interfaz de usuario.

Es posible que desee investigar el cambio del tipo de datos de propiedades a un tipo de referencia. Luego se pasará por referencia y cualquier cambio realizado se reflejará en la fuente. Suponiendo que el proceso de enlace de datos no crea una copia.

+0

¿No aparece en la caja si se devuelve como Objeto del método de coerción? –

1

Una nueva respuesta a una vieja pregunta: :-)

En el registro de la ValueProperty una instancia FrameworkPropertyMetadata se utiliza. Establezca la propiedad UpdateSourceTrigger de esta instancia en Explicit. Esto se puede hacer en una sobrecarga de constructor.

public static readonly DependencyProperty ValueProperty = 
    DependencyProperty.Register("Value", typeof(int), typeof(NumericUpDown), 
    new FrameworkPropertyMetadata(
     0, 
     FrameworkPropertyMetadataOptions.BindsTwoWayByDefault | FrameworkPropertyMetadataOptions.Journal, 
     HandleValueChanged, 
     HandleCoerceValue, 
     false 
     UpdateSourceTrigger.Explicit)); 

Ahora la fuente de unión de la ValueProperty no se actualizará automáticamente en PropertyChanged. Realice la actualización manualmente en su método HandleValueChanged (vea el código anterior). Este método se llama solo en los cambios 'reales' de la propiedad DESPUÉS de que se haya llamado al método de coerción.

Usted puede hacerlo de esta manera:

static void HandleValueChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args) 
{ 
    NumericUpDown nud = obj as NumericUpDown; 
    if (nud == null) 
     return; 

    BindingExpression be = nud.GetBindingExpression(NumericUpDown.ValueProperty); 
    if(be != null) 
     be.UpdateSource(); 
} 

De esta manera se puede evitar poner al día sus ataduras con valores no coaccionada de su DependencyProperty.

+0

Buena solución. :) –

Cuestiones relacionadas