WPF Shadowed TextBox (aka. WPF Watermark TextBox)

This article demonstrates a Shadowed TextBox in WPF.  The initial concept seemed like it would be easy.  I went through about 3 different iterations in order to tackle this situation.  The first two had potential, but both had odd issues that forced me to try different techniques.  The winning solution uses an adorner to decorate the Textbox with a label.

Failed Techniques

Change the Text and Style on Got/Lost Focus

In this technique I would just  change the Text of the Textbox and and the Style whenever the TextBox Got or Lost Focus.  I had used this technique previously in WinForms, but this App that I was building used Drag-and-Drop in the textboxes and the users had expressed the need for this feature.  The issues with Drag-and-Drop using this technique were a nightmare.  This on top of other issues such as Binding caused be to abandon this technique.

Changing the Textboxes Background to a VisualBrush with with a Label

In this technique I would change the background of the TextBox to a VisualBrush that contained a Label on Got/Lost Focus.  This method had some initial pitfalls and work arounds with sizing.  I finally got past that by setting the VisualBrush Stretch=None.  This technique worked great until I went to my DataBound Form.  If a textbox has a value with one record, when you changed records that textboxes background wouldn’t update.  I searched a long time for a method to override to force that update, but no luck.

Winning Technique

Textbox adorned by a Label

View Code Preview:  ShadowedTextBox.cs
Download Source Code: ShadowedTextBoxExample.zip (70.3 KB)

Shadowed TextBox Preview
Shadowed TextBox Preview

This final technique worked great and involves creating and attaching an AdornerLabel to the TextBox.  This version worked with styling, drag-drop, and Binding, right out of the box.  All you need to do to get the Shadowed TextBox working is add the required files and set the Label and LabelStyle of the ShadowedTextBox.

About these ads

1 Response to “WPF Shadowed TextBox (aka. WPF Watermark TextBox)”


  1. 1 sevenalive October 3, 2010 at 6:06 pm

    Here is my cleaned up version. it also fixes the bug of showing the adorner label when the textbox is not visible.

    #region
    
    using System;
    using System.ComponentModel;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Documents;
    using System.Windows.Media;
    
    #endregion
    
    namespace Microsoft.Windows.Controls
    {
        public sealed class InfoTextBox : TextBox
        {
            #region Constructors
    
            public InfoTextBox()
            {
                if (Resources.Count != 0)
                    return;
                var resourceDictionary = new ResourceDictionary {Source = new Uri("/Windows.Shell;component/Resources/Dictionary.xaml", UriKind.Relative)};
                Resources.MergedDictionaries.Add(resourceDictionary);
            }
    
            #endregion
    
            #region Properties
    
            // Using a DependencyProperty as the backing store for Note.  This enables animation, styling, binding, etc...
            public static readonly DependencyProperty NoteProperty = DependencyProperty.Register("Note", typeof (string), typeof (InfoTextBox), new UIPropertyMetadata("", LabelPropertyChanged));
    
            // Using a DependencyProperty as the backing store for NoteStyle.  This enables animation, styling, binding, etc...
            public static readonly DependencyProperty NoteStyleProperty = DependencyProperty.Register("NoteStyle", typeof (Style), typeof (InfoTextBox), new UIPropertyMetadata(null));
    
            private static readonly DependencyPropertyKey HasTextPropertyKey = DependencyProperty.RegisterReadOnly("HasText", typeof (bool), typeof (InfoTextBox), new PropertyMetadata(false));
    
            public static readonly DependencyProperty HasTextProperty = HasTextPropertyKey.DependencyProperty;
    
            public string Note { get { return (string) GetValue(NoteProperty); } set { SetValue(NoteProperty, value); } }
            public Style NoteStyle { get { return (Style) GetValue(NoteStyleProperty); } set { SetValue(NoteStyleProperty, value); } }
            public bool HasText { get { return (bool) GetValue(HasTextProperty); } private set { SetValue(HasTextPropertyKey, value); } }
    
            #endregion
    
            #region Callbacks
    
            private static void LabelPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
            {
                var infoTextBox = d as InfoTextBox;
    
                if (infoTextBox != null)
                    infoTextBox.UpdateAdorner(infoTextBox);
                var isVisiblePropertyDescriptor = DependencyPropertyDescriptor.FromProperty(IsVisibleProperty, typeof (InfoTextBox));
                isVisiblePropertyDescriptor.AddValueChanged(d, IsVisibleChanged);
            }
    
            #endregion
    
            private AdornerLabel myAdornerLabel;
            private AdornerLayer myAdornerLayer;
    
            private new static void IsVisibleChanged(object sender, EventArgs e)
            {
                var infoTextBox = sender as InfoTextBox;
                if (infoTextBox == null)
                    return;
    
                infoTextBox.UpdateAdorner(infoTextBox, !infoTextBox.IsVisible);
            }
    
            public override void OnApplyTemplate()
            {
                base.OnApplyTemplate();
    
                myAdornerLayer = AdornerLayer.GetAdornerLayer(this);
                myAdornerLabel = new AdornerLabel(this, Note, NoteStyle);
                UpdateAdorner(this);
    
                var focusProp = DependencyPropertyDescriptor.FromProperty(IsFocusedProperty, typeof (FrameworkElement));
                if (focusProp != null)
                    focusProp.AddValueChanged(this, delegate { UpdateAdorner(this); });
    
                var containsTextProp = DependencyPropertyDescriptor.FromProperty(HasTextProperty, typeof (InfoTextBox));
                if (containsTextProp != null)
                    containsTextProp.AddValueChanged(this, delegate { UpdateAdorner(this); });
            }
    
            protected override void OnTextChanged(TextChangedEventArgs e)
            {
                HasText = Text != "";
    
                base.OnTextChanged(e);
            }
    
            protected override void OnDragEnter(DragEventArgs e)
            {
                myAdornerLayer.RemoveAdorners(this);
    
                base.OnDragEnter(e);
            }
    
            protected override void OnDragLeave(DragEventArgs e)
            {
                UpdateAdorner(this);
    
                base.OnDragLeave(e);
            }
    
            private void UpdateAdorner(FrameworkElement elem, bool hide = false)
            {
                if (elem == null || myAdornerLayer == null)
                    return;
    
                myAdornerLabel = new AdornerLabel(this, Note, NoteStyle);
                myAdornerLayer.RemoveAdorners(elem);
    
                if (!((InfoTextBox) elem).HasText && !elem.IsFocused && !hide)
                    myAdornerLayer.Add(myAdornerLabel);
            }
        }
    
        // Adorners must subclass the abstract base class Adorner.
        public class AdornerLabel : Adorner
        {
            private readonly TextBlock textBlock;
    
            // Be sure to call the base class constructor.
            public AdornerLabel(UIElement adornedElement, string label, Style labelStyle) : base(adornedElement)
            {
                textBlock = new TextBlock {Style = labelStyle, Text = label};
            }
    
            protected override int VisualChildrenCount { get { return 1; } }
    
            //make sure that the layout system knows of the element
            protected override Size MeasureOverride(Size constraint)
            {
                textBlock.Measure(constraint);
                return constraint;
            }
    
            //make sure that the layout system knows of the element
            protected override Size ArrangeOverride(Size finalSize)
            {
                textBlock.Arrange(new Rect(finalSize));
                return finalSize;
            }
    
            //return the visual that we want to display
            protected override Visual GetVisualChild(int index)
            {
                return textBlock;
            }
    
            //return the count of the visuals
        }
    
        public static class AdornerExtensions
        {
            public static void RemoveAdorners(this AdornerLayer adr, UIElement elem)
            {
                var adorners = adr.GetAdorners(elem);
    
                if (adorners == null)
                    return;
    
                for (var i = adorners.Length - 1; i >= 0; i--)
                {
                    if (adorners[i] is T)
                        adr.Remove(adorners[i]);
                }
            }
    
            public static bool Contains(this AdornerLayer adr, UIElement elem)
            {
                if (adr == null)
                    return false;
                var adorners = adr.GetAdorners(elem);
    
                if (adorners == null)
                    return false;
    
                for (var i = adorners.Length - 1; i >= 0; i--)
                {
                    if (adorners[i] is T)
                        return true;
                }
                return false;
            }
    
            public static void RemoveAll(this AdornerLayer adr, UIElement elem)
            {
                try
                {
                    var adorners = adr.GetAdorners(elem);
    
                    if (adorners == null)
                        return;
    
                    foreach (var toRemove in adorners)
                        adr.Remove(toRemove);
                }
                catch
                {
                }
            }
    
            public static void RemoveAllRecursive(this AdornerLayer adr, UIElement element)
            {
                try
                {
                    Action recurse = null;
                    recurse = (delegate(UIElement elem)
                                   {
                                       adr.RemoveAll(elem);
                                       if (elem is Panel)
                                       {
                                           foreach (UIElement e in ((Panel) elem).Children)
                                               recurse(e);
                                       }
                                       else if (elem is Decorator)
                                           recurse(((Decorator) elem).Child);
                                       else if (elem is ContentControl)
                                       {
                                           if (((ContentControl) elem).Content is UIElement)
                                               recurse(((ContentControl) elem).Content as UIElement);
                                       }
                                   });
    
                    recurse(element);
                }
                catch
                {
                }
            }
        }
    }
    

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 )

Twitter picture

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

Facebook photo

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

Google+ photo

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

Connecting to %s





Follow

Get every new post delivered to your Inbox.

%d bloggers like this: