Sunday, January 13, 2008

Placeholder text using Adorner

In the previous post, I subclassed TextBox in order to introduce a placeholder text. I realised that there is a better way - one that allows me to use only 1 class to add placeholder text to a TextBox, RichTextBox and PasswordBox - and that is using an adorner.



To use it in xaml, simply add
xmlns:src="clr-namespace:Huan.WhiteDwarf.UI"
to the root element, e.g. Window, and add
src:Placeholder.Text="Your Placeholder"
to the TextBox/RichTextBox/PasswordBox.

To use it in code, use this
Huan.WhiteDwarf.UI.Placeholder.SetText(TextBoxName, PlaceholderText);

using System;

using System.Globalization;

using System.Windows;

using System.Windows.Controls;

using System.Windows.Controls.Primitives;

using System.Windows.Documents;

using System.Windows.Media;

 

namespace Huan.WhiteDwarf.UI

{

    /// <summary>

    ///   Represents an adorner that adds placeholder text to a <see cref="T:System.Windows.Controls.TextBox"/>,

    ///   <see cref="T:System.Windows.Controls.RichTextBox"/> or <see cref="T:System.Windows.Controls.PasswordBox"/>.

    /// </summary>

    public class Placeholder : Adorner

    {

        /// <summary>

        ///   Event handler for <see cref="E:System.Windows.Controls.Primitives.TextBoxBase.TextChanged" />.

        /// </summary>

        private readonly TextChangedEventHandler _textChangedHandler;

 

        /// <summary>

        ///   Event handler for <see cref="E:System.Windows.Controls.PasswordBox.PasswordChanged" />.

        /// </summary>

        private readonly RoutedEventHandler _passwordChangedHandler;

 

        /// <summary>

        ///   <see langword="true" /> when the placeholder text is visible, <see langword="false" /> otherwise.

        ///   Used to avoid calling <see cref="M:System.Windows.UIElement.InvalidateVisual"/> unnecessarily.

        /// </summary>

        private bool _isPlaceholderVisible;

 

        #region Dependency Property

        /// <summary>

        ///    Identifies the Huan.WhiteDwarf.UI.Placeholder.Text attached property.

        /// </summary>

        public static readonly DependencyProperty TextProperty = DependencyProperty.RegisterAttached(

            "Text", typeof(string), typeof(Placeholder),

            new FrameworkPropertyMetadata(string.Empty, new PropertyChangedCallback(OnTextChanged)));

        #endregion

 

        #region Constructors

        /// <summary>

        ///   Initializes a new instance of the <see cref="T:Huan.WhiteDwarf.UI.Placeholder"/> class.

        /// </summary>

        /// <param name="adornedElement">

        ///   The element to bind the adorner to.

        /// </param>

        /// <exception cref="T:System.ArgumentNullException">

        ///   Raised when adornedElement is null.

        /// </exception>

        protected Placeholder(Control adornedElement)

            : base(adornedElement)

        {

            this.IsHitTestVisible = false;

            _textChangedHandler = new TextChangedEventHandler(AdornedElement_ContentChanged);

            _passwordChangedHandler = new RoutedEventHandler(AdornedElement_ContentChanged);

 

            adornedElement.GotFocus += new RoutedEventHandler(AdornedElement_GotFocus);

            adornedElement.LostFocus += new RoutedEventHandler(AdornedElement_LostFocus);

        }

 

        /// <summary>

        ///   Initializes a new instance of the <see cref="T:Huan.WhiteDwarf.UI.Placeholder"/> class.

        /// </summary>

        /// <param name="adornedElement">

        ///   The element to bind the adorner to.

        /// </param>

        /// <exception cref="T:System.ArgumentNullException">

        ///   Raised when adornedElement is null.

        /// </exception>

        public Placeholder(PasswordBox adornedElement)

            : this((Control)adornedElement)

        {

            if (!adornedElement.IsFocused)

                adornedElement.PasswordChanged += _passwordChangedHandler;

        }

 

        /// <summary>

        ///   Initializes a new instance of the <see cref="T:Huan.WhiteDwarf.UI.Placeholder"/> class.

        /// </summary>

        /// <param name="adornedElement">

        ///   The element to bind the adorner to.

        /// </param>

        /// <exception cref="T:System.ArgumentNullException">

        ///   Raised when adornedElement is null.

        /// </exception>

        public Placeholder(TextBox adornedElement)

            : this((Control)adornedElement)

        {

            if (!adornedElement.IsFocused)

                adornedElement.TextChanged += _textChangedHandler;

        }

 

        /// <summary>

        ///   Initializes a new instance of the <see cref="T:Huan.WhiteDwarf.UI.Placeholder"/> class.

        /// </summary>

        /// <param name="adornedElement">

        ///   The element to bind the adorner to.

        /// </param>

        /// <exception cref="T:System.ArgumentNullException">

        ///   Raised when adornedElement is null.

        /// </exception>

        public Placeholder(RichTextBox adornedElement)

            : this((Control)adornedElement)

        {

            if (!adornedElement.IsFocused)

                adornedElement.TextChanged += _textChangedHandler;

        }

        #endregion

 

        #region Property Changed Callbacks

        /// <summary>

        ///   Invoked whenever Huan.WhiteDwarf.UI.Placeholder.Text attached property is changed.

        /// </summary>

        /// <param name="sender">

        ///   The object where the event handler is attached.

        /// </param>

        /// <param name="e">

        ///   Provides data about the event.

        /// </param>

        private static void OnTextChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)

        {

            Control adornedElement = sender as Control;

            if (adornedElement.IsLoaded)

                AddAdorner(adornedElement);

            else

                adornedElement.Loaded += new RoutedEventHandler(AdornedElement_Loaded);

        }

        #endregion

 

        #region Event Handlers

        /// <summary>

        ///   Event handler for AdornedElement.Loaded.

        /// </summary>

        /// <param name="sender">

        ///   The AdornedElement where the event handler is attached.

        /// </param>

        /// <param name="e">

        ///   Provides data about the event.

        /// </param>

        private static void AdornedElement_Loaded(object sender, RoutedEventArgs e)

        {

            Control adornedElement = (Control)sender;

            adornedElement.Loaded -= AdornedElement_Loaded;

            AddAdorner(adornedElement);

        }

 

        /// <summary>

        ///   Event handler for AdornedElement.GotFocus.

        /// </summary>

        /// <param name="sender">

        ///   The AdornedElement where the event handler is attached.

        /// </param>

        /// <param name="e">

        ///   Provides data about the event.

        /// </param>

        private void AdornedElement_GotFocus(object sender, RoutedEventArgs e)

        {

            TextBoxBase textBoxBase = AdornedElement as TextBoxBase;

            if (textBoxBase != null)

                textBoxBase.TextChanged -= AdornedElement_ContentChanged;

            else

            {

                PasswordBox passwordBox = AdornedElement as PasswordBox;

                if (passwordBox != null)

                    passwordBox.PasswordChanged -= AdornedElement_ContentChanged;

            }

 

            if (_isPlaceholderVisible)

                this.InvalidateVisual();

        }

 

        /// <summary>

        ///   Event handler for AdornedElement.LostFocus.

        /// </summary>

        /// <param name="sender">

        ///   The AdornedElement where the event handler is attached.

        /// </param>

        /// <param name="e">

        ///   Provides data about the event.

        /// </param>

        public void AdornedElement_LostFocus(object sender, RoutedEventArgs e)

        {

            TextBoxBase textBoxBase = AdornedElement as TextBoxBase;

            if (textBoxBase != null)

                textBoxBase.TextChanged += _textChangedHandler;

            else

            {

                PasswordBox passwordBox = AdornedElement as PasswordBox;

                if (passwordBox != null)

                    passwordBox.PasswordChanged += _passwordChangedHandler;

            }

 

            if (!_isPlaceholderVisible && IsElementEmpty())

                this.InvalidateVisual();

        }

 

        /// <summary>

        ///   Event handler for AdornedElement.ContentChanged.

        /// </summary>

        /// <param name="sender">

        ///   The AdornedElement where the event handler is attached.

        /// </param>

        /// <param name="e">

        ///   Provides data about the event.

        /// </param>

        private void AdornedElement_ContentChanged(object sender, RoutedEventArgs e)

        {

            if (_isPlaceholderVisible ^ IsElementEmpty())

                this.InvalidateVisual();

        }

        #endregion

 

        #region Attached Property Getters and Setters

        /// <summary>

        ///   Gets the value of the Huan.WhiteDwarf.UI.Placeholder.Text attached property for a specified element.

        /// </summary>

        /// <param name="adornedElement">

        ///   The element from which the property value is read.

        /// </param>

        /// <returns>

        ///   The placeholder text property value for the element.

        /// </returns>

        /// <exception cref="T:ArgumentNullException">

        ///   Raised when adornedElement is null.

        /// </exception>

        public static string GetText(Control adornedElement)

        {

            if (adornedElement == null)

                throw new ArgumentNullException("adornedElement");

 

            return (string)adornedElement.GetValue(TextProperty);

        }

 

        /// <summary>

        ///   Sets the value of the Huan.WhiteDwarf.UI.Placeholder.Text attached property to a specified element.

        /// </summary>

        /// <param name="adornedElement">

        ///   The element to which the attached property is written.

        /// </param>

        /// <param name="placeholderText">

        ///   The needed placeholder text value.

        /// </param>

        /// <exception cref="T:ArgumentNullException">

        ///   Raised when adornedElement is null.

        /// </exception>

        /// <exception cref="T:InvalidOperationException">

        ///   Raised when adornedElement is not a <see cref="T:System.Windows.Controls.TextBox"/>,

        ///   <see cref="T:System.Windows.Controls.RichTextBox"/> or <see cref="T:System.Windows.Controls.PasswordBox"/>.

        /// </exception>

        public static void SetText(Control adornedElement, string placeholderText)

        {

            if (adornedElement == null)

                throw new ArgumentNullException("adornedElement");

 

            if (!(adornedElement is TextBox || adornedElement is RichTextBox || adornedElement is PasswordBox))

                throw new InvalidOperationException();

 

            adornedElement.SetValue(TextProperty, placeholderText);

        }

        #endregion

 

        /// <summary>

        ///   Adds a <see cref="T:Huan.WhiteDwarf.UI.Placeholder"/> to the adorner layer.

        /// </summary>

        /// <param name="adornedElement">

        ///   The adorned element.

        /// </param>

        private static void AddAdorner(Control adornedElement)

        {

            AdornerLayer adornerLayer = AdornerLayer.GetAdornerLayer(adornedElement);

            if (adornerLayer == null)

                return;

 

            Adorner[] adorners = adornerLayer.GetAdorners(adornedElement);

            if (adorners != null)

                foreach (Adorner adorner in adorners)

                    if (adorner is Placeholder)

                        return;

 

            TextBox textBox = adornedElement as TextBox;

            if (textBox != null)

            {

                adornerLayer.Add(new Placeholder(textBox));

                return;

            }

 

            RichTextBox richTextBox = adornedElement as RichTextBox;

            if (richTextBox != null)

            {

                adornerLayer.Add(new Placeholder(richTextBox));

                return;

            }

 

            PasswordBox passwordBox = adornedElement as PasswordBox;

            if (passwordBox != null)

            {

                adornerLayer.Add(new Placeholder(passwordBox));

                return;

            }

        }

 

        /// <summary>

        ///   Checks if the content of the adorned element is empty.

        /// </summary>

        /// <returns>

        ///   Returns <see langword="true" /> if the content is empty, <see langword="false" /> otherwise.

        /// </returns>

        private bool IsElementEmpty()

        {

            UIElement adornedElement = AdornedElement;

            TextBox textBox = adornedElement as TextBox;

            if (textBox != null)

                return string.IsNullOrEmpty(textBox.Text);

 

            PasswordBox passwordBox = adornedElement as PasswordBox;

            if (passwordBox != null)

                return string.IsNullOrEmpty(passwordBox.Password);

 

            RichTextBox richTextBox = adornedElement as RichTextBox;

            if (richTextBox != null)

            {

                BlockCollection blocks = richTextBox.Document.Blocks;

                if (blocks.Count == 0)

                    return true;

                if (blocks.Count == 1)

                {

                    Paragraph paragraph = blocks.FirstBlock as Paragraph;

                    if (paragraph == null)

                        return false;

 

                    if (paragraph.Inlines.Count == 0)

                        return true;

 

                    if (paragraph.Inlines.Count == 1)

                    {

                        Run run = paragraph.Inlines.FirstInline as Run;

                        return (run != null && string.IsNullOrEmpty(run.Text));

                    }

                }

 

                return false;

            }

 

            return false;

        }

 

        /// <summary>

        ///    Computes the text alignment of the adorned element.

        /// </summary>

        /// <returns>

        ///   Returns the computed text alignment.

        /// </returns>

        private TextAlignment ComputedTextAlignment()

        {

            Control adornedElement = AdornedElement as Control;

            TextBox textBox = adornedElement as TextBox;

            if (textBox != null)

            {

                if (DependencyPropertyHelper.GetValueSource(textBox, TextBox.HorizontalContentAlignmentProperty)

                    .BaseValueSource != BaseValueSource.Local ||

                    DependencyPropertyHelper.GetValueSource(textBox, TextBox.TextAlignmentProperty)

                    .BaseValueSource == BaseValueSource.Local)

 

                    // TextAlignment dominates

                    return textBox.TextAlignment;

            }

 

            RichTextBox richTextBox = adornedElement as RichTextBox;

            if (richTextBox != null)

            {

                BlockCollection blocks = richTextBox.Document.Blocks;

                TextAlignment textAlignment = richTextBox.Document.TextAlignment;

                if (blocks.Count == 0)

                    return textAlignment;

 

                if (blocks.Count == 1)

                {

                    Paragraph paragraph = blocks.FirstBlock as Paragraph;

                    if (paragraph == null)

                        return textAlignment;

 

                    return paragraph.TextAlignment;

                }

 

                return textAlignment;

            }

 

            switch (adornedElement.HorizontalContentAlignment)

            {

                case HorizontalAlignment.Left:

                    return TextAlignment.Left;

                case HorizontalAlignment.Right:

                    return TextAlignment.Right;

                case HorizontalAlignment.Center:

                    return TextAlignment.Center;

                case HorizontalAlignment.Stretch:

                    return TextAlignment.Justify;

            }

 

            return TextAlignment.Left;

        }

 

        /// <summary>

        ///   Draws the content of a <see cref="T:System.Windows.Media.DrawingContext" /> object during the render pass of a <see cref="T:Huan.WhiteDwarf.UI.Placeholder"/> element.

        /// </summary>

        /// <param name="drawingContext">

        ///   The <see cref="T:System.Windows.Media.DrawingContext" /> object to draw. This context is provided to the layout system.

        /// </param>

        protected override void OnRender(DrawingContext drawingContext)

        {

            Control adornedElement = this.AdornedElement as Control;

            string placeholderText;

 

            if (adornedElement == null ||

                adornedElement.IsFocused ||

                !IsElementEmpty() ||

                string.IsNullOrEmpty(placeholderText = (string)adornedElement.GetValue(TextProperty)))

 

                _isPlaceholderVisible = false;

            else

            {

                _isPlaceholderVisible = true;

                Size size = adornedElement.RenderSize;

                TextAlignment computedTextAlignment = ComputedTextAlignment();

                // foreground brush does not need to be dynamic. OnRender called when SystemColors changes.

                Brush foreground = SystemColors.GrayTextBrush.Clone();

                foreground.Opacity = adornedElement.Foreground.Opacity;

                Typeface typeface = new Typeface(adornedElement.FontFamily, FontStyles.Italic, adornedElement.FontWeight, adornedElement.FontStretch);

                FormattedText formattedText = new FormattedText(placeholderText,

                                                  CultureInfo.CurrentCulture,

                                                  adornedElement.FlowDirection,

                                                  typeface,

                                                  adornedElement.FontSize,

                                                  foreground);

                formattedText.TextAlignment = computedTextAlignment;

                formattedText.MaxTextHeight = size.Height - adornedElement.BorderThickness.Top - adornedElement.BorderThickness.Bottom - adornedElement.Padding.Top - adornedElement.Padding.Bottom;

                formattedText.MaxTextWidth = size.Width - adornedElement.BorderThickness.Left - adornedElement.BorderThickness.Right - adornedElement.Padding.Left - adornedElement.Padding.Right - 4.0;

 

                double left;

                double top = 0.0;

                if (adornedElement.FlowDirection == FlowDirection.RightToLeft)

                    left = adornedElement.BorderThickness.Right + adornedElement.Padding.Right + 2.0;

                else

                    left = adornedElement.BorderThickness.Left + adornedElement.Padding.Left + 2.0;

 

                switch (adornedElement.VerticalContentAlignment)

                {

                    case VerticalAlignment.Top:

                    case VerticalAlignment.Stretch:

                        top = adornedElement.BorderThickness.Top + adornedElement.Padding.Top;

                        break;

                    case VerticalAlignment.Bottom:

                        top = size.Height - adornedElement.BorderThickness.Bottom - adornedElement.Padding.Bottom - formattedText.Height;

                        break;

                    case VerticalAlignment.Center:

                        top = (size.Height + adornedElement.BorderThickness.Top - adornedElement.BorderThickness.Bottom + adornedElement.Padding.Top - adornedElement.Padding.Bottom - formattedText.Height) / 2.0;

                        break;

                }

 

                if (adornedElement.FlowDirection == FlowDirection.RightToLeft)

                {

                    // Somehow everything got drawn reflected. Add a transform to correct.

                    drawingContext.PushTransform(new ScaleTransform(-1.0, 1.0, RenderSize.Width / 2.0, 0.0));

                    drawingContext.DrawText(formattedText, new Point(left, top));

                    drawingContext.Pop();

                }

                else

                    drawingContext.DrawText(formattedText, new Point(left, top));

            }

        }

    }

}