依赖属性值优先级 (WPF .NET)

Windows Presentation Foundation (WPF) 属性系统的工作会影响依赖属性的值。 本文说明 WPF 属性系统中基于属性的不同输入的优先级如何确定依赖属性的有效值。

重要

面向 .NET 7 和 .NET 6 的桌面指南文档正在撰写中。

先决条件

本文假定你对依赖属性有基本的了解,并且已阅读依赖属性概述。 若要理解本文中的示例,还应当熟悉 Extensible Application Markup Language (XAML) 并知道如何编写 WPF 应用程序。

WPF 属性系统

WPF 属性系统使用各种因素来确定依赖属性的值,例如实时属性验证、后期绑定和相关属性的属性更改通知。 尽管用于确定依赖属性值的顺序和逻辑比较复杂,但了解它有助于避免不必要的属性设置,还可查明设置依赖属性的尝试未生成预期值的原因。

在多个位置设置的依赖属性

以下 XAML 示例演示对按钮 Background 属性进行的三种不同的“设置”操作如何影响其值。

<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 值。

依赖属性优先级列表

下面列出了在将运行时值分配给依赖属性时,属性系统所使用的最终优先级顺序。 最高优先级最先列出。

  1. 属性系统强制。 有关强制转换的详细信息,请参阅强制转换和动画

  2. 活动动画或具有 Hold 行为的动画。 若要拥有实用效果,动画必须拥有比基值(无动画)更高的优先级,即使基值进行了本地设置也是如此。 有关详细信息,请参阅强制转换和动画

  3. 本地值。 可以通过“包装器”属性设置本地值,这相当于在 XAML 中设置特性或属性元素,或者使用特定实例的属性调用 SetValue API。 通过绑定或资源设置的本地值会具有与直接设置的值相同的优先级。

  4. TemplatedParent 模板属性值。 如果元素是通过模板创建(ControlTemplateDataTemplate)创建的,则具有 TemplatedParent。 有关详细信息,请参阅 TemplatedParent。 在通过 TemplatedParent 指定的模板中,优先级顺序为:

    1. 触发器。

    2. 属性集(通常通过 XAML 特性进行设置)。

  5. 隐式样式。 仅应用于 Style 属性。 Style 值是具有与元素类型匹配的 TargetType 值的任何样式资源。 样式资源必须存在于页面或应用程序中。 对隐式样式资源的查找不会扩展到主题中的样式资源。

  6. 样式触发器。 样式触发器是显式或隐式样式中的触发器。 样式必须存在于页面或应用程序中。 默认样式中的触发器的优先级较低。

  7. 模板触发器。 模板触发器是直接应用的模板或样式中的模板中的触发器。 样式必须存在于页面或应用程序中。

  8. 样式资源库值。 样式资源库值是样式中通过 Setter 应用的值。 样式必须存在于页面或应用程序中。

  9. 默认样式,也称为主题样式。 有关详细信息,请参阅默认(主题)样式。 在默认样式中,优先级顺序如下:

    1. 活动触发器。

    2. 资源库。

  10. 继承。 子元素的某些依赖属性从父元素继承其值。 因此,可能不需要在整个应用程序中对每个元素设置属性值。 有关详细信息,请参阅属性值继承

  11. 依赖属性元数据中的默认值。依赖属性可以在该属性的属性系统注册期间设置默认值。 继承依赖属性的派生类可以选择按照类型重写依赖属性元数据(包括默认值)。 有关详细信息,请参阅依赖属性元数据。 对于继承的属性,父元素的默认值优先于子元素的默认值。 因此,如果未设置可继承属性,则会使用根或父级的默认值,而不是子元素的默认值。

TemplatedParent

TemplatedParent 优先级不应用于在标准应用程序标记中直接声明的元素的属性。 只有对于通过应用模板而产生的可视化树中的子项而言,才存在 TemplatedParent 概念。 当属性系统搜索由 TemplatedParent 为元素的属性值指定的模板时,它会搜索创建该元素的模板。 来自 TemplatedParent 模板的属性值通常就像在元素上本地设置的值一样来应用,但是这些属性值的优先级低于实际本地值,因为模板可能进行共享。 有关详细信息,请参阅 TemplatedParent

样式属性

相同的优先级顺序应用于所有依赖属性(Style 属性除外)。 Style 属性的独特性在于它本身无法样式化。 不建议对 Style 属性进行强制转换或动画处理(对 Style 属性进行动画处理需要自定义动画类)。 因此,并非所有优先级项都适用。 有三种设置 Style 属性的方法:

  • 显式样式。 直接设置元素的 Style 属性。 Style 属性值的作用如同本地值一样,具有与优先级列表中的项 3 相同的优先级。 在大多数情形下,显式样式不会内联定义,而是作为资源显式引用,例如 Style="{StaticResource myResourceKey}"

  • 隐式样式。 不直接设置元素的 Style 属性。 而是当样式存在于页面或应用程序中的某个级别时应用样式,并且样式具有与应用样式的元素类型(例如 <Style TargetType="x:Type Button">)匹配的资源键。 类型必须完全匹配,例如 <Style TargetType="x:Type Button"> 不会应用于 MyButton 类型,即使 MyButton 派生自 ButtonStyle 属性值具有与优先级列表中的项 5 相同的优先级。 可以通过调用 DependencyPropertyHelper.GetValueSource 方法、传入 Style 属性以及在结果中检查 ImplicitStyleReference 来检测隐式样式值。

  • 默认样式,也称为主题样式。 不直接设置元素的 Style 属性。 相反,它来自 WPF 表示引擎进行的运行时主题评估。 在运行时之前,Style 属性值为 nullStyle 属性值具有与优先级列表中的项 9 相同的优先级。

默认(主题)样式

WPF 附带的每个控件都具有默认样式,该样式可能因主题而异,这便是默认样式有时称为主题样式的原因

ControlTemplate 是控件的默认样式中的一个重要项。 ControlTemplate 是样式的 Template 属性的资源库值。 如果默认样式不包含模板,自定义样式中没有自定义模板的控件没有可视化外观。 模板不仅定义控件的可视化外观,还定义在模板的可视化树中的属性与对应的控件类之间的联系。 每个控件都公开一组属性,这些属性可以影响控件的可视化外观,而无需替换模板。 例如,请考虑 Thumb 控件(它是 ScrollBar 组件)的默认可视化外观。

Thumb 控件具有某些可自定义属性。 Thumb 控件的默认模板可创建一个基本结构或可视化树(具有几个嵌套的 Border 组件),以创建棱台外观。 在模板中,旨在可由 Thumb 类自定义的属性通过 TemplateBinding 进行公开。 Thumb 控件的默认模板具有各种边界属性,这些属性与诸如 BackgroundBorderThickness 这类属性共享模板绑定。 但是,在属性或可视化排列的值在模板中进行硬编码,或者绑定到直接来自主题的值的情况下,只能通过替换整个模板来更改这些值。 一般而言,如果属性来自模板化父级,并且不是通过 TemplateBinding 进行公开,则不能通过样式更改属性值,因为没有方便的方法可以将其设置为目标。 但是,该属性仍然可能受所应用的模板中的属性值继承影响,或受默认值影响。

默认样式在其定义中指定一个 TargetType。 运行时主题评估会将默认样式的 TargetType 与控件的 DefaultStyleKey 属性进行匹配。 相反,隐式样式的查找行为使用控件的实际类型。 DefaultStyleKey 的值由派生类继承,因此可能没有关联样式的派生元素会获取默认可视化外观。 例如,如果从 Button 派生 MyButton,则 MyButton 会继承 Button 的默认模板。 派生类可以重写依赖属性元数据中的 DefaultStyleKey 的默认值。 因此,如果对 MyButton 需要其他可视化表示形式,则可以对 MyButton 上的 DefaultStyleKey 重写依赖属性元数据,然后定义你将随 MyButton 控件一起打包的相关默认样式(包括模板)。 有关详细信息,请参阅控件创作概述

动态资源

动态资源引用和绑定操作具有其设置位置的优先级。 例如,应用于本地值的动态资源具有与优先级列表中的项 3 相同的优先级。 作为另一个示例,应用于默认样式中属性资源库的动态资源绑定具有与优先级列表中的项 9 相同的优先级。 由于动态资源引用和绑定必须从应用程序的运行时状态中获取值,因此确定任何给定属性的属性值优先级的过程会扩展到运行时。

动态资源引用在技术上不是属性系统的一部分,具有其自己的与优先级列表交互的查找顺序。 本质上,动态资源引用的优先级是:元素到页面根、应用程序、主题,然后是系统。 有关详细信息,请参阅 XAML 资源

尽管动态资源引用和绑定具有其设置位置的优先级,但值会延迟。 这样的一个后果是,如果将动态资源或绑定设置为某个本地值,则对该本地值的任何更改都会完全替换该动态资源或绑定。 即使调用 ClearValue 方法清除本地设置的值,动态资源或绑定也不会还原。 实际上,如果对具有动态资源或绑定的属性(没“文字”本地值)调用 ClearValue,则会清除动态资源或绑定。

SetCurrentValue

SetCurrentValue 方法是设置属性的另一种方式,但它不在优先级列表中。 通过 SetCurrentValue 可以更改属性值而不会覆盖以前值的源。 例如,如果某个属性由触发器设置,然后你使用 SetCurrentValue分配另一个值,则下一个触发器操作会将该属性设置回触发器值。 每当要设置属性值但是不向该值提供某个本地值的优先级时,便可以使用 SetCurrentValue。 同样,可以使用 SetCurrentValue 更改属性值而不覆盖绑定。

强制转换和动画

强制转换和动画都对基值执行操作。 基值是优先级最高的依赖属性值,通过在优先级列表中向上评估来确定(直到达到项 2)。

如果动画没有为某些行为指定 FromTo 属性值,或者动画在完成时有意还原为基值,则基值可能会影响动画值。 若要了解实际效果,请运行目标值示例应用程序。 在示例中,对于矩形高度,尝试设置与任何 From 值不同的初始本地值。 示例动画立即使用 From 值(而不是基值)开始。 通过将 Stop 指定为 FillBehavior,完成时动画会将属性值重置为其基值。 正常优先级会用于动画结束后的基值确定。

可以将多个动画应用于单个属性,每个动画具有不同的优先级。 WPF 表示引擎可以组合动画值,而不是应用优先级最高的动画(具体取决于动画的定义方式和进行动画处理的值类型)。 有关详细信息,请参阅动画概述

强制转换处于优先级列表的顶部。 即使正在运行的动画也会受到值强制转换的制约。 WPF 中的某些现有依赖属性具有内置强制转换。 对于自定义依赖属性,可以通过编写在创建属性时作为元数据的一部分进行传递的 CoerceValueCallback 来定义强制转换行为。 还可以通过在派生类中重写现有属性的元数据来重写该属性的强制转换行为。 强制转换与基值的交互使强制转换上的约束就像当时存在这些约束一样进行应用,但基值仍将保留。 因此,如果强制转换中的约束后来被解除,强制转换将返回与基值最接近的可能值,并且一旦所有约束都解除,强制转换对属性的影响可能会立即停止。 有关强制转换行为的详细信息,请参阅依赖属性回调和验证

触发器行为

控件常常将触发器行为定义为其默认样式的一部分。 在控件上设置本地属性可能会与这些触发器发生冲突,从而防止触发器响应(在视觉上或行为上)用户驱动的事件。 属性触发器常用于控件状态属性,如 IsSelectedIsEnabled。 例如,默认情况下,当禁用 Button 时,主题样式触发器(IsEnabledfalse)会设置 Foreground 值,以使 Button 灰显。如果设置了本地 Foreground 值,则优先级较高的本地属性值会覆盖主题样式 Foreground 值,即使 Button 已禁用。 设置为控件重写主题级别触发器行为的属性值时,请注意不要过度干扰该控件的预期用户体验。

ClearValue

ClearValue 方法为元素清除依赖属性的任何本地应用值。 但是,调用 ClearValue 并不能保证注册属性时在元数据中指定的默认值就是新的有效值。 优先级列表中的所有其他参与者仍然处于活动状态,仅移除本地设置的值。 例如,如果对具有主题样式的属性调用 ClearValue,主题样式值将作为新值而不是基于元数据的默认值进行应用。 如果要将属性值设置为已注册的元数据默认值,则通过查询依赖属性元数据来获取默认元数据值,并通过对 SetValue 的调用在本地设置属性值。

另请参阅