Introduction
The .NET Framework provides many out-of-the-box WPF controls that you can use for
your application. Most of the time, these controls will suffice in designing
the user interface. However, there will be times when you need to create your
own control. For example, you will have to create a TimePicker control if you
want to specify the time component of a DateTime object. There are two types
of controls you can create in WPF, user control and custom control. Creating
a user control is just like creating a window and adding contents to it. You
can just drag controls to the designer when working with Visual Studio or Expression
Blend.
Meanwhile, creating a custom control requires some coding and creating a generic
theme for your control. Existing WPF controls are custom controls. You can change
how it looks by specifying another theme. You can also override how a control
is drawn. Creating a custom control is the choice if you need a control where
the theme can be easily changed. Also, you can extend the functionality of a
certain control by deriving from it. I would like to discuss in this article
the basics you need to know to create a custom control.
Getting Started
I’ve been using the WPF Toolkit for a while and there is a control there for selecting
dates, which is called the DatePicker. However, there is no control for selecting
time like 8:00 AM. Let’s create a control and name it TimePicker. I’ll be using
Visual Studio 2008 to create the control. First, create a project of type WPF
Custom Control Library. This creates two files: CustomControl1.cs and Generic.xaml.
Let’s rename CustomControl1 to TimePicker. The TimePicker class derives from
the Control class and has a static constructor. This constructor specifies the
default style key, which is usually the type of the control. This is necessary
to find the default style for the control. The Generic.xaml file, which is under
the Themes folder, is important. This is where the .NET runtime looks for a control’s
theme by default. If you are going to create many custom controls and want to
create a folder for each control, make sure you merge your resource dictionaries
in the Generic.xaml file (or another file depending on the theme). The following
shows the contents of the Generic.xaml file.
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:CustomControlsLib">
<Style TargetType="{x:Type local:TimePicker}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:TimePicker}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
Listing 1. Default TimePicker Style
If we use the control now in an application, we won’t see anything because the TimePicker
control is rendered only as a Border with default properties. We have to specify
a different template in the Generic.xaml file. Most of the time, a control expects
the template to have certain kinds of controls. This is where template parts
come into place.
Template Parts
Now let’s think of the controls that will make up our TimePicker control. I think
there should be three TextBox controls that correspond to the hour, minute and
second components. I’ll use the 24-hour format here. We also need two buttons
for incrementing and decrementing a selected component. We will let the user
of the control know that the control expects these four controls present in the
template. To do this, we must use the TemplatePartAttribute class. The following
code listing shows the updated TimePicker class.
[TemplatePart(Name = TimePicker.ElementHourTextBox, Type = typeof(TextBox))]
[TemplatePart(Name = TimePicker.ElementMinuteTextBox, Type = typeof(TextBox))]
[TemplatePart(Name = TimePicker.ElementSecondTextBox, Type = typeof(TextBox))]
[TemplatePart(Name = TimePicker.ElementIncrementButton, Type = typeof(Button))]
[TemplatePart(Name = TimePicker.ElementDecrementButton, Type = typeof(Button))]
public class TimePicker : Control
{
#region Constants
private const string ElementHourTextBox = "PART_HourTextBox";
private const string ElementMinuteTextBox = "PART_MinuteTextBox";
private const string ElementSecondTextBox = "PART_SecondTextBox";
private const string ElementIncrementButton = "PART_IncrementButton";
private const string ElementDecrementButton = "PART_DecrementButton";
#endregion
static TimePicker()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(TimePicker), new FrameworkPropertyMetadata(typeof(TimePicker)));
}
}
Listing 2. Specifying Template Parts
The convention in naming template parts is to prefix the name with the word PART.
Next, we need to override the OnApplyTemplate method of the Control class. We
will get the controls from the template and assign them to instance variables,
which can be accomplished by using the GetTemplateChild method. This method accepts
a string parameter which is the name of the control in the template to be searched.
The following code snippet shows the declaration of the instance variables and
the implementation of the OnApplyTemplate.
#region Data
private TextBox hourTextBox;
private TextBox minuteTextBox;
private TextBox secondTextBox;
private Button incrementButton;
private Button decrementButton;
#endregion Data
#region Public Methods
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
hourTextBox = GetTemplateChild(ElementHourTextBox) as TextBox;
minuteTextBox = GetTemplateChild(ElementMinuteTextBox) as TextBox;
secondTextBox = GetTemplateChild(ElementSecondTextBox) as TextBox;
incrementButton = GetTemplateChild(ElementIncrementButton) as Button;
decrementButton = GetTemplateChild(ElementDecrementButton) as Button;
}
#endregion Public Methods
Listing 3. Overriding the OnApplyTemplate Method
Now what if the user specified a different template for the control and did not put
the expected template parts? There are two things that we can do, throw an exception
or just ignore it. We’ll do the latter in this example. However, we’ll need to
check if the value of a variable is not null every time we are about to use it.
We’ll need to attach event handlers to these controls’ events that we are interested
in. For example, we’ll need to attach event handlers to the Click event of the
increment and decrement buttons so that we can change the selected time. But
before that, we still have to create a property for the selected time.
Dependency Properties
We can either use a normal property or a dependency property for setting or getting
the selected time. However, using a dependency property allows us to bind a property
using XAML and use it in styles and animations. It also allows property validation
and coercion. Most of the time, you will be defining dependency properties when
creating a custom control. The following code shows the SelectedTime dependency
property. I’ve also added MinTime and MaxTime dependency properties to illustrate
property coercion although this might not be necessary in most scenarios.
private const TimeSpan MinValidTime = new TimeSpan(0, 0, 0);
private const TimeSpan MaxValidTime = new TimeSpan(23, 59, 59);
#region Ctor
static TimePicker()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(TimePicker), new FrameworkPropertyMetadata(typeof(TimePicker)));
}
public TimePicker()
{
SelectedTime = DateTime.Now.TimeOfDay;
}
#endregion Ctor
#region Public Properties
#region SelectedTime
public TimeSpan SelectedTime
{
get { return (TimeSpan)GetValue(SelectedTimeProperty); }
set { SetValue(SelectedTimeProperty, value); }
}
public static readonly DependencyProperty SelectedTimeProperty =
DependencyProperty.Register(
"SelectedTime",
typeof(TimeSpan),
typeof(TimePicker),
new FrameworkPropertyMetadata(TimePicker.MinValidTime, new PropertyChangedCallback(TimePicker.OnSelectedTimeChanged), new CoerceValueCallback(TimePicker.CoerceSelectedTime)));
#endregion SelectedTime
#region MinTime
public TimeSpan MinTime
{
get { return (TimeSpan)GetValue(MinTimeProperty); }
set { SetValue(MinTimeProperty, value); }
}
public static readonly DependencyProperty MinTimeProperty =
DependencyProperty.Register(
"MinTime",
typeof(TimeSpan),
typeof(TimePicker),
new FrameworkPropertyMetadata(TimePicker.MinValidTime, new PropertyChangedCallback(TimePicker.OnMinTimeChanged)),
new ValidateValueCallback(TimePicker.IsValidTime));
#endregion MinTime
#region MaxTime
public TimeSpan MaxTime
{
get { return (TimeSpan)GetValue(MaxTimeProperty); }
set { SetValue(MaxTimeProperty, value); }
}
public static readonly DependencyProperty MaxTimeProperty =
DependencyProperty.Register(
"MaxTime",
typeof(TimeSpan),
typeof(TimePicker),
new FrameworkPropertyMetadata(TimePicker.MaxValidTime, new PropertyChangedCallback(TimePicker.OnMaxTimeChanged), new CoerceValueCallback(TimePicker.CoerceMaxTime)),
new ValidateValueCallback(TimePicker.IsValidTime));
#endregion MaxTime
#endregion Public Properties
Listing 4. Dependency Properties
To define a dependency property, we need to call the static Register method of the
DependencyProperty class. The required method parameters are the name of the
property, the type of the property and the type of the class containing the property.
Optional parameters are property metadata and callback method for validating
the value of the property. Using property metadata, we can set a property’s default
value, a callback method that is executed when the property’s value is changed,
and a callback method used for coercion, among others. Notice that there are
constants specifying the minimum and maximum valid time. We will try to limit
the values of the MinTime and MaxTime properties since we can also set days in
a TimeSpan object. Let’s take a look at the callback methods specified in these
dependency properties.
private static object CoerceSelectedTime(DependencyObject d, object value)
{
TimePicker timePicker = (TimePicker)d;
TimeSpan minimum = timePicker.MinTime;
if ((TimeSpan)value < minimum)
{
return minimum;
}
TimeSpan maximum = timePicker.MaxTime;
if ((TimeSpan)value > maximum)
{
return maximum;
}
return value;
}
private static object CoerceMaxTime(DependencyObject d, object value)
{
TimePicker timePicker = (TimePicker)d;
TimeSpan minimum = timePicker.MinTime;
if ((TimeSpan)value < minimum)
{
return minimum;
}
return value;
}
private static bool IsValidTime(object value)
{
TimeSpan time = (TimeSpan)value;
return MinValidTime <= time && time <= MaxValidTime;
}
protected virtual void OnSelectedTimeChanged(TimeSpan oldSelectedTime, TimeSpan newSelectedTime)
{
}
private static void OnSelectedTimeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
TimePicker element = (TimePicker)d;
element.OnSelectedTimeChanged((TimeSpan)e.OldValue, (TimeSpan)e.NewValue);
}
protected virtual void OnMinTimeChanged(TimeSpan oldMinTime, TimeSpan newMinTime)
{
}
private static void OnMinTimeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
TimePicker element = (TimePicker)d;
element.CoerceValue(MaxTimeProperty);
element.CoerceValue(SelectedTimeProperty);
element.OnMinTimeChanged((TimeSpan)e.OldValue, (TimeSpan)e.NewValue);
}
protected virtual void OnMaxTimeChanged(TimeSpan oldMaxTime, TimeSpan newMaxTime)
{
}
private static void OnMaxTimeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
TimePicker element = (TimePicker)d;
element.CoerceValue(SelectedTimeProperty);
element.OnMaxTimeChanged((TimeSpan)e.OldValue, (TimeSpan)e.NewValue);
}
Listing 5. Callback Methods
We have attached a corresponding event handler for each PropertyChanged event. Inside
an event handler, we will coerce the values of other properties that are dependent
on the property that is changed. The MaxTime property is dependent on the MinTime
property while the SelectedTime is dependent on the MinTime and MaxTime properties.
Meanwhile, the IsValidTime method checks if the MinTime or MaxTime is valid.
You can see that there are empty methods in the above code. We can raise an event
to notify other classes that a property is changed. But first, we have to know
about routed events.
Routed Events
One event that we can define for the TimePicker control is when the selected time
changes. The question is whether we are going to define the event as a routed
event or not. A routed event is an event wherein it may originate from one control
but can be raised on another control. The advantage of defining a routed event
is that you can provide the event handling code in another control, which could
be useful if you want to centralize your event handling code. You can also utilize
features like event triggers and setters.
#region SelectedTimeChangedEvent
public event RoutedPropertyChangedEventHandler<TimeSpan> SelectedTimeChanged
{
add { base.AddHandler(SelectedTimeChangedEvent, value); }
remove { base.RemoveHandler(SelectedTimeChangedEvent, value); }
}
public static readonly RoutedEvent SelectedTimeChangedEvent =
EventManager.RegisterRoutedEvent(
"SelectedTimeChanged",
RoutingStrategy.Bubble,
typeof(RoutedPropertyChangedEventHandler<TimeSpan>),
typeof(TimePicker));
#endregion SelectedTimeChangedEvent
protected virtual void OnSelectedTimeChanged(TimeSpan oldSelectedTime, TimeSpan newSelectedTime)
{
RoutedPropertyChangedEventArgs<TimeSpan> e = new RoutedPropertyChangedEventArgs<TimeSpan>(oldSelectedTime, newSelectedTime);
e.RoutedEvent = SelectedTimeChangedEvent;
base.RaiseEvent(e);
}
Listing 6. SelectedTimeChanged Routed Event
Defining a routed event is similar to defining a dependency property. I guess the
routing strategy needs a bit of explanation. This can be set to Tunnel, Bubble
or Direct. A bubbling event is an event that travels from the control that defines
the event upwards the tree of controls. A tunneling event is the reverse of a
bubbling event. Most of the tunneling events’ names are prefixed with the word
Preview. A tunneling event is useful when you do not want the corresponding bubbling
event executed. This can be done by marking the event as handled. Direct routed
events do not travel but make use of event triggers and setters.
User Interaction
Now we need to specify how the user will interact with our control. We’ll need to
hook up event handlers for the events of the different controls that make up
our TimePicker control. For example, we need to attach event handlers for the
Click event of the increment and decrement buttons. We also need to create a
variable that will store the currently selected TextBox, whether it is the hourTextBox
or minuteTextBox. This will determine if the hour or minute component of the
selected time is to be changed when the user clicks on the increment or decrement
button, which is demonstrated in the following code.
#region Public Methods
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
hourTextBox = GetTemplateChild(ElementHourTextBox) as TextBox;
if (hourTextBox != null)
{
hourTextBox.IsReadOnly = true;
hourTextBox.GotFocus += SelectTextBox;
}
minuteTextBox = GetTemplateChild(ElementMinuteTextBox) as TextBox;
if (minuteTextBox != null)
{
minuteTextBox.IsReadOnly = true;
minuteTextBox.GotFocus += SelectTextBox;
}
secondTextBox = GetTemplateChild(ElementSecondTextBox) as TextBox;
if (secondTextBox != null)
{
secondTextBox.IsReadOnly = true;
secondTextBox.GotFocus += SelectTextBox;
}
incrementButton = GetTemplateChild(ElementIncrementButton) as Button;
if (incrementButton != null)
{
incrementButton.Click += IncrementTime;
}
decrementButton = GetTemplateChild(ElementDecrementButton) as Button;
if (decrementButton != null)
{
decrementButton.Click += DecrementTime;
}
}
#endregion Public Methods
#region Private Methods
private void SelectTextBox(object sender, RoutedEventArgs e)
{
selectedTextBox = sender as TextBox;
}
private void IncrementTime(object sender, RoutedEventArgs e)
{
IncrementDecrementTime(1);
}
private void DecrementTime(object sender, RoutedEventArgs e)
{
IncrementDecrementTime(-1);
}
private void IncrementDecrementTime(int step)
{
if (selectedTextBox == null)
{
selectedTextBox = hourTextBox;
}
TimeSpan time;
if (selectedTextBox == hourTextBox)
{
time = SelectedTime.Add(new TimeSpan(step, 0, 0));
}
else if (selectedTextBox == minuteTextBox)
{
time = SelectedTime.Add(new TimeSpan(0, step, 0));
}
else
{
time = SelectedTime.Add(new TimeSpan(0, 0, step));
}
SelectedTime = time;
}
#endregion
Listing 7. Defining the User Interaction Logic
You will have to attach event handlers using code, not XAML, when creating a custom
control. Now let’s create the default template. The following XAML code shows
the default template found in Generic.xaml.
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:CustomControlsLib">
<ControlTemplate
x:Key="TimePickerButtonTemplate"
TargetType="ButtonBase">
<Border
x:Name="ContentContainer"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="1"
SnapsToDevicePixels="True">
<ContentPresenter
x:Name="Content"
RecognizesAccessKey="True"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
ContentStringFormat="{TemplateBinding ContentStringFormat}"
Margin="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter
TargetName="ContentContainer"
Property="Background"
Value="LightBlue">
</Setter>
</Trigger>
<Trigger Property="Button.IsDefaulted" Value="True">
<Setter
TargetName="ContentContainer"
Property="Background"
Value="LightBlue">
</Setter>
</Trigger>
<Trigger Property="ButtonBase.IsPressed" Value="True">
<Setter
TargetName="Content"
Property="RenderTransform">
<Setter.Value>
<TranslateTransform Y="0.5"/>
</Setter.Value>
</Setter>
</Trigger>
<Trigger Property="ToggleButton.IsChecked" Value="True">
<Setter
TargetName="Content"
Property="RenderTransform">
<Setter.Value>
<TranslateTransform Y="0.5"/>
</Setter.Value>
</Setter>
</Trigger>
<Trigger Property="UIElement.IsEnabled" Value="False">
<Setter Property="TextElement.Foreground">
<Setter.Value>
<DynamicResource ResourceKey="{x:Static SystemColors.GrayTextBrushKey}" />
</Setter.Value>
</Setter>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
<Style x:Key="{x:Type local:TimePicker}" TargetType="{x:Type local:TimePicker}">
<Setter Property="Width" Value="100"/>
<Setter Property="Height" Value="23"/>
<Setter Property="BorderBrush" Value="Black"/>
<Setter Property="BorderThickness" Value="0.5"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:TimePicker}">
<Border
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<StackPanel
Orientation="Horizontal"
DataContext="{TemplateBinding local:TimePicker.SelectedTime}">
<TextBox
x:Name="PART_HourTextBox"
Text="{Binding Hours, Mode=OneWay, StringFormat=00}"
BorderBrush="{x:Null}"
Width="23"/>
<TextBlock
Text=":"
VerticalAlignment="Center"/>
<TextBox
x:Name="PART_MinuteTextBox"
Text="{Binding Minutes, Mode=OneWay, StringFormat=00}"
BorderBrush="{x:Null}"
Width="23"/>
<TextBlock
Text=":"
VerticalAlignment="Center"/>
<TextBox
x:Name="PART_SecondTextBox"
Text="{Binding Seconds, Mode=OneWay, StringFormat=00}"
BorderBrush="{x:Null}"
Width="23"/>
</StackPanel>
<Grid
Grid.Column="1">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Button
x:Name="PART_IncrementButton"
Margin="1,1,1,0"
Width="20"
Template="{StaticResource TimePickerButtonTemplate}">
<TextBlock
Text="p"
FontFamily="Wingdings 3"
FontSize="6"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Button>
<Button
x:Name="PART_DecrementButton"
Grid.Row="1"
Margin="1,1,1,1"
Width="20"
Template="{StaticResource TimePickerButtonTemplate}">
<TextBlock
Text="q"
FontFamily="Wingdings 3"
FontSize="6"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Button>
</Grid>
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
Listing 8. TimePicker Default Template
Aside from the default style for your control, you can also add other things like
brushes, other control templates, etc. in the resource dictionary. The first
entry in the resource dictionary is the control template for the TimePicker buttons.
Here, a Button’s appearance is defined as a Border control having a ContentPresenter
inside it. If a Button’s content is a property of the Button class, how does
the ContentPresenter get the content so that it can display it? This can be solved
by using TemplateBinding. You can use TemplateBinding if you want to access the
properties of the control where the template will be applied. This is equivalent
to using Binding and setting the Source property to RelativeSource.TemplatedParent.
For example, the Content property of the ContentPresenter can also be set like
the following: Content="{Binding RelativeSource={RelativeSource TemplatedParent},
Path=Content}". However, take note that you must set the TargetType property
of the ControlTemplate. In our example, it is set to ButtonBase. You can also
use the more verbose {x:Type ButtonBase}.
Another important thing to know is how to add triggers to your template. You can
define here how the control will look like when it is focused, when the mouse
is over it, when it is disabled, etc. You need to supply the Property and Value
properties of a Trigger. The Property can be set to any dependency property of
the control where the template is applied. If the value of the dependency property
is equal to the value specified in the Value property of the Trigger, then the
corresponding setters are executed. Optionally, you can also set the SourceName
property just in case you want to specify a dependency property of a different
control inside the template. For example, instead of the Button, we may want
to know if the Border’s IsMouseOver property is set to true. As for the Setter,
it lets you set a value of a target control’s dependency property. There are
actually other types of triggers and setters that you might encounter like DataTrigger
and EventSetter among others.
Let’s see the default style applied to the TimePicker control. You can see that the
TargetType of the style is set to local:TimePicker. By convention, the word local
is used as the identifier of the local namespace, or the namespace containing
the control. We haven’t set the resource key of the style so that the style gets
applied to all TimePicker controls, unless the style is overridden. However,
you can still specify a resource key and have the style applied to all TimePicker
controls. The resource key must be the type of the control. In this case, the
resource key would be assigned as follows: x:Key=”{x:Type local:TimePicker}”.
You might notice that I set the DataContext property of a StackPanel using TemplateBinding.
Aside from the reason that the SelectedTime is used by the controls in the StackPanel
and saves us from typing more code, we can’t use TemplateBinding to bind to a
property of a property. For example, {TemplateBinding SelectedTime.Hours} will
not work because WPF will treat SelectedTime as the type and Hours as the property.
Using {TemplateBinding local:TimePicker.SelectedTime.Hours} will not work either.
Now let’s use the TimePicker control in an application.
<Window
x:Class="CustomControlsDemo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:CustomControlsLib;assembly=CustomControlsLib"
Title="MainWindow" Height="300" Width="300">
<Grid>
<controls:TimePicker
SelectedTime="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=Time}"
MinTime="8:00:00"
MaxTime="17:00:00"/>
</Grid>
</Window>
Listing 9. Using the TimePicker Control
Using your custom control is just like using an out-of-the-box WPF control. Just
remember to add a reference to your project and the correct namespace to use
it.

Figure 1. The TimePicker Control
The TimePicker control here is just created for learning purposes and could not be
used in production. The Visual Studio 2008 solution is available here. If you want to use a TimePicker control, I suggest you use Marlon Grech’s Avalon
Controls Library located at http://marlongrech.wordpress.com/avalon-controls-library/. These library contains other great WPF controls as well.