概述
依赖项属性是属性的一种全新的实现。通过对原有属性的升级,依赖项能够实现数据绑定,动画或者WPF的其他进阶功能。通过对于依赖项属性的封装,使得依赖项属性的使用与普通属性一样,这样既兼容了老的使用方法,又把WPF的新特性带到了普通的WPF程序中。
功能
- 每个依赖项属性都支持 更改通知和动态值识别,这也是依赖项属性的特点和基础
实现
定义依赖项属性
- 只能为依赖对象(继承自DependencyObject的类)添加依赖项属性,通过第二节的类图我们知道,几乎所有的WPF元素都继承自DependencyObject
- 定义的依赖项属性必须是静态的,这样才能通过类直接拿到属性而不需要实例化
- 约定属性的名称必须以Property结尾(约定而非必须)
public static readonly DependencyProperty VerticalStretchSizeProperty;
注册依赖项属性
- 注册必须在属性的静态构造函数中进行以保证在使用属性之前属性已经被注册。
- DependencyProperty不能被实例化,它的构造函数时私有的,只能通过Register()方法来实例化
- DependencyProperty是只读的,能保证它不能在创建后被修改
public class FormatedText { static FormatedText() // 注意是在静态的构造函数中注册 { // 首先要创建 FrameworkPropertyMetadata对象 // 这个对象说明了我们需要通过依赖项属性使用什么服务 PropertyMetadata VerticalStretchMeta = new PropertyMetadata(0.0, (DependencyObject d, DependencyPropertyChangedEventArgs e) => { (d as FormatedText).FlushTheText(); }); // 调用Register注册 // 参数: 属性名 VerticalStretchSize // 参数: 属性的数据类型 double // 参数: 谁拥有该属性 FormatedText // 参数: 可选的FrameworkPropertyMetadata对象 // 参数: ValidateValueCallback 可选的回调函数,用于验证属性 VerticalStretchSizeProperty = DependencyProperty.Register("VerticalStretchSize", typeof(double), typeof(FormatedText), VerticalStretchMeta); } }
- FrameworkPropertyMetadata对象可以用于给依赖项属性设置默认值,调整默认的绑定行为(单向双向),调整是否允许绑定,还能在属性改变之前纠正属性值,能监听到属性的变化,下面的图列出了FrameworkPropertyMetadata对象能做的事情
添加属性包装器
通过将依赖项属性包装成传统的属性将会让依赖项属性能像普通属性一样被使用,这也是创建依赖项属性必须的一步。
public double VerticalStretchSize
{
get
{
return (double)GetValue(VerticalStretchSizeProperty);
}
set
{
SetValue(VerticalStretchSizeProperty, value);
}
}
- 注意不能在这一步进行输入的验证,数据的处理,如果想验证属性或者引发事件,在这里的get set中进行处理是不能达到目的的,原因简单来说就是WPF会绕过属性包装器直接访问GetValue SetValue方法,比如在运行时解析编译过的Xaml的时候
- 如果需要进行输入的验证,需要在第二步,DependencyProperty.ValidateValueCallback中进行验证,即注册时候的最后一个可选参数
- 如果需要进行事件的触发,需要在FrameworkPropertyMetadata.PropertyChangedCallback中进行,即注册的时候倒数第二个参数
- 如果要进行值的修正,需要在FrameworkPropertyMetadata.CoerceValueCallback中修正
清除依赖项属性
清除依赖项属性用于将依赖项属性重置为从来没有设置过的状态
myControl.ClearValue(FrameworkElement.VerticalStretchSizeProperty)
更改通知
我们知道依赖项属性支持更改通知,那我们如何监听这种通知呢,方法有两种:
- 创建一个绑定
- 编写触发器
对于WinForm中,我们想要通知,就需要引发一个事件,依赖项属性同样支持在属性改变的时候引发事件,即在FrameworkPropertyMetadata.PropertyChangedCallback中进行,事实上WPF中已经有这样的实现了,比如TextBox的TextChanged事件,需要注意的是,这种使用对于性能有额外的消耗,所以这种实现方式并不是通用的,更像是对于之前WinForm使用方式的兼容和WPF绑定机制的补充。
动态值识别
动态值试别听起来很难理解,实际意思很简单,依赖项属性究竟值是多少是由一系列值决定的,换句话说,依赖项属性的值不是保存在某一块内存中,而是由许多因素决定的,要确定这个值是多少,只需要循着优先级查找即可,优先级是:
- 默认值
- 继承来的值,可能由父控件提供
- 来自主题样式的值
- 来自项目样式的值
- 本地值(即手动设置的值)
优先级高的覆盖优先级低的(即上图中的5覆盖1),有点像CSS的属性覆盖。这样做的优点十分明显,WPF不需要额外的内存开销去保存这些属性值,需要设置属性的时候只需要去相应的地方获取值,而这些值本就是以各种形式存在于内存中的,这在属性很多的情况下可以节约大笔的内存消耗。
实际上,WPF支持表达式赋值,支持转换器等,使得WPF确定属性值比我们上面描述的更复杂,有更多需要考虑的因素,实际上WPF确定属性值的顺序是:
- 由上面描述的流程确定一个基本值
- 如果有设置值的表达式,应该对表达式进行求值(数据绑定和资源都属于表达式)
- 属性是动画,应用动画
- 运行CoerceValueCallback修正值
共享的依赖项属性
我们注意到一个很有趣的事情,依赖项属性的定义都是静态的,而我们使用的属性,是通过属性包装器包装过的非静态的属性,这样就让不同的控件的属性值不会互相影响,但是根据上面的属性值确定的流程来看,如果不进行任何形式的赋值,属性的值最终由默认值来确定,默认值会由依赖属性在注册的时候确定,而依赖属性是静态的,一个类在定义的时候可以访问到别的类的静态属性,也就可以访问到别的类的依赖属性,进一步,也就可以使用别人的依赖属性定义来构造自己的依赖属性,这样的使用也确实存在。
很多跟文字相关的控件都拥有FontFamily这个依赖属性,我们只需定义一次,其他的需要使用这个依赖属性的类都可以拿来用,甚至不受继承关系的限制,要实现共享依赖属性需要下面的语法
// 使用下面的代码替换依赖属性的注册,可以省去设置默认值,默认行为等代码
public class TextBlock
{
public static readonly DependencyProperty FontFamilyProperty =
TextElement.FontFamilyProperty.AddOwner(typeof(TextBlock), new UIPropertyMetadata(null));
}
我们接着思考,如果这样设置了共享之后,我们进行任何属性的设置,是不是两个共享的属性最终使用默认值的时候使用的是同一块内存呢?答案是肯定的,这就导致了样式中如果自动设置了TextBlock.FontFamily属性,样式也会影响Control.FontFamily属性。
附加属性
我们之前讲过附加属性,比如Grid.Row属性。任何控件都可以使用,甚至不在Grid中的控件也可以正常使用,我们可以认为,附加属性,就是全局的依赖项属性,任何控件都可以使用。定义这样的附加属性,需要使用RegisterAttached方法进行注册。
注意:
- 需要使用RegisterAttached方法进行注册
- 因为它是全局的,不是某一个类的属性,所以不需要属性包装器
- 没有属性包装器,也需要实现Get Set方法,因为是全局的,显然 Get Set方法都应该是静态的
// 注册附加属性
Grid.RowProperty = DependencyProperty.RegisterAttached("Row", typeof(int), typeof(Grid), metadata, new ValidateValueCallback(Grid.IsIntValueNotNegative))
// 实现设置和获取属性值
public static int GetRow(UIElement element)
{
if(element == null)
{
throw new ArguementNullException(...);
}
return (int)element.GetValue(Grid.RowProperty);
}
public static void SetRow(UIElement element, int value)
{
if(element == null)
{
throw new ArguementNullException(...);
}
element.SetValue(Grid.RowProperty, value);
}
// 将元素放置到Grid的第二行
Grid.SetRow(txtElement, 1);
// 尝试不通过Grid的Set来直接放置元素
txtElement.SetValue(Grid.RowProperty, 1);
上面不通过Set直接设置值的方法其实给了我们一些启示,在修改一个依赖属性的值的时候,我们可以通过他的属性包装来设置这个值,同样,我们也可以直接调用属性包装器中调用的SetValue(VerticalStretchSizeProperty, value);这样的方法来改变依赖项属性的值。这也就印证了我们之前说的,不要在属性包装器的get set中尝试截获值,而是要在回调中处理属性值的变化。那么下面我们就详细讲讲属性验证和强制回调。
属性验证
属性生效之前我们有多少机会能够验证这个值是否合法呢,答案是两个:
- ValidateValueCallback 这个回调可以接受或者拒绝新值。用于捕获违反约束的明显错误,它是Register方法的一个参数,是回调函数。与下面的回调相比,在这个回调中我们不能检查其他属性值,原因是我们访问不到拥有这个属性的实际对象。
// 注意最后一个参数
MarginProperty = DependencyProperty.Register("Margin", typeof(Thickness), typeof(FrameworkElement), metadata, new ValidateValueCallback(FrameworkElement.IsMarginValid));
// 最后一个参数Callback中传递的参数必须是一个静态的方法
// 回调回来的参数值只有当前设置值,且不能访问其他的属性值
// 我们可以发现,这里能验证的值多是数据格式问题或者不能为负数这种明显错误
private static bool IsMarginValid(object value)
{
Thickness thickness1 = (Thickness) value;
return thickness1.IsValid(true, false, true, false);
}
- CoerceValueCallback(Coerce强制) 该回调函数用于修正值。常用于两个依赖属性相互影响的情况(例如A B两个依赖属性的和为100),它作为metadata中的一个参数注册进依赖属性中
FrameworkPropertyMetadata metadata = new FrameworkPropertyMetadata();
matadata.CoerceValueCallback = new CoerceValueCallback(CoerceMaximum);
DependencyProperty.Register("Maximum", typeof(double), typeof(RangeBase), metadata);
// 可以看到这个回调函数能够拿到参数d,就是这个依赖项属性的实例,自然可以获取这个依赖项属性中的所有的值
private static object CoerceMaximum(DependencyObject d, object value)
{
RangeBase base1 = (RangeBase)d;
if((double)value < base1.Minimum)
{
return base1.Minimum;
}
return value;
}
那么这两个方法的顺序是怎么样的呢?
- CoerceValueCallback有机会修正传来的属性值
- ValidateValueCallback生效,返回true接受值,返回false拒绝值。
- 触发PropertyChangedCallback,在这个里面可以引发自定义事件来新增自定义的监听逻辑
多个值互动
我们想一下滚动条的逻辑:
- 滚动条拥有最大值和最小值
- 最大值不能小于最小值,最小值不能大于最大值,Value的设置必须在最大值和最小值中间
我们看一下WPF如何处理这样的逻辑:
- 设置Maxmum的强制回调
private static object CoerceMaximum(DependencyObject d, object value) { RangeBase base1 = (RangeBase)d; if((double)value < base1.Minimum) { return base1.Minimum; } return value; }
- 监听Minimum的属性变化并且触发Maxmum和Value的强制回调
// metadata中可以设置PropertyChangedCallback private static void OnMinimumChanged(Dependency d, DependenctPropertyChangedEventArgs e) { RangeBase base1 = (RangeBase) d; base1.CoerceValue(RangeBase.MaximumProperty); base1.CoerceValue(RangeBase.ValueProperty); }
- 在最大值变化的时候触发Value的强制回调
这样优先权是 Mini > Max > Value
这里我们需要注意一个很有意思的事情,一旦我们设置了Value的值,这个值会被记录下来,如果不符合大小限制,将会使用最大值或者最小值,但是一旦我们在程序中将最大值最小值改变使得Value符合范围,则Value的值就会生效,这也符合我们的正常思维。