问题 为什么我的数据绑定会看到真实值而不是强制值?


我写的是真的 NumericUpDown/Spinner 控制作为练习学习自定义控件创作。我有大部分的行为,我正在寻找,包括适当的强制。然而,我的一项测试揭示了一个缺陷。

我的控件有3个依赖属性: ValueMaximumValue,和 MinimumValue。我用强制来确保这一点 Value 保持在最小值和最大值之间,包括在内。例如。:

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

我的测试只是为了确保数据绑定符合我的预期。我创建了一个默认的wpf windows应用程序,并引入了以下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>

非常简单的代码隐藏:

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

(我省略了控件演示的xaml)

现在如果我运行它,我会看到来自的值 NumericUpDown 在文本框中正确反映,但如果我输入的值超出范围,则超出范围的值会显示在测试文本框中,而 NumericUpDown 显示正确的值。

这是强制价值应该如何行动的?在ui中强制它是好的,但我也期望强制值也可以通过数据绑定。


5886
2017-07-24 01:36


起源

似乎绑定不支持承认val。你在TextBox中尝试过diff模式吗? - Lukasz Madon


答案:


哇,这很令人惊讶。在依赖项属性上设置值时,绑定表达式会在值强制运行之前更新!

如果查看Reflector中的DependencyObject.SetValueCommon,可以在方法的中途看到对Expression.SetValue的调用。在绑定已经更新之后,对将调用CoerceValueCallback的UpdateEffectiveValue的调用处于最后。

您也可以在框架类上看到这一点。从新的WPF应用程序中,添加以下XAML:

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

和以下代码:

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

如果您拖动滑块然后单击按钮,您将收到一条消息,如“值从11更改为-1;滑块从11更改为10”。


10
2017-07-24 02:53



有趣,所以这是预期的行为。我想我的下一步是弄清楚如何提供一个可绑定的 ActualValue 反映NumericUpDown中实际显示内容的属性。您是否知道一种更好的方法,而不仅仅是重新评估属性更改处理程序和设置中的强制 ActualValue 那里? - Greg D
我认为你应该得到一个以上的答案。 - Greg D


旧问题的新答案:-)

在注册 ValueProperty 一个 FrameworkPropertyMetadata 使用了实例。设置 UpdateSourceTrigger 此实例的属性为 Explicit。这可以在构造函数重载中完成。

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

现在的绑定源 ValueProperty 不会自动更新 PropertyChanged。 在您的手动更新 HandleValueChanged 方法(见上面的代码)。 在调用强制方法之后,仅在属性的“实际”更改时调用此方法。

你可以这样做:

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

通过这种方式,您可以避免使用DependencyProperty的非强制值更新绑定。


4
2017-08-05 11:45



好的解决方法。 :) - Greg D
旧答案的新问题:)。我有这个问题,但“GetBindingExpression”返回null。当价值受到约束时,我正试图强迫!它在用户设置值时有效。所以必须有一些上下文问题(没有完全加载或其他)?有什么指针吗? - Werner


你是在胁迫 v 这是一个int和这样的值类型。因此它存储在堆栈中。它绝不是与之相关的 baseValue。所以改变v不会改变baseValue。

同样的逻辑适用于baseValue。它是通过值(而不是通过引用)传递的,因此更改它不会更改实际参数。

v 返回并明确用于更新UI。

您可能希望调查将属性数据类型更改为引用类型。然后它将通过引用传递,所做的任何更改都将反映回源。假设数据绑定过程不创建副本。


-1
2017-07-24 02:08



如果从强制方法返回为Object,它不会被装箱吗? - Rob Perkins