第四章 依赖项属性
本章将学习 WPF 如何使用更高级的依赖项属性(dependency property)功能替换原来的.NET属性。
依赖项属性使用效率更高的保存机制,并支持附加功能,如更改通知(change notification)以及属性值继承(在元素树中向下传播默认属性值的能力)。
依赖项属性也是 WPF 许多重要功能的基础,包括动画、数据绑定以及样式。
幸运的是,尽管改变了这些基础,但在代码中仍可以使用与读取和设置传统的.NET属性相同的方式来读取和设置依赖项属性。
4.1 理解依赖性属性
依赖项属性是专门针对 WPF 创建的。但 WPF 库中的依赖项属性都使用普通的.NET 属性过程(property procedure)进行了封装。这样便可以通过常规方式使用它们,即使使用它们的代码不理解 WPF 依赖项属性系统也同样如此。用旧技术封装新技术看起来有些奇怪,但这正是 WPF 能够改变基础组成部分(如属性),而不会扰乱.NET领域中其他部分的原因。
4.1.1 定义依赖性属性
只能为依赖对象(继承自 DependencyObjcct的类)添加依赖项属性。
WPF 基础结构的关键部分中的大部分都间接继承自 DependencyObject类,最明显的例子就是元素
①第一步定义表示属性的对象,它是 DependencyProperty 类的实例。
属性信息应该始终保持可用,甚至可能需要在多个类之间共享这些信息(在 WPF 元素中这是十分普遍的)。因此,必须将 DependencyProperty 对象定义为与其相关联的类的静态字段。
例如,FrameworkElement 类定义了 Margin 属性,所有元素都共享该属性(Margin 属性是依赖项属性)。
这意味着,在 FrameworkElement 类中需要使用类似下面的代码来定义 Margin 属性:
public class FrameWorkElemet : UIElement
{public static readonly DependencyProperty MarginProperty;
}
根据约定,定义依赖项属性的字段的名称是在普通属性的末尾处加上单词“Property”。
根据这种命名方式,可从实际属性的名称中区分出依赖项属性的定义。
字段的定义使用了 readonly关键字,只能在 FrameworkElement 类的静态构造函数中对其进行设置。
4.1.2 注册依赖性属性
为了使用依赖项属性,还需要使用 WPF 注册创建的依赖项属性。这一步骤需要在任何使用属性的代码之前完成,因此必须在与其关联的类的静态构造函数中进行。
WPF 确保 DependencyProperty 对象不能被直接实例化,因为 DependencyProperty 类没有公有的构造函数。
相反,只能使用静态的 DependencyProperty.Register()方法创建 DependencyProperty实例。
所有DependencyProperty 成员都是只读的以确保在创建 DependencyProperty 对象后不能改变该对象。它们的值必须作为 Register()方法的参数来提供。
②第二步注册依赖项属性;注册依赖项属性需要经历两个步骤。
-
首先创建 FrameworkPropertyMetadata 对象,该对象指示希望通过依赖项属性使用什么服务(如支持数据绑定、动画以及日志)。
-
接下来通过调用DependencyProperty.Register()静态方法注册属性。在这一步骤中,需要提供以下几个要素:
-
属性名称(在该例中为 Margin)
-
属性使用的数据类型(在该例中为Thickness结构)
-
所有者类型(在该例中为FrameworkElement 类)
-
属性元数据(这里是metadata)
-
一个具有附加属性设置的 FrameworkPropertyMetadata 对象,该要素是可选的一个用于验证属性的回调函数
-
//将 IsMarginValid 定义为静态方法
static bool IsMarginValid(object value)
{Thickness margin = (Thickness)value;// 在这里添加你的验证逻辑// 例如,确保 Margin 的值是非负的return margin.Left >= 0 && margin.Right >= 0 && margin.Top >= 0 && margin.Bottom >= 0;
}static FrameworkElement()
{FrameworkPropertyMetadata metadata = new FrameworkPropertyMetadata(new Thickness(), FrameworkPropertyMetadataOptions.AffectsMeasure);MarginProperty = DependencyProperty.Register("Margin",//属性名typeof(Thickness),//所使用的数据类型typeof(FrameworkElement), //所有者类型metadata,//属性元数据new ValidateValueCallback(FrameworkElement.IsMarginValid));// 使用静态方法 IsMarginValid
}
使用 FrameworkPropertyMetadata 对象配置创建的依赖项属性的附加功能。
FrameworkPropertyMetadata 类的大多数属性是简单的 Boolean 标志,通过设置这些属性来翻转某项功能(每个 Boolean 标志的默认值为 false)。只有少数几个是指向用于执行特定任务的自定义方法的回调函数,其中一个是 FrameworkPropertyMetadata.Defaultvalue,用于设置在第一次初始化属性时WPF 将要应用的默认值。
| 名称 | 说明 |
|---|---|
| AffectsArange、AffectsMeasure 、AffectsParentArrange 和 AffectsParentMeasure | 如果为true,依赖项属性会影响在布局操作的测量过程和排列过程中如何放置相邻的元素或父元素。例如,Margin依赖项属性将AffectsMeasure 属性设置为true,表明如果一个元素的边距发生变化,那么布局容器需要重新执行测量步骤以确定元素新的布局 |
| AffectsRender | 如果为true,依赖项属性会对元素的绘制方式造成一定的影响,要求重新绘制元素 |
| BindsTwoWayByDefault | 如果为 true,默认情况下,依赖项属性将使用双向数据绑定而不是单向数据绑定。不过,当创建数据绑定时,可以明确指定所需的绑定行为 |
| Inherits | 如果为 true,就通过元素树传播该依赖项属性值,并且可以被嵌套的元素继承。例如,Font属性是可继承的依赖项属性——如果在更高层次的元素中为Font属性设置了值,那么该属性值就会被嵌套的元素继承(除非使用自己的字体设置明确地覆盖继承而来的值) |
| IsAnimationProhibited | 如果为 true,就不能将依赖项属性用于动画 |
| IsNotDataBindable | 如果为true,就不能使用绑定表达式设置依赖项属性 |
| Journal | 如果为true,在基于页面的应用程序中,依赖项属性将被保存到日志(浏览过的页面的历史记录)中 |
| SubPropertiesDoNotAffectRender | 如果为 true,并且对象的某个子属性(属性的属性)发生了变化,WPF 将不会重新渲染该对象 |
| DefaultUpdateSourceTrigger | 当该属性用于绑定表达式时,该属性用于为 Binding.UpdateSourceTrigger 属性设置默认值。UpdateSourceTrigger 属性决定了数据绑定值在何时应用自身的变化。当创建绑定时,可以手动设置 UpdateSourceTrigger属性 |
| DefaultValue | 该属性用于为依赖项属性设置默认值 |
| CoerceValueCallback | 该属性提供了一个回调函数,用于在验证依赖项属性之前尝试“纠正”属性值 |
| PropertyChangedCallback | 该属性提供了一个回调函数,当依赖项属性的值发生变化时调用该回调函数 |
4.1.3 添加属性包装器
③最后一步,使用传统的.NET属性封装 WPF 依赖项属性
WPF 属性的属性过程是使用在DependencyObject基类中定义的 GetValue( )和 SetValue( )方法。
当创建属性封装器时,应当只包含对 SetValue()和 GetValue()方法的调用。不应当添加任何验证属性值的额外代码、引发事件的代码等。这是因为 WPF 中的其他功能可能会忽略属性封装器,并直接调用 SetValue( )和 GetValue( )方法(比如:在运行时解析编译过的 XAML 文件)。SetValue( )和 GetValue( )方法都是公有的。
属性封装器不是验证数据或引发事件的正确位置。WPF使用依赖项属性回调函数可以验证数据或引发事件。
可通过前面介绍的 DependencyProperty.ValidateValueCallback 回调函数进行验证操作,而事件的触发应当在 4.1.4节中将要介绍的 FrameworkPropertyMetadata.PropertyChangedCallback 回调函数中进行。
依赖项属性遵循严格的优先规则来确定它们的当前值。即使您没有直接设置依赖项属性,它也可能已经有了数值——该数值可能是由数据绑定、样式或动画提供的,也可能是通过元素树继承来的(4.1.4节“WPF 使用依赖项属性的方式”将介绍有关这些优先规则的更多内容)。
只要直接设置了属性值,设置的属性值就会覆盖所有其他的影响。以后,可能希望删除本地值设置,并像从来没有设置过那样确定属性值。显然,这不能通过设置一个新值来实现。反而需要使用另一个继承自 DependencyObject类的方法:ClearValue( )。
using System.Windows;namespace WpfApp1
{/// <summary>/// MainWindow.xaml 的交互逻辑/// </summary>public partial class MainWindow : Window{public MainWindow(){InitializeComponent();FrameworkElement myElement = new FrameworkElement();myElement.Margin = new Thickness(10, 20, 30, 40);myElement.ClearValue(FrameworkElement.MarginProperty);}private void yy_LongText_Checked(object sender, RoutedEventArgs e){this.yy_TextBox.Text = "This is a test that demonstrates how buttons adapt themselves to fit the content they contain when they aren't explicitly sized.This behavior makes localization much easier, as the text can be adjusted without affecting the layout of the buttons.";}private void yy_LongText_Unchecked(object sender, RoutedEventArgs e){this.yy_TextBox.Text = "This is a test that demonstrates how buttons adapt themselves to fit the content they contain " +"\twhen they aren't explicitly sized." +"\tThis behavior makes localization much easier, " +"\nas the text can be adjusted without affecting the layout of the buttons.";}}public class FrameworkElement : UIElement{public static readonly DependencyProperty MarginProperty;//将 IsMarginValid 定义为静态方法static bool IsMarginValid(object value){Thickness margin = (Thickness)value;// 在这里添加你的验证逻辑// 例如,确保 Margin 的值是非负的return margin.Left >= 0 && margin.Right >= 0 && margin.Top >= 0 && margin.Bottom >= 0;}static FrameworkElement(){FrameworkPropertyMetadata metadata = new FrameworkPropertyMetadata(new Thickness(), FrameworkPropertyMetadataOptions.AffectsMeasure);MarginProperty = DependencyProperty.Register("Margin",typeof(Thickness), typeof(FrameworkElement), metadata,// 使用静态方法 IsMarginValidnew ValidateValueCallback(FrameworkElement.IsMarginValid));}public Thickness Margin {get { return (Thickness)GetValue(MarginProperty); }set { SetValue(MarginProperty, value); }}}
}
<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="200" Width="380"><Grid Margin="3,3,10,3"><Grid.RowDefinitions><RowDefinition Height="*"/><RowDefinition Height="Auto"/></Grid.RowDefinitions><Grid.ColumnDefinitions><ColumnDefinition Width="Auto"></ColumnDefinition><ColumnDefinition Width="*"></ColumnDefinition></Grid.ColumnDefinitions><StackPanel Grid.Row="0" Grid.Column="0"><Button Name="yy_Prev" Margin="10,10,10,3">Prev</Button><Button Name="yy_Next" Margin="10,3,10,10">Next</Button><CheckBox Name="yy_LongText" Margin="10,10,10,10" Checked="yy_LongText_Checked" Unchecked="yy_LongText_Unchecked">Show Long Text</CheckBox></StackPanel><TextBox Grid.Row="0" Grid.Column="1" Margin="0,10,10,10" x:Name="yy_TextBox" TextWrapping="WrapWithOverflow" Grid.RowSpan="2">Text...</TextBox><Button Grid.Row="1" Grid.Column="0" Name="yy_Close" Margin="10,3,10,10">Close</Button></Grid>
</Window>
4.1.4 WPF使用依赖性的方式
WPF的许多功能都需要使用依赖项属性。
所有这些功能都是通过每个依赖项属性都支持的两个关键行为进行工作的——更改通知和动态值识别。
当属性值发生变化时,依赖项属性不会自动引发事件以通知属性值发生了变化。相反,它们会触发受保护的名为OnPropertyChangedCallback()的方法。
该方法通过两个 WPF 服务(数据绑定和触发器)传递信息,并调用 PropertyChangedCallback 回调函数(如果已经定义了该函数)。
换句话说,当属性变化时,如果希望进行响应,有两种选择——可以使用属性值创建绑定(详见第8章),也可以编写能够自动改变其他属性或开始动画的触发器(详见第 11 章)。
但依赖项属性没有提供一种通用的方法以触发一些代码,从而对属性的变化进行响应。
如果正在处理一个已经创建的控件,可使用属性回调机制或者引发一个事件来响应属性值的变化。
许多通用控件为与用户提供的信息相对应的属性使用了该技术。例如,TextBox控件提供了 TextChanged 事件,ScrollBar 控件提供了 ValueChanged 事件。控件可使用 Property-ChangedCallback 实现类似的功能,但出于性能方面的考虑,依赖项属性没有以通用的方式提供这一功能。
对于依赖项属性工作很重要的第二个功能是动态值识别。这意味着当从依赖项属性检索值时,WPF 需要考虑多个方面。
依赖项属性因该行为得名——本质上,依赖项属性依赖于多个属性提供者,每个提供者都有各自的优先级。
当从属性检索值时,WPF属性系统会通过一系列步骤获取最终值。首先通过考虑以下因素(按优先级从低到高的顺序排列)来决定基本值(base value):
- 默认值(由FrameworkPropertyMetadata 对象设置的值)。
- 继承而来的值(假设设置了 FrameworkPropertyMetadata.Inherits 标志,并为包含层次中的某个元素提供了值)。
- 来自主题样式的值(将在第 18 章讨论)。
- 来自项目样式的值(将在第 11 章讨论)。
- 本地值(使用代码或XAM工 直接为对象设置的值)。
该系统的一个优点是它占用的资源较少。
如果没有为属性设置本地值,WPF 将从样式、其他元素或默认值中检索值。
这时,就不需要内存来保存值。如果为窗体添加了几个按钮,立刻就可以注意到对内存的节省。每个按钮都有很多属性,如果它们都通过这些机制中的某个机制进行设置,那么根本就不需要占用内存。
WPF 按照上面的列表确定依赖项属性的基本值。但基本值未必就是最后从属性中检索到的值。这是因为 WPF 还需要考虑其他几个可能改变属性值的提供者。
WPF 决定属性值的四步骤过程如下:
-
确定基本值(如上所述)。
-
如果属性是使用表达式设置的,就对表达式进行求值。当前,WPF支持两类表达式:数据绑定(详见第8章)和资源(详见第10章)。
-
如果属性是动画的目标,就应用动画。
-
运行 CoerceValueCallback 回调函数来修正属性值(后面的 4.2节“属性验证”将介绍如何使用该技术)。
本质上,依赖项属性被硬编码连接到一小部分 WPF 服务中。
4.1.5 共享的依赖项属性
尽管一些类具有不同的继承层次,但它们会共享同一依赖项属性。例如,TextBlock.FontFamily属性和 Control.FontFamily 属性指向同一个静态的依赖项属性,该属性实际上是在 TextElement类中定义的 TextElement.FontFamilyProperty 依赖项属性。
TextElement 类的静态构造函数注册该属性,而TextBlock类和 Control类的静态构造函数只是通过调用 DependencyProperty.AddOwner()方法重用该属性:
TextBlock.FontFamilyProperty = TextElement.FontFamilyProperty.AddOwner(typeof(TextBlock));
可以使用相同的技术来创建自己的自定义类(假定在所继承的父类中还没有提供属性,否则直接重用即可)。
还可以使用重载的 AddOwner()方法来提供验证回调函数以及仅应用于依赖项属性新用法的新 FrameworkPropertyMetadata 对象。
在 WPF 中重用依赖项属性可得到一些奇异的效果,最有名的是样式。例如,如果使用样式自动设置 TextBlock.FontFamily 属性,样式也会影响 Control.FontFamily 属性,因为在后台这两个类使用同一个依赖项属性。在第11章将看到这一现象。
4.1.6 附加的依赖性属性
第2章(2.3.4 附加属性)介绍了一类特殊的依赖项属性,称为附加属性。附加属性是一种依赖项属性,由WPF 属性系统管理。不同之处在于附加属性被应用到的类并非定义附加属性的那个类。
第 3 章介绍的布局容器中列举了最常见的附加属性例子。例如,Grid 类定义了 Row 和Column 附加属性,这两个属性被用于设置 Gird 面板包含的元素,以指明这些元素应被放到哪个单元格中。类似地,DockPanel 类定义了 Dock 附加属性,而 Canvas 类定义了 Lef、Right、Top 和 Bottom 附加属性。
为了定义附加属性,需要使用 RegisterAttached()方法,而不是使用 Register()方法。下面列举了一个注册 Grid.Row 属性的例子:
// 注册附加属性
FrameworkPropertyMetadata metadata1 = new FrameworkPropertyMetadata(0, new PropertyChangedCallback(Grid.OnCellAttachedPropertyChanged));Grid.RowProperty = DependencyProperty.RegisterAttached("Row", typeof(int),typeof(Grid), metadata1,new ValidateValueCallback(Grid.IsIntValidNotNegative));
与普通的依赖项属性一样,可提供 FrameworkPropertyMetadata对象和 ValidateValueCallback回调函数。
当创建附加属性时,不必定义.NET 属性封装器。这是因为附加属性可以被用于任何依赖对象。例如,Grid.Row 属性可能被用于 Grid 对象(如果在 Grid 控件中嵌套了另一个 Grid 控件),也可能被用于其他元素。实际上,Grid.Row 属性甚至可以被用于并不位于 Grid 控件中的元素甚至在元素树中根本就不存在 Grid 对象。
不是使用.NET属性封装器,反而附加属性需要调用两个静态方法来设置和获取属性值,这两个方法使用为人熟知的 SetValue()和 GetValue()方法(继承自 DependencyObject 类)。这两个静态方法应当命名为 SetPropertyName()和 GetPropertyName()。
public static int GetRow(UIElement element)
{if(element == null) { throw new ArgumentNullException("element"); }return (int)element.GetValue(Grid.RowProperty);
}public static void SetRow(UIElement element, int value)
{if(element == null) { throw new ArgumentNullException("element"); }element.SetValue(Grid.RowProperty, value);
}
使用代码将元素放到Grid控件中的第一行:
Grid.SetRow(txtElement, 0);txtElement.SetValue(Grid.RowProperty, 0);//也可以调用SetValue()或者GetValue()方法,从而绕过这两个静态方法
使用 SetValue()方法设置附加属性的过程不符合一般人的思维习惯。
可在代码中使用重载版本的 SetValue()方法,为任何依赖项属性附加一个值,即使该属性没有被定义为附加属性也同样如此。
ComboBox comboBox = new ComboBox();
comboBox.SetValue(PasswordBox.PasswordCharProperty, "*");
为 ComboBox 对象设置了 PasswordBox.PasswordChar 属性值,尽管 Password Box.PasswordCharProperty 属性被注册为普通的依赖项属性而不是附加属性。该操作不会改变ComboBox 的工作方式——毕竟,ComboBox的内部代码不会去査找它并不知道的属性的值,但在自己的代码中可以对 PasswordChar 值进行操作。尽管使用不同的方法注册附加属性和常规的依赖项属性,但对于 WPF而言它们没有实质性区别。唯一的区别是XAML解析器是否允许。除非将属性注册为附加属性,否则在标记的其他元素中无法设置。
4.2 属性验证
在定义任何类型的属性时,都需要面对错误设置属性的可能性。对于传统的.NET属性,可尝试在属性设置器中捕获这类问题。但对于依赖项属性而言,这种方法不合适,因为可能通过WPF属性系统使用SetValue0方法直接设置属性。作为代替,WPF 提供了两种方法来阻止非法值:
-
ValidateValueCallback:该回调函数可接受或拒绝新值。通常,该回调函数用于捕获违反属性约束的明显错误。可作为 DependencyProperty.Register( )方法的一个参数提供该回调函数。
-
CoerceValueCalback:该回调函数可将新值修改为更能被接受的值。该回调函数通常用于处理为相同对象设置的依赖项属性值相互冲突的问题。这些值本身可能是合法的,但当同时应用时它们是不相容的。为了使用这个回调函数,当创建FrameworkPropertyMetadata 对象时(然后该对象将被传递到 DependencyProperty.Register( )方法),作为构造函数的一个参数提供该回调函数。
当应用程序试图设置依赖项属性时,所有这些内容的作用过程如下:
- 首先,CoerceValueCallback 方法有机会修改提供的值(通常,使提供的值和其他属性相容),或者返回 DependencyProperty.UnsetValue,这会完全拒绝修改。
- 接下来激活 ValidateValueCallback方法。该方法返回 true 以接受一个值作为合法值,或者返回 false 拒绝值。与 CoerceValueCallback 方法不同,ValidateValueCallback 方法不能访问设置属性的实际对象,这意味着不能检查其他属性值。
- 最后,如果前两个阶段都获得成功,就会触发 PropertyChangedCallback 方法。此时,如果希望为其他类提供通知,可以引发更改事件。
4.2.1 验证回调
DependencyProperty.Register()方法接受可选的验证回调函数:
MarginProperty = DependencyProperty.Register("Margin",typeof(Thickness), typeof(FrameworkElement), metadata,// 使用静态方法 IsMarginValidnew ValidateValueCallback(FrameworkElement.IsMarginValid));
可使用这个回调函数加强验证,验证通常应被添加到属性过程的设置部分。提供的回调函数必须指向一个接受对象参数并返回 Boolean 值的方法。返回 true 以接受对象是合法的,返回false 拒绝对象。
对 FrameworkElement.Margin 属性的验证十分枯燥乏味,因为它依赖于内部的Thickness.IsValid()方法。该方法确保当前使用的 Thickness 对象(表示边距)是合法的。例如,可能构造了一个完全可以接受的 Thickness 对象(却不适于设置边距)。一个例子是 Thickness 对象使用了负值。如果提供的 Thickmess 对象对于边距是不合法的,IsMarginValid 方法将返回 false:
private static bool IsMarginValid(object value)
{Thickness thickness1 = (Thickness)value;return (thickness1.IsValid(true,false, true, false));
}
对于验证回调函数有一个限制:必须是静态方法而且无权访问正在被验证的对象。
所有能够获得的信息只有刚刚应用的数值。尽管这样更便于重用属性,但可能无法创建考虑其他属性的验证例程。典型的例子是具有 Maximum 和 Minimum 属性的元素。显然,为 Maximum属性设置的值不能小于为 Minimum 属性设置的值。但是,不能使用验证回调函数来实施这一逻辑,因为一次只能访问一个属性。
解决这一问题更好的方法是使用数值强制(coercion)。强制是在验证之前发生的一个步骤,它允许修改数值,使其更加容易被接受(例如,增大 Maximum 属性值使其至少等于 Minimum 属性值)或者根本就不允许改变。强制步骤是通过另一个回调函数进行的,但这个回调函数是关联到 FrameworkPropertyMetadata 对象的方法(见 4.2.2 节的讨论)。
4.2.2 强制回调
通过 FrameworkPropertyMetadata 对象使用 CoerceValueCallback 回调函数。
FrameworkPropertyMetadata metadata = new FrameworkPropertyMetadata();
metadata.CoerceValueCallback = new CoerceValueCallback(CoerceMaximum);DependencyProperty.Register("Maximum", typeof(double), typeof(RangeBase), metadata);
可以通过 CoerceValueCallback 回调函数处理相互关联的属性。例如,ScrollBar 控件提供了Maximum、Minimum 和 Vale 属性,这些属性都继承自 RangeBase 类。保持对这些属性进行调整的一种方法是使用属性强制。
例如,当设置 Maximum 属性时,必须使用强制以确保不能小于 Minimum 属性的值:
private static object CoerceMaximum(DependencyObject d, object value)
{RangeBase rangeBase = (RangeBase)d;double newValue = (double)value;if (newValue < rangeBase.Minimum){return rangeBase.Minimum;}return newValue;
}
换句话说,如果应用于 Maximum 属性的值小于 Minimum 属性的值,就用 Minimum 属性的值设置 Maximum 属性。注意,CoerceValueCallback 传递两个参数——准备使用的数值和该数值将要应用到的对象。
当设置 Value 属性时,会发生类似的强制过程。对 Value 属性进行强制,确保不会超出由Minimum 和 Maximum 属性定义的范围,使用下面的代码:
internal static object ConstrainToRange(DependencyObject d, object value)
{double newValue = (double)value;RangeBase rangeBase = (RangeBase)d;double minimum = rangeBase.Minimum;if (newValue < minimum) { return minimum; }double maxium = rangeBase.Maximum;if (newValue > maxium) { return maxium; }return newValue;
}
Minimum 属性根本不使用值强制。相反,一旦值发生变化,就触发 PropertyChangedCallback,然后通过手动触发 Maximum 和 Value 属性的强制过程,使它们适应 Minimum 属性值的变化:
private static void OnMinmumChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{RangeBase rangeBase = (RangeBase)d;// ...rangeBase.CoerceValue(RangeBase.MaximumProperty);rangeBase.CoerceValue(RangeBase.ValueProperty);
}
类似地,一旦设置或强制 Maximum 属性的值,那么也会手动强制 Value 属性以适应Maximum 属性值的变化:
private static void OnMaximumChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{RangeBase rangeBase = (RangeBase)d;// ...rangeBase.CoerceValue(RangeBase.ValueProperty);rangeBase.OnMaximumChanged((double) e.OldValue, (double) e.NewValue);
}
如果设置的值相互冲突,最终结果是 Minimum 属性具有优先权,其次是 Maximum 属性(并且可能会被 Minimum 属性强制),最后是 Value 属性(并且可能会被 Maximum 和 Minimum 属性强制)。
步骤序列令人感到有些困惑,它的目的是确保当以不同的顺序设置 ScrollBar 控件的属性时不会出错。
这是一个重要的初始化考虑事项,例如,当为XAML 文档创建窗口时。所有 WPF控件保证它们的属性可按任何顺序进行设置,而不会引起任何行为变化。如果仔细分析上面进行属性强制的代码,就会发现问题。例如,考虑下面的代码:
ScrollBar bar = new ScrollBar();//首次创建 ScrollBar 控件时,Value 属性的值为 0,Minimum 属性的值为 0,而 Maximum 属性的值为 1
bar.Value = 100;//Value 属性被强制为 1(因为最初 Maximum 属性被设置为默认值 1),即bar.Value=1
bar.Minimum = 1;//bar.Value=1//当 Maximum 属性被改变后,它会触发对 Minimum 和 Value 属性的强制。这一强制作用于最初设定的值。
//WPF依赖项属性系统仍然保存了本地值 100,并且现在该数值是可以接受的,它可以被应用到 Value属性。因此执行完第4行后,两个属性都发生了变化。
bar.Maximum = 200;//bar.Value=100
该行为与何时设置 Maximum 属性无关。例如,如果加载窗口时将 Value 属性设置为 100,并在后面当用户单击按钮时设置 Maximum 属性,此时 Value 属性仍然会恢复为合法的值 100(为阻止这一行为的发生,唯一方法是设置不同的值,或使用继承自DependencyObject 类的ClearValue()方法删除应用过的本地值)。
该行为是由 WPF的属性识别系统造成的,在前面学习过该系统。尽管 WPF 在内部保存曾经设置的精确本地值,但当读取属性时会(通过强制以及其他几方面的考虑)评估应当是哪个属性。
4.3 小结
本章深入分析了 WPF 依赖项属性。首先介绍如何定义和注册依赖项属性,接下来介绍了如何将它们插入到其他 WPF 服务中,以及它们如何支持验证和强制。下一章将研究另一个对传统的.NET基础结构的核心部分进行扩展的WPF功能:路由事件。
学习更多 WPF 内部运行原理的最好方法之一是查看 WPF 基本元素的代码,如 Button 类、UIElement 类以及 FrameworkElement 类。浏览这些代码最好的工具之一是 Reflector,可以在https://www.red-gate.com/products/reflector/trial/thank-you/上找到该工具。使用 Reflector,可查看依赖项属性的定义,浏览初始化它们的静态构造函数代码,甚至可以分析在类代码中使用它们的方式。您还可以得到类似的与路由事件相关的低级信息,下一章将介绍路由事件。
| 区别 | 属性 | 依赖项属性 | 附加属性 |
|---|---|---|---|
| 套餐举例 | 吃饭时候的套餐,比如套餐A:面包、牛奶、猪脚饭 没得选,这几样都是一个整体,一卖就是完整一套 | 自己家的自助餐,自家人想吃啥拿啥 我不喜欢牛奶,我就可以不拿牛奶 弟弟不喜欢面包就可以不吃面包 | 饭店的自助餐,所有人都可以想吃啥拿啥 它本身是依赖项属性的一个特例 |
| 声明方式 | private int a=0; | DependencyProperty.Register(); | DependencyProperty.RegisterAttached(); |
