Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Customizing WPF Expander with ControlTemplate

0.00/5 (No votes)
8 Sep 2013 10  
Talks about how to customize the look and behaviour of WPF Expander by templating it.

ExpanderTemplateArticle/simpleexpander.gifExpanderTemplateArticle/stretchy.gifExpanderTemplateArticle/reveal.gif

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:

ExpanderTemplateArticle/Default_Expander_pic.png

Building the Template

ExpanderTemplateArticle/simpleexpander.gif

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>
        <!--Change the sign to minus when toggled-->
        <Trigger Property="IsChecked"
                 Value="True">
            <Setter Property="Data" 
               TargetName="Sign" Value="M 0,5 H 10 Z"/>
        </Trigger>

        <!-- MouseOver, Pressed behaviours-->
        <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 move the header parts (e.g., the ContentPresenter) out of the ToggleButton's template and put it in the Expander's template instead.

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.

<!-- Simple Expander Template-->
<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>
    <!-- Animate arrow when toggled-->
    <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

ExpanderTemplateArticle/stretchy.gif

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

ExpanderTemplateArticle/reveal.gif

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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here