目录
4.1 理解依赖项属性
依赖项属性是标准.NET属性的全新实现——具有大量新增价值。在WPF的核心特性(如动画、数据绑定以及样式)中需要嵌入依赖项属性。WPF元素提供的大多数属性都是依赖项属性。
依赖项属性是专门针对WPF创建的。但WPF库中的依赖项属性都是使用普通的.NET属性过程进行了封装。这样便可以通过常规方式使用它们,即使用它们的代码不理解WPF依赖项属性系统也同样如此。
4.1.1 定义依赖项属性
只能为依赖对象(继承自DependencyObject的类)添加依赖项属性。
第一步是定义表示属性的对象,他是DependencyProperty类的实例。属性信息应该始终保持可用,甚至可能需要多个类之间共享这些信息。因此,必须将DependencyProperty对象定义为与其相关联的类的静态字段。
例如,FrameworkElement类定义了Margin属性,所有元素都共享该属性。这意味着,在FrameworkElement类中需要使用类是下面的代码来定义Margin属性:
public class FrameworkElement:UIElement,...
{
public static readonly DependencyProperty MarginProperty;
...
}
根据约定,定义依赖项属性的字段的名称是在普通属性的末尾加上单词“Property”。根据这种命名方式,可从实际属性的名称中区分出依赖项属性的定义。字段的定义使用了readonly关键字,这意味着只能在FrameworkElement类的静态构造函数中对其进行设置。
4.1.2 注册依赖项属性
为了使用依赖项属性,还需注册创建的依赖项属性。这一步骤需要在任何使用属性的代码之前完成,因此必须在与其关联的类的静态构造函数中进行。
WPF确保DependencyProperty对象不能被直接实例化,因为DependencyProperty类没有公有的构造函数。相反,只能使用静态的DependencyProperty.Register()方法创建DependencyProperty实例。WPF还确保在创建DependencyProperty对象后不能该变该对象,因为所有DependencyProperty成员都是只读的。它们的值必须作为Register()方法的参数来提供。
下面的代码显示了如何创建DependencyProperty对象。在此,FrameworkElement类使用静态构造函数来初始化MarginProperty:
static FrameworkElement()
{
FrameworkPropertyMetadata metadata = new FrameworkPropertyMetadata(new Thickness(), FrameworkPropertyMetadataOptions.AffectsMeasure);
MarginProperty = DependencyProperty.Register("Margin",
typeof(Thickness),
typeof(FrameworkElement),
metadata,
new CalidateCalueCallback(FrameworkElement.IsMarginValid);
...
}
注册依赖项属性需要经历两个步骤。首先创建FrameworkPropertyMetadata对象,该对象指示希望通过依赖项属性使用什么服务(如,支持数据绑定、动画以及日志)。接下来通过调用DependencyProperty.Register()静态方法注册属性。
- 属性名称
- 属性使用的数据类型
- 拥有该属性的类型
- 一个具有附加属性设置的FrameworkPropertyMetadata对象(可选)
- 一个用于验证属性的回调函数(可选)
FrameworkPropertyMetadata对象配置依赖项属性的附加功能。FrameworkPropertyMetadata类大多数属性是简单的bool标志,通过设置这些属性来翻转某项功能(默认值均为false)。只有烧出几个是指向用于执行特定任务的自定义方法的回调函数,其中一个是FrameworkPropertyMetadata.DefaultValue,用于设置在第一次初始化属性时WPF讲演应用的默认值。
名称 | 说明 |
---|---|
AffectsArrange AffectsMeasure AffectsParentArrange AffectsParentMeasure | 如果为true,依赖项属性会影响在布局操作的测量过程和排列过程中如何放置相邻的元素或父元素。 例如,Margin依赖项属性将AffectsMeasure属性设置为true,表明如果一个元素的边距发生变化,那么布局容器需要重新执行测量步骤以确定元素新的布局 |
AffectsRender | 如果为true,依赖项属性会对元素的绘制方式造成一定的影响,要求重新绘制元素 |
BindsTwoWaysByDefault | 如果为true,默认情况下,依赖项属性使用双向数据绑定而不是单向数据绑定。不过,当创建数据绑定时,可以明确指定所需的绑定行为 |
Inherits | 如果为true,就通过元素树传播该依赖项属性值,并且可以被嵌套的元素继承。 例如,Font属性时可继承的依赖项属性——如果在更高层次中的元素中为Font属性设置了值,那么该属性值就会被嵌套的元素继承(除非使用自己的字体设置明确地覆盖继承而来的值) |
IsAnimationProhibited | 如果为true,就不能将依赖项属性用于动画 |
IsNotDataBindable | 如果为true,就不能使用绑定表示设置依赖项属性 |
Journal | 如果为true,在基于页面的应用程序中,依赖项属性将被保存到日志 |
SubPropertiesDoNotAffectRender | 如果为true,并且对象的某个子属性发生了变化,WPF将不会重新渲染该对象 |
DefaultUpdateSourceTrigger | 当该属性用于绑定表达式时,该属性用于为Binding.UpdateSourceTrigger属性设置默认值。UpdateSourceTrigger属性决定了数据绑定值在合适应用自身的变化。当创建绑定时,可以手动设置UpdateSourceTrigger属性。 |
DefaultValue | 该属性用于为依赖项属性设置默认值 |
DefaultValueCallback | 该属性提供了一个回调函数,用于在验证依赖项属性之前尝试"纠正"属性值 |
PropertyChangedCallback | 该属性提供了一个回调函数,当依赖项属性的值发生变化时调用该回调函数 |
4.1.3 添加属性包装器
创建依赖项属性的最后一个步骤是使用传统的.NET属性封装WPF依赖项属性。但典型的属性过程是检索或设置某个私有字段的值,而WPF属性的属性过程是使用在DependencyObject基类中定义的GetValue()和SetValue()方法。
public Thickness Margin
{
set {SetValue(MarginProperty, value);}
set {return (Thickness)GetValue(MarginProperty);}
}
当创建属性封装器时,应当只包含对SetValue()和GetValue()方法的调用。不应添加热河验证属性值的额外代码、引发事件的代码等。这是因为WPF中的其他功能可能会忽略属性封装器,并直接调用SetValue()和Get Value()方法。
属性封装器不是验证数据或引发事件的正确位置。WPF的确提供了使用依赖项属性回调函数。
验证操作:DependencyProperty.ValidateValueCallback
事件操作:FrameworkPropertyMetadata.PropertyChangedCallback
可能在某些时候希望删除本地值设置,并像从来没有设置过那样确定属性值。显然,这不能通过设置一个新值来实现。反而需要使用另一个继承自DependencyObject类的方法ClearValue().
myElement.ClearValue(FrameworkElement.MafinProperty);
4.1.4 WPF 使用依赖项属性的方式
WPF的许多功能都需要使用依赖项属性。但是,所有这些功能都是通过每个依赖项属性都支持的两个关键行为进行工作的——更改通知和动态值识别。当属性之发生变化时,依赖项属性不会自动引发事件已通知属性值发生变化。相反,它们会触发受保护的名为OnPropertyChangedCallback()方法。该方法通过两个WPF服务(数据绑定和触发器)出点信息,并调用PropertyChangedCallback回电函数。
换句话说,当属性变化时,如果希望尽心给相应,有两种选择——可以使用属性值创建绑定,也可编写能够自动改变其他属性或开始动的触发器。但依赖项属性没有提供已中通用的方法以触发一些代码,从而对属性的变化进行响应。
当从属性检索值时,WPF属性系统会通过一些列步骤获取最终值。首先,通过考虑以下因素(按优先级从低到高排列)来决定基本值。
- 默认值(由FrameworkPropertyMetadata对象设置的值)
- 继承而来的值(假设设置了FrameworkPropertyMetadata.Inherits标志,并为包含层次中的某个元素提供了值)
- 来自主题样式的值
- 来自项目样式的值
- 本地值(使用diamagnetic或XAML直接为对象设置的值)
如上面的列表所示,可通过直接应用一个值来覆盖整个层次。如果不这么左,属性值可有上面列表中的下一个可用项确定。
WPF按照上面的列表确定依赖项的基本值。但基本值未必就是最后从属性中检索到的值。这是因为WPF还需要考虑其他几个可能改变属性值的提供者。下面列出WPF决定属性值的四步骤:
- 确定基本值
- 如果实行时使用表达式设置的,就对表达式进行求值。当前,WPF支持两类表达式:数据绑定和资源
- 如果属性时动画的目标,就应用动画
- 运行CoerceValueCallback回调函数来修正属性值
4.1.5 共享依赖项属性
尽管一些类具有不同的继承层次,但它们会共享同一依赖项属性。例如,TextBlock.FontFamily属性和Control.FontFamily属性指向同一静态的依赖项属性,该属性实际上在TextElement类中定义的TextElement.FontFamilyProperty依赖项属性。TextElement类的静态构造函数注册该属性,而TextBlock类和Control类的静态构造函数只是通过DependencyProperty.AddOwner()方法重用该属性:
TextBlock.FontFamilyProperty = TextElement.FontFamilyProperty.AddOwner(typeof(TextBlock));
在WPF中重用依赖项属性可得到一些奇异的效果,最有名的是样式。例如,如果使用样式设置TextBlock.FontFamily属性,样式也会影响Control.FontFamily属性,因为在后台这两个类使用同一个依赖项属性。
4.1.6 附加依赖项属性
附加属性是一种依赖项属性。为了定义附加属性,需要使用RegisterAttached()方法。
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属性封装器,反而附加属性需要调用两个经他i方法来设置和获取属性值。
public static int GetRow(UIElement element)
{
return (int)elementGetValue(element);
}
public static void SetRow(UIElement element, int value)
{
element.SetValue(Grid.RowProperty, value);
}
4.2 属性验证
在定义任何类型的属性时,都需要面对错误设置属性的可能性。对于传统的.NET属性,可尝试在属性设置器中保活这类问题。但对于依赖项属性而言,这种方式不合适。
作为代替,WPF提供了两种方法来组织非法值:
- ValidateValueCallback:该回调函数可接受或拒绝新值。通常,该回调函数用于捕获违反属性约束的明显错误。可作为DependencyProperty.Register()方法的一个参数提供该回调函数。
- CoerceValueCallback:该回调函数可将新值修该为更能接受的值。该回调函数通常用于处理为相同兑现共设置的依赖项属性值相互冲突的问题。这些本身可能是合法的,但当同时应用时它们是不相容的。为了使用这个回调函数,当创建FrameworkPropertyMetadata对象时,作为构造函数的一个参数提供该回调函数。
下面时应用程序试图设置依赖项属性时,所有这些内容的作用过程:
- 首先,CoerceValueCallback方法有机会修改提供的值,或者返回DependencyProperty.UnsetValue,这会拒绝修改。
- 接下来激活ValidateValueCallback方法。该方法返回true以接受一个值作为合法值,或者返回false拒绝值。与CoerceValueCallback方法不同,ValidateValueCallback方法不能访问设置属性的实际对象,这意味着不能检查其他属性值。
- 最后,如果前两个阶段都获得成功,就会触发PropertyChangedCallback方法。此时,如果希望为其他类提供通知,可以引发更改事件。
4.2.1 验证回调
MarginProperty = DependencyProperty.Register("Margin",
typeof(Thickness),
typeof(FrameworkElement),
metadata,
new CalidateCalueCallback(FrameworkElement.IsMarginValid);
可使用这个回调函数加强验证,验证通常应该被添加到属性过程的设置部分。提供的回调函数必须指向一个接受对象参数并返回Boolean值的方法。返回true以接受对象时合法的,返回false拒绝对象。
对于验证回调函数有一个限制:它们必须是静态方法而且无权访问正在被验证的对象。所有 能够获得的信息只有芳芳应用的数值。尽管这样更便于重用属性,但可能无法创建考虑其他属性的验证过程。典型的例子是具有Maximum和Minimum属性的元素。显然,为Maximum属性设置的值不能小于为Minimum属性设置的值。但是,不能使用验证会带哦函数来实施这一逻辑,因为一次只能访问一个属性。
4.2.2 强制回调
var metadata = new FrameworkPropertyMetadata();
metadata.CoerceValueCallback = new CoerceValueCallback(CoerceMaximum);
DependencyProperty.Register("Maximum", typeof(double), typeof(RangeBase), metadata);
可以通过CoerceValueBackcall回调函数处理相互关联的属性。例如,ScrollBar控件提供了Maximum、Minimum和Value属性,这些属性都继承自RangeBase类。保持对这些属性进行调整的一种方法是使用属性强制。
例如,当设置Maximum属性时,必须使用强制以确保不能小于Minimum属性的值。
private static object CoerceMaximum(DependencyObject d, object value)
{
var base1 = (RangeBase)d;
if(((double) value) < base1.Minimum)
{
return base1.Minimum;
}
return value;
}
换句话说,如果应用于Maximum属性的值小于Minimum属性值,就用Minimum属性的值设置Maximum属性。注意,CoerceValueBackcall传递两个参数——准备使用的数值和该数值将要应用到的对象。
当设置Value属性时,会发生类似的强制过程。对Value属性进行强制,确保不会超出由Minimum和Maximum属性定义的范围:
internal static object ConstrainToRange(DependencyObject d, object value)
{
var newValue = (double) value;
var base1 = (RangeBase)d;
if(newValue < base1.Minimum) return base1.Minimum;
if(newValue > base1.Maximum) return base1.Maximum;
return newValue;
}
Minimum属性根本不使用值强制。相反,一旦值发生变化,就触发PropertyChangedCallback,然后通过手动触发Maximum和Value属性的强制过程,使它们适应Minimum属性值的变化:
private static object OnMinimumChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var base1 = (RangeBase)d;
...
base1.CoerceValue(RangeBase.MaximumProperty);
base1.CoerceValue(RangeBase.ValueProperty);
}
类似地,一旦设置或强制Maximum属性值,那么也会手动强制Value属性以适应Maximum属性值的变化:
private static object OnMaximumChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var base1 = (RangeBase)d;
...
base1.CoerceValue(RangeBase.ValueProperty);
}