绑定值到有CoerceValueCallback的DependencyProperty

2018-9-19 之前的理解有误,在文章后面有进行补充


阅读本文前,请确保对DependencyProperty有一定的了解

DependencyProperty在WPF中被广泛使用。而使用CoerceValueCallback进行数据校正的控件也不在少数,比如常见的Slider。很多带有最大值和最小值的控件,都使用CoerceValueCallback进行校验。

一般的情况下,使用这些控件不会出现问题。今天举一个特殊的例子,来探讨一下CoerceValueCallback和Binding之间的奥秘。
首先自定义一个控件,这个控件有一个属性是使用回调校验数值的。回调中我随便写了两个数来表示最大值和最小值。


    
        
            
            
        
    

    public partial class UserControl1 : UserControl
    {
        public static readonly DependencyProperty ValueProperty = DependencyProperty.Register("Value", typeof(double), typeof(UserControl1),
            new FrameworkPropertyMetadata(default(double), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnPropertyChanged, CoerceValue));

        private static void OnPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
        }

        private static object CoerceValue(DependencyObject d, object baseValue)
        {
            var value = (double)baseValue;
            if (value < 600)
                value = 600;
            else if (value > 5000)
                value = 5000;
            return value;
        }

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

        public UserControl1()
        {
            InitializeComponent();
        }

        private void TextBox_TextChanged(object sender, TextChangedEventArgs e)
        {
            SetCurrentValue(ValueProperty, double.Parse((sender as TextBox).Text));
        }
    }

非常简单的一个控件,声明的依赖属性也是很常规的声明方式,相信很多朋友已经写了不下千八百变了。

现在来使用这个控件。这里我用了两个TextBlock来分别显示ViewModel的值和自定义控件的值。


    
        
            
            
            
        
    

namespace WpfApp1
{
    /// 
    /// Interaction logic for MainWindow.xaml
    /// 
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            this.DataContext = new ViewModel();
        }
    }

    public class ViewModel//:DependencyObject
    {
        public double Value { get; set; }
    }
}

好了,见证奇迹的时刻了。


绑定值到有CoerceValueCallback的DependencyProperty_第1张图片
神奇的一幕出现了

我们输入了一个远超5000的数,而从结果来看,ViewModel的值是我们输入的值,而控件的值,是校验之后的值。而ViewModel的值是直接绑定到控件之上的,不得不说,很诡异。
通过调试(请读者自行尝试),我们可以发现,往ViewModel设置值是先于回调校验的,而校验完成之后,并没有再次往ViewModel去设置值。

StackOverFlow上有大神看了源码,说源码就是这样的,调用的逻辑就是现在我们看到的逻辑,没毛病。感兴趣的同学可以去研究一下Binding的源码。

不管怎么样,我们希望的是控件的值和绑定的ViewModel的值是一致的。所以看起来最科学最直接的方式不能满足我们的需求,那就只能想想别的办法了。幸运的是,在CoerceValueCallback完成之后,才会去调用PropertyChangedCallback。所以我们可以在PropertyChangedCallback里面手动的去更新绑定值。
修改一下UserControl1的后台代码。

    public partial class UserControl1 : UserControl
    {
        //修改触发方式为手动触发
        public static readonly DependencyProperty ValueProperty = DependencyProperty.Register("Value", typeof(double), typeof(UserControl1),
            new FrameworkPropertyMetadata(default(double), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnPropertyChanged, CoerceValue, false, UpdateSourceTrigger.Explicit));

        private static void OnPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            //手动触发更新
            var control = d as UserControl1;
            BindingExpression binding = control.GetBindingExpression(UserControl1.ValueProperty);
            binding?.UpdateSource();
        }

        private static object CoerceValue(DependencyObject d, object baseValue)
        {
            var value = (double)baseValue;
            if (value < 600)
                value = 600;
            else if (value > 5000)
                value = 5000;
            return value;
        }

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

        public UserControl1()
        {
            InitializeComponent();
        }

        private void TextBox_TextChanged(object sender, TextChangedEventArgs e)
        {
            SetCurrentValue(ValueProperty, double.Parse((sender as TextBox).Text));
        }
    }

好了,我们打上断点一步步调试过去。

  • 没有在CoerceValueCallback之前去设置ViewModel的值,很好。
  • CoerceValueCallback之后调用PropertyChangedCallback,很好,值也是对的。
  • 好,UpdateSource
  • 欢声笑语中打出GG


    绑定值到有CoerceValueCallback的DependencyProperty_第2张图片
    然并卵

    绝望的发现,即使是控件的Value值是正确的情况下,UpdateSource依旧设置的是校验之前的值到ViewModel。没错,巨硬就是这么犀利。
    只能假设,UpdateSource就是设置的是校验之前的值,那么,把校验之后的值直接往属性上设置,应该就能解决问题了。按这个思路尝试一下。

    public partial class UserControl1 : UserControl
    {
        //修改触发方式为手动触发
        public static readonly DependencyProperty ValueProperty = DependencyProperty.Register("Value", typeof(double), typeof(UserControl1),
            new FrameworkPropertyMetadata(default(double), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnPropertyChanged, CoerceValue, false, UpdateSourceTrigger.Explicit));

        private static void OnPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            //手动触发更新
            var control = d as UserControl1;
            //重新设置一遍
            control.SetCurrentValue(ValueProperty, e.NewValue);
            BindingExpression binding = control.GetBindingExpression(UserControl1.ValueProperty);
            binding?.UpdateSource();
        }

        private static object CoerceValue(DependencyObject d, object baseValue)
        {
            var value = (double)baseValue;
            if (value < 600)
                value = 600;
            else if (value > 5000)
                value = 5000;
            return value;
        }

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

        public UserControl1()
        {
            InitializeComponent();
        }

        private void TextBox_TextChanged(object sender, TextChangedEventArgs e)
        {
            SetCurrentValue(ValueProperty, double.Parse((sender as TextBox).Text));
        }
    }
绑定值到有CoerceValueCallback的DependencyProperty_第3张图片
大功告成

哎,也不知道这算不算Bug。


2018-9-19 补充

前文中提到的校验导致了View和ViewModel之前数据不同的情况,其实应该是使用不当。
如果你的数据需要进行校验,应当在ViewModel中进行。CoerceValue应当是用来对View界面的一个约束,是为了保证在后台数据错误的情况下,前台界面显示不会出错。不应CoerceValue中做后台数据校验工作。

你可能感兴趣的:(绑定值到有CoerceValueCallback的DependencyProperty)