2018-9-19 之前的理解有误,在文章后面有进行补充
阅读本文前,请确保对
DependencyProperty
有一定的了解
DependencyProperty
在WPF中被广泛使用。而使用CoerceValueCallback
进行数据校正的控件也不在少数,比如常见的Slider
。很多带有最大值和最小值的控件,都使用CoerceValueCallback
进行校验。
一般的情况下,使用这些控件不会出现问题。今天举一个特殊的例子,来探讨一下CoerceValueCallback和Binding之间的奥秘。
首先自定义一个控件,这个控件有一个属性是使用回调校验数值的。回调中我随便写了两个数来表示最大值和最小值。
<UserControl x:Class="WpfApp1.UserControl1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:WpfApp1"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Grid>
<StackPanel>
<TextBlock Text="{Binding RelativeSource={RelativeSource Mode=AncestorType=local:UserControl1},Path=Value}"/>
<TextBox TextChanged="TextBox_TextChanged"/>
</StackPanel>
</Grid>
</UserControl>
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的值和自定义控件的值。
<Window x:Class="WpfApp1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApp1"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<StackPanel>
<local:UserControl1 Value="{Binding Value}" x:Name="test"/>
<TextBlock Text="{Binding Value}"/>
<TextBlock Text="{Binding ElementName=test,Path=Value}"/>
</StackPanel>
</Grid>
</Window>
namespace WpfApp1
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new ViewModel();
}
}
public class ViewModel//:DependencyObject
{
public double Value { get; set; }
}
}
好了,见证奇迹的时刻了。
我们输入了一个远超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
绝望的发现,即使是控件的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));
}
}
哎,也不知道这算不算Bug。
2018-9-19 补充
前文中提到的校验导致了View和ViewModel之前数据不同的情况,其实应该是使用不当。
如果你的数据需要进行校验,应当在ViewModel中进行。CoerceValue应当是用来对View界面的一个约束,是为了保证在后台数据错误的情况下,前台界面显示不会出错。不应CoerceValue中做后台数据校验工作。