ControlTemplate/Style 设置控件的(触发)属性时触发器不起作用 locally/directly

ControlTemplate/Style triggers are not working, when setting control's (triggering) properties locally/directly

我想在聚焦或悬停控件时更改 BorderBrush
它工作得很好,除了在 window 中我设置默认值 BorderBrush.
在那种情况下,即使我聚焦或悬停控件,BorderBrush 也不会再改变。

我已经有了一个解决方案:创建另一个 属性 以避免直接更改主要默认值 属性,并将主要 属性 绑定到默认值。
但我想知道是否有另一种解决方案而不添加无用的 属性,并且几乎不需要在每个触发器上复制和粘贴整个模板。

<Style TargetType="{x:Type local:IconTextBox}"
       BasedOn="{StaticResource {x:Type TextBox}}">

    <Setter Property="BorderThickness" Value="1"/>
    <Setter Property="BorderBrush" Value="Black"/>

    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type local:IconTextBox}">
                <Grid>
                    <Image Source="{TemplateBinding Icon}" 
                            HorizontalAlignment="Left"
                            SnapsToDevicePixels="True"/>
                    <Border Margin="{TemplateBinding InputMargin}"
                                BorderThickness="{TemplateBinding BorderThickness}"
                                BorderBrush="{TemplateBinding BorderBrush}"
                                Background="{TemplateBinding Background}"
                                SnapsToDevicePixels="True">
                        <ScrollViewer x:Name="PART_ContentHost"
                                      Margin="0" />
                    </Border>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>

    <Style.Triggers>
        <Trigger Property="IsMouseOver" Value="True">
            <Setter Property="BorderBrush" Value="RoyalBlue"/>
        </Trigger>
        <Trigger Property="IsFocused" Value="True">
            <Setter Property="BorderBrush" Value="SteelBlue"/>
        </Trigger>
        <Trigger Property="IsEnabled" Value="False">
            <Setter Property="BorderBrush" Value="Gray"/>
        </Trigger>
    </Style.Triggers>

</Style>

我对 BorderBrushValue 的解决方案:

<Style TargetType="{x:Type local:IconTextBox}"
       BasedOn="{StaticResource {x:Type TextBox}}">

    <Setter Property="BorderThickness" Value="1"/>
    <Setter Property="BorderBrush" Value="Black"/>

    <!-- FIX -->
    <Setter Property="BorderBrushValue"
            Value="{Binding RelativeSource={RelativeSource Self}, Path=BorderBrush}"/>
    <!-- FIX -->

    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type local:IconTextBox}">
                <Grid>
                    <Image Source="{TemplateBinding Icon}" 
                            HorizontalAlignment="Left"
                            SnapsToDevicePixels="True"/>
                    <Border Margin="{TemplateBinding InputMargin}"
                                BorderThickness="{TemplateBinding BorderThickness}"
                                BorderBrush="{TemplateBinding BorderBrushValue}"
                                Background="{TemplateBinding Background}"
                                SnapsToDevicePixels="True">
                        <ScrollViewer x:Name="PART_ContentHost"
                                      Margin="0" />
                    </Border>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>

    <Style.Triggers>
        <Trigger Property="IsMouseOver" Value="True">
            <Setter Property="BorderBrushValue" Value="RoyalBlue"/>
        </Trigger>
        <Trigger Property="IsFocused" Value="True">
            <Setter Property="BorderBrushValue" Value="SteelBlue"/>
        </Trigger>
        <Trigger Property="IsEnabled" Value="False">
            <Setter Property="BorderBrushValue" Value="Gray"/>
        </Trigger>
    </Style.Triggers>

</Style>

Window:

<Window
        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:WpfApp"
        x:Class="WpfApp.Home"
        mc:Ignorable="d"
        Title="Home" Height="450" Width="800">
    <Grid Background="#FF272727">

        <!-- this one, with first style, not works -->
        <local:IconTextBox HorizontalAlignment="Left" VerticalAlignment="Top"
                           Width="200" Height="25" Margin="80,80,0,0"
                           BorderBrush="#FF1E1E1E"/>

    </Grid>
</Window>

可能我理解错了,不过你应该可以在样式上设置一个键。如果它是用键设置的,它不会覆盖默认值,因为您必须专门将样式应用于控件。

根据您的风格设置调子

<Style x:Key="myStyle" ...

为需要的 UserControl 设置特定样式

<local:IconTextBox Style="{StaticResource myStyle}"...

在本地设置 属性,即直接设置将始终覆盖此 属性 的 Style 设置。由于触发器绑定到属性,因此它们也会被覆盖。有关详细信息,请参阅 Microsoft Docs: Dependency Property Setting Precedence List

这通常不是问题,因为您定义隐式 Style 是为了创建默认主题。 UI 设计的一般规则是保持外观一致。

解决方案 1:触发器(和 DataTrigger)- 不推荐

如前所述,TriggerDataTrigger 是 属性 绑定或基于 属性 的状态。当解析器尝试解析 属性 值(在本例中将是触发操作)时,触发器被解析。

由于 DependencyProperty 值优先级,您应该定义一个专门的 Style 来覆盖默认值。本地值将停止 XAML 解析器查找 属性 的任何 Style 设置,因此忽略所有 属性 特定触发器。

专门的 Style 应该基于默认值以允许选择性覆盖:

<Window>
  <Window.Resources>
   
    <!-- Implicit default Style -->
    <Style TargetType="TextBox">
      <Setter Property="BorderBrush" Value="Black" />

      <Setter Property="Template">
        <Setter.Value>
          <ControlTemplate TargetType="TextBox">
            <Border BorderThickness="{TemplateBinding BorderThickness}"
                    BorderBrush="{TemplateBinding BorderBrush}"
                    Background="{TemplateBinding Background}">
              <ScrollViewer x:Name="PART_ContentHost"
                            Margin="0" />
            </Border>

            <ControlTemplate.Triggers>
              <Trigger Property="IsMouseOver" Value="True">
                <Setter Property="BorderBrush" 
                        Value="RoyalBlue" />
              </Trigger>
            </ControlTemplate.Triggers>
          </ControlTemplate>
        </Setter.Value>
      </Setter>
    </Style>

    <!-- Explicit specialized Style based on the implicit default Style -->
    <Style x:Key="SpecializedStyle" 
           TargetType="TextBox"
           BasedOn="{StaticResource {x:Type TextBox}}">
      <Setter Property="BorderBrush" Value="Blue" />
    </Style>
  </Window.Resources>

  <!-- This now works -->
  <local:IconTextBox Style="{StaticResource SpecializedStyle}" />
</Window>

方案二:VisualStateManager(推荐)

如果您更喜欢处理视觉效果而不必定义专门的样式,则应该使用 VisualStateManager。由于这种触发是基于事件的(与基于 TriggerDataTrigger 的 属性 状态相反),因此在本地设置 属性 值不会覆盖触发器。

这就是为什么您可以默认在每个控件上设置 Background 等属性而不会破坏视觉效果 - 默认情况下,控件是使用 VisualStateManager 来处理视觉状态的。

可选:当您想保持外观灵活(主题)时,您可以使用 ComponentResourceKey。定义 ResourceKey 允许通过使用相同的 x:Key.

定义新资源来替换主题资源

VisualStateManager使控件的使用和自定义更加方便:

IconTextBox.cs(可选)

class IconTextBox : TextBox
{
  public static ComponentResourceKey BorderBrushOnMouseOverKey 
    => new ComponentResourceKey(typeof(IconTextBox), "BorderBrushOnMouseOver");
}

Generic.xaml

<ResourceDictionary>

  <!-- Define the original resource (optional)-->
  <Color x:Key="{ComponentResourceKey {x:Type IconTextBox}, BorderBrushOnMouseOver}">RoyalBlue</Color>

  <Style TargetType="IconTextBox">
    <Setter Property="Template">
      <Setter.Value>
        <ControlTemplate TargetType="IconTextBox">
          <Border x:Name="Border"
                  BorderThickness="{TemplateBinding BorderThickness}"
                  BorderBrush="{TemplateBinding BorderBrush}"
                  Background="{TemplateBinding Background}">
            <VisualStateManager.VisualStateGroups>
              <VisualStateGroup x:Name="CommonStates">
                <VisualStateGroup.Transitions>
                  <VisualTransition GeneratedDuration="0:0:0.5" />
                </VisualStateGroup.Transitions>
                <VisualState x:Name="Normal" />
                <VisualState x:Name="MouseOver">
                  <Storyboard>
                    <ColorAnimationUsingKeyFrames
                      Storyboard.TargetProperty="(Border.BorderBrush).(SolidColorBrush.Color)"
                      Storyboard.TargetName="Border">
                      <EasingColorKeyFrame KeyTime="0"
                                           Value="{DynamicResource {x:Static IconTextBox.BorderBrushOnMouseOverKey}}" />
                    </ColorAnimationUsingKeyFrames>
                  </Storyboard>
                </VisualState>
              </VisualStateGroup>
            </VisualStateManager.VisualStateGroups>

            <ScrollViewer x:Name="PART_ContentHost" 
                          Margin="0" />
          </Border>
        </ControlTemplate>
      </Setter.Value>
    </Setter>
  </Style>
</ResourceDictionary>

App.xaml(可选) 覆盖默认颜色资源:

<ResourceDictionary>

  <!-- Override the original resource -->
  <Color x:Key="{x:Static IconTextBox.BorderBrushOnMouseOverKey}" >Blue</Color>
</ResourceDictionary>

访问Microsoft Docs: Control Styles and Templates to find the default style of the requested control. You will find all required template parts as well as all available visual states that are defined by the specific control. You can use this information to implement the VisualStateManager. E.g. Microsoft Docs: TextBox States

除了 之外,我发现的第三个解决方案是在对触发器有了更多了解并考虑到我的上下文之后,不更改样式的本地属性,而是直接更改属性控件模板的元素,使用 TargetName 定位它们。

所以这是我的第三个解决方案:

<Style TargetType="{x:Type local:IconTextBox}"
       BasedOn="{StaticResource {x:Type TextBox}}">

    <Setter Property="BorderThickness" Value="1"/>
    <Setter Property="BorderBrush" Value="Black"/>

    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type local:IconTextBox}">
                <Grid>
                    <Image Source="{TemplateBinding Icon}" 
                            HorizontalAlignment="Left"
                            SnapsToDevicePixels="True"/>
                    <Border x:Name="BorderElement"
                            Margin="{TemplateBinding InputMargin}"
                            BorderThickness="{TemplateBinding BorderThickness}"
                            BorderBrush="{TemplateBinding BorderBrush}"
                            Background="{TemplateBinding Background}"
                            SnapsToDevicePixels="True">
                        <ScrollViewer x:Name="PART_ContentHost"
                                      Margin="0" />
                    </Border>
                </Grid>

                <ControlTemplate.Triggers>
                    <Trigger Property="IsMouseOver" Value="True">
                        <Setter TargetName="BorderElement" Property="BorderBrush"
                                Value="RoyalBlue"/>
                    </Trigger>
                    <Trigger Property="IsFocused" Value="True">
                        <Setter TargetName="BorderElement" Property="BorderBrush"
                                Value="SteelBlue"/>
                    </Trigger>
                    <Trigger Property="IsEnabled" Value="False">
                        <Setter TargetName="BorderElement" Property="BorderBrush"
                                Value="Gray"/>
                    </Trigger>
                </ControlTemplate.Triggers>

            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

注意Triggers必须是控件模板的一部分,否则不能引用BorderElement(编译错误,因为不能在style中使用TargetName!)

所以,这现在可以正常工作了:

<local:IconTextBox HorizontalAlignment="Left" VerticalAlignment="Top"
                   Width="200" Height="25" Margin="80,80,0,0"
                   BorderBrush="#FF1E1E1E"/>