【问题标题】:Force Propagation of Coerced Value强制传播价值
【发布时间】:2023-03-13 20:10:03
【问题描述】:

tl;dr:强制值不会跨数据绑定传播。当代码隐藏不知道绑定的另一端时,如何强制更新数据绑定?


我在 WPF 依赖项属性上使用 CoerceValueCallback,但遇到强制值不会传播到绑定的问题。

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 属性。

内部控件有一个CoerceValueCallback 注册了它的Value 属性,其效果是这个Value 属性只能分配偶数。

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

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

我的问题是:如何在文本框中强制更新(理想情况下也可以在外部控件的 Value 属性中进行更新)?

我找到了an SO question on the same problem,但并没有真正提供解决方案。它暗示使用属性更改事件处理程序来重置值,但据我所知,这意味着将评估代码复制到外部控件......这并不可行,因为我的实际评估代码依赖于一些信息基本上只有内部控制才知道(不费力气)。

此外,this blogpost 建议在CoerceValueCallback 中的TextBox.Text 绑定上调用UpdateTarget,但首先,如上所述,我的内部控件不可能对文本框有任何了解,其次,我可能必须首先在内部控件的Value 属性的绑定上调用UpdateSource。但是,我看不到在哪里执行此操作,因为在 CoerceValue 方法中,尚未设置强制值(因此更新绑定还为时过早),而在该值由 @ 重置的情况下987654345@,属性值将保持原样,因此不会调用属性更改回调(this discussion 中也暗示了这一点)。

我想到的一个可能的解决方法是将SomeControl 中的依赖属性替换为常规属性和INotifyPropertyChanged 实现(因此即使值已被强制,我也可以手动触发PropertyChanged 事件)。但是,这意味着我不能再声明对该属性的绑定,所以这不是一个真正有用的解决方案。

【问题讨论】:

  • 如果你找到了这个答案,请告诉我=)

标签: wpf xaml data-binding dependency-properties coerce


【解决方案1】:

一段时间以来,我一直在寻找这个相当讨厌的错误的答案。 一种不需要在绑定上强制 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 语句只会引发一次具有有效值的事件。

我希望这会有所帮助!

【讨论】:

  • 有希望 - 我会检查一下,然后决定是否接受答案,但由于到那时可能需要一些时间,所以现在 +1。
  • 当你设置这样的值时要小心(在它自己的类中)。如果 Value 属性上存在双向绑定,它将顺利进行,但如果此属性上存在单向绑定,则绑定将通过为其分配一些其他值而被破坏。您可以更好地使用 someControl.SetCurrentValue(ValueProperty, newValue),这样可以保持绑定不变。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-12-23
  • 1970-01-01
  • 2016-04-10
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多