强制传播强制值

12

简述:强制转换的值在数据绑定中不会传递。当代码后台不知道绑定的另一侧时,如何强制更新数据绑定?


我正在使用CoerceValueCallback对WPF依赖属性进行操作,但我卡在了一个问题上,强制转换的值无法通过绑定传播到其他地方。

Window1.xaml.cs

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Media;

namespace CoerceValueTest
{
    public class SomeControl : UserControl
    {
        public SomeControl()
        {
            StackPanel sp = new StackPanel();

            Button bUp = new Button();
            bUp.Content = "+";
            bUp.Click += delegate(object sender, RoutedEventArgs e) {
                Value += 2;
            };

            Button bDown = new Button();
            bDown.Content = "-";
            bDown.Click += delegate(object sender, RoutedEventArgs e) {
                Value -= 2;
            };

            TextBlock tbValue = new TextBlock();
            tbValue.SetBinding(TextBlock.TextProperty,
                               new Binding("Value") {
                                Source = this
                               });

            sp.Children.Add(bUp);
            sp.Children.Add(tbValue);
            sp.Children.Add(bDown);

            this.Content = sp;
        }

        public static readonly DependencyProperty ValueProperty = DependencyProperty.Register("Value",
                                                                                              typeof(int),
                                                                                              typeof(SomeControl),
                                                                                              new PropertyMetadata(0, ProcessValueChanged, CoerceValue));

        private static object CoerceValue(DependencyObject d, object baseValue)
        {
            if ((int)baseValue % 2 == 0) {
                return baseValue;
            } else {
                return DependencyProperty.UnsetValue;
            }
        }

        private static void ProcessValueChanged(object source, DependencyPropertyChangedEventArgs e)
        {
            ((SomeControl)source).ProcessValueChanged(e);
        }

        private void ProcessValueChanged(DependencyPropertyChangedEventArgs e)
        {
            OnValueChanged(EventArgs.Empty);
        }

        protected virtual void OnValueChanged(EventArgs e)
        {
            if (e == null) {
                throw new ArgumentNullException("e");
            }

            if (ValueChanged != null) {
                ValueChanged(this, e);
            }
        }

        public event EventHandler ValueChanged;

        public int Value {
            get {
                return (int)GetValue(ValueProperty);
            }
            set {
                SetValue(ValueProperty, value);
            }
        }
    }

    public class SomeBiggerControl : UserControl
    {
        public SomeBiggerControl()
        {
            Border parent = new Border();
            parent.BorderThickness = new Thickness(2);
            parent.Margin = new Thickness(2);
            parent.Padding = new Thickness(3);
            parent.BorderBrush = Brushes.DarkRed;

            SomeControl ctl = new SomeControl();
            ctl.SetBinding(SomeControl.ValueProperty,
                           new Binding("Value") {
                            Source = this,
                            Mode = BindingMode.TwoWay
                           });
            parent.Child = ctl;

            this.Content = parent;
        }

        public static readonly DependencyProperty ValueProperty = DependencyProperty.Register("Value",
                                                                                              typeof(int),
                                                                                              typeof(SomeBiggerControl),
                                                                                              new PropertyMetadata(0));

        public int Value {
            get {
                return (int)GetValue(ValueProperty);
            }
            set {
                SetValue(ValueProperty, value);
            }
        }
    }

    public partial class Window1 : Window
    {
        public Window1()
        {
            InitializeComponent();
        }
    }
}

Window1.xaml

<Window x:Class="CoerceValueTest.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="CoerceValueTest" Height="300" Width="300"
    xmlns:local="clr-namespace:CoerceValueTest"
    >
    <StackPanel>
        <local:SomeBiggerControl x:Name="sc"/>
        <TextBox Text="{Binding Value, ElementName=sc, Mode=TwoWay}" Name="tb"/>
        <Button Content=" "/>
    </StackPanel>
</Window>

即有两个用户控件,一个嵌套在另一个中,而外部的一个在窗口中。内部用户控件具有绑定到外部控件的“Value”依赖属性的“Value”依赖属性。在窗口中,“TextBox.Text”属性被绑定到外部控件的“Value”属性。

内部控件的“Value”属性已注册了一个“CoerceValueCallback”,其效果是该“Value”属性只能被分配为偶数。

请注意,此代码为演示目的简化。真实版本不会在构造函数中初始化任何内容;两个控件实际上有控件模板,在各自的构造函数中完成所有操作。也就是说,在实际代码中,外部控件不知道内部控件。

将偶数写入文本框并更改焦点(例如通过聚焦文本框下面的虚拟按钮),两个“Value”属性都会得到适当的更新。然而,将奇数写入文本框时,内部控件的“Value”属性不会更改,而外部控件的“Value”属性和“TextBox.Text”属性显示奇数。

我的问题是:我如何强制更新文本框(理想情况下还要更新外部控件的“Value”属性)?

我发现与此问题相同的 SO 问题,但没有真正提供解决方案。它暗示使用属性更改事件处理程序重置值,但据我所见,这意味着在外部控件中复制计算代码...这不是很可行,因为我的实际计算代码依赖于一些基本上仅已知(不需要太多努力)的内部控件信息。

此外,这篇博客文章建议在CoerceValueCallback中对 TextBox.Text的绑定调用UpdateTarget方法。但是首先,如上所述,我的内部控件不可能有关于文本框的任何了解;其次,我可能需要先在内部控件的Value属性的绑定上调用UpdateSource方法。然而,在 CoerceValue 方法中,被强制转换的值尚未设置(所以更新绑定还为时过早),而如果该值被 CoerceValue 重置,则属性值将保持不变,因此属性更改回调也不会被调用(正如这个讨论中所示)。
我曾考虑过一种可能的解决方法,即使用常规属性和 INotifyPropertyChanged 实现来替换 SomeControl 中的依赖属性(这样即使在值被强制转换时也可以手动触发 PropertyChanged 事件)。然而,这意味着我不能再声明该属性的绑定,所以这不是一个非常有用的解决方案。

2
如果你找到了答案,请告诉我 =) - Federico Berasategui
1个回答

5

我自己也一直在寻找解决这个相当棘手的 bug 的方法。一种方法是,不需要强制更新绑定的 UpdateTarget,做如下操作:

  • 移除你的 CoerceValue 回调函数。
  • 将 CoerceValue 回调的逻辑转移到 ProcessValueChanged 回调中。
  • 当数值为奇数时,将强制转换后的数值分配给 Value 属性。
  • 这样会导致 ProcessValueChanged 回调被触发两次,但是强制转换后的数值最终会被有效地推送到你的绑定中。

根据你的代码,你的依赖属性声明应该如下所示:

public static readonly DependencyProperty ValueProperty = 
                       DependencyProperty.Register("Value",
                                                   typeof(int),
                                                   typeof(SomeControl),
                                                   new PropertyMetadata(0, ProcessValueChanged, null));

接下来,你的ProcessValueChanged将变成这样:

private static void ProcessValueChanged(object source, DependencyPropertyChangedEventArgs e)
    {
        int baseValue = (int) e.NewValue;
        SomeControl someControl = source as SomeControl;
        if (baseValue % 2 != 0) 
        {
            someControl.Value = DependencyProperty.UnsetValue;
        }
        else
        {
            someControl.ProcessValueChanged(e);
        }
    }

我稍微修改了你的逻辑,以防止在需要强制转换值时引发事件。如前所述,将强制转换后的值分配给 someControl.Value 将导致你的 ProcessValueChanged 连续调用两次。加入 else 语句只会触发一次具有有效值的事件。

希望这可以帮到你!


很有前途 - 我会稍后检查并决定是否接受答案,但由于这可能需要一些时间,现在先点赞。 - O. R. Mapper
1
在设置这样的值时(在它自己的类中),请注意。如果Value属性上存在双向绑定,那么它会正常工作,但是如果该属性上有单向绑定,则通过将其他值分配给它来破坏绑定。您最好使用someControl.SetCurrentValue(ValueProperty, newValue),这将保持绑定完整性。 - Rob van Daal

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