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

发布于 2024-09-11 08:47:37 字数 2742 浏览 2 评论 0原文

我正在编写一个真正的 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 显示正确的值。

这就是强制价值观应该发挥的作用吗?很好,它在用户界面中被强制,但我希望强制值也能通过数据绑定运行。

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.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

南七夏 2024-09-18 08:47:37

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

如果您查看 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”的消息。

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".

给妤﹃绝世温柔 2024-09-18 08:47:37

老问题的新答案::-)

在注册 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 的非强制值更新绑定。

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.

李不 2024-09-18 08:47:37

您正在强制 v 它是一个 int ,也是一个值类型。因此它被存储在堆栈中。它与baseValue没有任何关系。所以改变 v 不会改变 baseValue。

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

v 被返回,显然是用来更新 UI 的。

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

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.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文