Textbox avec placeholder automatique

Introduction

L’objectif est de créer un style de textbox simple permettant de gérer automatiquement un placeholder.
Cela permettra d’éviter de manipuler la propriété Text ainsi que les événements TextChanged et Focused depuis le code PowerShell.

Le projet commence

Démarrage du projet WPF sous Visual Studio et création du XAML de base :

<!-- XAML complet -->
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Height="200" Width="300" Title="Textbox with placeholder">
    <Grid>
        <TextBox Name="TBX_InputBox"  Width="128" Height="32"/>
    </Grid>
</Window>

Le comportement souhaité du textbox necéssite une modification plus poussée du visuel par défaut.
Pour continuer, on va modifier une copie du style natif :

Image

Image

Le nommage du style est arbitraire, mieux vaut bien choisir pour s’y retrouver si le projet évolue.
Après validation, le style à modifier est généré dans les ressources de la fenêtre et est renseigné au niveau de la propriété Style du textbox :

<!-- XAML complet -->
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Height="200" Width="300" Title="Textbox with placeholder">
    <Window.Resources>
        <SolidColorBrush x:Key="TextBox.Static.Border" Color="#FFABAdB3"/>
        <SolidColorBrush x:Key="TextBox.MouseOver.Border" Color="#FF7EB4EA"/>
        <SolidColorBrush x:Key="TextBox.Focus.Border" Color="#FF569DE5"/>
        <Style x:Key="ST_TextBoxWithPlaceholder" TargetType="{x:Type TextBox}">
            <Setter Property="Background" Value="{DynamicResource {x:Static SystemColors.WindowBrushKey}}"/>
            <Setter Property="BorderBrush" Value="{StaticResource TextBox.Static.Border}"/>
            <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
            <Setter Property="BorderThickness" Value="1"/>
            <Setter Property="KeyboardNavigation.TabNavigation" Value="None"/>
            <Setter Property="HorizontalContentAlignment" Value="Left"/>
            <Setter Property="FocusVisualStyle" Value="{x:Null}"/>
            <Setter Property="AllowDrop" Value="true"/>
            <Setter Property="ScrollViewer.PanningMode" Value="VerticalFirst"/>
            <Setter Property="Stylus.IsFlicksEnabled" Value="False"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type TextBox}">
                        <Border x:Name="border" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" SnapsToDevicePixels="True">
                            <ScrollViewer x:Name="PART_ContentHost" Focusable="false" HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Hidden"/>
                        </Border>
                        <ControlTemplate.Triggers>
                            <Trigger Property="IsEnabled" Value="false">
                                <Setter Property="Opacity" TargetName="border" Value="0.56"/>
                            </Trigger>
                            <Trigger Property="IsMouseOver" Value="true">
                                <Setter Property="BorderBrush" TargetName="border" Value="{StaticResource TextBox.MouseOver.Border}"/>
                            </Trigger>
                            <Trigger Property="IsKeyboardFocused" Value="true">
                                <Setter Property="BorderBrush" TargetName="border" Value="{StaticResource TextBox.Focus.Border}"/>
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
            <Style.Triggers>
                <MultiTrigger>
                    <MultiTrigger.Conditions>
                        <Condition Property="IsInactiveSelectionHighlightEnabled" Value="true"/>
                        <Condition Property="IsSelectionActive" Value="false"/>
                    </MultiTrigger.Conditions>
                    <Setter Property="SelectionBrush" Value="{DynamicResource {x:Static SystemColors.InactiveSelectionHighlightBrushKey}}"/>
                </MultiTrigger>
            </Style.Triggers>
        </Style>
    </Window.Resources>  
    <Grid>
        <TextBox Style="{DynamicResource ST_TextBoxWithPlaceholder}" Name="TBX_InputBox"  Width="128" Height="32"/>
    </Grid>
</Window>

Le style par défaut du textbox est l’un des plus simple à comprendre.

Explication du style par défaut

On y retrouve principalement les balises Setter qui permettent de modifier des propriétés du textbox.
Dans notre cas, on pourra en enlever certaines qui ne nous intéressent pas.
La plupart des propriétés ont des valeurs par défaut et les retirer du style n’est pas forcément impactant.

<!-- XAML partiel -->
<Style x:Key="ST_TextBoxWithPlaceholder" TargetType="{x:Type TextBox}">
    <Setter Property="Background" Value="{DynamicResource {x:Static SystemColors.WindowBrushKey}}"/>
    <Setter Property="BorderBrush" Value="{StaticResource TextBox.Static.Border}"/>
    <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
    <Setter Property="BorderThickness" Value="1"/>
    <Setter Property="KeyboardNavigation.TabNavigation" Value="None"/>
    <Setter Property="HorizontalContentAlignment" Value="Left"/>
    <Setter Property="FocusVisualStyle" Value="{x:Null}"/>
    <Setter Property="AllowDrop" Value="true"/>
    <Setter Property="ScrollViewer.PanningMode" Value="VerticalFirst"/>
    <Setter Property="Stylus.IsFlicksEnabled" Value="False"/>
    <!-- [...] -->
</Style>

Nous avons aussi le ControlTemplate, élément définissant en partie le comportement et le visuel du textbox.
En partie car certaines choses ne sont pas traitées dans le XAML mais en arrière plan, notamment la gestion du contenu au niveau du ScrollViewer nommé “PART_ContentHost”.

<!-- XAML partiel -->
<Style x:Key="ST_TextBoxWithPlaceholder" TargetType="{x:Type TextBox}">
    <!-- [...] -->
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type TextBox}">
                <Border x:Name="border" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" 
                        BorderThickness="{TemplateBinding BorderThickness}" SnapsToDevicePixels="True">
                    <ScrollViewer x:Name="PART_ContentHost" Focusable="false" HorizontalScrollBarVisibility="Hidden" 
                                  VerticalScrollBarVisibility="Hidden"/>
                </Border>
                <ControlTemplate.Triggers>
                    <Trigger Property="IsEnabled" Value="false">
                        <Setter Property="Opacity" TargetName="border" Value="0.56"/>
                    </Trigger>
                    <Trigger Property="IsMouseOver" Value="true">
                        <Setter Property="BorderBrush" TargetName="border" Value="{StaticResource TextBox.MouseOver.Border}"/>
                    </Trigger>
                    <Trigger Property="IsKeyboardFocused" Value="true">
                        <Setter Property="BorderBrush" TargetName="border" Value="{StaticResource TextBox.Focus.Border}"/>
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
    <!-- [...] -->
</Style>

Présentation du ControlTemplate

Comme dit précedemment, le ControlTemplate permet de définir le visuel du composant concerné.
Dans le cas du textbox, son apparence est juste composée par un élément Border.
Les propriétés Background et BorderBrush de la bordure sont définies par liaison aux propriétés du textbox via TemplateBinding.

<!-- XAML partiel -->
<ControlTemplate TargetType="{x:Type TextBox}">
    <Border x:Name="border" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" 
            BorderThickness="{TemplateBinding BorderThickness}" SnapsToDevicePixels="True">
        <ScrollViewer x:Name="PART_ContentHost" Focusable="false" HorizontalScrollBarVisibility="Hidden" 
                        VerticalScrollBarVisibility="Hidden"/>
    </Border>
    <ControlTemplate.Triggers>
        <Trigger Property="IsEnabled" Value="false">
            <Setter Property="Opacity" TargetName="border" Value="0.56"/>
        </Trigger>
        <Trigger Property="IsMouseOver" Value="true">
            <Setter Property="BorderBrush" TargetName="border" Value="{StaticResource TextBox.MouseOver.Border}"/>
        </Trigger>
        <Trigger Property="IsKeyboardFocused" Value="true">
            <Setter Property="BorderBrush" TargetName="border" Value="{StaticResource TextBox.Focus.Border}"/>
        </Trigger>
    </ControlTemplate.Triggers>
</ControlTemplate>

On retrouve aussi la propriété Triggers du ControlTemplate, permettant de définir les comportements du textbox.
Par exemple, le déclencheur avec la propriété IsMouseOver qui permet de changer la couleur de bordure lorsque la souris passe sur le textbox.
C’est d’ailleurs de là que provient la couleur bleu qu’on ne pas modifier par défaut.

<!-- XAML partiel -->
<SolidColorBrush x:Key="TextBox.MouseOver.Border" Color="#FF7EB4EA"/>
<!-- [...] -->
<ControlTemplate TargetType="{x:Type TextBox}">
    <Border x:Name="border" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" 
            BorderThickness="{TemplateBinding BorderThickness}" SnapsToDevicePixels="True">
        <ScrollViewer x:Name="PART_ContentHost" Focusable="false" HorizontalScrollBarVisibility="Hidden" 
                        VerticalScrollBarVisibility="Hidden"/>
    </Border>
    <ControlTemplate.Triggers>
        <Trigger Property="IsEnabled" Value="false">
            <Setter Property="Opacity" TargetName="border" Value="0.56"/>
        </Trigger>
        <Trigger Property="IsMouseOver" Value="true">
            <Setter Property="BorderBrush" TargetName="border" Value="{StaticResource TextBox.MouseOver.Border}"/>
        </Trigger>
        <Trigger Property="IsKeyboardFocused" Value="true">
            <Setter Property="BorderBrush" TargetName="border" Value="{StaticResource TextBox.Focus.Border}"/>
        </Trigger>
    </ControlTemplate.Triggers>
</ControlTemplate>

Maintenant que c’est plus clair, on peut passer à la suite !

Nettoyage préliminaire

Les choses qui ne nous intéressent pas sont retirées du style, si besoin on pourra les remettre au cas par cas plus tard.
On se retrouve avec un style simple, pas très beau mais fonctionnel :

<!-- XAML complet -->
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Height="200" Width="300" Title="Textbox with placeholder">
    <Window.Resources>
        <Style x:Key="ST_TextBoxWithPlaceholder" TargetType="{x:Type TextBox}">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type TextBox}">
                        <Border x:Name="border" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" 
                                BorderThickness="{TemplateBinding BorderThickness}" SnapsToDevicePixels="True">
                            <ScrollViewer x:Name="PART_ContentHost" Focusable="false" HorizontalScrollBarVisibility="Hidden" 
                                          VerticalScrollBarVisibility="Hidden"/>
                        </Border>
                        <ControlTemplate.Triggers>
                            <Trigger Property="IsMouseOver" Value="true">
                                <Setter Property="BorderBrush" TargetName="border" Value="LightBlue"/>
                            </Trigger>
                            <Trigger Property="IsFocused" Value="true">
                                <Setter Property="BorderBrush" TargetName="border" Value="Blue"/>
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </Window.Resources>  
    <Grid>
        <TextBox Style="{DynamicResource ST_TextBoxWithPlaceholder}" Name="TBX_InputBox"  Width="128" Height="32"/>
    </Grid>
</Window>

Image

Modification du template

On peut maintenant modifier le ControlTemplate afin d’y ajouter le placeholder.
La modification est très libre et peut être réalisée de différentes manières.

Dans notre cas, le plus simple est d’ajouter un nouveau composant TextBlock au même niveau que le ScrollViewer.
En revanche, un élément Border ne peut avoir qu’un seul enfant, il faut aussi ajouter un Grid pour régler le problème.

L’élément TextBlock est nommé arbitrairement “PART_PlaceHolder” pour pouvoir le cibler dans les déclencheurs.

<!-- XAML partiel -->
<ControlTemplate TargetType="{x:Type TextBox}">
    <!--<Border x:Name="border" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" 
            BorderThickness="{TemplateBinding BorderThickness}" SnapsToDevicePixels="True">
        <ScrollViewer x:Name="PART_ContentHost" Focusable="false" HorizontalScrollBarVisibility="Hidden" 
                        VerticalScrollBarVisibility="Hidden"/>
    </Border>-->
    <Border x:Name="border" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" 
            BorderThickness="{TemplateBinding BorderThickness}" SnapsToDevicePixels="True">
        <Grid>
            <TextBlock Text="PlaceHolder" Foreground="Gray" Name="PART_PlaceHolder"/>
            <ScrollViewer x:Name="PART_ContentHost" Focusable="false" HorizontalScrollBarVisibility="Hidden" 
                            VerticalScrollBarVisibility="Hidden"/>
        </Grid>
    </Border>
    <!-- [...] -->
</ControlTemplate>

Image

Le placeholder est maintenant visible mais ne se cache pas automatiquement lors du focus ou lorsqu’une saisie est présente.

Ajout d’un DataTrigger

Comme le Trigger, le DataTrigger est un moyen de modifier une propriété en se basant sur le test d’une valeur.
L’avantage du DataTrigger est qu’il n’est pas limité à une simple propriété (grâce au Binding).

On peut ainsi tester la valeur de la propriété Text.Length et changer l’état de visibilité du placeholder en conséquence.

<!-- XAML complet -->
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Height="200" Width="300" Title="Textbox with placeholder">
    <Window.Resources>
        <Style x:Key="ST_TextBoxWithPlaceholder" TargetType="{x:Type TextBox}">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type TextBox}">
                        <Border x:Name="border" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" 
                                BorderThickness="{TemplateBinding BorderThickness}" SnapsToDevicePixels="True">
                            <Grid>
                                <TextBlock Name="PART_PlaceHolder" Text="PlaceHolder" Visibility="Hidden" Foreground="Gray"/>
                                <ScrollViewer x:Name="PART_ContentHost" Focusable="false" HorizontalScrollBarVisibility="Hidden" 
                                              VerticalScrollBarVisibility="Hidden"/>
                            </Grid>
                        </Border>
                        <ControlTemplate.Triggers>
                            <DataTrigger Binding="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Text.Length}" Value="0">
                                <Setter Property="Visibility" TargetName="PART_PlaceHolder" Value="Visible"/>
                            </DataTrigger>
                            <Trigger Property="IsMouseOver" Value="true">
                                <Setter Property="BorderBrush" TargetName="border" Value="LightBlue"/>
                            </Trigger>
                            <Trigger Property="IsFocused" Value="true">
                                <Setter Property="BorderBrush" TargetName="border" Value="Blue"/>
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </Window.Resources>   
    <Grid>
        <TextBox Style="{DynamicResource ST_TextBoxWithPlaceholder}" Name="TBX_InputBox"  Width="128" Height="32"/>
    </Grid>
</Window>

La syntaxe Binding RelativeSource={RelativeSource Mode=Self} correspond à une liaison de données du textbox sur lui même afin d’accéder à sa propriété Text.

Par défaut, on cache le placeholder (Visibility="Hidden"), sa visibilité est rendue lorsque la longueur du texte est égale à 0.

Il est aussi possible aussi d’ajouter facilement le masquage du placeholder lorsque le textbox prend le focus.
Ça sera implémenté plus tard mais rien n’empêche que vous essayiez de le faire par vous même avant.

Changement du texte de placeholder

Suivant le besoin, le placeholder peut être amené à changer de texte (“Rechercher…”,”Nom d’utilisateur…”).
Pour l’instant le texte est spécifié en dur dans le ControlTemplate : <TextBlock Text="PlaceHolder" [...]\>.

Afin de pouvoir configurer le placeholder et donc la propriété Text, on peut se servir de la propriété Tag.
Et comme vu précédemment, on peut réaliser une liaison de données dans le template grâce au TemplateBinding :

<!-- XAML complet -->
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Height="200" Width="300" Title="Textbox with placeholder">

    <Window.Resources>
        <Style x:Key="ST_TextBoxWithPlaceholder" TargetType="{x:Type TextBox}">
            <Setter Property="Tag" Value="Input..."/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type TextBox}">
                        <Border x:Name="border" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" 
                                BorderThickness="{TemplateBinding BorderThickness}" SnapsToDevicePixels="True">
                            <Grid>
                                <TextBlock Text="{TemplateBinding Tag}" Foreground="Gray" Name="PART_PlaceHolder" Visibility="Hidden"/>
                                <ScrollViewer x:Name="PART_ContentHost" Focusable="false" HorizontalScrollBarVisibility="Hidden" 
                                          VerticalScrollBarVisibility="Hidden"/>
                            </Grid>
                        </Border>
                        <ControlTemplate.Triggers>
                            <DataTrigger Binding="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Text.Length}" Value="0">
                                <Setter Property="Visibility" TargetName="PART_PlaceHolder" Value="Visible"/>
                            </DataTrigger>

                            <Trigger Property="IsMouseOver" Value="true">
                                <Setter Property="BorderBrush" TargetName="border" Value="LightBlue"/>
                            </Trigger>
                            <Trigger Property="IsFocused" Value="true">
                                <Setter Property="BorderBrush" TargetName="border" Value="Blue"/>
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </Window.Resources>
    <UniformGrid Rows="3">
        <!--Default placeholder value-->
        <TextBox Style="{DynamicResource ST_TextBoxWithPlaceholder}" 
                 Name="TBX_InputBox" Width="128" Height="32"/>

        <!--Changed placeholder-->
        <TextBox Style="{DynamicResource ST_TextBoxWithPlaceholder}" 
                 Width="128" Height="32" Tag="Search..."/>
        <TextBox Style="{DynamicResource ST_TextBoxWithPlaceholder}" 
                 Width="128" Height="32" Tag="Computer..."/>
    </UniformGrid>
</Window>

Les propriétés définies dans le style sont toujours écrasés si spécifiés directement au niveau du composant.

Image

Amélioration finale

Dans un premier temps, on ajoute le masquage du placeholder quand le TextBox prend le focus :

<!-- XAML partiel -->
<ControlTemplate.Triggers>
    <!-- [...] -->
    <Trigger Property="IsFocused" Value="true">
        <Setter Property="Visibility" TargetName="PART_PlaceHolder" Value="Hidden"/>
        <Setter Property="BorderBrush" TargetName="border" Value="Blue"/>
    </Trigger>
</ControlTemplate.Triggers>

Puis on peut modifier le design comme on le souhaite :

  • Coins arrondis avec La propriété CornerRadius de la bordure.
  • Modification dynamique des couleurs lors des interactions (IsFocused, IsMouseOver…).
  • Couleur de texte du placeholder en se basant sur le Foreground parent et en jouant sur la propriété Opacity.
  • Alignements et marges.
<!-- XAML complet -->
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Height="200" Width="300" Title="Textbox with placeholder">
    <Window.Resources>
        <Style x:Key="ST_TextBoxWithPlaceholder" TargetType="{x:Type TextBox}">
            <Setter Property="Tag" Value="Input..."/>
            <Setter Property="VerticalContentAlignment" Value="Center"/>
            <Setter Property="Padding" Value="8,0"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type TextBox}">
                        <Border x:Name="border" CornerRadius="3"
                                Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" 
                                BorderThickness="{TemplateBinding BorderThickness}" SnapsToDevicePixels="True">
                            <Grid>
                                <TextBlock VerticalAlignment="{TemplateBinding VerticalContentAlignment}" 
                                           Foreground="{TemplateBinding Foreground}" Opacity="0.5" Margin="{TemplateBinding Padding}"
                                           Text="{TemplateBinding Tag}" Name="PART_PlaceHolder" Visibility="Hidden"/>
                                <ScrollViewer x:Name="PART_ContentHost" Focusable="false" HorizontalScrollBarVisibility="Hidden" 
                                              VerticalScrollBarVisibility="Hidden"/>
                            </Grid>
                        </Border>
                        <ControlTemplate.Triggers>
                            <DataTrigger Binding="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Text.Length}" Value="0">
                                <Setter Property="Visibility" TargetName="PART_PlaceHolder" Value="Visible"/>
                            </DataTrigger>
                            <Trigger Property="IsMouseOver" Value="true">
                                <!--Blending of textbox Borderbrush color and white to make it lighter-->
                                <Setter Property="BorderBrush" TargetName="border">
                                    <Setter.Value>
                                        <LinearGradientBrush SpreadMethod="Repeat" EndPoint="0,0">
                                            <GradientStop Color="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=TextBox}, Path=BorderBrush.Color}" Offset="0" />
                                            <GradientStop Color="#FFF" Offset="1"/>
                                        </LinearGradientBrush>
                                    </Setter.Value>
                                </Setter>
                            </Trigger>
                            <Trigger Property="IsFocused" Value="true">
                                <Setter Property="Visibility" TargetName="PART_PlaceHolder" Value="Hidden"/>
                                <Setter Property="BorderThickness" Value="2"/>
                                <!--Blending of textbox Borderbrush color and semi-transparent black to make it darker-->
                                <Setter Property="BorderBrush" TargetName="border">
                                    <Setter.Value>
                                        <LinearGradientBrush SpreadMethod="Repeat" EndPoint="0,0">
                                            <GradientStop Color="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=TextBox}, Path=BorderBrush.Color}" Offset="0" />
                                            <GradientStop Color="#01000000" Offset="1"/>
                                        </LinearGradientBrush>
                                    </Setter.Value>
                                </Setter>
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </Window.Resources>
    <UniformGrid Rows="3">
        <TextBox Width="128" Height="32" Style="{DynamicResource ST_TextBoxWithPlaceholder}"
                 Foreground="Blue" BorderBrush="Blue" Tag="User...">
        </TextBox>
        <TextBox Name="TBX_InputBox" Width="128" Height="32" Style="{DynamicResource ST_TextBoxWithPlaceholder}"
                 Tag="Search...">
        </TextBox>
        <TextBox Width="128" Height="32" Style="{DynamicResource ST_TextBoxWithPlaceholder}"
                 Foreground="Red" BorderBrush="Red">
        </TextBox>
    </UniformGrid>
</Window>

La syntaxe {Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=TextBox}, Path=BorderBrush.Color} correspond à une liaison de données sur le premier composant parent TextBox identifié grâce au mode FindAncestor.
Cette liaison permet de récupérer dynamiquement la couleur de la propriété BorderBrush et de la changer au niveau des Trigger.

Image