Search code examples
wpfpowershell

WPF TextBox Style to show hint text - binding ToolTip or Tag property to Style resource


I found in this post How can I add a hint text to WPF textbox? an inline style I liked to add a hint text to the background of a textbox. I used it to create a style with control template in the <Window.Resources> section Text1:

<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"
    Name="Window"  Title="MainWindow" Height="120" Width="1000">                                
<Window.Resources>
    <Style x:Key="Text1" TargetType="{x:Type TextBox}" >
      <Setter Property="SnapsToDevicePixels" Value="True"/>
      <Setter Property="OverridesDefaultStyle" Value="True"/>
      <Setter Property="KeyboardNavigation.TabNavigation" Value="None"/>
      <Setter Property="FocusVisualStyle" Value="{x:Null}"/>
      <Setter Property="AllowDrop" Value="true"/>
      <Setter Property="BorderThickness" Value="2,2,2,2" />
      <Setter Property="FontSize" Value="16" />
      <Setter Property="Margin" Value="1" />
      <Setter Property="Template">
        <Setter.Value>
         <ControlTemplate TargetType="{x:Type TextBox}" xmlns:sys="clr-namespace:System;assembly=mscorlib" >
            <ControlTemplate.Resources>
                <SolidColorBrush x:Key="DisabledForegroundBrush" Color="#888" />
                <SolidColorBrush x:Key="DisabledBackgroundBrush" Color="#EEE" />
                <SolidColorBrush x:Key="SolidBorderBrush" Color="#888" />
                <SolidColorBrush x:Key="WindowBackgroundBrush" Color="#FFF" />
                <SolidColorBrush x:Key="Whity" Color="White" />
                <VisualBrush x:Key="CueBannerBrush" AlignmentX="Left" AlignmentY="Center" Stretch="None">
                    <VisualBrush.Visual>
                        <Label Content="This is a hint text" Foreground="LightGray" />
                    </VisualBrush.Visual>
                </VisualBrush>
                <Storyboard x:Key="SrcWrongInput" TargetName="Src" >     
                    <ColorAnimation Storyboard.TargetProperty="(BorderBrush).(SolidColorBrush.Color)" To="Red" AutoReverse="True" RepeatBehavior="4x" Duration="0:0:0:0.3"/>        
                </Storyboard>
            </ControlTemplate.Resources> 
            <Border Name="Border"  CornerRadius="2"  Padding="2"  Background="{StaticResource WindowBackgroundBrush}"  BorderBrush="{StaticResource SolidBorderBrush}" BorderThickness="1" >
              <ScrollViewer Margin="0" x:Name="PART_ContentHost"/>
            </Border>
            <ControlTemplate.Triggers>
                <Trigger Property="Text" Value="{x:Static sys:String.Empty}">
                    <Setter TargetName="Border" Property="Background" Value="{StaticResource CueBannerBrush}"/>
                </Trigger>
                <Trigger Property="Text" Value="{x:Null}">
                    <Setter TargetName="Border" Property="Background" Value="{StaticResource CueBannerBrush}"/>
                </Trigger>
                <Trigger Property="IsKeyboardFocused" Value="True">
                    <Setter TargetName="Border" Property="Background" Value="{StaticResource Whity}"/>
                </Trigger>
            </ControlTemplate.Triggers> 
          </ControlTemplate>
        </Setter.Value>
      </Setter>
    </Style>
</Window.Resources>
<Grid>
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="7*" />
            <ColumnDefinition Width="2*" />
            <ColumnDefinition Width="1*" />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="5*" />
            <RowDefinition Height="5*" />
        </Grid.RowDefinitions>
        <TextBox   Name="Src"  Grid.Column="0" Grid.Row="0" Style="{StaticResource Text1}" ToolTip="Enter path to exe or folder C:\Program Files\..." VerticalContentAlignment="Center" />
        <TextBox   Name="Name" Grid.Column="1" Grid.Row="0" Style="{StaticResource Text1}" ToolTip="Enter name" VerticalContentAlignment="Center"/>
        <TextBox   Name="Rslt" Grid.Column="0" Grid.Row="1" Style="{StaticResource Text1}" ToolTip="Result of procedure" VerticalContentAlignment="Center">
            <TextBox.Resources>
                <Storyboard x:Key="RsltWrongInput" TargetName="Rslt" >   
                    <ColorAnimation Storyboard.TargetProperty="(BorderBrush).(SolidColorBrush.Color)" To="Red" AutoReverse="True" RepeatBehavior="4x" Duration="0:0:0:0.3"/>        
                </Storyboard>   
            </TextBox.Resources>
        </TextBox>
        <Button    Name="Do"   Grid.Column="2" Grid.Row="0" />
    </Grid>
</Grid>

Its loaded with this ps1 script:

Add-Type -AssemblyName presentationframework
[xml]$XAML = Get-Content "MainWindowStack.xaml"
$reader=(New-Object System.Xml.XmlNodeReader $xaml) 
try{$Form=[Windows.Markup.XamlReader]::Load( $reader )}
catch{Write-Host "Unable to load Windows.Markup.XamlReader"; exit}
$xaml.SelectNodes("//*[@Name]") | ForEach-Object {Set-Variable -Name ($_.Name) -Value $Form.FindName($_.Name)}
$DataContextRslt = New-Object System.Collections.ObjectModel.ObservableCollection[Object]
$do.Add_Click({
    $rsltWrongInputStory = $Rslt.Resources["RsltWrongInput"]
    $rsltWrongInputStory.begin()
})
$Form.ShowDialog() | out-null

It loads and shows the hint text, which disappears as soon as the text is not empty or the textbox gets keyboard focus, which is exactly the behavior I intended. But I am not able to bind the content of the label in the VisualBrush x:Key="CueBannerBrush" to another property of the textbox (e.g. Tag or ToolTip) or to a DataContext to make the hint text adjustable. I tried all kinds of different Binding / TemplateBinding expressions, with relativeSource, ancestors with no result.

I also checked other posts on this topic: In Parameterized style to display hint text in WPF TextBox [duplicate] there is a comment on how and why this could not work followed by this link Adding placeholder text to textbox which has an example for a different approach:

<Style x:Key="placeHolder" TargetType="{x:Type TextBox}" BasedOn="{StaticResource {x:Type TextBox}}">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type TextBox}">
                <Grid>
                    <TextBox Text="{Binding Path=Text,
                                            RelativeSource={RelativeSource TemplatedParent}, 
                                            Mode=TwoWay,
                                            UpdateSourceTrigger=PropertyChanged}"
                             x:Name="textSource" 
                             Background="Transparent" 
                             Panel.ZIndex="2" />
                    <TextBox Text="{TemplateBinding Tag}" Background="{TemplateBinding Background}" Panel.ZIndex="1">
                        <TextBox.Style>
                            <Style TargetType="{x:Type TextBox}">
                                <Setter Property="Foreground" Value="Transparent"/>
                                <Style.Triggers>
                                    <DataTrigger Binding="{Binding Path=Text, Source={x:Reference textSource}}" Value="">
                                        <Setter Property="Foreground" Value="LightGray"/>
                                    </DataTrigger>
                                </Style.Triggers>
                            </Style>
                        </TextBox.Style>
                    </TextBox>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

It does work, using this style I can set the hint text using the Tag property of the textbox, however it has the restriction that it is not reacting on keyboard focus like the first, the hint text only disappears once a letter is entered.

My attempt to add that behavior with this trigger failed:

<DataTrigger Binding="{Binding Path=IsKeyboardFocused, Source={x:Reference textSource}}" Value="">
                                        <Setter Property="Foreground" Value="LightGray"/>
                                    </DataTrigger>

Yet another problem accrued with the first style, when trying to add VerticalContentAlignment="Center" to the setters, the Window does not load anymore, therefore I added it to every textbox individually. There are also two more problems which the execution of the storyboard "SrcWrongInput" which I added to the Sources of the style. For one, I don't know how to address it, inside the style resources, so I also added it to resource of the last textbox named "Rslt" to at least make sure that it is compatible with the style. It is not, I triggered it over the button "Do" click handler in the ps1-script and got this exception:

"Von der BorderBrush-Eigenschaft wird nicht auf "DependencyObject" im Pfad "(BorderBrush).(0)" verwiesen."
The BorderBrush property is not linked to the dependency object at the path (BorderBrush).(0)


Solution

  • Your first example uses a VisualBrush to render the hint text. Instead you should overlay the original text site with a TextBlock. For performance reasons it's generally not recommended to use a Label and bind the Label.Content property to a string. Instead you should use the highly optimized TextBlock control to display simple text.

    Your second solution implements a very bad understanding of the TextBox. It doesn't make sense to implement the Controltemplate of a TextBlock using an inner TextBox. Apart from making no sense, you will run into weird behavior when for example you want to handle text or input events of the templated TextBox: because the user types into the inner TextBox, related events won't be raised as expected (on the templated TextBox).

    The recommended solution is to overlay the text site in the ControlTemnplate with a TextBlock. You can then bind the TextBlock.Text property to the templated TextBox (for example TextBox.Tag or a dedicated custom property).
    Use the CollectionView.IsEmpty property as a trigger to toggle the visibility of the hint TextBlock. This avoids two separate triggers in order to check for an empty string or null.
    A MultiTrigger or in this case a MultiDataTrigger allows to merge the text empty and focus conditions to further simplify the template. To avoid losing customization of the TextBox you should use TemplateBinding extension and bind internal properties like Border.Background to the properties of the templated TextBox for example TextBox.Background property.
    Finally, set default values for e.g. TextBox.Background using Style setters: this way you will be able to customize/override the control without the need to create a new ControlTemplate:

    <Style x:Key="Text1"
           TargetType="{x:Type TextBox}">
      <Style.Resources>
        <SolidColorBrush x:Key="SolidBorderBrush"
                         Color="#888" />
        <SolidColorBrush x:Key="WindowBackgroundBrush"
                         Color="#FFF" />
      </Style.Resources>
      
      <Setter Property="Background"
              Value="{StaticResource WindowBackgroundBrush}" />
      <Setter Property="BorderBrush"
              Value="{StaticResource SolidBorderBrush}" />
      <Setter Property="BorderThickness"
              Value="1" />
      <Setter Property="Template">
        <Setter.Value>
          <ControlTemplate TargetType="{x:Type TextBox}">
    
            <!-- Connect internals to the templated parent using '{TemplateBinding}' 
                 to preserve customization -->
            <Border Name="Border"
                    CornerRadius="2"
                    Padding="2"
                    Background="{TemplateBinding Background}"
                    BorderBrush="{TemplateBinding BorderBrush}"
                    BorderThickness="{TemplateBinding BorderThickness}">
    
              <!-- Use the Grid panel's implicit z-indexing of its children
                   to overlay the text site with a hint text -->
              <Grid>
                <ScrollViewer x:Name="PART_ContentHost"
                              Margin="0" />
                <TextBlock x:Name="CueBannerPresenter"
                           Text="{TemplateBinding Tag}"
                           Visibility="Collapsed" />
              </Grid>
            </Border>
    
            <ControlTemplate.Triggers>
              <MultiDataTrigger>
                <MultiDataTrigger.Conditions>
                  <Condition Binding="{Binding RelativeSource={RelativeSource Self}, Path=Text.IsEmpty}"
                             Value="True" />
                  <Condition Binding="{Binding RelativeSource={RelativeSource Self}, Path=IsKeyboardFocusWithin}"
                             Value="False" />
                </MultiDataTrigger.Conditions>
    
                <Setter TargetName="CueBannerPresenter"
                        Property="Visibility"
                        Value="Visible" />
              </MultiDataTrigger>
            </ControlTemplate.Triggers>
          </ControlTemplate>
        </Setter.Value>
      </Setter>
    </Style>