Click here to Skip to main content
15,867,991 members
Articles / Desktop Programming / WPF

Animating Expander Control

Rate me:
Please Sign up or sign in to vote.
4.59/5 (17 votes)
1 Apr 2009CPOL5 min read 73.2K   3.1K   23   14
A custom content control that can be expanded/collapsed with animation.

Sample Image

Introduction

While developing our software in Whitebox Security, we came upon the requirement to build a control which can be expanded and collapsed, while holding different content.

The requirements from the control were:

  • Take minimal screensize when minimized.
  • When maximized, the control can be resized by the user to a custom size.
  • If the user collapses and expands the control, it would expand to the last expanded length (height or width), as set by the user.
  • When the size of the control changes, it would resize other controls on screen.
  • Expand and collapse operations would be animated.
  • The control can be simply configured to expand to different directions.

Requirements

The project was written in Visual Studio 2008, and built on .NET 3.5 SP1.

Prior knowledge

To use the control, you should have basic WPF knowledge.

If you want to understand how the control is written, you should be familiar with:

The demo application 

The demo application is composed of a Window split into four with a Grid. On each quarter of the screen, an AnimatingExpanderControl is placed holding some content.

Each of the four controls demonstrates a slightly different configuration. Some of the things that differ are:

  • The expand direction.
  • Starting as collapsed or expanded.
  • Using animation for expanding and collapsing.
  • Limiting the maximum width/height of the content.

Using the control

Here is the XAML of one of the four controls:

XML
<!--Expand Left-->
<Border
    BorderBrush="Black" 
    BorderThickness="2" 
    Grid.Row="1"
    Grid.Column="1"
    Padding="3">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="Auto"/>
        </Grid.ColumnDefinitions>
        <Viewbox
        Stretch="Uniform"
        Grid.Column="0">
            <Image
            Source="..\Images\cat_rope.gif">
            </Image>
        </Viewbox>
        <local:AnimatingExpanderControl 
        ExpandDirection="Left"
        Style="{StaticResource AnimatingExpanderControlStyle}"
        Grid.Column="1"
        Title="Expand Content Left"
        >
            <Viewbox
            Stretch="Uniform">
                <StackPanel
                Orientation="Vertical">
                    <Button>
                        Content
                    </Button>
                    <TextBlock>
                    Some more content
                </TextBlock>
                    <StackPanel
                    Orientation="Vertical">
                        <RadioButton Content="First Choice"/>
                        <RadioButton Content="Second Choice"/>
                        <RadioButton Content="Third Choice"/>
                    </StackPanel>
                </StackPanel>
            </Viewbox>
        </local:AnimatingExpanderControl>
    </Grid>
</Border>

As you can see, the control is defined with a ViewBox as its content. This is so the content would be resized according to the space it is given. The control’s “neighboring” content is also placed in a ViewBox for the same reason.

The custom Dependency Properties that can be set on the control are:

  • ExpandDirection - Determines the direction to expand to. Values can be Up, Down, Left, and Right.
  • IsCollapsed - Flag which shows if the control is collapsed or not when the control is initialized.
  • InitialExpandedLength - This can be set by the user to control the initial value (width or height) that the expander will expand to.
  • AnimationEnabled - Flag which enables or disables the expand and collapse animation on the control.
  • Title - This property sets the title of the control which is displayed next to the expand/collapse button.

One of the ways to achieve dynamically resizing the different components on the screen, as demonstrated in the demo, is to place the contents on a Grid with the AnimatingExpanderControl in a Column (or Row) with Width set to “Auto” and the rest of the screen’s content in a Column with Width “*”. In addition, ViewBoxes are used to hold the content.

To limit the maximum size the control can be expanded to, set the MaxWidth (or MaxHeight) on the Content of the control. This can be seen in the demo on the control that expands right.

The structure of the control

XML
<Border BorderBrush="{TemplateBinding BorderBrush}"
    BorderThickness="{TemplateBinding BorderThickness}"
    Background="{TemplateBinding Background}"
    CornerRadius="3"
    SnapsToDevicePixels="true">
    <DockPanel 
        Name="TemplateDockPanel"
        DataContext="{Binding RelativeSource={RelativeSource TemplatedParent}}">
        <Thumb 
           DockPanel.Dock="Top"
           Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}"
           BorderBrush="White"
           x:Name="PART_Thumb"
           Cursor="SizeNS"
           DragDelta="GridSplitter_DragDelta" 
           HorizontalAlignment="Stretch"
           VerticalAlignment="Bottom"
           Margin="5,0,5,5" 
           IsEnabled="{Binding ElementName=PART_CheckBox, Path=IsChecked, Mode=OneWay}"
           Style="{StaticResource GridSplitterStyle}"/>
        <ContentPresenter 
            DockPanel.Dock="Bottom"
            Margin="{TemplateBinding Padding}"
            x:Name="PART_Content"/>
        <CheckBox 
            x:Name="PART_CheckBox" 
            Checked="BottomCB_Checked" 
            Unchecked="BottomCB_UnChecked"
            Style="{StaticResource CheckBox_Expander}" 
            Margin="1" 
            Content="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Title}"
            Foreground="{TemplateBinding Foreground}"
            Padding="3"
            FontFamily="{TemplateBinding FontFamily}"
            FontSize="{TemplateBinding FontSize}"
            FontStyle="{TemplateBinding FontStyle}"
            FontStretch="{TemplateBinding FontStretch}"
            FontWeight="{TemplateBinding FontWeight}">
        </CheckBox>
    </DockPanel>
</Border>

The control is made of three components, placed in a DockPanel. These components are set in the ControlTemplate of the control:

  1. A Thumb which simulates a GridSplitter. The GridSplitter allows for resizing the control by dragging it. The GridSplitter is disabled if the control is collapsed.
  2. The content of the control, presented in a ContentPresenter.
  3. The CheckBox which simulates a button with an arrow. When pressed, the control is expanded/collapsed, and the arrow changes direction. The control’s Title Dependency Property is presented next to the CheckBox.

Some basic styles are applied to the Thumb and the Checkbox, which I won’t go into.

In addition, some triggers are set in the ControlTemplate:

XML
<ControlTemplate.Triggers>
    <!--Trigger for up direction-->
    <Trigger 
        Property="ExpandDirection"
        Value="Up">
        <Setter Property="DockPanel.Dock"
            Value="Top"
            TargetName="PART_Thumb"/>
        <Setter Property="DockPanel.Dock"
            Value="Bottom"
            TargetName="PART_Content"/>
        <Setter Property="HorizontalAlignment"
                Value="Stretch"
                TargetName="PART_Thumb"/>
        <Setter Property="VerticalAlignment"
                Value="Bottom"
                TargetName="PART_Thumb"/>
        <Setter Property="Height"
                Value="5"
                TargetName="PART_Thumb"/>
        <Setter Property="HorizontalAlignment"
                Value="Center"
                TargetName="PART_CheckBox"/>
    </Trigger>
    <!--Trigger for down direction-->
    <Trigger 
        Property="ExpandDirection"
        Value="Down">
        <Setter Property="DockPanel.Dock"
            Value="Bottom"
            TargetName="PART_Thumb"/>
        <Setter Property="DockPanel.Dock"
            Value="Top"
            TargetName="PART_Content"/>
        <Setter Property="HorizontalAlignment"
                Value="Stretch"
                TargetName="PART_Thumb"/>
        <Setter Property="VerticalAlignment"
                Value="Bottom"
                TargetName="PART_Thumb"/>
        <Setter Property="Height"
                Value="5"
                TargetName="PART_Thumb"/>
        <Setter Property="HorizontalAlignment"
                Value="Center"
                TargetName="PART_CheckBox"/>
        <Setter Property="Style"
                TargetName="PART_CheckBox"
                Value="{StaticResource CheckBoxUpSideDown_Expander}"
                />
    </Trigger>
    <!--Trigger for left direction-->
    <Trigger 
        Property="ExpandDirection"
        Value="Left">
        <Setter Property="DockPanel.Dock"
            Value="Left"
            TargetName="PART_Thumb"/>
        <Setter Property="DockPanel.Dock"
            Value="Right"
            TargetName="PART_Content"/>
        <Setter Property="HorizontalAlignment"
                Value="Center"
                TargetName="PART_Thumb"/>
        <Setter Property="VerticalAlignment"
                Value="Stretch"
                TargetName="PART_Thumb"/>
        <Setter Property="Cursor"
                Value="SizeWE"
                TargetName="PART_Thumb"/>
        <Setter Property="Width"
                Value="5"
                TargetName="PART_Thumb"/>
        <Setter Property="LayoutTransform"
                TargetName="PART_CheckBox"
                >
            <Setter.Value>
                <RotateTransform Angle="-90"/>
            </Setter.Value>
            
        </Setter>
        <Setter Property="VerticalAlignment"
                Value="Center"
                TargetName="PART_CheckBox"/>
    </Trigger>

    <!--Trigger for right direction-->
    <Trigger 
        Property="ExpandDirection"
        Value="Right">
        <Setter Property="DockPanel.Dock"
            Value="Right"
            TargetName="PART_Thumb"/>
        <Setter Property="DockPanel.Dock"
            Value="Left"
            TargetName="PART_Content"/>
        <Setter Property="HorizontalAlignment"
                Value="Center"
                TargetName="PART_Thumb"/>
        <Setter Property="VerticalAlignment"
                Value="Stretch"
                TargetName="PART_Thumb"/>
        <Setter Property="Cursor"
                Value="SizeWE"
                TargetName="PART_Thumb"/>
        <Setter Property="Width"
                Value="5"
                TargetName="PART_Thumb"/>
        <Setter Property="LayoutTransform"
                TargetName="PART_CheckBox"
                >
            <Setter.Value>
                <RotateTransform Angle="90"/>
            </Setter.Value>
            
        </Setter>
        <Setter Property="VerticalAlignment"
                Value="Center"
                TargetName="PART_CheckBox"/>
    </Trigger>
    <Trigger Property="IsEnabled"
         Value="false">
        <Setter Property="Foreground"
            Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
    </Trigger>
</ControlTemplate.Triggers>

These triggers basically set different properties according to the ExpandDirection Dependency Property of the control.

The code-behind

Here is the code for handling the user clicking on the expand/collapse button (actually a CheckBox) while the control is collapsed:

C#
private void BottomCB_Checked(object sender, RoutedEventArgs e) {
    //determine the DP that needs to be changed
    DependencyProperty actualContentLengthDP;
    this.DetermineLengthDPToChange(out actualContentLengthDP);
    
    //change the determined DP with animation
    if (AnimationEnabled) {
        this.ExpandWithAnimation(actualContentLengthDP);
    }
    //change the determined DP without animation
    else {
        this.ActualContent.SetValue(actualContentLengthDP, this.ExpandedValue);
    }
    this.IsCollapsed = false;
}

If animation is disabled, the actual content’s height (or width) is set to the ExpandedValue. If this is the first time the control is expanded, the ExpandedValue is taken from the InitialExpandedLength Dependency Property. Otherwise, the value used is the last height (width) that the control was sized to, with the “GridSplitter”.

The code below handles the expansion, when animation is used:

C#
private void ExpandWithAnimation(
        DependencyProperty actualContentLengthDP
) {
    Storyboard sb = new Storyboard();
    DoubleAnimation expandAnimation = new DoubleAnimation();
    expandAnimation.From = 0;
    //Content is collapsed. Expand to last saved this.ExpandedValue.
    expandAnimation.To = this.ExpandedValue;
    expandAnimation.Duration = new TimeSpan(0, 0, 0, 0, _animationDuration);
    sb.Children.Add(expandAnimation);
    sb.FillBehavior = FillBehavior.Stop;
    this.BeginChangeSizeAnimation(
        sb, 
        expandAnimation,
        actualContentLengthDP
    );    
}

private void BeginChangeSizeAnimation(
    Storyboard sb, 
    DoubleAnimation lengthAnimation,
    DependencyProperty actualContentLengthDP
) {
    //execute Animation
    Storyboard.SetTargetProperty(lengthAnimation, 
             new PropertyPath(actualContentLengthDP.Name));
    if (null != this.ActualContent) {
        sb.Completed += delegate {
            this.ActualContent.SetValue(actualContentLengthDP, 
                                        lengthAnimation.To.Value);
        };
        sb.Begin(this.ActualContent);
    }
}

This time, the content’s height/width is set to the expanded value by using a simple DoubleAnimation. After the animation is completed, a delegate sets the value of the actual content’s width/height to the .To value.

Once the control is expanded, it can be resized by the user. This is done with a Thumb, which simulates a GridSplitter. To ensure the Thumb can’t be dragged when the control is collapsed, recall the following line from the Structure of the Control section:

XML
IsEnabled="{Binding ElementName=PART_CheckBox, Path=IsChecked, Mode=OneWay}"

The IsEnabled property of the Thumb has a binding to the IsChecked Dependency Property of the CheckBox. This means that only if CheckBox IsChecked, meaning the control is expanded, the Thumb IsEnabled.

This is the event handler for the thumb dragged event:

C#
private void GridSplitter_DragDelta(object sender, DragDeltaEventArgs e) {
    
    if (null != this.ActualContent) {
        //set this.ExpandedValue to new dragged value, then resize the content.
        //if new value is smaller then zero, or bigger the Content's Max Length,
        //reset length accordingly
        switch (this.ExpandDirection) {
            case ExpandDirection.Down:
                this.ExpandedValue = this.ActualContent.Height + e.VerticalChange;
                this.CheckExpandedValue(this.ActualContent.MaxHeight);
                this.ActualContent.Height = this.ExpandedValue; 
                break;
            case ExpandDirection.Up:
                this.ExpandedValue = this.ActualContent.Height - e.VerticalChange;
                this.CheckExpandedValue(this.ActualContent.MaxHeight);
                this.ActualContent.Height = this.ExpandedValue; 
                break;
            case ExpandDirection.Left:
                this.ExpandedValue = this.ActualContent.Width - e.HorizontalChange;
                this.CheckExpandedValue(this.ActualContent.MaxWidth);
                this.ActualContent.Width = this.ExpandedValue;
                break;
            case ExpandDirection.Right:
                this.ExpandedValue = this.ActualContent.Width + e.HorizontalChange;
                this.CheckExpandedValue(this.ActualContent.MaxWidth);
                this.ActualContent.Width = this.ExpandedValue;
                break;
        }
    }
}

The method first determines the vertical or horizontal change, and sets the ExpandedValue accordingly. It then verifies the new ExpandedValue is legal. Next, it sets the Height or Width property of the control’s content to the new ExpandedValue. The next time the control is collapsed and then expanded, it will expand to the saved ExpandedValue.

Here is the code for checking that the ExpandedValue is legal, and resetting it if it isn’t:

C#
private void CheckExpandedValue(double MaxLength) {
    if (this.ExpandedValue < 0) {
        this.ExpandedValue = 0;
    }
    if (this.ExpandedValue > MaxLength) {
        this.ExpandedValue = MaxLength;
    }
}

What do you think?

This is my first article for CodeProject. Please let me know if the article helped you, and what you think about it.

Credits

When writing the triggers for the ExpandDirection, I was inspired by some of Microsoft’s original WPF control templates.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Other SandboxModel (www.sandboxmodel.com) - project mana
Israel Israel
SandboxModel is the developer of the Project Team Builder™ (PTB™). The PTB™ is a project management tool, designed to improve project results.

The PTB™ for Training solution is used for improving the project management skills of students, project managers and project teams. Trainees get a chance to simulate the management of real projects, in a safe controlled environment. Learning by doing provides many advantages over other methods of learning.
Learn more about using the PTB™ for Training at http://www.sandboxmodel.com/Training_PTB.html

The PTB™ Analytics solution enhances decision making. It allows project managers to perform "what-if" analysis and to see the impact of their decisions in a safe, simulated environment. This enables risk management and mitigation.
Learn more about using the PTB™ Analytics to make better decisions at http://www.sandboxmodel.com/DSS_PTB.html

Comments and Discussions

 
Questionhow about not animated area? Pin
coderprime31-Oct-11 5:11
coderprime31-Oct-11 5:11 
Questioncatching events Pin
Member 831864914-Oct-11 1:21
Member 831864914-Oct-11 1:21 
QuestionBinding inside the expander Pin
Keykoa20-Sep-11 3:25
Keykoa20-Sep-11 3:25 
AnswerRe: Binding inside the expander Pin
Guy Shtub20-Sep-11 20:31
Guy Shtub20-Sep-11 20:31 
GeneralRe: Binding inside the expander Pin
Keykoa21-Sep-11 23:54
Keykoa21-Sep-11 23:54 
GeneralSpiffy ! Pin
desfen13-Jun-11 22:46
desfen13-Jun-11 22:46 
GeneralRe: Spiffy ! Pin
Guy Shtub12-Jul-11 2:24
Guy Shtub12-Jul-11 2:24 
GeneralRe: Spiffy ! Pin
Jac27-Aug-11 7:02
Jac27-Aug-11 7:02 
GeneralMy vote of 4 Pin
Leo5614-Apr-11 1:44
Leo5614-Apr-11 1:44 
GeneralProblem facing in Name property used in Content of Expander Control Pin
Vaidehi P23-Nov-09 18:30
Vaidehi P23-Nov-09 18:30 
GeneralRe: Problem facing in Name property used in Content of Expander Control Pin
Guy Shtub30-Nov-09 5:25
Guy Shtub30-Nov-09 5:25 
GeneralRe: Problem facing in Name property used in Content of Expander Control Pin
Vaidehi P4-Dec-09 17:33
Vaidehi P4-Dec-09 17:33 
GeneralAbout named controls Pin
keozcigisoft23-Apr-09 20:40
keozcigisoft23-Apr-09 20:40 
GeneralRe: About named controls [modified] Pin
Guy Shtub25-Apr-09 20:44
Guy Shtub25-Apr-09 20:44 
Thanks for the feedback.
One solution I can think of, is to keep references to the controls inside the expander, in the defining window. You can set these references on the Loaded event. I'll give a small example to make it clearer:

<AnimatingExpanderControl>
<stackpanel>
<toolbartray>
<toolbar>
Loaded="ToolBar_Loaded"
<label>
Start Date:
</label>
<dg:datepicker>
</dg:datepicker>
<label>
End Date:
</label>
<dg:datepicker>
</dg:datepicker>
</toolbar>
</toolbartray>
</stackpanel>
</AnimatingExpanderControl>

And the code behind (in the window where the control is defined):

private void ToolBar_Loaded(object sender, RoutedEventArgs e) {
ToolBar tb = sender as ToolBar;
if (null != tb) {
this.StartDatePicker = (DatePicker)tb.Items[1];
this.EndDatePicker = (DatePicker)tb.Items[3];
}
}

Guy.

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.