为什么我的数据绑定看到的是实际值而不是强制转换后的值?

15

我正在编写一个真正的NumericUpDown/Spinner控件,以练习自定义控件的编写。我已经实现了大部分我需要的行为,包括适当的强制转换。然而,我的一个测试揭示了一个缺陷。

我的控件有3个依赖属性:ValueMaximumValueMinimumValue。我使用强制转换来确保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中强制转换很好,但我期望强制转换的值也能通过数据绑定进行处理。


似乎绑定不支持关于val的问题。您尝试过TextBox中的diff模式吗? - Lukasz Madon
3个回答

13

哇,这真是令人惊讶。当您设置依赖属性的值时,绑定表达式会在值强制转换运行之前更新!

如果您查看Reflector中的DependencyObject.SetValueCommon方法,您可以看到该方法中途调用了Expression.SetValue。在已经更新绑定的情况下,UpdateEffectiveValue调用会调用您的CoerceValueCallback。

您也可以在框架类中看到这一点。从新的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”。

2
有趣的是,这是预期的行为。我想我的下一步是找出如何提供一个可绑定的ActualValue属性,以反映在NumericUpDown中实际显示的内容。您是否知道比在属性更改处理程序中重新评估强制转换并在那里设置ActualValue更好的方法? - Greg D

7

一道老问题的新答案::-)

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 的绑定。

新问题,旧答案 :). 我有这个问题,但是 "GetBindingExpression" 返回 null。我尝试在值被绑定时强制转换!当用户设置值时,它可以工作。所以必须有一些上下文问题(未完全加载或其他什么)?有任何建议吗? - Werner
如果您尝试在某个时刻执行 Value = x,那么它将清除绑定。请改用 SetCurrentValue - Miral

-1

你正在强制转换一个 int 类型的变量 v,因此它是值类型,存储在堆栈上。它与 baseValue 没有任何关联。因此更改 v 不会更改 baseValue。

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

v 被返回并明显用于更新 UI。

您可能希望将属性数据类型更改为引用类型。然后它将被按引用传递,对其进行的任何更改都将反映回源。假设数据绑定过程不会创建副本。


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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接