Why does my data binding see the real value instead of the coerced value?

人走茶凉 提交于 2019-12-18 03:16:33

问题


I'm writing a real NumericUpDown/Spinner control as an exercise to learn custom control authoring. I've got most of the behavior that I'm looking for, including appropriate coercion. One of my tests has revealed a flaw, however.

My control has 3 dependency properties: Value, MaximumValue, and MinimumValue. I use coercion to ensure that Value remains between the min and max, inclusive. E.g.:

// 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;
}

My test is just to ensure that data binding works how I expect. I created a default wpf windows application and threw in the following 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>

with very simple codebehind:

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();
    }
}

(I'm omitting the xaml for the control's presentation)

Now if I run this I see the value from the NumericUpDown reflected appropriately in the textbox, but if I type in a value that's out of range the out of range value gets displayed in the test textbox while the NumericUpDown shows the correct value.

Is this how coerced values are supposed to act? It's good that it's coerced in the ui, but I expected the coerced value to run through the databinding as well.


回答1:


Wow, that is surprising. When you set a value on a dependency property, binding expressions are updated before value coercion runs!

If you look at DependencyObject.SetValueCommon in Reflector, you can see the call to Expression.SetValue halfway through the method. The call to UpdateEffectiveValue that will invoke your CoerceValueCallback is at the very end, after the binding has already been updated.

You can see this on framework classes as well. From a new WPF application, add the following XAML:

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

and the following code:

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; }

If you drag the Slider and then click the button, you'll get a message like "Value changed from 11 to -1; Slider changed from 11 to 10".




回答2:


A new answer for an old question: :-)

In the registration of the ValueProperty an FrameworkPropertyMetadata instance is used. Set the UpdateSourceTrigger property of this instance to Explicit. This can be done in a constructor overload.

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

Now the binding source of the ValueProperty will not be updated automatically on PropertyChanged. Do the update manually in your HandleValueChanged method (see code above). This method is called only on 'real' changes of the property AFTER the coerce method has been called.

You can do it this way:

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();
}

In this way you can avoid to update your bindings with non-coerced values of your DependencyProperty.




回答3:


You are coercing v which is an int and as such a value type. It is therefore stored on the stack. It is in no way connected to baseValue. So changing v will not change baseValue.

The same logic applies to baseValue. It is passed by value (not by reference) so changing it will not change the actual parameter.

v is returned and is clearly used to update the UI.

You may want to investigate changing the properties data type to a reference type. Then it will be passed by reference and any changes made will reflect back to source. Assuming the databinding process does not create a copy.



来源:https://stackoverflow.com/questions/3323461/why-does-my-data-binding-see-the-real-value-instead-of-the-coerced-value

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!