user3605220
user3605220

Reputation:

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

I want to change BorderBrush when I focus or hover the control.
It works great, except when in window I set the default BorderBrush.
In that case the BorderBrush is not changed anymore even if I focus or hover the control.

I have already a solution: create another property to avoid directly changing the main default property, and bind the main property to that, for default value.
But I want to know if there is another solution without adding an useless property, and without almost copy & paste the entire template on every trigger.

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

My solution with 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>

Upvotes: 0

Views: 1456

Answers (3)

BionicCode
BionicCode

Reputation: 29028

Setting a property locally i.e. directly will always override the Style setting of this property. Since triggers are bound to properties they are overridden too. See Microsoft Docs: Dependency Property Setting Precedence List for more info.

This usually isn't an issue as you define an implicit Style with the intention to create a default theme. And a general rule of UI design is to keep the look consistent.

Solution 1: Trigger (and DataTrigger) - not recommended

As mentioned before, Trigger an DataTrigger are property bound or based on the property's state. The trigger is resolved when the parser tries to resolve the property value (which will be in this case a trigger action).

Because of the DependencyProperty value precedence, you should define a specialized Style to override the default. Local values will stop the XAML parser to lookup for any Style setting of the property and therefore ignore all property specific triggers.

The specialized Style should be based on the default to allow selective overrides:

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

Solution 2: VisualStateManager (recommended)

If you prefer to handle visual effects without having to define specialzed styles, you should use the VisualStateManager. Since this kind of triggering is event based (opposed to the property state based Trigger and DataTrigger), setting property values locally won't override the trigger.

This is why you can set properties like Background on every control by default without breaking the visual effects - by default controls are implemented using the VisualStateManager to handle visual states.

Optional: when you want to keep the appearance flexible (theming) you can make use of the ComponentResourceKey. Defining a ResourceKey allows to replace a theme resource by defining a new resource using the same x:Key.

VisualStateManager makes the usage of controls and their customization more convenient:

IconTextBox.cs (optional)

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 (optional) Override the default color resource:

<ResourceDictionary>

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

Visit 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

Upvotes: 0

user3605220
user3605220

Reputation:

A third solution that I found other than the solutions of @BionicCode, after a little more understanding of triggers, and considering my context, is not change the local properties of the style and, instead, change directly the properties of the control template's elements, targeting them with TargetName.

So this is my third solution:

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

Note that Triggers must be a part of control template, otherwise you cannot reference BorderElement (you will get a compile error because you cannot use TargetName in style!).

So, this will work correctly now:

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

Upvotes: 1

Tronald
Tronald

Reputation: 1585

Maybe I am misunderstanding the problem, but you should be able to just set a key on the style. If it's set with a key it won't override the default as you have to specifically apply the style to the control.

Set the key on your style

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

Set specific style on the desired UserControlthat needs it

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

Upvotes: 0

Related Questions