Avalonia 的 Setters 按照 BindingPriority 的顺序应用,然后是视觉树的局部性,最后是 Styles 集合中的顺序。优先级针对每个 StyledProperty 单独应用,以便样式可以从组合中受益。DirectProperty 和 CLR 属性不能被设置样式,因此不参与此优先级。
BindingPriority 值
Animation = -1, // Highest priority
LocalValue = 0,
StyleTrigger,
Template,
Style,
Inherited,
Unset = int.MaxValue, // Lowest priority
XAML 中的 BindingPriority 如何分配?
BindingPriority 不能在 XAML 中显式设置。以下示例演示了 BindingPriority 在不同场景中是如何隐式分配的。这对于设计和排查按预期工作的样式至关重要。
动画
Animation 拥有最高的 BindingPriority,并应用于 Keyframe 内的 Setter 和整个过渡系统。
<Button Background="Green" Content="Bounces from Red to Blue">
<Button.Styles>
<Style Selector="Button">
<Style.Animations>
<Animation IterationCount="Infinite" Duration="0:0:2">
<KeyFrame Cue="0%">
<Setter Property="Background" Value="Red" />
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="Background" Value="Blue" />
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
</Button.Styles>
</Button>
LocalValue
当一个 XAML 属性在 ControlTemplate 外直接设置时,会分配此值。下面两个 Background 的 Setter 都将具有 LocalValue 优先级。
<Button Background="Orange" />
<Button Background="{DynamicResource ButtonBrush}" />
提示
资源标记扩展对优先级没有影响。
StyleTrigger
当一个 Selector 具有条件激活时,Setter 的 BindingPriority 会从 Style 提升到 StyleTrigger。任何具有条件激活的两个选择器都将具有相同的优先级,无论激活器的数量和在选择器语法中的位置如何。Avalonia 没有 CSS 中的特异性(Specificity)概念。
<Style Selector="Button:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="Orange" />
</Style>
提示
样式类、伪类、子元素位置和属性匹配选择器是条件性的。控件名称选择器不是条件性的。
模板
当一个属性直接在 ControlTemplate 中设置时,下面的 BorderThickness、Background 和 Padding 具有 Template 优先级。
<ControlTemplate>
<Border BorderThickness="2">
<Button Background="{DynamicResource ButtonBrush}" Padding="{TemplateBinding Padding}" />
</Border>
</ControlTemplate>
样式
当在 Style 中定义了一个 Setter 而无需条件激活时。
<Style Selector="Button /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="Orange" />
</Style>
提示
特别需要注意的是,其优先级低于
Template。因此,这些选择器不能用于覆盖上面Template示例中提到的属性。
继承
当一个属性没有被设置时,它可能会从其父级继承属性值。这必须在注册属性时或通过 OverrideMetadata 来指定。
public static readonly StyledProperty<bool> UseLayoutRoundingProperty =
AvaloniaProperty.Register<Layoutable, bool>(
nameof(UseLayoutRounding),
defaultValue: true,
inherits: true);
视觉树局部性
具有相同 BindingPriority 的 Setter 会根据它们在视觉树中相对于 Control 的位置来选择。所需跨越的节点数最少的 Setter 将具有优先权。内联样式的 Setter 在此步骤中具有最高优先级。
<Window>
<Window.Styles>
<Style Selector="Button">
<Setter Property="FontSize" Value="16" />
<Setter Property="Foreground" Value="Red" />
</Style>
</Window.Styles>
<StackPanel>
<StackPanel.Styles>
<Style Selector="Button">
<Setter Property="FontSize" Value="24" />
</Style>
</StackPanel.Styles>
<Button Content="This Has FontSize=24 with Foreground=Red" />
</StackPanel>
</Window>
样式集合顺序
当 BindingPriority 和视觉树局部性都相等时,最终的决定因素是 Styles 集合中的顺序。最后一个适用的 Setter 将具有优先权。
<StackPanel>
<StackPanel.Styles>
<Style Selector="Button.small">
<Setter Property="FontSize" Value="12" />
</Style>
<Style Selector="Button.big">
<Setter Property="FontSize" Value="24" />
</Style>
</StackPanel.Styles>
<Button Classes="small big" Content="This Has FontSize=24" />
<Button Classes="big small" Content="This Also Has FontSize=24" />
</StackPanel>
提示
这些按钮以不同的顺序指定了它们的样式类,但这对设置优先级没有影响。
BindingPriority 不会传递
回顾上面的 Animation 示例。当你悬停时,动画背景会被静态背景替换,尽管 BindingPriority.Animation 有最高优先级。这是因为 Selector 定位到了错误的 Control。需要检查 ControlTheme 来诊断原因。
Button 的简化 ControlTheme:
<ControlTheme x:Key="{x:Type Button}" TargetType="Button">
<Setter Property="Background" Value="{DynamicResource ButtonBackground}"/>
<Setter Property="Template">
<ControlTemplate>
<ContentPresenter x:Name="PART_ContentPresenter"
Background="{TemplateBinding Background}"/>
</ControlTemplate>
</Setter>
<Style Selector="^:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundPointerOver}"/>
</Style>
</ControlTheme>
顶部的 Setter 将 ButtonBackground 应用于 Button,具有 Style 优先级。Background 的渲染由具有 Template 优先级的 ContentPresenter 处理,它会获取已应用于 Button 的 ButtonBackground。
然而,当 Button 被悬停时,:pointerover 选择器被激活,其具有 StyleTrigger 优先级,覆盖了 TemplateBinding 并获取 ButtonBackgroundPointerOver 而不是原来的 Button 的 Background。这绕过了我们原始 Animation 选择器所针对的 Background。以下表格总结了这一点:
| 悬停时的 Background 设置和样式 | 优先级 | 位置 |
|---|---|---|
| ~~Background="Green"~~ | LocalValue | Button |
| Background="Red" | Animation (覆盖 LocalValue) | Keyframe |
~~<ContentPresenter Background="{TemplateBinding Background}"/>~~ |
Template | ControlTemplate |
^:pointerover /template/ ContentPresenter#PART_ContentPresenter |
StyleTrigger (覆盖 Template) | ControlTheme |
相反,我们应该使用至少具有 StyleTrigger 优先级的 Setter 来针对 ContentPresenter。BindingPriority.Animation 符合这一要求。这是一个不检查原始 ControlTemplate 就无法得出的观察结果,并强调仅依赖优先级不足以有效地对应用程序进行样式设置。
修正以覆盖 :pointerover 优先级
<Button Background="Green" Content="Bounces from Red to Blue">
<Button.Styles>
<Style Selector="Button /template/ ContentPresenter#PART_ContentPresenter">
<Style.Animations>
<Animation IterationCount="Infinite" Duration="0:0:2">
<KeyFrame Cue="0%">
<Setter Property="Background" Value="Red" />
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="Background" Value="Blue" />
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
</Button.Styles>
</Button>
Comments