<Style TargetType="local:Expander">
<Setter Property="Margin" Value="0"></Setter>
<Setter Property="BorderBrush" Value="SteelBlue"></Setter>
<Setter Property="BorderThickness" Value="1"></Setter>
<Setter Property="CornerRadius" Value="0"></Setter> <!-- Must be defined in the class -->
<Setter Property="Background">
<Setter.Value>
<LinearGradientBrush StartPoint="0.547587,0.322135" EndPoint="0.547587,0.992095">
<GradientStop Color="#FFFFFFFF" Offset="0"/>
<GradientStop Color="#FFDCEAF9" Offset="1"/>
</LinearGradientBrush>
</Setter.Value>
</Setter>
<Setter Property="Template">
Because the CornerRadius property isn't a default property of the ContentsControl we need to add it to the Expander class. Open the Expander.cs class and add a region called Dependency Properties in which you write the following dependency property. A dependency property is a way to expose properties of a control that wouldn't be available otherwise. It can be properties of a child control for instance. A dependency property always consists of two parts; the first part is a static read only dependency property that holds the value, the second part is the actual property that sets the value. The dependency property must be read only because it can only be set once, and it must be static to make it available without first creating an instance of the control.
#region Dependency Properties
public static readonly DependencyProperty CornerRadiusProperty =
DependencyProperty.Register("CornerRadius", typeof(CornerRadius), typeof(Expander), null);
public CornerRadius CornerRadius
{
get { return (CornerRadius)GetValue(CornerRadiusProperty); }
set { SetValue(CornerRadiusProperty, value); }
}
#endregion
Border, Background and radius of the control
Let's use the properties we defined earlier. Open the generic.xaml document and add a Border node inside the grid node. We will use TemplateBinding to connect the properties of the border with the dependency properties we defined earlier. This type of binding can only be used within a ControlTemplate definition in xaml.
Let's also define the grid that will hold the contents of the control. We will need two rows and two columns. In the first row we will need one column to present the header contents and one column to hold the toggle button. In the second row we will span the two columns making it into one column for the detail contents, the contents that will be expanded and collapsed. Height and Width properties can be set to constant values, Auto or star (*). Auto distributes the space evenly based on the size of the content that is within a row or a column. Star (*) means that the height or width of that column or row will receive a weighted proportion of the remaining available space.
<Border
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}"
Background="{TemplateBinding Background}">
<Grid x:Name="LayoutRoot">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"></ColumnDefinition>
<ColumnDefinition Width="50"></ColumnDefinition>
</Grid.ColumnDefinitions>
</Grid>
</Border>
A first look at the control
We have written a bunch of code that hasn't yielded any visible output. I bet you're ready to see some results; so with no further ado let's have a sneak preview of the control. To test the control open the Page.xaml document and add the following xaml code inside the Grid node. The StackPanel is a control that can accommodate several other controls and display them horizontally or vertically. You can run the application by pressing F5 on the keyboard.
<Grid x:Name="LayoutRoot" Background="White">
<StackPanel VerticalAlignment="Top">
<exp:Expander Margin="2" Height="20"></exp:Expander>
</StackPanel>
</Grid>
The output should look something like this.

Header Content
Now that the basic outline of the control is finished we will continue by adding a header content area. This area can contain any type of information; we will however use it to display text. There are two steps to accomplish this task. First we need to add a new dependency property that holds the header content. Secondly we need to define the header content as a ContentPersenter in the template. Let's start with the dependency property. Open the Expander.cs class and add the following dependency property to the Dependency Properties region. The first parameter of the Register method is the name of the dependency property, the second parameter is the type of object that the property can hold, the third parameter is the control type, and the fourth parameter can contain additional information.
public static readonly DependencyProperty HeaderContentProperty =
DependencyProperty.Register("HeaderContent", typeof(object),
typeof(Expander), null);
public object HeaderContent
{
get { return (object)GetValue(HeaderContentProperty); }
set { SetValue(HeaderContentProperty, value); }
}
Once the property has been added to the Expander.cs class we can add it to the innermost grid node in the generic.xaml document, below the grid row and column definitions. Note that we use TemplateBinding to display the contents of the dependency property.
<ContentPresenter x:Name="HeaderContent" Content="{TemplateBinding
HeaderContent}" Margin="5"></ContentPresenter>
If you switch to the Page.xaml document and replace the Height property with the HeaderContent property and assign it the text "Header content" and compile the solution you should see the following result.
<exp:Expander Margin="2" HeaderContent="Header content"></exp:Expander>
Collapsible content area
Let's continue by adding the collapsible contents area, this is the part of the control that can be hidden or visible. In the finished control the user will be able to collapse or expand this region by clicking on a button.
We need to add a TemplatePart that defines the Content attribute. This attribute exists by default; we only need to make it visible through the Expander control. We will define this attribute as FrameworkElement; this means that it can store any type of framework element, such as TextBlock, TextBox, List controls, and many more. To add an attribute to the class simply add the attribute immediately before the class definition in the Expander.cs class.
[TemplatePart(Name = "Content", Type = typeof(FrameworkElement))]
public class Expander : ContentControl
Switch to the generic.xaml document. Add a ContentPresenter node below the ContentPresenter already present in the template. Set the Grid.Row property to 1 and the Grid.ColumnSpan to 2; this will ensure that the content will be displayed in the second row and that the content and span both columns. Name it Content using the x:Name attribute and use TemplateBindning to get the value stored in the TemplatePart with the same name. Set the margin to 5 to get some distance to the Header contents.
<ContentPresenter Grid.Row="1" Grid.ColumnSpan="2" x:Name="Content" Content="{TemplateBinding Content}" Margin="5"></ContentPresenter>
Switch to the Page.xaml document and add the Content attribute giving it a value of "Content area". The output should look something like this.
<exp:Expander Margin="2" HeaderContent="Header content" Content="Content area"></exp:Expander>
Creating the expand/collapse button
Let's start by adding a button to the second column of the first row. Later we will alter the buttons' appearance making it more appealing, by changing its template. We will also expose the button through an attribute that we will call ExpandCollapseButton; this will make it available through the Expand class and make it possible to alter the button appearance through a new template. Once the button is visually ready we will continue by creating the methods and events needed to control the button and its animation.
Switch to the Expander.cs class and add a new attribute named ExpandCollapseButton of type ToggleButton.
[TemplatePart(Name = "ExpandCollapseButton", Type = typeof(ToggleButton))]
[TemplatePart(Name = "Content", Type = typeof(FrameworkElement))]
public class Expander : ContentControl
Now switch to the generic.xaml document and add a ToggleButton node named ExpandCollapseButton to the template; add it between the two ContentPresenter nodes.
<ContentPresenter x:Name="HeaderContent" Content="{TemplateBinding
HeaderContent}" Margin="5"></ContentPresenter>
<ToggleButton Grid.Column="1" Grid.Row="0" x:Name="ExpandCollapseButton"
Margin="3">
</ToggleButton>
<ContentPresenter Grid.Row="1" Grid.ColumnSpan="2" x:Name="Content"
Content="{TemplateBinding Content}" Margin="5"></ContentPresenter>
The control should look something like this. Note the button in the right corner.
Let's continue by making the button a little more appealing to the eye. Let's make it round. To achieve this we need to alter the button template in the generic.xaml document; we do this by adding a ToggleButton.Template node inside the ToggleButton node. Inside the template node we need to override the existing control template, we do this by adding a ControlTemplate node inside the ToggleButton.Template node. Inside the ControlTemplate node we create an Elipse node inside a Grid node.
<ToggleButton Grid.Column="1" Grid.Row="0" x:Name="ExpandCollapseButton"
Margin="3">
<ToggleButton.Template>
<ControlTemplate>
<Grid>
<Ellipse Stroke="#FFA9A9A9" Fill="AliceBlue" Width="19"
Height="19"></Ellipse>
</Grid>
</ControlTemplate>
</ToggleButton.Template>
</ToggleButton>
If you switch to the Page.xaml page the updated control should look something like this. Note that the button in the right corner is round.
The next step is to create an arrow on the button. This arrow will later be made to rotate 180° and expand or collapse the content region when the button is clicked. Switch to the generic.xaml document and add the following node to the template below the Ellipse node. The Path member is used to draw a series of connected lines and curves. The Data dependency property sets a Geometry object that specifies the shape to be drawn. Stroke sets the Brush that is used to paint the outline of the shape. StrokeThickness sets the outline width. HorizontalAlignment and VerticalAlignment determine the alignment of the control when used in a parent element.
<Path Data="M1,1.5L4.5,5 8,1.5" Stroke="#FF666666" StrokeThickness="2" HorizontalAlignment="Center" VerticalAlignment="Center"></Path>
If you switch to the Page.xaml document and update the control it should now have a round button with a downward pointing arrow illustrating that the content area is visible.
Animating the button
Let's make the button animated to really illustrate that something is happening when the button is clicked. Switch to the generic.xaml document. To pull this off we need to add a ToggleButton.RenderTransform node to the ToggleButton node; place it below the ToggleButton.Template node. We also need to add the RenderTransformOrigin to the ToggleButton node. The RenderTransformOrigin is a Point object that defines the center point declared by the RenderTransform object, relative to the bounds of the element. The value is set between 0 and 1. A value of (0.5, 0.5) will cause the transform to be centered on the element. If you leave out tis attribute the transform will behave strangely.
<ToggleButton Grid.Column="1" Grid.Row="0" x:Name="ExpandCollapseButton"
Margin="3" RenderTransformOrigin="0.5,0.5">
...
<ToggleButton.RenderTransform>
<RotateTransform x:Name="RotateButtonTransform"></RotateTransform>
</ToggleButton.RenderTransform>
</ToggleButton>
Now switch to Expander.cs class. Add a region named Fields and add a private ToggleButton container named btnExpandOrCollapse. This container will hold a reference to the button in the control. Add a field named contentElement of type FrameworkElement that will hold the contents of the Content area. The state is stored in a field of VisualState type named collapsedState.
#region Fields
private ToggleButton btnExpandOrCollapse;
private FrameworkElement contentElement;
private VisualState collapsedState;
#endregion
Now we have to write a method called ChangeVisualState that will change the visual state of the button. This method is dependent on two view states that must be declared with attributes on the class. The two states are named Collapsed and Expanded. Start by define the two states.
[TemplateVisualState(Name = "Expanded", GroupName = "ViewStates")]
[TemplateVisualState(Name = "Collapsed", GroupName = "ViewStates")]
public class Expander : ContentControl
Next add a dependency property to the Dependency Properties region called IsExpandedProperty that will hold the value stating if the control is in its expanded or collapsed state.
public static readonly DependencyProperty IsExpandedProperty =
DependencyProperty.Register("IsExpanded", typeof(bool), typeof(Expander), new PropertyMetadata(true));
public bool IsExpanded
{
get { return (bool)GetValue(IsExpandedProperty); }
set
{
SetValue(IsExpandedProperty, value);
if (btnExpandOrCollapse != null) btnExpandOrCollapse.IsChecked = IsExpanded;
}
}
Next create a region called Methods and write the ChangeVisualState method in that region. The VisualStateManagers' GoToState method is used to alter the state of a control. The useTransitions parameter tells the visual state manager if transitions such as a time interval will be used.
#region Methods
private void ChangeVisualState(bool useTransitions)
{
// Apply the current state from the ViewStates group.
if (IsExpanded)
{
VisualStateManager.GoToState(this, "Expanded", useTransitions);
}
else
{
VisualStateManager.GoToState(this, "Collapsed", useTransitions);
}
}
#endregion
Next add a public method called ExpandOrCollapse that can be called to alter the state of the control.
public void ExpandOrCollapse(bool useTransitions)
{
IsExpanded = !IsExpanded;
ChangeVisualState(useTransitions);
}
Create a region named Event Methods and add an event method called btnExpandOrCollapse_Click in the event method, call the ExpandCollapse method with its parameter value set to true.
#region Event Methods
private void btnExpandOrCollapse_Click(object sender, RoutedEventArgs e)
{
ExpandOrCollapse(true);
}
#endregion
Now override the OnApplyTemplate method and get the template definition for the ExpandCollapseButton and create an event handler for the buttons' Click event.
#region Overridden Methods
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
btnExpandOrCollapse = GetTemplateChild("ExpandCollapseButton") as ToggleButton;
if (btnExpandOrCollapse != null)
{
btnExpandOrCollapse.Click += btnExpandOrCollapse_Click;
}
ChangeVisualState(false);
}
#endregion
Now we need to add a VisualStateGroup, for the two states, in the generic.xaml document. These two states will be used from the Expander class through the Expanded and Collapsed attributes defined on the class.
Add a VisualStateManager.VisualStateGroups node directly under the outermost Grid node. Create a VisualStateGroup node inside the VisualStateManager.VisualStateGroups node and add two VisualState nodes, one called Expanded and one called Collapsed. Create a Storyboard node inside each of the two VisualState nodes. Create a DoubleAnimation node inside each of the two StoryBoard nodes and give them the same StoryBoard.TargetName, RoteteButtonTransform.
<ControlTemplate TargetType="local:Expander">
<Grid>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="ViewStates">
<VisualState x:Name="Expanded">
<Storyboard>
<DoubleAnimation
Storyboard.TargetName="RotateButtonTransform"
Storyboard.TargetProperty="Angle" Duration="0"
To="180"></DoubleAnimation>
</Storyboard>
</VisualState>
<VisualState x:Name="Collapsed">
<Storyboard>
<DoubleAnimation
Storyboard.TargetName="RotateButtonTransform"
Storyboard.TargetProperty="Angle" Duration="0"
To="0"></DoubleAnimation>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
Now, for the final touch, we will set the IsExpanded property value to false in the constructor.
public Expander()
{
DefaultStyleKey = typeof(Expander);
IsExpanded = false;
}
Expanding and collapsing the content area
This is the final step towards finishing the control. The only thing left for us to do is to make the content area toggle between expanded and collapsed.
Let's start by adding some code to the ChangeVisualState method. We need to check if the IsExpanded property is true and if the contentElement not is null, if that is the case then we set the Visibility property of the contnentElement equal to Visibility.Visible. If on the other hand the IsExpanded property is false, the collapsedState is null and the contentElement is not null then we set the Visibility property of the contnentElement equal to Visibility.Collapsed.
private void ChangeVisualState(bool useTransitions)
{
// Apply the current state from the ViewStates group.
if (IsExpanded)
{
if (contentElement != null) contentElement.Visibility =
Visibility.Visible;
VisualStateManager.GoToState(this, "Expanded", useTransitions);
}
else
{
VisualStateManager.GoToState(this, "Collapsed", useTransitions);
if (collapsedState == null)
{
// There is no state animation, so just hide the content
//region immediately.
if (contentElement != null) contentElement.Visibility =
Visibility.Collapsed;
}
}
}
Next we add an event method called collapsedStoryboard_Completed to the Event Methods region. This method will be called when a storyboard finishes its work and in it we will set the Visibility property of the contentElement to Visibility.Collapsed.
private void collapsedStoryboard_Completed(object sender, EventArgs e)
{
contentElement.Visibility = Visibility.Collapsed;
}
Next we add some code to the overridden OnApplyTemplate method.Firstly we need to get the contetns of the content area. Next we check it the contentElement is open, and if so we collapse that control and attach an event handler to the states' storyboard.
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
contentElement = GetTemplateChild("Content") as FrameworkElement;
if (contentElement != null)
{
collapsedState = GetTemplateChild("Collapsed") as VisualState;
if ((collapsedState != null) && (collapsedState.Storyboard != null))
{
collapsedState.Storyboard.Completed +=
collapsedStoryboard_Completed;
}
}
Now start the application by pressing F5 on the keyboard. The control should now be expanding and collapsing its contents area when the button is clicked.