Wednesday, June 2, 2010

How to add Watermark Text to TextBox and PasswordBox in WPF?

I had a requirement in my sample application to show the watermark text (Help text) in input TextBox and PasswordBox (like Win7 Style of authentication). After a few minutes of search through internet, I got lots of sample application for showing watermark text in TextBox. But I couldn't get a sample application for showing watermark text to the PasswordBox. Again I searched for PasswordBox and finally I got an idea from this forum post. 

Since the PasswordBox is a sealed class, you cannot inherit a custom class from PasswordBox. Also Password property is not a dependency property hence you cannot write triggers. Hence I have created a custom class with attached properties such as WatermarkText (for showing the help text about the box), IsMonitoring (for monitoring the input), TextLength (for finding the input text length) and HasText (an internal property which decides whether the watermark text needs to be shown in box or not). The following code and style will help you to achieve this functionality.

WaterMarkTextHelper class:-

public class WaterMarkTextHelper : DependencyObject
    {
        #region Attached Properties

        public static bool GetIsMonitoring(DependencyObject obj)
        {
            return (bool)obj.GetValue(IsMonitoringProperty);
        }

        public static void SetIsMonitoring(DependencyObject obj, bool value)
        {
            obj.SetValue(IsMonitoringProperty, value);
        }

        public static readonly DependencyProperty IsMonitoringProperty =
            DependencyProperty.RegisterAttached("IsMonitoring", typeof(bool), typeof(WaterMarkTextHelper), new UIPropertyMetadata(false, OnIsMonitoringChanged));

        
        public static bool GetWatermarkText(DependencyObject obj)
        {
            return (bool)obj.GetValue(WatermarkTextProperty);
        }

        public static void SetWatermarkText(DependencyObject obj, string value)
        {
            obj.SetValue(WatermarkTextProperty, value);
        }

        public static readonly DependencyProperty WatermarkTextProperty =
            DependencyProperty.RegisterAttached("WatermarkText", typeof(string), typeof(WaterMarkTextHelper), new UIPropertyMetadata(string.Empty));

        
        public static int GetTextLength(DependencyObject obj)
        {
            return (int)obj.GetValue(TextLengthProperty);
        }

        public static void SetTextLength(DependencyObject obj, int value)
        {
            obj.SetValue(TextLengthProperty, value);

            if (value >= 1)
                obj.SetValue(HasTextProperty, true);
            else
                obj.SetValue(HasTextProperty, false);
        }

        public static readonly DependencyProperty TextLengthProperty =
            DependencyProperty.RegisterAttached("TextLength", typeof(int), typeof(WaterMarkTextHelper), new UIPropertyMetadata(0));

        #endregion

        #region Internal DependencyProperty

        public bool HasText
        {
            get { return (bool)GetValue(HasTextProperty); }
            set { SetValue(HasTextProperty, value); }
        }
        
        private static readonly DependencyProperty HasTextProperty =
            DependencyProperty.RegisterAttached("HasText", typeof(bool), typeof(WaterMarkTextHelper), new FrameworkPropertyMetadata(false));

        #endregion

        #region Implementation

        static void OnIsMonitoringChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            if (d is TextBox)
            {
                TextBox txtBox = d as TextBox;

                if ((bool)e.NewValue)
                    txtBox.TextChanged += TextChanged;
                else
                    txtBox.TextChanged -= TextChanged;
            }
            else if (d is PasswordBox)
            {
                PasswordBox passBox = d as PasswordBox;

                if ((bool)e.NewValue)
                    passBox.PasswordChanged += PasswordChanged;
                else
                    passBox.PasswordChanged -= PasswordChanged;
            }
        }

        static void TextChanged(object sender, TextChangedEventArgs e)
        {
            TextBox txtBox = sender as TextBox;
            if (txtBox == null) return;
            SetTextLength(txtBox, txtBox.Text.Length);
        }

        static void PasswordChanged(object sender, RoutedEventArgs e)
        {
            PasswordBox passBox = sender as PasswordBox;
            if (passBox == null) return;
            SetTextLength(passBox, passBox.Password.Length);
        }
        
        #endregion
    }

XAML Style:-
<Style TargetType="{x:Type PasswordBox}"> <Setter Property="local:WaterMarkTextHelper.IsMonitoring" Value="True"/> <Setter Property="local:WaterMarkTextHelper.WatermarkText" Value="Password" /> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type PasswordBox}"> <ControlTemplate.Resources> <Storyboard x:Key="enterGotFocus" > <DoubleAnimation Duration="0:0:0.4" To=".2" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="Message"/> </Storyboard> <Storyboard x:Key="exitGotFocus" > <DoubleAnimation Duration="0:0:0.4" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="Message"/> </Storyboard> <Storyboard x:Key="enterHasText" > <DoubleAnimation Duration="0:0:0.4" From=".2" To="0" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="Message"/> </Storyboard> <Storyboard x:Key="exitHasText" > <DoubleAnimation Duration="0:0:0.4" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="Message"/> </Storyboard> </ControlTemplate.Resources> <Border Name="Bd" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}"> <Grid> <ScrollViewer x:Name="PART_ContentHost" VerticalAlignment="Center" Margin="1" /> <TextBlock x:Name="Message" FontStyle="Italic" Text="{TemplateBinding local:WaterMarkTextHelper.WatermarkText}" Foreground="Gray" IsHitTestVisible="False" FontFamily="Calibri" Opacity="0.8" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="6,0,0,0"/> </Grid> </Border> <ControlTemplate.Triggers> <Trigger Property="IsEnabled" Value="false"> <Setter TargetName="Bd" Property="Background" Value="{DynamicResource {x:Static SystemColors.ControlBrushKey}}"/> <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/> </Trigger> <Trigger Property="IsEnabled" Value="True"> <Setter Property="Opacity" Value="1" TargetName="Bd"/> </Trigger> <MultiTrigger> <MultiTrigger.Conditions> <Condition Property="local:WaterMarkTextHelper.HasText" Value="False"/> <Condition Property="IsFocused" Value="True"/> </MultiTrigger.Conditions> <MultiTrigger.EnterActions> <BeginStoryboard Storyboard="{StaticResource enterGotFocus}"/> </MultiTrigger.EnterActions> <MultiTrigger.ExitActions> <BeginStoryboard Storyboard="{StaticResource exitGotFocus}"/> </MultiTrigger.ExitActions> </MultiTrigger> <Trigger Property="local:WaterMarkTextHelper.HasText" Value="True"> <Trigger.EnterActions> <BeginStoryboard Storyboard="{StaticResource enterHasText}"/> </Trigger.EnterActions> <Trigger.ExitActions> <BeginStoryboard Storyboard="{StaticResource exitHasText}"/> </Trigger.ExitActions> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> </Setter.Value> </Setter> </Style> <Style x:Key="WaterMarkTextBox" TargetType="{x:Type TextBox}"> <Setter Property="local:WaterMarkTextHelper.IsMonitoring" Value="True"/> <Setter Property="local:WaterMarkTextHelper.WatermarkText" Value="Username" /> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type TextBox}"> <ControlTemplate.Resources> <Storyboard x:Key="enterGotFocus" > <DoubleAnimation Duration="0:0:0.4" To=".2" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="Message"/> </Storyboard> <Storyboard x:Key="exitGotFocus" > <DoubleAnimation Duration="0:0:0.4" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="Message"/> </Storyboard> <Storyboard x:Key="enterHasText" > <DoubleAnimation Duration="0:0:0.4" From=".2" To="0" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="Message"/> </Storyboard> <Storyboard x:Key="exitHasText" > <DoubleAnimation Duration="0:0:0.4" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="Message"/> </Storyboard> </ControlTemplate.Resources> <Border Name="Bd" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}"> <Grid> <ScrollViewer x:Name="PART_ContentHost" VerticalAlignment="Center" Margin="1" /> <TextBlock x:Name="Message" Text="{TemplateBinding local:WaterMarkTextHelper.WatermarkText}" FontStyle="Italic" Foreground="Gray" IsHitTestVisible="False" FontFamily="Calibri" Opacity="0.8" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="6,0,0,0"/> </Grid> </Border> <ControlTemplate.Triggers> <Trigger Property="IsEnabled" Value="false"> <Setter TargetName="Bd" Property="Background" Value="{DynamicResource {x:Static SystemColors.ControlBrushKey}}"/> <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/> </Trigger> <MultiTrigger> <MultiTrigger.Conditions> <Condition Property="local:WaterMarkTextHelper.HasText" Value="False"/> <Condition Property="IsFocused" Value="True"/> </MultiTrigger.Conditions> <MultiTrigger.EnterActions> <BeginStoryboard Storyboard="{StaticResource enterGotFocus}"/> </MultiTrigger.EnterActions> <MultiTrigger.ExitActions> <BeginStoryboard Storyboard="{StaticResource exitGotFocus}"/> </MultiTrigger.ExitActions> </MultiTrigger> <Trigger Property="local:WaterMarkTextHelper.HasText" Value="True"> <Trigger.EnterActions> <BeginStoryboard Storyboard="{StaticResource enterHasText}"/> </Trigger.EnterActions> <Trigger.ExitActions> <BeginStoryboard Storyboard="{StaticResource exitHasText}"/> </Trigger.ExitActions> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> </Setter.Value> </Setter> </Style>
Since the custom class contains attached properties, we can use these attached properties to any TextBox control in our application for showing Watermark. You just need to merge the “ResourceDictionary” which contains the Style of Textbox and then need to assign the Watermark text to the TextBox using “WatermarkText” attached property. The sample code is given below.
<Window.Resources> <ResourceDictionary> <ResourceDictionary.MergedDictionaries> <ResourceDictionary Source="Resource.xaml" /> </ResourceDictionary.MergedDictionaries> </ResourceDictionary> </Window.Resources> <GroupBox Header="Dial-up Setting" HorizontalAlignment="Center" VerticalAlignment="Center" Grid.Column="1"> <StackPanel VerticalAlignment="Center"> <TextBox Height="25" Width="200" Margin="10" Style="{StaticResource WaterMarkTextBox}"/> <PasswordBox Height="25" Width="200" /> <TextBox Height="25" Width="200" Margin="10" Style="{StaticResource WaterMarkTextBox}" local:WaterMarkTextHelper.WatermarkText="Domain"/> </StackPanel> </GroupBox>

Sample Output:-



Put a comment if you have any questions. Happy coding.