Watermark Behavior

There are a lot of scenarios where you have to apply a watermark on a WPF control. Since there is no such functionality on the standard WPF controls, the WPF Control Toolkit provides an extended text box that meets the requirements.

This was hardly sufficient in my case and of course, I thought I can do better.

I developed an attached behavior instead, since I wanted to apply the watermark on multiple types of control. Also, I intended to keep some elements of the original background in sight, so a lot of the code written is focused in that direction.

You can download the complete solution from here.

WatermarkedTextBox

For the above textbox, the XAML code looks like this (after defining the namespace where the behavior is located):

 <TextBox Height="23"
                 local:WatermarkBehavior.IsWatermarkEnabled="True"
                 local:WatermarkBehavior.WatermarkText="Watermark"
                 Background="Gold"
                 Name="textBox1"
                 Width="120"/>

Let’s have a look of the implementation.

First of all, there are a couple of attached properties registered:

Property for enabling the watermark:

 public static readonly DependencyProperty IsWatermarkEnabledProperty =
    DependencyProperty.RegisterAttached("IsWatermarkEnabled", typeof(bool), typeof(WatermarkBehavior), new UIPropertyMetadata(false, OnIsWatermarkEnabled));

Property for defining the text that will appear as watermark:

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

The watermark can be a different element, not just text. This property defines the UIElement that will be placed as watermark:

public static readonly DependencyProperty WatermarkUIElementProperty =
            DependencyProperty.RegisterAttached("WatermarkUIElement", typeof(UIElement), typeof(WatermarkBehavior), new UIPropertyMetadata(null));

Here an example for using this property:
ComplexWatermark

<RichTextBox local:WatermarkBehavior.IsWatermarkEnabled="True"
                     Height="100"
                     Name="richTextBox1"
                     Width="214">
            <local:WatermarkBehavior.WatermarkUIElement>
                <StackPanel Orientation="Horizontal">
                    <Label Content="watermark"/>
                    <Image Source="http://www.osa-opn.org/opn/media/Images/ImageOfTheWeek/12-10-22.jpg?width=1024&amp;height=1024&amp;ext=.jpg"/>
                </StackPanel>
            </local:WatermarkBehavior.WatermarkUIElement>
        </RichTextBox>

Property for defining the property of the control that is checked before applying the watermark. If this property is not set, a couple of predefined properties are checked.

         public static readonly DependencyProperty WatermarkPropertyProperty =
            DependencyProperty.RegisterAttached("WatermarkProperty", typeof(string), typeof(WatermarkBehavior), new UIPropertyMetadata(string.Empty));

An example is the watermarked button used above:

 <Button local:WatermarkBehavior.IsWatermarkEnabled="True"
                local:WatermarkBehavior.WatermarkText="Watermark"
                local:WatermarkBehavior.WatermarkProperty="Content"                
                Height="23"                
                Name="button1"
                Width="120" />

Setting the property IsWatermarkEnabled to true will register the handlers for the Loaded, GotFocus and LostFocus events of the control:

        private static void OnIsWatermarkEnabled(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var frameworkElement = d as FrameworkElement;
            if (frameworkElement == null)
            {
                return;
            }

            var isEnabled = (bool)e.NewValue;
            if (isEnabled)
            {
                // hook to the events of the control
                frameworkElement.GotFocus += ControlGotFocus;
                frameworkElement.LostFocus += ControlLostFocus;
                frameworkElement.Loaded += ControlLoaded;
            }
            else
            {
                // unhook to the events of the control
                frameworkElement.GotFocus -= ControlGotFocus;
                frameworkElement.LostFocus -= ControlLostFocus;
                frameworkElement.Loaded -= ControlLoaded;
            }
        }

The class ControlBackground holds the logic of the background creation:
ControlBackgroundClassDiagramThis class contains the property WatermarkLabel, which will be used when applying the watermark in the behavior class.
Also, the class stores the original background of the control. This way, when the watermark is no longer needed, the control’s background can return to it’s original state.
The behavior class holds a list of ControlBackgrounds. Every time a control is loaded, the original background is stored in a new record of this list that also holds the correspondent watermark.

private static readonly List OriginalTextBoxBackgrounds = new List();

private static void ControlLoaded(object sender, RoutedEventArgs e)
        {
            var control = sender as Control;
            if (control == null)
            {
                return;
            }

            // Save the original look of the control
            var newControlBackground = new ControlBackground
                                           {
                                               Control = control,
                                               OriginalBackground = control.Background
                                           };
            CreateWatermark(newControlBackground);
            OriginalTextBoxBackgrounds.Add(newControlBackground);
            var stringProperty = GetWatermarkProperty(control);
            var propertyIsEmpty = string.IsNullOrWhiteSpace(stringProperty) ? CheckDefaultPropertiesEmptyOrNull(control) : PropertyIsEmpty(control, stringProperty);
            if (propertyIsEmpty)
            {
                ApplyWatermark(control, newControlBackground.WatermarkLabel);
            }
        }

The watermark is created by changing the background of the control with a new VisualBrush that has the Visual of this label.

 private static void ApplyWatermark(Control control, Label label)
        {
            var customVisualBrush = new VisualBrush { Stretch = Stretch.None, Visual = label };
            control.Background = customVisualBrush;
        }

The watermark is applied when the control is loaded and when it has lost the focused. When the control is focused, the background must return to the original value:

 private static void ControlLostFocus(object sender, RoutedEventArgs e)
        {
            var control = sender as Control;
            if (control == null)
            {
                return;
            }

            var stringProperty = GetWatermarkProperty(control);
            var propertyIsEmpty = string.IsNullOrWhiteSpace(stringProperty) ? CheckDefaultPropertiesEmptyOrNull(control) : PropertyIsEmpty(control, stringProperty);

            if (!propertyIsEmpty)
            {
                return;
            }

            var controlBackground =
                OriginalTextBoxBackgrounds.FirstOrDefault(cb => cb.Control.GetHashCode() == control.GetHashCode());
            if (controlBackground != null && controlBackground.WatermarkLabel != null)
            {
                ApplyWatermark(control, controlBackground.WatermarkLabel);
            }
        }

 private static void ControlGotFocus(object sender, RoutedEventArgs e)
        {
            var control = sender as Control;
            var dependencyPropertyName = GetWatermarkProperty(control);
            if (control == null)
            {
                return;
            }

            var hashCode = control.GetHashCode();
            var controlBackground = OriginalTextBoxBackgrounds.FirstOrDefault(cb => cb.Control.GetHashCode() == hashCode);

            if (string.IsNullOrWhiteSpace(dependencyPropertyName) && controlBackground != null)
            {
                control.Background = controlBackground.OriginalBackground;
            }
        }

In the case of lost focus, before applying the watermark, the property provided in WatermarkProperty is checked for it’s default value. If no property is provided, a few default properties are checked:

 private static bool CheckDefaultPropertiesEmptyOrNull(Control control)
        {
            // For Password
            var passwordPropertyInfo = control.GetType().GetProperty("Password");
            if (passwordPropertyInfo != null && passwordPropertyInfo.GetValue(control, null).ToString() == string.Empty)
            {
                return true;
            }

            // For rich textbox
            var richTextBoxPropertyInfo = control.GetType().GetProperty("Document");
            if (richTextBoxPropertyInfo != null)
            {
                var richTextBoxvalue = richTextBoxPropertyInfo.GetValue(control, null) as FlowDocument;
                if (richTextBoxvalue != null)
                {
                    var textRange = new TextRange(richTextBoxvalue.ContentStart, richTextBoxvalue.ContentEnd);
                    if (string.IsNullOrWhiteSpace(textRange.Text))
                    {
                        return true;
                    }
                }
            }

            // For Selector
            var comboboxPropertyInfo = control.GetType().GetProperty("SelectedItem");

            if (comboboxPropertyInfo != null && comboboxPropertyInfo.GetValue(control, null) == null)
            {
                return true;
            }

            // For textbox
            var textPropertyInfo = control.GetType().GetProperty("Text");
            return textPropertyInfo != null && textPropertyInfo.GetValue(control, null).ToString() == string.Empty;
        }

For any improvements or issues of this solution, please feel free to comment.

Advertisement

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s