Building custom controls in WPF can provide you with lots of flexibility. It allows you to entirely separate the behavior of the control from the look of the control. This is the premise behind most of what WPF offers. In this post I will show you how you can build a simple control similar to the search control in Outlook 2007.
Add a new WPF Application project.
Then add a WPF User Control Library.
Delete the generated UserControl1.xaml that was given to you.
Add a new WPF Custom Control.
Your solution should now look like this:
The template gives you a FilterTextBox.cs and Generic.xaml file.
public class FilterTextBox : Control
{
static FilterTextBox()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(FilterTextBox),
new FrameworkPropertyMetadata(typeof(FilterTextBox)));
}
}
The default Generic.xaml is the default look for your custom control, and is found in the Theme directory. It is just an empty border for now:
<Style TargetType="{x:Type local:FilterTextBox}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:FilterTextBox}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Now lets start building the behavior of our control. We'll start by adding a 'Text' dependency property. This will be the filter text that the user types in. Notice that I've created callbacks to be notified when the property has changed. And as always, do NOT put any code within the getter and setter of the CLR property, because WPF bypasses this property at runtime and calls GetValue and SetValue directly. However the CLR property is still needed to use the property in xaml.
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register("Text",
typeof(String),
typeof(FilterTextBox),
new UIPropertyMetadata(null,
new PropertyChangedCallback(OnTextChanged),
new CoerceValueCallback(OnCoerceText))
);
private static object OnCoerceText(DependencyObject o, Object value)
{
FilterTextBox filterTextBox = o as FilterTextBox;
if (filterTextBox != null)
return filterTextBox.OnCoerceText((String)value);
else
return value;
}
private static void OnTextChanged(DependencyObject o,
DependencyPropertyChangedEventArgs e)
{
FilterTextBox filterTextBox = o as FilterTextBox;
if (filterTextBox != null)
filterTextBox.OnTextChanged((String)e.OldValue, (String)e.NewValue);
}
protected virtual String OnCoerceText(String value)
{
return value;
}
protected virtual void OnTextChanged(String oldValue, String newValue)
{
}
public String Text
{
// IMPORTANT: To maintain parity between setting a property in XAML
// and procedural code, do not touch the getter and setter inside
// this dependency property!
get
{
return (String)GetValue(TextProperty);
}
set
{
SetValue(TextProperty, value);
}
}
We'll want users of the control to be notified when the text in our TextBox has changed, so lets create a 'TextChanged' event.
public static readonly RoutedEvent TextChangedEvent =
EventManager.RegisterRoutedEvent("TextChanged",
RoutingStrategy.Bubble,
typeof(RoutedEventHandler),
typeof(FilterTextBox));
public event RoutedEventHandler TextChanged
{
add { AddHandler(TextChangedEvent, value); }
remove { RemoveHandler(TextChangedEvent, value); }
}
Now lets go back and modify our OnTextChanged method to raise our TextChanged event.
protected virtual void OnTextChanged(String oldValue, String newValue)
{
// fire text changed event
this.RaiseEvent(new RoutedEventArgs(FilterTextBox.TextChangedEvent, this));
}
With the base behavior mostly done, we can move on to creating a generic look for our control. Lets add a DockPanel with a Button and a TextBox, the Button docked to the right. Lets also bind the 'Text' property of the TextBox to the 'Text' property of our control. We want the 'UpdateSourceTrigger' to be 'PropertyChanged' so that the 'TextChanged' event we created will be fired every time the user types something into the TextBox. Notice that we don't want the TextBox to have a border because then we'd have two borders.
<ControlTemplate TargetType="{x:Type local:FilterTextBox}">
<Border
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="3">
<DockPanel
LastChildFill="True"
Margin="1">
<Button
x:Name="PART_ClearFilterButton"
Content="X"
Width="20"
ToolTip="Clear Filter"
DockPanel.Dock="Right" />
<TextBox
x:Name="PART_FilterTextBox"
Text="{Binding Path=Text,
Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged,
RelativeSource={RelativeSource TemplatedParent}}"
BorderBrush="{x:Null}"
BorderThickness="0"
VerticalAlignment="Center" />
</DockPanel>
</Border>
</ControlTemplate>
Take special note of the names of the controls. They both begin with 'PART_'. This is the standard way in WPF to signify controls that need to be replaced if you decide to change the template of the control. If someone templates your control, you need some way to identify that the types used for your parts need to be ones that your control can use. You can do this by using the TemplatePart attribute and giving it the name and type of your control parts.
[TemplatePart(Name = "PART_FilterTextBox", Type = typeof(TextBox))]
[TemplatePart(Name = "PART_ClearFilterButton", Type = typeof(Button))]
public class FilterTextBox : Control
{
...
}
We only want the 'Clear Filter' button to be showing when there is text in the TextBox, so lets create a DataTrigger to accomplish that for us.
<ControlTemplate TargetType="{x:Type local:FilterTextBox}">
<Border>
...
</Border>
<ControlTemplate.Triggers>
<DataTrigger Binding="{Binding Path=Text.Length, ElementName=PART_FilterTextBox}" Value="0">
<Setter TargetName="PART_ClearFilterButton" Property="Visibility" Value="Collapsed" />
</DataTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
Now we want to be able to handle when a user clicks on the 'Clear Filter' button. You do this by overriding the OnApplyTemplate method. You can get a reference to your controls by calling GetTemplateChild and passing the name of the control. When the user clicks the 'Clear Filter' button we just want to remove any text in the TextBox. We'll use the same strategy to get a reference to the TextBox control.
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
Button clearFilterButton = base.GetTemplateChild("PART_ClearFilterButton") as Button;
if (clearFilterButton != null)
{
clearFilterButton.Click += new RoutedEventHandler(ClearFilterButton_Click);
}
}
private void ClearFilterButton_Click(Object sender, RoutedEventArgs e)
{
TextBox textBox = base.GetTemplateChild("PART_FilterTextBox") as TextBox;
if (textBox != null)
{
textBox.Text = String.Empty;
}
}
In the WPF Application project add a reference to the custom control project.
Now you can create a namespace for the controls project, labeled as controls here, and add your control to the form.
<Window
x:Class="FilterTextBoxDemo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:FilterTextBox;assembly=FilterTextBox"
Title="FilterTextBox Demo"
Height="300"
Width="300">
<StackPanel>
<controls:FilterTextBox />
</StackPanel>
</Window>
And we have a working control!
To make sure that the control still works when templated, lets go ahead and make a simple template for it.
<Window ...>
<Window.Resources>
<Style x:Key="LOCAL_MyStyle" TargetType="{x:Type controls:FilterTextBox}">
<Setter Property="BorderBrush" Value="CornFlowerBlue" />
<Setter Property="BorderThickness" Value="2" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type controls:FilterTextBox}">
<Border
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="10">
<DockPanel
LastChildFill="True"
Margin="5">
<Button
x:Name="PART_ClearFilterButton"
Content="--"
Width="30"
ToolTip="Clear Filter"
DockPanel.Dock="Left" />
<TextBox
x:Name="PART_FilterTextBox"
Text="{Binding Path=Text,
Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged,
RelativeSource={RelativeSource TemplatedParent}}"
BorderBrush="{x:Null}"
BorderThickness="0"
VerticalAlignment="Center" />
</DockPanel>
</Border>
<ControlTemplate.Triggers>
<DataTrigger Binding="{Binding Path=Text.Length, ElementName=PART_FilterTextBox}" Value="0">
<Setter TargetName="PART_ClearFilterButton" Property="Visibility" Value="Collapsed" />
</DataTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<StackPanel>
<controls:FilterTextBox
BorderBrush="#ACBFE4"
Margin="10"
TextChanged="FilterTextBox_TextChanged"/>
<controls:FilterTextBox
Style="{StaticResource LOCAL_MyStyle}"
Margin="10"
TextChanged="FilterTextBox_TextChanged"/>
</StackPanel>
</Window>
Hope that helps!
Joe