文章目录
一、前言
在上篇文“依赖属性概述”中提到了依赖属性优先级,虽然知道了局部值具有高优先级,但是细节上略去太多。
这就导致了在实际遇到问题时,可能知道是优先级导致的,但不知道应该怎么处理。因此,有必要进一步学习。
二、依赖属性优先级
WPF属性系统的工作方式会影响依赖属性的值。本文解释了WPF属性系统中不同的基于属性的输入的优先级是如何确定依赖属性的有效值的。
2.1 前置知识
本文假设你已具备依赖属性的基本知识。
如果还不知道什么是依赖属性,建议先看看上一篇文 “依赖属性概述” 。
2.2 WPF属性系统
WPF属性系统会根据各种因素来确定依赖属性的值,例如实时属性验证、延迟绑定以及相关属性的属性更改通知。
尽管确定依赖属性值的顺序和逻辑很复杂,但了解它们有助于你避免不必要的属性设置,和找出尝试设置依赖属性却没有得到预期值的原因。
要求程序员像机器(这边指WPF属性系统)一样,脑内运行这套逻辑显然不现实,但是了解这套系统的几个关键点,对实际项目中排查问题很有帮助。
⭐2.2.1 在多处设置依赖属性
下面XAML示例展示了按钮的 Background 属性上的三种不同的"设置"操作是如何影响其值的。
<StackPanel>
<StackPanel.Resources>
<ControlTemplate x:Key="ButtonTemplate" TargetType="{x:Type Button}">
<Border Background="{TemplateBinding Background}" BorderThickness="{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 。除了强制值和动画外,局部设置属性值具有最高优先级,因此按钮将变成红色——即使在鼠标悬停时也是如此。但若你从按钮中删除局部设置的值,那么它将从样式中获取背景值。在样式中,触发器是优先的,因此鼠标悬停时按钮将为黄色,否则为蓝色。该示例替换了按钮的默认 ControlTemplate ,因为默认模板具有硬编码(hard-coded,即写死的固定值)的鼠标悬停背景值。
⭐2.3 依赖属性优先级列表
以下列表是属性系统在给依赖属性分配运行时值时所使用的明确优先级顺序。
这个列表非常重要,从第三项开始,一般项目中会频繁用到。
如果项目中出现了多处设置同一依赖属性值的情况,那不可避免地会用到该列表。
有时遇到设置的属性值不是自己所期望的情况,那八成就是优先级问题。
最高优先级列在最前。
注意
该优先级列表并不绝对,或者说只看该表无法应对所有实际情况,还有许多补充,这也是WPF属性系统复杂的原因之一。
- 属性系统强制 (Property system coercion)。
- 活动动画或具有保持行为的动画。为了有实际效果,动画值必须优先于基本值,即使基本值是局部的。
- 局部值(Local value,也有翻译成本地值的)。通过包装器属性设置局部值,相当于在XAML中设置attribute或属性元素,或者通过使用特定实例的属性调用SetValue API来设置局部值。 通过绑定或资源设置的局部值将与直接设置的值具有相同的优先级。
- TemplatedParent模板属性值。如果元素是由模板(ControlTemplate或DataTemplate)创建的,则该元素具有TemplatedParent。在TemplatedParent指定的模板中,优先级为:
a. 触发器
b. 通过XAML attribute的属性设置 - 隐式样式,仅适用于 Style 属性。Style 值是其TargetType值与元素类型匹配的任何样式资源。样式资源必须存在于页面或应用程序中。
隐式样式资源的查找不会扩展到主题中的样式资源。你可能会疑惑,为什么没说显式样式,因为显式样式也属于局部值。
- 样式触发器。样式触发器是显式或隐式样式内的触发器。该样式必须存在于页面或应用程序中。默认样式的触发器中的优先级较低。
- 模板触发器。模板触发器是来自直接应用的模板,或样式内的模板的触发器。该样式必须存在于页面或应用程序中。
- 样式设置器值(Setter)。样式设置器值是由样式内的Setter应用的值。样式必须存在于页面或应用程序中。
- 默认样式,也称为主题样式。默认样式中,优先顺序为:
a. 活动的触发器
b. 设置器 - 继承。子元素的某些依赖属性可能会从其父元素继承。因此,一般无需在整个应用程序的每个元素上都设置属性值。
- 依赖属性元数据的默认值。依赖属性可以在该属性的属性系统注册期间设置默认值。继承依赖属性的派生类可以基于每个类型重写依赖属性元数据(包括默认值)。详细信息,参阅“依赖属性元数据”章节。对于继承的属性,父元素的默认值优先于子元素的默认值。因此,如果未设置可继承的属性,则使用根元素或父元素的默认值,而不是子元素的默认值。
2.4 模板父级/模板化父级 TemplatedParent
TemplatedParent 优先级不适用于直接在标准应用程序标记中声明的元素的属性。 TemplatedParent 概念仅适用于通过应用程序模板而存在的可视化树中的子项。
可视化树
Visual Tree,即将文档页面上的控件进一步展开,进入其内部,是逻辑树的延申。
比如说页面上有一个按钮,逻辑树到按钮这一层就停止了,可视化树可以进到按钮内部,连里面有几个矩形,几个边框都知道。
①当属性系统在 TemplatedParent 指定的模板中搜索元素的属性值时,它实际上是在搜索创建该元素的模板。 ②TemplatedParent 模板中属性值通常表现得像元素上设置的局部值一样,但因为模板可能被共享,它们的优先级低于实际的局部值。
①持有模板的控件,搜索属性值要和一般控件区分开来;
②前半句大概是值应用了模板的控件元素,往往可以在元素上设置局部值来更改模板的外观,这时模板的外观是跟着元素上的局部值走的,即像元素上设置的局部值一样。后半句是说,因为模板可能被多个元素共享使用的,为了能复用且表现出一定差异性,模板中的值通常不会硬编码。由于模板的这种共享性质,当模板中的值与元素上的局部值发生冲突时,局部值通常会覆盖模板值。因为局部值是针对特定元素的具体设置,模板值则是通用设置。也即设计系统时,局部值拥有更高优先级。
2.5 Style 属性
上述的优先级顺序列表适用于所有依赖属性,除了Style。Style 属性的独特之处在于它本身无法被样式化。不建议对 Style 属性进行强制或动画处理(而且对Style属性进行动画处理需要自定义动画类)。因此,并非所有优先级项都适用。设置 Style 属性只有三种方法:
- 显式样式,元素的 Style 属性是直接设置的。 Style 属性值就像它的局部值一样,并且与优先级列表中的项3具有相同优先级(相同优先级并不意味着同时设置时就能生效,同一级内还有优先级之分;事实上,局部值的优先级是高于显式样式的)。大多数情况下,显式样式不是内联定义的,而是显式引用为资源,例如
Style="{StaticResource myResourceKey}"
。 - 隐式样式,元素的 Style 属性不是直接设置的。相反,当样式存在于页面或应用程序中的某一级,并且具有与该样式所应用的元素类型匹配的资源键时,例如
<Style TargetType="x:Type Button">
,该样式就会被应用。类型必须完全匹配,例如,即使 MyButton 派生自 Button ,<Style Target="x:Type Button">
也不会应用于 MyButton 类型。 该 Style 属性值与优先级列表中的项5具有相同优先级。可以通过调用 DependencyPropertyHelper.GetValueSource 方法,传入 Style 属性并检查结果中的 ImplicitStyleReference 来检测隐式样式值。 - 默认样式,也称为主题样式。其元素的 Style 属性也不是直接设置的。它来自WPF渲染引擎的运行时主题评估。在运行之前,Style 属性值为null。Style属性值与优先级列表中的项9具有相同的优先级。
🔺2.6 默认(主题)样式
WPF推出的每个控件都有一个默认样式,该样式可能因主题而异,这就默认样式有时也称为主题样式的原因。
ControlTemplate 是控件默认样式中的一个重要项,它是样式的Template属性的设置器值。如果默认样式不包含模板,则控件(应用了自定义样式,但样式中没有自定义模板的)将没有视觉外观。模板不仅定义控件的视觉外观,还定义了模板可视化树中的属性与相应控件类之间的连接。每个控件都公开一组属性,这些属性可以影响控件的视觉外观,而无需替换模板。例如,考虑Thumb控件的默认视觉外观,它是一个 ScrollBar 组件。(这个thumb不是点赞的拇指啊👍,指的是滑块控件)
Thumb 控件具有某些可自定义的属性。 Thumb 控件的默认模板创建一个基本结构和可视化树,并使用多个嵌套的 Border 组件来创建斜角外观。在模板内,想通过 Thumb 类修改的属性值可由 TemplateBinding 公开。 Thumb 控件的默认模板具有各种边框属性,这些属性与 Background 、 BorderThickness 等属性共享模板绑定。但是,如果属性或视觉排列的值在模板中是硬编码的,或者绑定到直接来自主题的值,则你只能通过替换整个模板来更改这些值。通常,如果属性来自模板化父亲并且未由 TemplateBinding 公开,则该属性值无法通过样式更改,因为没有便捷的方法来定位它。但是,该属性仍然会受到所应用模板中的属性值继承或默认值的影响。
默认样式在其定义中指定一个 TargetType 。运行时主题评估将默认样式的 TargetType 与控件的 DefaultStyleKey 属性相匹配。相反,隐式样式的查找行为使用控件的实际类型。 DefaultStyleKey 的值由派生类继承,因此可能没有关联样式的派生元素将获得默认的视觉外观。例如,如果从 Button 派生 MyButton ,则 MyButton 将继承 Button 的默认模板。派生类可以重写依赖属性元数据中 DefaultStyleKey 的默认值。因此,如果你希望 MyButton 具有不同的视觉表现形式,则可以重写 MyButton 上 DefaultStyleKey 的依赖属性元数据,然后定义相关的默认样式(包括模板),并将其与 MyButton 控件一起打包。
2.7 动态资源
动态资源引用和绑定操作具有它们设置位置的优先级。例如,应用于局部值的动态资源与优先级列表中的项3具有相同优先级;应用于默认样式内的属性设置器的动态资源绑定具有与优先级列表中的项9相同的优先级。由于动态资源引用和绑定必须从应用程序的运行时状态获取值,因此确定任何给定属性的属性值优先级都会延申到运行时。
从技术上讲,动态资源引用不是属性系统的一部分,并且具有自己的与优先级列表交互的查找顺序。本质上,动态资源引用的优先级时:到页面根目录的元素、应用程序、主题,然后是系统。
尽管动态资源引用和绑定具有其设置位置的优先级,但该值会被延迟。这样做的后果之一是,如果你将动态资源或绑定设置为局部值,则对局部值的任何更改都会完全替换动态资源或绑定。即使调用 ClearValue 方法清除局部设置值,动态资源或绑定也不会恢复。实际上,如果你对具有动态资源或绑定(没有文字局部值)的属性调用ClearValue,则动态资源或绑定将被清除(就不单单是清除值那么简单,是清除整个绑定)。
2.8 SetCurrentValue
SetCurrentValue 方法是设置属性的另一种方法,但它不在优先级列表中。 SetCurrentValue 允许你更改属性的值,而无需覆盖先前值的源。例如,如果某个属性由触发器设置,然后你使用 SetCurrentValue 分配另一个值,则下一个触发器操作会将该值设置回触发器值。每当你想要设置属性值而不为该值设定局部值的优先级时,都可以使用 SetCurrentValue 。同样,你可以使用 SetCurrentValue 更改属性的值,而无需覆盖绑定。
2.9 强制与动画
强制与动画都作用于基本值。基本值是具有最高优先级的依赖属性值,通过优先级列表向上计算直到第二项来确定。
如果动画没有为某些行为指定From和To属性值,或者动画在完成时有意恢复为基本值,则基本值能够影响动画值。
2.10 触发器行为
控件通常将触发行为定义为其默认样式的一部分。在控件上设置局部属性可能会与这些触发器发生冲突,从而阻止触发器响应(视觉上或行为上)用户驱动的事件。属性触发器的常见用途是控制状态属性,如 IsSelected 或 IsEnabled。例如,默认情况下,当禁用 Button 时,主题样式触发器(IsEnabled为false)会设置 Foreground值以使Button显示为灰色。如果你设置了局部 Foreground 值,则较高优先级的局部属性值会推翻主题样式的前景值,即使按钮处于禁用状态。当设置覆盖控件主题级别触发行为的属性值时,请注意不要过度干扰该控件的预期用户体验。
2.11 ClearValue
ClearValue方法清除元素的依赖属性的任何局部应用值。但是,调用 ClearValue并不能保证在属性注册期间在元数据中建立的默认值是新的有效值。优先级列表中的所有其他参与者仍然处于活动状态,并且仅删除本地设置的值。
三、总结
这章算相当复杂了,如果对依赖属性和应用场景没有一定了解,看原文很难看懂。
我觉得不好看懂的主要原因是每种优先级对应的场景不清楚,如果你了解应用场景,并能将不同优先级对应的场景做对比,那会事半功倍。
我觉得第一遍只要了解有优先级那么一回事儿,大致记一下设置属性的优先级顺序。
后面遇到具体问题再具体分析。