依赖属性值优先级【WPF .NET】
Windows Presentation Foundation (WPF) 属性系统的工作会影响依赖属性的值。 本文介绍 WPF 属性系统中不同基于属性的输入的优先级如何确定依赖属性的有效值。
先决条件
本文假设对依赖属性有一个基本的了解,并且你已阅读 依赖项属性概述。 若要遵循本文中的示例,如果熟悉可扩展应用程序标记语言(XAML),并且知道如何编写 WPF 应用程序,则很有帮助。
WPF 属性系统
WPF 属性系统使用各种因素来确定依赖属性的值,例如实时属性验证、后期绑定和相关属性的属性更改通知。 尽管用于确定依赖属性值的顺序和逻辑很复杂,但学习有助于避免不必要的属性设置,并找出为何尝试设置依赖属性不会导致预期值。
在多个位置设置的依赖项属性
以下 XAML 示例演示按钮 Background 属性上的三种不同的“set”操作如何影响其值。
<StackPanel>
<StackPanel.Resources>
<ControlTemplate x:Key="ButtonTemplate" TargetType="{x:Type Button}">
<Border Background="{TemplateBinding Background}" BorderThickness="{TemplateBinding BorderThickness}"
BorderBrush="{TemplateBinding BorderBrush}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
</ControlTemplate>
</StackPanel.Resources>
<Button Template="{StaticResource ButtonTemplate}" Background="Red">
<Button.Style>
<Style TargetType="{x:Type Button}">
<Setter Property="Background" Value="Blue"/>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="Yellow" />
</Trigger>
</Style.Triggers>
</Style>
</Button.Style>
Which color do you expect?
</Button>
</StackPanel>
在此示例中,Background
属性本地设置为 Red
。 但是,按钮作用域中声明的隐式样式尝试将 Background
属性设置为 Blue
。 并且,当鼠标悬停在按钮上时,隐式样式中的触发器将尝试将 Background
属性设置为 Yellow
。 除强制和动画外,本地设置的属性值具有最高优先级,因此按钮将为红色,即使在鼠标悬停时也是如此。 但是,如果从按钮中删除本地设置值,它将从样式中获取其 Background
值。 在样式中,触发器优先,因此按钮在鼠标移过时会变成黄色,否则则是蓝色。 该示例替换了按钮的默认颜色 ControlTemplate,因为默认模板中鼠标悬停时的颜色已硬编码为 Background
。
依赖属性优先级列表
以下列表是属性系统将运行时值分配给依赖属性时使用的明确优先级顺序。 首先列出了最高优先级。
属性系统强制。 有关强制的详细信息,请参阅 强制和动画。
活动动画或具有“静止”行为的动画。 若要产生实际效果,动画值必须优先于基本(未动画)值,即使基本值是本地设置的。 有关详细信息,请参阅 强制和动画。
本地值。 可以通过“包装器”属性设置本地值,这等同于在 XAML 中设置属性或属性元素,或者通过使用特定实例的属性调用 SetValue API。 通过绑定或资源设置的本地值将具有与直接设置的值相同的优先级。
TemplatedParent 模板属性值。 如果元素是由模板(ControlTemplate 或 DataTemplate)创建的,则元素具有 TemplatedParent。 详细信息请参阅 TemplatedParent。 在
TemplatedParent
指定的模板中,优先级顺序为:触发器。
属性的设定,通常通过 XAML 属性。
隐式样式。 仅适用于 Style 属性。
Style
值是具有与元素类型匹配的 TargetType 值的任何样式资源。 样式资源必须存在于页面或应用程序中。 查找隐式样式资源不会扩展到主题中的样式资源。样式触发器。 样式触发器是显式或隐式样式中的触发器。 样式必须存在于页面或应用程序中。 默认样式中的触发器的优先级较低。
模板触发器。 模板触发器是来源于直接应用的模板或样式中包含的模板的触发器。 样式必须存在于页面或应用程序中。
样式设定值。 样式设置器值是在样式中由 Setter 应用的值。 样式必须存在于页面或应用程序中。
默认样式,也称为 主题样式。 有关详细信息,请参阅 默认(主题)样式。 在默认样式中,优先级顺序为:
主动触发器。
设定者。
继承。 子元素的某些依赖属性从父元素继承其值。 因此,可能不需要在整个应用程序中为每个元素设置属性值。 有关详细信息,请参阅 属性值继承。
依赖属性元数据中的默认值 依赖属性可以在此属性的属性值系统注册过程中设置默认值。 继承依赖属性的派生类可以针对每个具体类型重写依赖属性的元数据(包括默认值)。 有关详细信息,请参阅 依赖属性元数据。 对于继承的属性,父元素的默认值优先于子元素的默认值。 因此,如果未设置可继承属性,则使用根或父元素的默认值,而不是子元素的默认值。
TemplatedParent
TemplatedParent 优先级不适用于在标准应用程序标记中直接声明的元素的属性。 TemplatedParent
概念仅适用于可视化树中通过模板应用而存在的子项。 当属性系统在 TemplatedParent
指定的模板中搜索元素的属性值时,它会搜索创建该元素的模板。 TemplatedParent
模板中的属性值通常与元素上的本地设置值一样,但优先级低于实际本地值,因为模板可能会共享。 有关详细信息,请参阅 TemplatedParent。
Style 属性
优先级顺序相同,适用于所有依赖属性,Style 属性除外。 Style
属性是唯一的,因为它本身无法设置样式。 不建议强制或对 Style
属性进行动画处理(并且对 Style
属性进行动画处理需要自定义动画类)。 因此,并非所有优先项都适用。 只有三种方法可以设置 Style
属性:
明确样式。 直接设置元素的
Style
属性。Style
属性值就像是本地值一样,其优先级与 优先列表中的项 3 相同,。 在大多数情况下,显式样式不会内联定义,而是会被显式引用为资源,例如Style="{StaticResource myResourceKey}"
。隐式样式。 元素的
Style
属性没有被直接设置。 相反,样式在页面或应用程序中存在某个级别时应用,并且具有与样式所适用的元素类型匹配的资源键,例如<Style TargetType="x:Type Button">
。 该类型必须完全匹配,例如,即使MyButton
派生自Button
,<Style TargetType="x:Type Button">
也不会应用于MyButton
类型。属性值的优先级与 优先列表的第 5 项相同。 可以通过调用 DependencyPropertyHelper.GetValueSource 方法、传入 Style
属性以及检查结果中的ImplicitStyleReference
来检测隐式样式值。默认样式,也称为 主题样式。 未直接设置元素的
Style
属性。 相反,该机制来源于 WPF 展示引擎的运行时主题评估。 在运行时之前,Style
属性的值是null
。Style
属性值的优先级与 优先列表中的项 9 相同。
默认(主题)样式
随 WPF 附带的每个控件都有一个默认样式,该样式可能因主题而异,因此默认样式有时称为 主题样式。
ControlTemplate 是控件的默认样式中的一个重要项。 ControlTemplate
是样式的 Template 属性的 setter 值。 如果默认样式不包含模板,则没有自定义模板的控件作为自定义样式的一部分将没有视觉外观。 模板不仅定义控件的视觉外观,还定义模板可视化树中属性与相应控件类之间的连接。 每个控件都会公开一组属性,这些属性可以影响控件的视觉外观,而无需替换模板。 例如,请考虑 Thumb 控件的默认视觉外观,该控件是 ScrollBar 组件。
Thumb 控件具有某些可自定义属性。 Thumb
控件的默认模板会创建一个基本结构或可视化树,其中通过嵌套多个 Border 组件来呈现斜面外观。 在模板中,Thumb
类可自定义的属性通过 TemplateBinding公开。 Thumb
控件的默认模板包括多种边框属性,这些属性分别与 Background 或 BorderThickness等属性共享模板绑定。 但是,如果属性或视觉排列的值在模板中硬编码,或者绑定到直接来自主题的值,则只能通过替换整个模板来更改这些值。 一般情况下,如果属性来自模板化的父对象并且未由 TemplateBinding
显式公开,那么由于无法方便地锁定该属性,样式无法更改其属性值。 但是,该属性可能仍受应用模板中的属性值继承或默认值的影响。
默认样式在其定义中指定 TargetType。 运行时主题评估将默认样式的 TargetType
与控件的 DefaultStyleKey 属性匹配。 相比之下,隐式样式的查找行为会根据控件的实际类型进行。 DefaultStyleKey
的值由派生类继承,因此可能没有关联样式的派生元素会获得默认视觉外观。 例如,如果从 Button派生 MyButton
,MyButton
将继承 Button
的默认模板。 派生类可以替代依赖属性元数据中 DefaultStyleKey
的默认值。 因此,如果希望 MyButton
使用不同的视觉表示形式,则可以替代 MyButton
上 DefaultStyleKey
的依赖项属性元数据,然后定义相关的默认样式(包括模板),以便使用 MyButton
控件打包。 有关详细信息,请参阅 控件创作概述。
动态资源
动态资源 引用和绑定操作在设置它们的位置上具有优先级。 例如,应用于本地值的动态资源与 优先列表中第 3 项的优先级相同,。 另一个示例是,应用于默认样式中的属性集的动态资源绑定的优先级与
在技术层面上,动态资源引用并不是属性系统的一部分,它具有自己的查找顺序,这与 优先列表相互作用。 从本质上讲,动态资源引用的优先级是:从元素到页根、应用程序、主题,然后是系统。 有关详细信息,请参阅 XAML 资源。
尽管动态资源引用和绑定的优先级取决于它们设置的位置,但其值会被推迟生效。 其一个后果是,如果将动态资源或绑定设置为本地值,则对本地值所做的任何更改将完全替换动态资源或绑定。 即使调用 ClearValue 方法来清除本地设置值,也不会还原动态资源或绑定。 事实上,如果在具有动态资源或绑定的属性上调用 ClearValue
(没有文本本地值),则会清除动态资源或绑定。
SetCurrentValue
SetCurrentValue 方法是设置属性的另一种方法,但它不在 优先列表中。 SetCurrentValue
允许你更改属性的值,而无需覆盖之前数值的来源。 例如,如果某个属性由触发器设置,然后使用 SetCurrentValue
分配另一个值,则下一个触发器操作会将该属性重新设置为触发器值。 每当想要设置属性值而不为该值提供本地值的优先级别时,都可以使用 SetCurrentValue
。 同样,可以使用 SetCurrentValue
来更改属性的值,而不需要覆盖现有的绑定。
强制和动画
强制和动画都作用于 基值。 基值 是优先级最高的依赖属性值,通过 优先列表 向上计算,直到达到第 2 项为止。
如果动画未指定某些行为的 From 和 To 属性值,或者动画在完成后故意还原为基值,则基值可能会影响动画值。 若要在实践中了解这一点,请运行 目标值 示例应用程序。 在示例中,对于矩形高度,请尝试设置与任何 From
值不同的初始本地值。 示例动画从立即使用 From
值开始,而不是使用基础值。 通过将 Stop 指定为 FillBehavior,在完成时,动画会将属性值重置为其基值。 正常优先级用于动画结束后的基值确定。
可以将多个动画应用于单个属性,每个动画具有不同的优先级。 相较于应用优先级较高的动画,WPF 呈现引擎可能会组合动画值,具体取决于动画的定义方式和所动画化的值类型。 有关详细信息,请参阅 动画概述。
强制位于 优先列表顶部。 即使是正在运行的动画也会受到值强制约束。 WPF 中的一些现有依赖项属性具有内置强制。 对于自定义依赖属性,可以通过编写在创建属性时作为元数据的一部分传递的 CoerceValueCallback 来定义强制行为。 还可以通过重写派生类中该属性的元数据来替代现有属性的强制行为。 强制与基值的交互方式是,在强制条件按其当前存在的形态被应用的同时,基值仍然被保留。 因此,如果以后解除强制约束,强制将返回可能与基值最近的值,并且一旦解除所有约束,对属性的强制影响可能会立即停止。 有关强制行为的详细信息,请参阅 依赖属性回调和验证。
触发器行为
控件通常将触发器行为定义为其 默认样式的一部分。 在控件上设置本地属性可能会与这些触发器冲突,从而阻止触发器以视觉方式或行为方式响应用户驱动事件。 属性触发器的常见用途是控制状态属性,如 IsSelected 或 IsEnabled。 例如,默认情况下,禁用 Button 时,主题样式触发器(IsEnabled
为 false
)设置 Foreground 值以使 Button
显示为灰显。如果已设置本地 Foreground
值,则优先级较高的本地属性值将覆盖主题样式 Foreground
值,即使禁用 Button
也是如此。 设置替代控件主题级触发器行为的属性值时,请注意不要过度干扰该控件的预期用户体验。
ClearValue
ClearValue 方法清除某个元素的依赖属性中任何本地应用的值。 但是,调用 ClearValue
不能保证在属性注册期间在元数据中建立的默认值是新的有效值。 优先级列表中所有其他参与者 仍然处于活动状态,仅本地设定的值被删除。 例如,如果对具有主题样式的属性调用 ClearValue
,则主题样式值将作为新值应用,而不是基于元数据的默认值。 如果要将属性值设置为已注册的元数据默认值,则通过查询依赖属性元数据获取默认元数据值,并使用调用 SetValue在本地设置属性值。