


Introduction
This article will demonstrate how to create a ControlTemplate for the WPF Expander control to customize its appearance and behavior. First, a simplified version
of the default template is explained in detail. Then, a couple more complex versions will be built on top of the default template with customizations such as adding animation
and changing the look and feel. The code shown in this article was designed to be both .NET 3.5 and .NET 4 compatible.
Background
It is expected that the reader has basic understanding of basic WPF concepts such as Binding and Triggers. A basic knowledge of Animations will be very helpful.
The reader should also be familiar (or willing to lookup) commonly used controls such as Grid, DockPanel, ToggleButton, etc..
Visual "Parts" of Expander
Typically, an Expander control is visually composed of three parts. I'm going to call those parts as "icon",
"header", and "content" for easier reference. For example, "header" means the part in the diagram, while "Header"
refers to some property in the XAML. Please excuse my MS Paint skills:

Building the Template

The ControlTemplate that will be described here uses a templated ToggleButton
for the header/icon parts, and a ContentPresenter for the actual
Expander content. All of these parts are then laid out using a DockPanel,
but it really could be any layout control of your choice.
Expander ToggleButton Template
The first step is to create a ControlTemplate for the ToggleButton (Expander's button); this will later be used within
the Expander's template. Inside the ToggleButton's template, we will use WPF Shapes to draw our icon and another ContentPresenter
to be the header, which are then laid out using a Grid. Some Triggers are set for IsMouseOver/IsPressed to change the icon's look,
and also for IsChecked to change the icon when the button is toggled. The code is shown below. I will also talk about a few key points after that.
<ControlTemplate x:Key="SimpleExpanderButtonTemp"
TargetType="{x:Type ToggleButton}">
<Border x:Name="ExpanderButtonBorder"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Padding="{TemplateBinding Padding}"
>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Rectangle Fill="Transparent"
Grid.ColumnSpan="2"/>
<Ellipse Name="Circle"
Grid.Column="0"
Stroke="DarkGray"
Width="20"
Height="20"
HorizontalAlignment="Center"
VerticalAlignment="Center"
/>
<Path x:Name="Sign"
Grid.Column="0"
Data="M 0,5 H 10 M 5,0 V 10 Z"
Stroke="#FF666666"
Width="10"
Height="10"
StrokeThickness="2"
HorizontalAlignment="Center"
VerticalAlignment="Center"
RenderTransformOrigin="0.5,0.5"
>
<Path.RenderTransform>
<RotateTransform Angle="0"/>
</Path.RenderTransform>
</Path>
<ContentPresenter x:Name="HeaderContent"
Grid.Column="1"
Margin="4,0,0,0"
ContentSource="Content"/>
</Grid>
</Border>
<ControlTemplate.Triggers>
-->
<Trigger Property="IsChecked"
Value="True">
<Setter Property="Data"
TargetName="Sign" Value="M 0,5 H 10 Z"/>
</Trigger>
-->
<Trigger Property="IsMouseOver"
Value="true">
<Setter Property="Stroke"
Value="#FF3C7FB1"
TargetName="Circle"/>
<Setter Property="Stroke"
Value="#222"
TargetName="Sign"/>
</Trigger>
<Trigger Property="IsPressed"
Value="true">
<Setter Property="Stroke"
Value="#FF526C7B"
TargetName="Circle"/>
<Setter Property="StrokeThickness"
Value="1.5"
TargetName="Circle"/>
<Setter Property="Stroke"
Value="#FF003366"
TargetName="Sign"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
Binding to Templated Parent
We can "expose" properties of the component controls inside a template by binding them to the templated parent (ToggleButton).
For example, we can bind the Background property of the Border inside the template to the background of the templated parent (the TargetType ToggleButton)
like in the above code. In this way, when we are using this template, if we set the ToggleButton's Background property to some colour,
the Border's background will be set to that colour as well.
There are actually two standard ways to bind to the templated parent:
- Using
TemplateBinding
Example, inside ToggleButton's template:
<Border ... Background="{TemplateBinding Background}"/>
TemplateBinding is the more optimized version of standard binding as it is evaluated at compile time, but it does have some limitations.
For example, it only supports the OneWay binding mode and does not allow you to set attributes such as Converters or StringFormat that standard Binding has.
Using Binding with RelativeSource set to TemplatedParent
Example, inside Expander's template:
<ToggleButton ... IsChecked="{Binding Path=IsExpanded, RelativeSource={RelativeSource TemplatedParent}}" />
This method is more flexible but slower than TemplateBinding as it provides all the features that standard Binding has.
Clickable Area
Whatever we place inside the ToggleButton will be clickable for toggling the visibility of the Expander's content. If you do not want the user to toggle
the Expander by clicking header components, then you can leave the header parts (e.g., the ContentPresenter) out of the ToggleButton's template.
Furthermore, for any Shape whose Fill property is null or unset will only be clickable on the "outline" of the
Shape. This is by design. To work around this, one way is to wrap a Border with a set Background around all of the components of the template,
but you must make sure that the Background property does not remain unset or null at runtime (even if it is set using Binding). Another way is to put a
Rectangle with Fill set to Transparent on top of any Shape you are using
which was done above. The key idea is to avoid unset or null values for Background/Fill properties.
Expander Template
Using the ToggleButton template created above, we can put the ToggleButton on top of a ContentPresenter. We can then bind the button's
IsChecked to the Expander's IsExpanded and set a Trigger that will make the ContentPresenter collaspe when IsExpanded
is false. Note that this does not handle ExpandDirection and only includes a few basic TemplateBindings. For ExpandDirection,
you will need need to set Triggers to change the layout of the entire template according to ExpandDirection's value. It is best that you download "Default WPF Themes"
on Control Styles and Templates and look at the default Expander template in one of the theme XAMLs.
<!---->
<ControlTemplate x:Key="SimpleExpanderTemp" TargetType="{x:Type Expander}">
<DockPanel>
<ToggleButton x:Name="ExpanderButton"
DockPanel.Dock="Top"
Template="{StaticResource SimpleExpanderButtonTemp}"
Content="{TemplateBinding Header}"
IsChecked="{Binding Path=IsExpanded,
RelativeSource={RelativeSource TemplatedParent}}"
OverridesDefaultStyle="True"
Padding="1.5,0">
</ToggleButton>
<ContentPresenter x:Name="ExpanderContent"
Visibility="Collapsed"
DockPanel.Dock="Bottom"/>
</DockPanel>
<ControlTemplate.Triggers>
<Trigger Property="IsExpanded" Value="True">
<Setter TargetName="ExpanderContent"
Property="Visibility" Value="Visible"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
Customizations
Customizing the Expander is just like creating a regular WPF GUI except you use more basic controls to build a more complex one. There are many things you can do to customize
the template, I'm only going to list a couple of common examples here.
Using the Tag Property
The Tag property is desgined to store custom information;
its type is Object so you can mostly use it for anything you want. All FrameworkElements and descendants inherit this property. For example, suppose you wanted separate
background colours for the header and content parts of the Expander. In this case, we can use Tag to store the Background value for the header and
do some binding to expose it.
In the Expander's ToggleButton template, you must have Background exposed through binding like this:
<ControlTemplate x:Key="SimpleExpanderButtonTemp" TargetType="{x:Type ToggleButton}">
<Border x:Name="ExpanderButtonBorder"
Background="{TemplateBinding Background}"
...
Add Background TemplateBinding to Expander template's ToggleButton like this:
<ControlTemplate x:Key="SimpleExpanderTemp" TargetType="{x:Type Expander}">
...
<ToggleButton x:Name="ExpanderButton"
Template="{StaticResource SimpleExpanderButtonTemp}"
...
Background="{TemplateBinding Tag}"
</ToggleButton>
...
Then we can set Tag to a colour that we want for the ToggleButton on the Expander when using it. Please note that since we are using
Tag to set the Background property indirectly, we cannot use strings like "Red" or "Green"
to set Tag=>Background as our string value is not automatically converted. Thus, we need to pass a Brush object instead, which is the type for
the Background property.
<Expander ...>
<Expander.Tag>
<SolidColorBrush Color="Red" />
</Expander.Tag>
What if you want more than one custom property or do something more extensive? Then the best way is to create a new class that inherits from Expander
and declare the extra properties in that class. However, creating new custom/composite controls is not within the scope of this article.
Make Header the Same Width as the Content
If you want the content to be the same width as the header, then you can swap the DockPanel for a Grid and lay out the ToggleButton and
ContentPresenter in two rows. Very simple:
<ControlTemplate x:Key="SimpleExpanderTemp" TargetType="{x:Type Expander}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<ToggleButton x:Name="ExpanderButton"
...
</ToggleButton>
<ContentPresenter x:Name="ExpanderContent"
Grid.Row="1"
.../>
</Grid>
...
Expand/Collapse Animation
Rotating Arrow
To animate rotation of the arrow, we can use RenderTransform.RotateTransform
to rotate the element over time. Since this is using RenderTransform, it will only affect how the element is drawn and not disturb the layout.
<ControlTemplate.Triggers>
-->
<Trigger Property="IsChecked"
Value="True">
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetName="Arrow"
Storyboard.TargetProperty=
"(Path.RenderTransform).(RotateTransform.Angle)"
To="180"
Duration="0:0:0.4"/>
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
<Trigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetName="Arrow"
Storyboard.TargetProperty=
"(Path.RenderTransform).(RotateTransform.Angle)"
To="0"
Duration="0:0:0.4"/>
</Storyboard>
</BeginStoryboard>
</Trigger.ExitActions>
</Trigger>
</ControlTemplate.Triggers>
Stretch Out Content

To get the content to "stretch" out when expanded, we can use
LayoutTransform.ScaleTransform
and animate its ScaleY property. In this case, we use LayoutTransform instead of RenderTransform since we need the height (layout) of the control
to change not just how the control is drawn (rendered). You can see the difference by replacing LayoutTransform with RenderTransform and experimenting
with that. Animations are done through Triggers. In the code below, note the syntax for referencing the ScaleY property and declaring LayoutTransform under
ContentPresenter:
<ControlTemplate x:Key="StretchyExpanderTemp" TargetType="{x:Type Expander}">
<DockPanel>
<ToggleButton .../>
<ContentPresenter ...>
<ContentPresenter.LayoutTransform>
<ScaleTransform ScaleY="0"/>
</ContentPresenter.LayoutTransform>
</ContentPresenter>
</DockPanel>
<ControlTemplate.Triggers>
<Trigger Property="IsExpanded" Value="True">
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetName="ExpanderContent"
Storyboard.TargetProperty=
"(ContentPresenter.LayoutTransform).(ScaleTransform.ScaleY)"
To="1"
Duration="0:0:0.4"/>
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
<Trigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetName="ExpanderContent"
Storyboard.TargetProperty=
"(ContentPresenter.LayoutTransform).(ScaleTransform.ScaleY)"
To="0"
Duration="0:0:0.4"/>
</Storyboard>
</BeginStoryboard>
</Trigger.ExitActions>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
...
"Reveal" Content

To get the content to "reveal" (not sure what to call it) is slightly more complicated than the above animations. At first, I thought to wrap
the ContentPresenter inside a ScrollViewer and animate the ScrollViewer's height from 0 to the content's ActualHeight. However, according
to MSDN, data binding in ControlTemplate Storyboard properties is not supported, so you can't do something
like To="{Binding ...,Path=ActualHeight}". Thus, a workaround was used instead (below), and it is credited
to Justin in this thread.
Referring to the code below, the basic idea is to bind the ScrollViewer's Height to (Tag * ActualHeight of the content) which was done using
MultiBinding and a Converter. In this way, when expanding/collapsing,
we can animate the Tag property between 0 and 1 which will in effect scale the Height of the ScrollViewer. In the initial collapsed state,
Tag is set to 0 so that the ScrollViewer's Height is set to 0. In expanded state,
Tag will be set to 1 and ScrollViewer's Height will be set to the ActualHeight of the content.
Namespace declarations required for the XAML:
xmlns:local ="clr-namespace:SampleExpander"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
Below is the XAML for the "reveal" animation. Note the first line to declare the converter that was used.
<local:MultiplyConverter x:Key="multiplyConverter" />
<ControlTemplate x:Key="RevealExpanderTemp" TargetType="{x:Type Expander}">
<DockPanel>
<ToggleButton x:Name="ExpanderButton" ... />
<ScrollViewer x:Name="ExpanderContentScrollView" DockPanel.Dock="Bottom"
HorizontalScrollBarVisibility="Hidden"
VerticalScrollBarVisibility="Hidden"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Bottom"
>
<ScrollViewer.Tag>
<sys:Double>0.0</sys:Double>
</ScrollViewer.Tag>
<ScrollViewer.Height>
<MultiBinding Converter="{StaticResource multiplyConverter}">
<Binding Path="ActualHeight" ElementName="ExpanderContent"/>
<Binding Path="Tag" RelativeSource="{RelativeSource Self}" />
</MultiBinding>
</ScrollViewer.Height>
<ContentPresenter x:Name="ExpanderContent" ContentSource="Content"/>
</ScrollViewer>
</DockPanel>
<ControlTemplate.Triggers>
<Trigger Property="IsExpanded" Value="True">
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetName="ExpanderContentScrollView"
Storyboard.TargetProperty="Tag"
To="1"
Duration="0:0:0.4"/>
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
<Trigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetName="ExpanderContentScrollView"
Storyboard.TargetProperty="Tag"
To="0"
Duration="0:0:0.4"/>
</Storyboard>
</BeginStoryboard>
</Trigger.ExitActions>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
Below is the "MultiplyConverter" that was used in the XAML from the code-behind. It must be in the same namespace as the code-behind.
It simply multiplies all the values that was fed to it in MultiBinding. In this case its Tag*(ExpanderContent's ActualHeight).
The result is then returned and fed into ExpanderContentScrollView's Height.
public class MultiplyConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType,
object parameter, CultureInfo culture)
{
double result = 1.0;
for (int i = 0; i < values.Length; i++)
{
if (values[i] is double)
result *= (double)values[i];
}
return result;
}
public object[] ConvertBack(object value, Type[] targetTypes,
object parameter, CultureInfo culture)
{
throw new Exception("Not implemented");
}
}
Performance Limitation
Don't put too many controls inside the animated expanders. Layout/Render transform animations may not look smooth when there are too many elements to process at once.
Using the Code
Sample code is provided at the top of this article. I've declared all the templates as StaticResources. When copy-pasting the templates, be sure to include
all the required components. For example, for the animated Expander templates, you will need to copy both the ToggleButton template
and the Expander template itself.
Thanks for Reading!
Please rate it! This is my first article on CodeProject, so let me know if there are any questions or concerns in the comments below. I hope that this article was useful for you.