This article is compatible with the latest version of Silverlight.
Introduction
When a control almost does what you want it to – if only it had another button or behaved slightly differently – you may be able to extend it by writing a custom control. Custom controls let you change an existing control or write a completely new control.
In this article I am going to describe the steps to extend an existing control. It will inherit from a TextBox and add a watermark and a button to clear existing text.
Here is the final result being used as a filter for a data grid, I’ve wired up the WatermarkedTextBox to act as a filter for the items in the list. The “Styled” Checkbox will apply a new theme to the app, including the WatermarkedTextBox:
You can grab the sample project, including the source code for the control, here. To compile it you need to either have Blend 4 installed, or Visual Studio 2010 with the free Blend 4 SDK.
UserControl vs. Custom Control
UserControls are great when you need a quick way to group some related controls together, such as an address form, or a grid-based browser with navigation controls. Sometimes they come in handy when an existing control almost does what you need it to but just needs a little enhancing. There are advantages and disadvantages of using a UserControl and when the disadvantages start to outweigh the advantages it’s time to look at a custom control.
Step 1 – Create the Projects
Start a new project in Expression Blend (you can also create it in Visual Studio). Select a “Silverlight Application + Website” project type and give it a name. This is going to be the test-bed for the new control.
At this point it’s easier to create the control library in Visual Studio. Blend is not designed for serious code entry and it doesn’t help us create any custom visual elements for our control.
Right click the main solution at the top of the “Projects” tab and select “Edit in Visual Studio”. Once in Visual Studio, right-click the main solution and select “Add” and “New Project…”. Choose the “Silverlight Class Library” project type and give it a name – this will be the project containing the new custom control. Visual Studio will create a default class in the new project that you can delete and then right click the new project and select “Add” and “New Item…”. Choose the “Class” item type and give it the name “WatermarkedTextBox”.
Step 2 – Subclass TextBox
The new control will subclass TextBox. It will also have a default style, referenced in the constructor like this:
public class WatermarkedTextBox : TextBox
{
/// <summary>
/// Initializes a new instance of the <see cref="WatermarkedTextBox"/> class.
/// </summary>
public WatermarkedTextBox()
{
DefaultStyleKey = typeof(WatermarkedTextBox);
}
}
The code above tells Silverlight to look for a style that uses TargetType=”WatermarkedTextBox” for this control. It will look for the default style in a specific folder in the project named “Themes”. Right-click the project containing the WatermarkedTextBox class and select “Add” and “New Folder”; name the folder “Themes”. Now add the resource library by right-clicking the “Themes” folder, selecting “Add” and “New Item…”. Choose the “Silverlight Resource Dictionary” item type and name it “generic.xaml”. All Silverlight control libraries must have their default styles defined in a resource dictionary file called “generic.xaml” in a folder called “Themes”.
Step 3 – Create the Default Style
The control needs to have a default style defined in generic.xaml. You can create this by hand, but when you are extending an existing control, it is much easier to start with the default style from the control you are inheriting from. The easiest way to get the default style from the inherited TextBox class is to do the following:
- swap back to Blend and, in MainPage.xaml, create a new TextBox on the design surface.
- reset any properties that Blend changed when you added it.
- right-click the TextBox and select “Edit Template” and “Edit a Copy”.
- choose “Apply to all” rather than giving it a Name (key), and choose “This document” for the “Define in” option
Now switch to XAML view for MainPage.xaml and you will see, in the Resources section of the UserControl, a control template with the key “ValidationToolTemplate” and a style with the TargetType set to “TextBox”. Copy this XAML and paste it, back in Visual Studio, into the generic.xaml resource dictionary.
The Resource dictionary needs a namespace declaration for the local control like this:
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:CustomControls"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
>
The “local” allows you to reference the new control and the “sys” allows you to reference types in the System namespace in the core (which we will be using later). Change the TargetType from “TextBox” to “local:WatermarkedTextBox”:
<Style TargetType="local:WatermarkedTextBox">
We now have our WatermarkedTextBox at the same level as the basic TextBox. We could compile it now and use it in our project, but it wouldn’t behave any differently from a regular TextBox.
Step 4 – Add Custom Properties
Now we start creating the custom functionality. In this case, for the WatermarkedTextBox, I am going to add the following properties:
- Watermark : the text that displays when the TextBox is empty.
- WatermarkStyle : a Style that will be applied to the Watermark
- WatermarkForeground : quick access to the brush used for the Watermark. This will override the Foreground specified in the WatermarkStyle property.
- TextRemoverStyle : a Style that will be applied to the button that clears any entered text
- TextRemoverToolTip : text for a tooltip for the text removal button
- IsUpdateImmediate : a Boolean property (which requires the system namespace reference in the XAML) that, if set to True, will cause any Binding source on the WatermarkTextBox.Text property to be updated whenever the text changes, rather than just when the control loses focus (which is the default behavior for the TextBox). This is especially useful if the control is used as a filter for a collection of items.
I added the WatermarkForeground property as a quicker way to set the foreground brush for the watermark text rather than having to edit the WatermarkStyle, and to use as an example of precedence in TemplateBinding default XAML when I get to it.
Custom properties don’t have to be dependency properties, but it is a good idea to use dependency properties if you want them to support binding. In the downloadable sample project, I have created a dependency property and a class property for each of the above. Here is the code for the Watermark property:
public class WatermarkedTextBox : TextBox
{
/// <summary>
/// Watermark text dependency property
/// </summary>
public static readonly DependencyProperty WatermarkProperty = DependencyProperty.Register(
"Watermark",
typeof(string),
typeof(WatermarkedTextBox),
new PropertyMetadata("Enter text here"));
/// <summary>
/// Gets or sets the watermark.
/// </summary>
/// <value>The watermark.</value>
[Description("Gets or sets the watermark")]
[Category("Watermark")]
public string Watermark
{
get { return (string)GetValue(WatermarkProperty); }
set { SetValue(WatermarkProperty, value); }
}
...
There are some important points to note about this code:
- The default value for the dependency property (“Enter text here”) should have a matching setter in the default style XAML. In this case, the style has a setter for the Watermark property that matches the default value given in the PropertyMetadata for the WatermarkProperty.
- The Description attribute (in the System.ComponentModel namespace) is picked up by Blend and used as a tooltip for the property.
- The Category attribute is used by Blend to create a new grouping category on the Properties tab, shown in the image to the right.
- The accessors for the Watermark property do nothing other than get/set the value of the dependency property.
The other properties all follow the same pattern.
Step 5 – Add Custom Elements
In this step we add any extra visual elements to the control template in the default style XAML. For this control I’ll add a TextBlock for the watermark and a Button for the text-remover:
<TextBlock x:Name="Watermark" Style="{TemplateBinding WatermarkStyle}" Foreground="{TemplateBinding WatermarkForeground}"
Text="{TemplateBinding Watermark}" IsHitTestVisible="False" Grid.ColumnSpan="2"/>
<Button x:Name="TextRemover" Height="14" HorizontalAlignment="Center" Margin="0,3,3,3"
Style="{TemplateBinding TextRemoverStyle}" VerticalAlignment="Center" Width="14" Content="Button"
Grid.Column="1" IsTabStop="False" RenderTransformOrigin="0.5,0.5"
ToolTipService.ToolTip="{TemplateBinding TextRemoverToolTip}">
</Button>
Use “{TemplateBinding …}” to bind properties on the visual elements to the properties on the control class. In the XAML above you can see that I’ve bound both the Style and the Foreground properties of the TextBlock. As mentioned earlier, the Foreground is there for convenience and will override the Foreground brush specified in the style.
The default WatermarkStyle is defined in the XAML as a normal Setter like this:
<Setter Property="WatermarkStyle">
<Setter.Value>
<Style TargetType="TextBlock">
<Setter Property="HorizontalAlignment" Value="Left" />
<Setter Property="Margin" Value="3,0,0,0" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="FontStyle" Value="Italic" />
<Setter Property="TextWrapping" Value="NoWrap" />
</Style>
</Setter.Value>
</Setter>
The TextRemoverStyle is defined similarly, except it has a control template as well. To create the control template for the TextRemoverStyle, I created a button back in Blend, customized the template and style to achieve the look I was after and then copied the XAML from that button, and pasted it into the control template for the TextRemoverStyle.
Step 6 – Add Custom States
This control is going to have two new state groups:
- WatermarkStates : A state for when the watermark is visible and a state for when it is hidden
- TextRemoverStates: A state for when the text-remover button is visible and a state for when it is hidden.
For now we will just add empty place holders in the XAML:
<VisualStateGroup x:Name="TextRemoverStates">
<VisualState x:Name="TextRemoverVisible">
</VisualState>
<VisualState x:Name="TextRemoverHidden">
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="WatermarkStates">
<VisualState x:Name="WatermarkVisible">
</VisualState>
<VisualState x:Name="WatermarkHidden">
</VisualState>
</VisualStateGroup>
These are placed in the same location as the inherited state groups for the TextBox. It isn’t very easy to design good looking states and transitions in XAML by hand, so we will come back to this at the end and use Blend.
Blend will know what to do with our states if we decorate our class with the TemplateVisualState attribute for each state we added. We can also use the TemplatePart attribute to let Blend know that our control expects specific visual elements to work with. The StyleTypedProperty lets Blend know the target type of the two style properties we added:
[StyleTypedProperty(Property = "TextRemoverStyle", StyleTargetType = typeof(Button)),
StyleTypedProperty(Property = "WatermarkStyle", StyleTargetType = typeof(TextBlock)),
TemplatePart(Name = "TextRemover", Type = typeof(Button)),
TemplatePart(Name = "Watermark", Type = typeof(TextBlock)),
TemplateVisualState(Name = "WatermarkVisible", GroupName = "WatermarkStates"),
TemplateVisualState(Name = "WatermarkHidden", GroupName = "WatermarkStates"),
TemplateVisualState(Name = "TextRemoverVisible", GroupName = "TextRemoverStates"),
TemplateVisualState(Name = "TextRemoverHidden", GroupName = "TextRemoverStates")]
public class WatermarkedTextBox : TextBox
{
Step 7 – Add Custom Behavior
There are a few things left to do to make our code and the new visual elements work together. The OnApplyTemplate method must be overridden to wire up the text remover button:
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
// remove old button handler
if (null != this.textRemoverButton)
{
this.textRemoverButton.Click -= this.TextRemoverClick;
}
// add new button handler
this.textRemoverButton = GetTemplateChild("TextRemover") as Button;
if (null != this.textRemoverButton)
{
this.textRemoverButton.Click += this.TextRemoverClick;
}
this.UpdateState();
}
And event handlers need to be hooked up in the constructor for events that are important to this control:
public WatermarkedTextBox()
{
DefaultStyleKey = typeof(WatermarkedTextBox);
this.GotFocus += this.TextBoxGotFocus;
this.LostFocus += this.TextBoxLostFocus;
this.TextChanged += this.TextBoxTextChanged;
}
private void TextBoxGotFocus(object sender, RoutedEventArgs e)
{
VisualStateManager.GoToState(this, "WatermarkHidden", false);
this.isFocused = true;
this.UpdateState();
}
private void TextBoxLostFocus(object sender, RoutedEventArgs e)
{
this.isFocused = false;
this.UpdateState();
}
private void TextBoxTextChanged(object sender, TextChangedEventArgs e)
{
this.UpdateState();
if (!this.IsUpdateImmediate)
{
return;
}
BindingExpression binding = this.GetBindingExpression(TextBox.TextProperty);
if (null != binding)
{
binding.UpdateSource();
}
}
The “UpdateSource” method causes the control to transition correctly to the appropriate visual state:
private void UpdateState()
{
if (string.IsNullOrEmpty(this.Text))
{
VisualStateManager.GoToState(this, "TextRemoverHidden", true);
if (!this.isFocused)
{
VisualStateManager.GoToState(this, "WatermarkVisible", true);
}
}
else
{
VisualStateManager.GoToState(this, "TextRemoverVisible", true);
VisualStateManager.GoToState(this, "WatermarkHidden", false);
}
}
Step 8 – Define Default Visual States
The last thing to do is to define the visual changes in the states for the controls. The easiest way to do this is to follow these steps:
- get the control to the point where it compiles.
- go into Blend and create an instance of the control on the design surface and customize the template for the created instance.
- make the changes to each element in the template for each state
- view the XAML and copy-paste the TextRemoverStates and the WatermarkStates into the appropriate section in generic.xaml.
That pasted XAML now becomes part of the default template for the control when you rebuild it.
Summary
In this article I described in 8 steps how to extend an existing TextBox control into a WatermarkedTextBox custom control. I showed how you can use Blend to extract the default styles and control templates from an existing control and how to add your own visual elements in generic.xaml and wire them up to the new type to create a new custom control.
References:
Sample Data: MSXML SDK. Artwork one, two, three.