理解依赖属性
依赖属性支持的特征包括:动画、数据绑定、样式。由WPF元素暴露的属性大部分都是依赖属性。依赖属性和常规属性的使用方法相同。
WPF设计了依赖属性支持其特有的动态特性,并且不干扰其他系统的.net代码。
定义依赖属性
创造一个依赖属性的语法完全不同于创造一个普通的.NET属性。
第一步是定义一个代表属性的对象。这是DependencyProperty
类的一个实例。关于你属性的信息需要一直是可用的,甚至可能在类之间共享(常见在WPF元素中)。因此,你的DependencyProperty
对象必须被定义为相关类的一个静态字段。
例如,FrameworkElement
类定义了一个Margin
属性。Margin
被所有元素共享,是一个依赖属性:
public class FrameworkElement: UIElement, ...
{
public static readonly DependencyProperty MarginProperty;
...
}
根据命名约定,定义依赖属性的字段名字是正常属性加上单词Property
后缀。那样,你能区分依赖属性定义和实际属性的名字。字段使用readonly
关键字,这意味着它只能在FrameworkElement
类的静态构造函数中设置,如何设置见下节。
注册依赖属性
下一步是注册你的依赖属性。因为要在使用属性之前完成注册,必须在相关类的一个静态构造函数中执行它。
不能直接实例化DependencyProperty
对象,因为DependencyProperty
类没有公开的构造函数。代替,一个DependencyObject
实例只能使用静态的DependencyProperty.Register()
方法被创造。DependencyProperty
对象被创造之后不能再修改,因为所有的DependencyProperty
成员是只读的。代替,它们的值必须作为Register()
方法的参数被提供。
下面例子显示FrameworkElement
类的Margin
属性是如何被注册的:
static FrameworkElement()
{
var metadata = new FrameworkPropertyMetadata(
new Thickness(), FrameworkPropertyMetadataOptions.AffectsMeasure);
MarginProperty = DependencyProperty.Register("Margin",
typeof(Thickness), typeof(FrameworkElement), metadata,
new ValidateValueCallback(FrameworkElement.IsMarginValid));
...
}
注册一个依赖属性包括二步。首先,你创造一个FrameworkPropertyMetadata
对象,指明希望使用依赖属性的什么服务(诸如支持数据绑定,动画,和日志)。其次,依靠调用DependencyProperty.Register()
静态方法注册属性。这时,你负责提供一些关键成分:
- 属性名称
- 属性的数据类型
- 属性所在类的类型
- 可选,附带有属性设置的
FrameworkPropertyMetadata
对象 - 可选,执行属性验证的回调
两个可选属性值得研究。FrameworkPropertyMetadata
的详细描述见95页。
包装依赖属性
创造依赖属性的最后一步是用一个传统.NET属性包装它。WPF属性使用的是定义在DependencyObject
基类的GetValue()
和SetValue()
方法。
public Thickness Margin
{
set { SetValue(MarginProperty, value); }
get { return (Thickness)GetValue(MarginProperty); }
}
当你创造属性包装时,你应该仅调用SetValue()
和GetValue()
,如在前例中。你不应该添加任何额外的代码验证值,引起事件,等等。那是因为另外的特征可能旁路属性包装,直接调用SetValue()
和GetValue()
。(一个例子是在运行时当一个编译XAML文件被解析时。)SetValue()
和GetValue()
都是公开的。
验证输入值应使用DependencyProperty.ValidateValueCallback
。
引发事件应使用FrameworkPropertyMetadata.PropertyChangedCallback
。
现在可以使用属性了:
myElement.Margin = new Thickness(5);
之后,可能希望移除局部值设置,仿佛你从未设置它。依靠从DependencyObject
继承的ClearValue()
方法。
myElement.ClearValue(FrameworkElement.MarginProperty);
使用依赖属性
依赖属性支持两个关键的行为:改变通知和动态值求解。
改变通知
如果你希望对一个属性的改变作出反应,你有二个选择—创造一个绑定,或写一个触发器。但是,依赖属性没有提供响应属性值改变的事件。
动态值求解
依赖属性因动态值求解的行为而得名。一个依赖属性依赖于多个属性提供者,每个提供者带有它自己的优先级。当你从一个属性值取回一个值时,WPF属性系统经历一系列步骤求得最终值。首先,它通过考虑下列因素决定属性的基值。优先级从最低的到最高排列(最下面的赢):
- 默认值(由
FrameworkPropertyMetadata
对象设置) - 继承的值(如果
FrameworkPropertyMetadata.Inherits
标记被设置,并且一个值已经应用到某个祖先元素) - 主题样式值
- 工程样式值
- 本地值(使用代码或标记直接设置的值)
基值不一定是最终的属性值,WPF通过4个步骤求得属性值:
- 基值
- 表达式(数据绑定和资源)
- 动画
- 通过
CoerceValueCallback
修正值
共享依赖属性
一些类共享依赖属性,即使他们有独立的类层次结构。例如,TextBlock.FontFamily
和Control.FontFamily
都指向同一个依赖属性,它实际上被定义在TextElement
类中以及TextElement.FontFamilyProperty
。TextElement
类的静态构造函数注册属性,但是TextBlock
和Control类的静态构造函数简单地调用DependencyProperty.AddOwner()
方法重用它:
TextBlock.FontFamilyProperty =
TextElement.FontFamilyProperty.AddOwner(typeof(TextBlock));
自定义类
副作用
附加依赖属性
附加属性是依赖属性,并且它被WPF属性系统管理。区别是应用附加属性的类不是定义它的类。
为定义一个附加属性,你使用RegisterAttached()
方法而不是Register()
。这是一个注册Grid.Row
属性的例子:
var metadata = new FrameworkPropertyMetadata(
0, new PropertyChangedCallback(Grid.OnCellAttachedPropertyChanged));
Grid.RowProperty = DependencyProperty.RegisterAttached("Row", typeof(int),
typeof(Grid), metadata, new ValidateValueCallback(Grid.IsIntValueNotNegative));
就像一个普通的依赖属性,你能提供一个FrameworkPropertyMetadata
对象和一个ValidateValueCallback
。
当创造附加属性时,你没有定义.NET属性包装。那是因为附加属性能被设置在任何依赖对象上。例如,Grid.Row
属性可能被设置在一个Grid对象上(如果你有一内部嵌套另一个Grid
的Grid
)或在另外的一些元素上。事实上,即使元素不在一个Grid
内,和即使在你的元素树上不存在Grid
对象,Grid.Row
属性也能被设置在那个元素上。
代替使用一个.NET属性包装,附加属性要求能调用的一对静态方法去设置和获得属性值。这些方法使用熟悉的SetValue()
和GetValue()
方法(从DependencyObject
类继承)。静态的方法应该被命名SetPropertyName()
和GetPropertyName()
。
这里是实现Grid.Row
附加属性的静态方法:
public static int GetRow(UIElement element)
{
if (element == null)
{
throw new ArgumentNullException("");
}
return (int)element.GetValue(Grid.RowProperty);
}
public static void SetRow(UIElement element, int value)
{
if (element == null)
{
throw new ArgumentNullException("");
}
element.SetValue(Grid.RowProperty, value);
}
使用代码设置一个元素位于网格的第一行:
Grid.SetRow(txtElement, 0);
可选地,你能直接调用SetValue()
或GetValue()
方法,旁路掉静态方法:
txtElement.SetValue(Grid.RowProperty, 0);
属性验证
WPF提供两种方法阻止无效值:
ValidateValueCallback
:这回调能接受或拒绝新值。通常,这回调被用来捕获违反属性的约束明显错误。提供它作为DependencyProperty.Register()
方法的参数。CoerceValueCallback
:这回调能修改新值为更可接受的值。通常,这回调被用来处理被设置在同一对象的依赖属性值之间的冲突。这些值可能单独是有效的,但是当应用在一起时不一致。为使用这回调,当创造FrameworkPropertyMetadata
对象时,提供它作为构造函数的参数,此对象随后被传递到DependencyProperty.Register()
方法。
这里是当应用程序试图设置一个依赖属性时:所有的部件如何起作用:
- 首先,
CoerceValueCallback
方法有机会修改提供值(通常,为使它与其它属性一致)或返回DependencyProperty.UnsetValue
,这完全拒绝改变。 - 其次,
ValidateValueCallback
被引发。这方法返回真接受一个值作为有效,或返回假拒绝它。不同于CoerceValueCallback
,ValidateValueCallback
不访问设置属性的实际对象,这意味着你不能检查其它的属性值。 - 最后,如果先前两个阶段都成功了,
PropertyChangedCallback
被触发。这时,你能引发事件去通知其它类。
Validation回调
相当于正常属性的set
部分中的验证。
其签名为接受一个object
输入参数,返回布尔值。返回值为真表示接受,为假表示拒绝。
private static bool IsMarginValid(object value)
{
Thickness thickness1 = (Thickness) value;
return thickness1.IsValid(true, false, true, false);
}
有一个限制,它是一个静态方法,不能访问正被验证的对象,不能使用对象中的其他属性。
Coercion回调
用于验证互相关联的属性
private static object CoerceMaximum(DependencyObject d, object value)
{
RangeBase base1 = (RangeBase)d;
if (((double) value) < base1.Minimum)
{
return base1.Minimum;
}
return value;
}