Watermark TextBox Behavior (Silverlight 4)
December 13, 2011 at 2:37 pm Leave a comment
Introduction
Recently I implemented a behavior, which is adds to any TextBox control an ability of watermark. But before I going to implementation details, let’s explain what the watermark is. The “watermark” on TextBox control is a text, which is displayed inside while the TextBox is empty (sometimes it also has to be not focused). The idea of watermark text is to hint to user about the purpose of TextBox control. For example, when you logging into the Windows 7, if it password protected you will see a “Password” text inside it. Typing inside it any text will hide the watermark text. Additional example is a search textbox on the top of MSDN site (www.msdn.com). It behaves slightly different, but the idea is the same – inside the search textbox you may notice the “watermark” text: “Search MSDN with Bing”.
If you will think about how to do it in WPF, probably first you will consider to use adorners to render a watermark text above the TextBox control (I know, since it was my initial idea also). But – how unfortunate – there is no adorners in Silverlight (not in current fourth version, at least). Therefore, we have to do some kind of workaround. The common workaround is to create custom control, which template will be very similar to usual TextBox, but will have a Panel atop of it. This Panel is used then to display content above the TextBox, and this way to simulate adorner functionality. While this approach is working, it has some drawbacks:
- In every place you would like to have watermark functionality, you will need to use special custom control.
- You will have to maintain its style, in addition to regular TextBox style.
- What about existing behaviors/attached properties for TextBox control? They are not designed to work with something else.
- Due to my personal preference, I think functionality like this should be attached to existing TextBox control, rather than implementing new control from the scratch.
So, I decided to create special Behavior, which will add watermark functionality to any regular TextBox control. If there were adorners in Silverlight, the task would be trivial. I would add from my behavior an adorner to the attached element – and that’s it. However, like I said before, there are no adorners in Silverlight. Also, I cannot add a Panel above a TextBox, since it already has a parent (and generally this is a bad idea to manipulate logical tree like this). So I had to choose another path: something had to change on the TextBox control itself. I can’t play with the content of the TextBox – since setting it from the behavior could affect bound objects and/or kill a binding (Silverlight 4), in addition to other side effects. But there is something I could actually use – it is a TextBox’s background.
The concept
My idea is simple: create a TextBlock with a watermark text inside. Make it size and font you like. After that, render it to brush. Merge this brush with existing background brush (if any) – this step is essential, since I don’t want to override the exist background. Finally, set the resulting brush to the background of the attached TextBox. When I have to show watermark – I using rendered brush, when I don’t need to show it – I using the original background brush. Now, after I explained the basic idea, let’s dive into the implementation details.
Rendering the control to Brush
If I would do it in WPF, I would use VisualBrush – and case is solved. However, we are in Silverlight and life is not picnic. Lucky me, WriteableBitmap class together with ImageBrush do exist in SL, and we will use them to achieve the same result. The code rendering Control into the brush looks as the following:
WriteableBitmap bitmap = new WriteableBitmap(_watermarkHolder, null);
ImageBrush brush= new ImageBrush() { ImageSource = bitmap, Stretch = Stretch.None };
After that, the brush is ready to be set as a background of TextBox control.
Merging brushes
Since we want to keep the original background of TextBox and yet to show our watermark, we have somehow to merge these two brushes. The only way I know is to take two controls, set each of the brushes to each of the controls, and then put one control on top of the second one. You may give them some opacity if you want for even more “realistic” merge.
Therefore I decided to took a Canvas panel, set its background to background of TextBox, and as its content I set a TextBlock (with watermark text). After that I render all this to the brush – and whoa: I have an original background merged with the watermark text.
Questions:
Q1: Why I had to use a Panel to hold my TextBlock? Why not to take a regular ContentControl for the same purpose?
A1: The TextBlock holder is not part of any visual tree – it is used only to render a background together with TextBlock to brush. Some very simple elements, like Canvas and TextBlock able to render themselves correctly even in this scenario. However, ContentControl, Button, Grid and some other elements I tested cannot perform layout correctly without being part of some visual tree. Even calling explicitly Measure and Arrange overrides does not solves the problem.
Q2: Why I need any holder at all? Why not set a background directly to TextBlock?
A2: TextBlock is not control, and it has not Background property. Canvas is not control either, but Panel class has Background property of its own.
Q3: But if you will set yourself the background of TextBox, you may kill any bindings to the Background property. Furthermore, what if the background of TextBox will change (for example, due to new value from binding) – you need to catch somehow this change and re-render the new merged brush.
A3: This one is tricky, and deserves a separate topic –
Managing background
We have two typical scenarios: first one – the background property set directly; and the second – background property set from binding. I can test whether the property is bound:
BindingExpression expresion = textBox.GetBindingExpression(Control.BackgroundProperty);
If the expression is null – that means the first case – this property is not bound. This is the simplest case: all we need to do is to take the current background, create merged brush and set it back. However, if the expression is not null we have to handle this differently. Instead, we will hijack the binding from TextBox to Canvas. This way, every time the value of binding source will change, instead of setting it to background of TextBox, we will redirect it to background of Canvas. After that, we will recreate the merged brush and set it to background of TextBox:
BindingExpression expresion = AssociatedObject.GetBindingExpression(Control.BackgroundProperty); Binding originalBinding = expresion.ParentBinding; Binding clonedBinding = new Binding(); clonedBinding.Path = originalBinding.Path; clonedBinding.Source = expresion.DataItem; _watermarkHolder.SetBinding(Panel.BackgroundProperty, clonedBinding); AssociatedObject.ClearValue(Control.BackgroundProperty); //clear old binding
However, this is not enough. There are still two problems we have to solve: first – we need to know somehow when the Background of Canvas being changed – to recreate the merged brush. Second – truly tricky problem, which took me few days to solve: at the time when the Behavior being attached, the Binding is not fully initialized yet. If I create it during attachment of Behavior, the Binding will not work and I will not receive notifications. So, in order to make it right, I have to wait until the value from original Binding received, and that’s the right time to redirect it.
Both these problem have the same solution – I need to know somehow when the DependencyProperty is being changed. In WPF you have easy way to do that: by using DependencyPropertyDescriptor.AddValueChanged method you can subscribe to changes of dependency property. However, as you may guess, it is not supported in Silverlight. And again, there is a workaround for this problem: by using intermediate object you can bind to it, and then raise property changed from inside. There is plenty sources where you can read about this, for example here: http://www.amazedsaint.com/2009/12/silverlight-listening-to-dependency.html. Based on this idea, I created a helper with two extension methods:
public static void NotifyOn<T>(this T vmb,string path,Action<object,object> callback) public static void UnNotifyOn<T>(this T vmb,string path)
For clarity (and due to confidentiality), I do not providing here the implementation details of this helper – but it pretty much the same as I described.
Additional Point of Interest
Obviously, the size of Canvas should be the same as the size of TextBox. This is necessary in order to render watermark text correctly. So, my initial idea was to bind height and width of Canvas to the actual height and width of TextBox. However, in Silverlight, this property is buggy and the notifications about value changes are not raised. The workaround is to subscribe to SizeChanged event of TextBox, and then update the size of Canvas manually.
The code
Here the code of my behavior. It may look very long, but most of it is different dependency properties, allowing controlling the rendering of watermark text.
/// <summary>
/// Adds watermark behavior to any <see cref="TextBox"/> control.
/// </summary>
public class WatermarkBehavior : Behavior<TextBox>
{
#region Fields
private Canvas _watermarkHolder = null;
private TextBlock _watermark = null;
#endregion
#region Dependency Properties
#region WatermarkText
/// <summary>
/// Gets or sets the watermark text.
/// </summary>
public static readonly DependencyProperty WatermarkTextProperty =
DependencyProperty.Register(
"WatermarkText",
typeof(string),
typeof(WatermarkBehavior),
new PropertyMetadata(null));
/// <summary>
/// Gets or sets the watermark text.
/// </summary>
/// <value>
/// The watermark text.
/// </value>
public string WatermarkText
{
get { return (string)GetValue(WatermarkTextProperty); }
set { SetValue(WatermarkTextProperty, value); }
}
#endregion
#region WatermarkFontSize
/// <summary>
/// Gets or sets the size of the watermark font.
/// </summary>
public static readonly DependencyProperty WatermarkFontSizeProperty =
DependencyProperty.Register(
"WatermarkFontSize",
typeof(double),
typeof(WatermarkBehavior),
new PropertyMetadata(12.0));
/// <summary>
/// Gets or sets the size of the watermark font.
/// </summary>
/// <value>
/// The size of the watermark font.
/// </value>
public double WatermarkFontSize
{
get { return (double)GetValue(WatermarkFontSizeProperty); }
set { SetValue(WatermarkTextProperty, value); }
}
#endregion
#region WatermarkFontFamily
/// <summary>
/// Gets or sets the watermark font family.
/// </summary>
public static readonly DependencyProperty WatermarkFontFamilyProperty =
DependencyProperty.Register(
"WatermarkFontFamily",
typeof(FontFamily),
typeof(WatermarkBehavior),
new PropertyMetadata(new FontFamily("Century Gothic")));
/// <summary>
/// Gets or sets the watermark font family.
/// </summary>
/// <value>
/// The watermark font family.
/// </value>
public FontFamily WatermarkFontFamily
{
get { return (FontFamily)GetValue(WatermarkFontFamilyProperty); }
set { SetValue(WatermarkFontFamilyProperty, value); }
}
#endregion
#region WatermarkFontWeight
/// <summary>
/// Gets or sets the watermark font weight.
/// </summary>
public static readonly DependencyProperty WatermarkFontWeightProperty =
DependencyProperty.Register(
"WatermarkFontWeight",
typeof(FontWeight),
typeof(WatermarkBehavior),
new PropertyMetadata(FontWeights.Thin));
/// <summary>
/// Gets or sets the watermark font weight.
/// </summary>
/// <value>
/// The watermark font weight.
/// </value>
public FontWeight WatermarkFontWeight
{
get { return (FontWeight)GetValue(WatermarkFontWeightProperty); }
set { SetValue(WatermarkFontWeightProperty, value); }
}
#endregion
#region WatermarkFontStyle
/// <summary>
/// Gets or sets the watermark font style.
/// </summary>
public static readonly DependencyProperty WatermarkFontStyleProperty =
DependencyProperty.Register(
"WatermarkFontStyle",
typeof(FontStyle),
typeof(WatermarkBehavior),
new PropertyMetadata(FontStyles.Italic));
/// <summary>
/// Gets or sets the watermark font style.
/// </summary>
/// <value>
/// The watermark font style.
/// </value>
public FontStyle WatermarkFontStyle
{
get { return (FontStyle)GetValue(WatermarkFontStyleProperty); }
set { SetValue(WatermarkFontStyleProperty, value); }
}
#endregion WatermarkFontStyle
#region WatermarkHorizontalAlignment
/// <summary>
/// Gets or sets the watermark horizontal alignment.
/// </summary>
public static readonly DependencyProperty WatermarkHorizontalAlignmentProperty =
DependencyProperty.Register(
"WatermarHorizontalAlignment",
typeof(HorizontalAlignment),
typeof(WatermarkBehavior),
new PropertyMetadata(HorizontalAlignment.Stretch));
/// <summary>
/// Gets or sets the watermark horizontal alignment.
/// </summary>
/// <value>
/// The watermark horizontal alignment.
/// </value>
public HorizontalAlignment WatermarkHorizontalAlignment
{
get { return (HorizontalAlignment)GetValue(WatermarkHorizontalAlignmentProperty); }
set { SetValue(WatermarkHorizontalAlignmentProperty, value); }
}
#endregion WatermarkHorizontalAlignmentProperty
#region WatermarkFontStretch
/// <summary>
/// Gets or sets the watermark font stretch.
/// </summary>
public static readonly DependencyProperty WatermarkFontStretchProperty =
DependencyProperty.Register(
"WatermarkFontStretch",
typeof(FontStretch),
typeof(WatermarkBehavior),
new PropertyMetadata(FontStretches.Normal));
/// <summary>
/// Gets or sets the watermark font stretch.
/// </summary>
/// <value>
/// The watermark font stretch.
/// </value>
public FontStretch WatermarkFontStretch
{
get { return (FontStretch)GetValue(WatermarkFontStretchProperty); }
set { SetValue(WatermarkFontStretchProperty, value); }
}
#endregion WatermarkFontStretch
#region WatermarkForeground
/// <summary>
/// Gets or sets the watermark foreground.
/// </summary>
public static readonly DependencyProperty WatermarkForegroundProperty =
DependencyProperty.Register(
"WatermarkForeground",
typeof(Brush),
typeof(WatermarkBehavior),
new PropertyMetadata(new SolidColorBrush(Colors.Gray)));
/// <summary>
/// Gets or sets the watermark foreground.
/// </summary>
/// <value>
/// The watermark foreground.
/// </value>
public Brush WatermarkForeground
{
get { return (Brush)GetValue(WatermarkForegroundProperty); }
set { SetValue(WatermarkForegroundProperty, value); }
}
#endregion
#region WatermarkMargin
/// <summary>
/// Gets or sets the watermark margin.
/// </summary>
public static readonly DependencyProperty WatermarkMarginProperty =
DependencyProperty.Register(
"WatermarkMargin",
typeof(Thickness),
typeof(WatermarkBehavior),
new PropertyMetadata(default(Thickness)));
/// <summary>
/// Gets or sets the watermark margin.
/// </summary>
/// <value>
/// The watermark margin.
/// </value>
public Thickness WatermarkMargin
{
get { return (Thickness)GetValue(WatermarkMarginProperty); }
set { SetValue(WatermarkMarginProperty, value); }
}
#endregion
#endregion
#region Methods
/// <summary>
/// Called when behavior attached.
/// </summary>
protected override void OnAttached()
{
base.OnAttached();
TextBox textBox = this.AssociatedObject;
_watermarkHolder = new Canvas();
_watermark = new TextBlock();
Binding textBinding = new Binding("WatermarkText");
textBinding.Source = this;
BindingOperations.SetBinding(_watermark, TextBlock.TextProperty, textBinding);
Binding horizontalAlignmentBinding = new Binding("WatermarkHorizontalAlignment") { Source = this };
BindingOperations.SetBinding(_watermark, FrameworkElement.HorizontalAlignmentProperty, horizontalAlignmentBinding);
Binding watermarkFontSizeBinding = new Binding("WatermarkFontSize") { Source = this };
BindingOperations.SetBinding(_watermark, TextBlock.FontSizeProperty, watermarkFontSizeBinding);
Binding watermarkFontFamily = new Binding("WatermarkFontFamily");
watermarkFontFamily.Source = this;
BindingOperations.SetBinding(_watermark, TextBlock.FontFamilyProperty, watermarkFontFamily);
Binding watermarkFontWeight = new Binding("WatermarkFontWeight");
watermarkFontWeight.Source = this;
BindingOperations.SetBinding(_watermark, TextBlock.FontWeightProperty, watermarkFontWeight);
Binding watermarkFontStyle = new Binding("WatermarkFontStyle");
watermarkFontStyle.Source = this;
BindingOperations.SetBinding(_watermark, TextBlock.FontStyleProperty, watermarkFontStyle);
Binding watermarkFontStretch = new Binding("WatermarkFontStretch");
watermarkFontStretch.Source = this;
BindingOperations.SetBinding(_watermark, TextBlock.FontStretchProperty, watermarkFontStretch);
Binding watermarkForeground = new Binding("WatermarkForeground");
watermarkForeground.Source = this;
BindingOperations.SetBinding(_watermark, TextBlock.ForegroundProperty, watermarkForeground);
Binding watermarkMargin = new Binding("WatermarkMargin");
watermarkMargin.Source = this;
BindingOperations.SetBinding(_watermark, FrameworkElement.MarginProperty, watermarkMargin);
_watermarkHolder.Children.Add(_watermark);
textBox.SizeChanged += OnSizeChanged;
textBox.TextChanged += OnTextBoxTextChanged;
BindingExpression expresion = textBox.GetBindingExpression(Control.BackgroundProperty);
if (expresion != null) //we have something bound to background of textbox - move it to background of grid
{
textBox.NotifyOn("Background", OnOriginalBackgroundSet);
}
else
{
if (textBox.Background != null)
_watermarkHolder.Background = textBox.Background;
UpdateWatermark();
}
}
/// <summary>
/// Called when behavior detaching.
/// </summary>
protected override void OnDetaching()
{
base.OnDetaching();
TextBox textBox = this.AssociatedObject;
textBox.TextChanged -= OnTextBoxTextChanged;
textBox.SizeChanged -= OnSizeChanged;
}
/// <summary>
/// Called when adorned textbox size changed.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The <see cref="System.Windows.SizeChangedEventArgs"/> instance containing the event data.</param>
private void OnSizeChanged(object sender, SizeChangedEventArgs e)
{
_watermarkHolder.Width = e.NewSize.Width;
_watermarkHolder.Height = e.NewSize.Height;
UpdateWatermark();
}
/// <summary>
/// Updates the watermark.
/// </summary>
private void UpdateWatermark()
{
TextBox textBox = this.AssociatedObject;
WriteableBitmap bitmap = new WriteableBitmap(_watermarkHolder, null);
textBox.Background = new ImageBrush() { ImageSource = bitmap, Stretch = Stretch.None };
}
/// <summary>
/// Called when background of adorner change.
/// </summary>
/// <param name="newValue">The new value.</param>
/// <param name="oldValue">The old value.</param>
private void OnBackgroundChange(object newValue, object oldValue)
{
UpdateWatermark();
}
/// <summary>
/// Called when original background set. Used as workaround to solve not initialized binding.
/// </summary>
/// <param name="newValue">The new value.</param>
/// <param name="oldValue">The old value.</param>
private void OnOriginalBackgroundSet(object newValue, object oldValue)
{
AssociatedObject.UnNotifyOn("Background");
_watermarkHolder.NotifyOn("Background", OnBackgroundChange);
BindingExpression expresion = AssociatedObject.GetBindingExpression(Control.BackgroundProperty);
Binding originalBinding = expresion.ParentBinding;
Binding clonedBinding = new Binding();
clonedBinding.Path = originalBinding.Path;
clonedBinding.Source = expresion.DataItem;
_watermarkHolder.SetBinding(Panel.BackgroundProperty, clonedBinding);
AssociatedObject.ClearValue(Control.BackgroundProperty); //clear old binding
}
/// <summary>
/// Called when text box text changed. This method handles the logic of watermark.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The <see cref="System.Windows.Controls.TextChangedEventArgs"/> instance containing the event data.</param>
private void OnTextBoxTextChanged(object sender, TextChangedEventArgs e)
{
bool watermarkVisible = AssociatedObject.Text.Length == 0;
if (watermarkVisible && _watermark.Visibility == Visibility.Collapsed)
{
_watermark.Visibility = Visibility.Visible;
UpdateWatermark();
}
if (!watermarkVisible && _watermark.Visibility == Visibility.Visible)
{
_watermark.Visibility = Visibility.Collapsed;
UpdateWatermark();
}
}
#endregion
}
}
Now, the WatermarkBehavior can be used on any regular TextBox control. Here is usage example:
<TextBox x:Name="filterBox"
Width="180"
VerticalAlignment="Center"
Text="{Binding Filter, Mode=TwoWay}"
TextAlignment="Left">
<i:Interaction.Behaviors>
<Behaviors:WatermarkBehavior
WatermarkText="Type to filter"
WatermarkMargin="5,5,0,0"
/>
</i:Interaction.Behaviors>
</TextBox>
Enjoy,
Evgeni
Entry filed under: WPF. Tags: Silverlight.

Trackback this post | Subscribe to the comments via RSS Feed