前面我们看到一个依赖属性的注册最全的形式是下面这样子的:
public static DependencyProperty Register(string name, Type propertyType, Type ownerType, PropertyMetadata typeMetadata, ValidateValueCallback validateValueCallback);
第一个参数是该依赖属性的名字,第二个参数是依赖属性的类型,第三个参数是该依赖属性的所有者的类型,第五个参数就是一个验证值的回调委托,那么最使我们感兴趣的还是这个可爱的 PropertyMetadata ,也就是我们接下来要讲的元数据。 提到WPF属性元数据,大家可能第一想到的是刚才的PropertyMetadata,那么这个类到底是怎样的呢?我们应该怎样使用它呢?首先我们看它的构造函数(我们选参数最多的来讲):
public PropertyMetadata(object defaultValue, PropertyChangedCallback propertyChangedCallback, CoerceValueCallback coerceValueCallback);
其中的第一个参数是默认值,最后两个分别是PropertyChanged(变化通知)以及Coerce(强制)的两个委托变量,我们在实例化的时候,只需要把这两个委托变量关联到具体的方法上即可。
事实上,除了PropertyMetadata以外,常见的还有 FrameworkPropertyMetadata,UIPropertyMetadata。他们的继承关系是F->U->P。其中以 FrameworkPropertyMetadata参数最多,亦最为复杂。
FrameworkPropertyMetadata的构造函数提供了很多重载,我们挑选最为复杂的重载来看它到底有哪些参数以及提供了哪些功能:
public FrameworkPropertyMetadata(object defaultValue, FrameworkPropertyMetadataOptions flags, PropertyChangedCallback propertyChangedCallback, CoerceValueCallback coerceValueCallback, bool isAnimationProhibited, UpdateSourceTrigger defaultUpdateSourceTrigger);
其中第一个参数是默认值,最后两个参数分别是是否允许动画,以及绑定时更新的策略(在Binding当中相信大家并不陌生),这个不详细解释 了。重点看一下里第三、四两个参数,两个 CallBack的委托。结合前面Register的时候提到的ValidateValueCallback共组成三大”金刚“,这三个Callback 分别代表Validate(验证),PropertyChanged(变化通知)以及Coerce(强制)。当然,作为 Metadata,FrameworkPropertyMetadata只是储存了该依赖属性的策略信息,WPF属性系统会根据这些信息来提供功能并在适 当的时机回调传入的delegate,所以最重要的还是我们定义的这些方法,通过他们传入委托才能起到真正的作用。
上面讲了元数据暴露给我们的构造函数,其实在其内部还提供了两个方法,这个在做自定义控件的时候,也很值得注意:
protected virtual void Merge(PropertyMetadata baseMetadata, DependencyProperty dp) { // 实现元数据继承之间的合并
} protected virtual void OnApply(DependencyProperty dependencyProperty, Type targetType) { // 当元数据被这个属性应用,OnApply就会被触发,在此时元数据也将被密封起来。
}
前面讲了这么多,那么我们现在就来看看依赖属性回调、验证及强制值到底是怎么使用的呢?大家千万要坚持住,后面内容更加精彩!
我们通过下面的这幅图,简单介绍一下WPF属性系统对依赖属性操作的基本步骤:
前面我们讲了基本的流程,下面我们就用一个小的例子来进行说明:
namespace SampleProcess_DPs { class Program
{ static void Main(string[] args) { SimpleDPClass sDPClass = new SimpleDPClass(); sDPClass.SimpleDP = 8; Console.ReadLine(); } } public class SimpleDPClass : DependencyObject
{ public static readonly DependencyProperty SimpleDPProperty = DependencyProperty.Register("SimpleDP", typeof(double), typeof(SimpleDPClass), new FrameworkPropertyMetadata((double)0.0, FrameworkPropertyMetadataOptions.None, new PropertyChangedCallback(OnValueChanged), new CoerceValueCallback(CoerceValue)), new ValidateValueCallback(IsValidValue)); public double SimpleDP { get { return (double)GetValue(SimpleDPProperty); } set { SetValue(SimpleDPProperty, value); } } private static void OnValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { Console.WriteLine("当值改变时,我们可以做的一些操作,具体可以在这里定义: {0}", e.NewValue); } private static object CoerceValue(DependencyObject d, object value) { Console.WriteLine("对值进行限定,强制值: {0}", value); return value; } private static bool IsValidValue(object value) { Console.WriteLine("验证值是否通过,返回bool值,如果返回True表示严重通过,否则会以异常的形式暴露: {0}", value); return true; } } }
结果如下:
当SimpleDP属性变化之后,PropertyChangeCallback就会被调用。可以看到结果并没有完全按照我们先前的流程先 Coerce后Validate的顺序执行,有可能是WPF内部做了什么特殊处理,当属性被修改时,首先会调用Validate来判断传入的value是 否有效,如果无效就不继续后续的操作,这样可以更好的优化性能。从上面的结果上看出,CoerceValue后面并没有立即ValidateValue, 而是直接调用了PropertyChanged。这是因为前面已经验证过了value,如果在Coerce中没有改变value,那么就不用再验证了。如 果在 Coerce中改变了value,那么这里还会再次调用ValidateValue操作,和前面的流程图执行的顺序一样,在最后我们会调用 ValidateValue来进行最后的验证,这就保证最后的结果是我们希望的那样了(正如打游戏一样,打了小怪,在最后过总关的时候还是需要打大怪才能 闯关的)。
上面简单介绍了处理流程,下面我们就以一个案例来具体看一看上面的流程到底有没有出入,这个例子改编于Sacha Barber 的Dependency Properties代码示例,我相信通过这段代码你会对这个上面讲的概念有更清晰地认识。
UI很简单,黄色部分显示当前值,我们在初始化的时候把它设置为100,然后它的最小值和最大值分别设置为0和500,按钮”设置为-100 “企图把当前值设为-100,按钮”设置为1000“试图把当前值设为1000。具体大家看代码(我都写了注释,很容易理解的).
依赖属性代码文件如下:
namespace Callback_Validation_DPs { public class Gauge : Control
{ public Gauge() : base() { } //注册CurrentReading依赖属性,并添加PropertyChanged、CoerceValue、ValidateValue的回调委托
public static readonly DependencyProperty CurrentReadingProperty = DependencyProperty.Register( "CurrentReading", typeof(double), typeof(Gauge), new FrameworkPropertyMetadata( Double.NaN, FrameworkPropertyMetadataOptions.None, new PropertyChangedCallback(OnCurrentReadingChanged), new CoerceValueCallback(CoerceCurrentReading) ), new ValidateValueCallback(IsValidReading) ); //属性包装器,通过它来暴露CurrentReading的值
public double CurrentReading { get { return (double)GetValue(CurrentReadingProperty); } set { SetValue(CurrentReadingProperty, value); } } //注册MinReading依赖属性,并添加PropertyChanged、CoerceValue、ValidateValue的回调委托
public static readonly DependencyProperty MinReadingProperty = DependencyProperty.Register( "MinReading", typeof(double), typeof(Gauge), new FrameworkPropertyMetadata( double.NaN, FrameworkPropertyMetadataOptions.None, new PropertyChangedCallback(OnMinReadingChanged), new CoerceValueCallback(CoerceMinReading) ), new ValidateValueCallback(IsValidReading)); //属性包装器,通过它来暴露MinReading的值
public double MinReading { get { return (double)GetValue(MinReadingProperty); } set { SetValue(MinReadingProperty, value); } } //注册MaxReading依赖属性,并添加PropertyChanged、CoerceValue、ValidateValue的回调委托
public static readonly DependencyProperty MaxReadingProperty = DependencyProperty.Register( "MaxReading", typeof(double), typeof(Gauge), new FrameworkPropertyMetadata( double.NaN, FrameworkPropertyMetadataOptions.None, new PropertyChangedCallback(OnMaxReadingChanged), new CoerceValueCallback(CoerceMaxReading) ), new ValidateValueCallback(IsValidReading) ); //属性包装器,通过它来暴露MaxReading的值
public double MaxReading { get { return (double)GetValue(MaxReadingProperty); } set { SetValue(MaxReadingProperty, value); } } //在CoerceCurrentReading加入强制判断赋值
private static object CoerceCurrentReading(DependencyObject d, object value) { Gauge g = (Gauge)d; double current = (double)value; if (current < g.MinReading) current = g.MinReading; if (current > g.MaxReading) current = g.MaxReading; return current; } //当CurrentReading值改变的时候,调用MinReading和MaxReading的CoerceValue回调委托
private static void OnCurrentReadingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { d.CoerceValue(MinReadingProperty); d.CoerceValue(MaxReadingProperty); } //当OnMinReading值改变的时候,调用CurrentReading和MaxReading的CoerceValue回调委托
private static void OnMinReadingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { d.CoerceValue(MaxReadingProperty); d.CoerceValue(CurrentReadingProperty); } //在CoerceMinReading加入强制判断赋值
private static object CoerceMinReading(DependencyObject d, object value) { Gauge g = (Gauge)d; double min = (double)value; if (min > g.MaxReading) min = g.MaxReading; return min; } //在CoerceMaxReading加入强制判断赋值
private static object CoerceMaxReading(DependencyObject d, object value) { Gauge g = (Gauge)d; double max = (double)value; if (max < g.MinReading) max = g.MinReading; return max; } //当MaxReading值改变的时候,调用MinReading和CurrentReading的CoerceValue回调委托
private static void OnMaxReadingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { d.CoerceValue(MinReadingProperty); d.CoerceValue(CurrentReadingProperty); } //验证value是否有效,如果返回True表示验证通过,否则会提示异常
public static bool IsValidReading(object value) { Double v = (Double)value; return (!v.Equals(Double.NegativeInfinity) && !v.Equals(Double.PositiveInfinity)); } } }
XAML代码如下:
<Window x:Class="Callback_Validation_DPs.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Callback_Validation_DPs"
WindowStartupLocation="CenterScreen"
Title="Callback_Validation_DPs" Height="400" Width="400">
<StackPanel Orientation="Vertical">
<local:Gauge x:Name="gauge1" MaxReading="100" MinReading="0" />
<Label Content="可以设置最小值为0和最小大值为500" Height="30"/>
<StackPanel Orientation="Horizontal" Height="60">
<Label Content="当前值为 : "/>
<Label Background="Yellow" BorderBrush="Black" BorderThickness="1"
IsEnabled="False" Content="{Binding ElementName=gauge1, Path=CurrentReading}" Height="25" VerticalAlignment="Top" />
</StackPanel>
<Button x:Name="btnSetBelowMin" Content="设置为 -100"
Click="btnSetBelowMin_Click"/>
<Button x:Name="btnSetAboveMax" Content="设置为 1000"
Click="btnSetAboveMax_Click"/>
</StackPanel>
</Window>
XAML的后台代码如下:
public partial class Window1 : Window
{ public Window1() { InitializeComponent(); //设置CurrentReading的值,这个时候会触发哪些变化?调试代码吧!
gauge1.CurrentReading = 100; } private void btnSetBelowMin_Click(object sender, RoutedEventArgs e) { //设置CurrentReading的值,这个时候会触发哪些变化?调试代码吧!
gauge1.CurrentReading = -100; } private void btnSetAboveMax_Click(object sender, RoutedEventArgs e) { //设置CurrentReading的值,这个时候会触发哪些变化?调试代码吧!
gauge1.CurrentReading = 10000; } }
在上面的例子中,一共有三个依赖属性相互作用——CurrentReading、MinReading和MaxReading,这些属性相互作 用,但它们的规则是MinReading≤CurrentReading≤MaxReading。根据这个规则,当其中一个依赖属性变化时,另外两个依赖 属性必须进行适当的调整,这里我们要用到的就是CoerceValue这个回调委托,那么实现起来也非常的简单,注册MaxReading的时候加入 CoerceValueCallback,在CoerceMaxReading函数中做处理:如果Maximum的值小于MinReading,则使 MaxReading值等于MinReading;同理在CurrentReading中也加入了CoerceValueCallback进行相应的强制 处理。然后在MinReading的ChangedValueCallback被调用的时候,调用CurrentReading和MaxReading的 CoerceValue回调委托,这样就可以达到相互作用的依赖属性一变应万变的”千机变“。
换句话说,当相互作用的几个依赖属性其中一个发生变化时,在它的PropertyChangeCallback中调用受它影响的依赖属性的CoerceValue,这样才能保证相互作用关系的正确性。 前面也提高ValidateValue主要是验证该数据的有效性,最设置了值以后都会调用它来进行验证,如果验证不成功,则抛出异常。