9 comments
  •   posted in: 
  • WPF

Notice that when you use an expander and expand / collapse it that there is a jerkiness depending on the number of elements that you have in it. So to solve this problem you can animate the expand / collapse events that get fired to smoothly open and close. Easy, right?

Well yes and no. You can animate the expanded event pretty easily using the routedEvent "Expander.Expanded" and then apply some transformation that either calculates the height, has a predefined height or (what I like to do) use a layoutTransform and animate the y scale from 0 to 1. Ok ok, that's not terribly hard to accomplish; here's how:

<StackPanel>

  <StackPanel.Triggers>

    <EventTrigger RoutedEvent="Expander.Expanded" SourceName="expander">

      <EventTrigger.Actions>

        <BeginStoryboard>

          <Storyboard>

            <DoubleAnimation From="0" To="1" Duration="0:0:0.25" Storyboard.TargetName="listBox" Storyboard.TargetProperty="(FrameworkElement.LayoutTransform).(ScaleTransform.ScaleY)"/>

          </Storyboard>

        </BeginStoryboard>

      </EventTrigger.Actions>

    </EventTrigger>

  </StackPanel.Triggers>

  <Expander x:Name="expander" Header="Expander">

    <ListBox x:Name="listBox">

      <ListBoxItem Content="ListBoxItem" />

      <ListBoxItem Content="ListBoxItem" />

      <ListBoxItem Content="ListBoxItem" />

      <ListBox.LayoutTransform>

        <ScaleTransform ScaleX="1" ScaleY="0"/>

      </ListBox.LayoutTransform>

    </ListBox>

  </Expander>

</StackPanel>

Now that works pretty nice, but wait, try to collapse it ... ugly! So we need to do the same thing to the routedEvent "Expander.Collapsed". Hrmm ... on second thought that won't work because the collapsed event sets the expandSites (the expanders content that expands / collapses) visibility to collapsed right away!

So we need to fix this. Here's one approach that overrides the template of the expander and removes the setter that sets the visibility to collapsed. Instead, we are going to animate the layoutTransform on the y scale to 0 for a smooth collapse. We will also do this on the expand event as well, and since we are using the expanders template we will actually be using the IsExpanded property on the expander. This allows us to have a trigger that has enterActions and exitActions so we can easily start off the appropriate animation.

I used Blend to override the template (and thus messy code is produced :) ) and then edited the IsExpanded property appropriately. Attached is the full source code in all xaml that gets the job done. Note 'Timeline1' and 'Timeline2' as they are the storyboards for animating the expanded and collapsed events, respectively. If you have any questions or ways to improve this please let me know!

filefile size
ExpanderAnimateCollapsed.xaml19.42 KB

Comments

Comment by Rich John

Hi,

Thanks for providing this code. It is working really well for me at runtime, but at design time the control is not expanded. I have tried to set various things in the ControlTemplate to make it be expanded at design time, with no luck.

Could you offer any suggestions?

Thanks again.

Rich John

Rich John
Comment by matt

On the actual expander control, set IsExpanded = true. That should allow the expander to be expanded with whatever template you have defined.

Comment by Anonymous

Hi

Thanks for the code example - really useful.

I have a suggestion..

In my case, I had some controls in the header of the expander eg. for the entry of mandatory information, with optional information enterable in controls displayed in the dropdown content of the expander. Bit hacky maybe, but I liked it..

However, clicking anywhere outside the controls in the header would cause the expander to toggle, as the ToggleButton included the content of the header. Not a massive issue, but potentially annoying..

Solution was as follows - split the ToggleButton and Header content into separate entities, thus:

(In the ExpanderStyle1 style's DockPanel, replacing the ToggleButton control)

<StackPanel Orientation="Horizontal" DockPanel.Dock="Top">
<ToggleButton FontFamily="{TemplateBinding FontFamily}" FontSize="{TemplateBinding FontSize}" FontStretch="{TemplateBinding FontStretch}"
FontStyle="{TemplateBinding FontStyle}" FontWeight="{TemplateBinding FontWeight}"
Foreground="{TemplateBinding Foreground}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
Padding="{TemplateBinding Padding}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
FocusVisualStyle="{StaticResource ExpanderHeaderFocusVisual}"
Margin="1" MinHeight="0" MinWidth="0" x:Name="HeaderSite"
Style="{StaticResource ExpanderDownHeaderStyle}"
IsChecked="{Binding Path=IsExpanded, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" />
<ContentPresenter
Content="{TemplateBinding Header}"
ContentTemplate="{TemplateBinding HeaderTemplate}"
ContentTemplateSelector="{TemplateBinding HeaderTemplateSelector}">
</ContentPresenter>
</StackPanel>

Thanks again.

Kelly

Anonymous
Comment by Fernando

Hi,

Many thanks for the example. I've searching for this for a few days. I tried to solve using binding but was not possible freeze the obtained property in a nonfreezable binding. This was very helpfull.

Fernando
Comment by Jack

I am trying to use this code as the groupstyle in a listview, so I have an ItemsPresenter as the content of the expander, but it does not show up. Any suggestions?

Jack
Comment by Lucas

Many,many thanks for the example.

It's really helping me.

Lucas
Comment by Jeff Smith

This code uses the above approach but with keyframes and can be placed inside the control template and targets the "ExpandSite" rather than always being restricted to just to using a stackpanel

<ControlTemplate.Resources>
<Storyboard x:Key="sbExpand">
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(FrameworkElement.LayoutTransform).(ScaleTransform.ScaleY)" Storyboard.TargetName="ExpandSite">
<EasingDoubleKeyFrame KeyTime="0:0:0.4" Value="1">
<EasingDoubleKeyFrame.EasingFunction>
<CubicEase EasingMode="EaseOut"/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
</DoubleAnimationUsingKeyFrames>
<ThicknessAnimationUsingKeyFrames Storyboard.TargetProperty="(FrameworkElement.Margin)" Storyboard.TargetName="ExpandSite">
<EasingThicknessKeyFrame KeyTime="0:0:0.4" Value="6">
<EasingThicknessKeyFrame.EasingFunction>
<CubicEase EasingMode="EaseOut"/>
</EasingThicknessKeyFrame.EasingFunction>
</EasingThicknessKeyFrame>
</ThicknessAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Opacity)" Storyboard.TargetName="ExpandSite">
<EasingDoubleKeyFrame KeyTime="0:0:0.3" Value="0"/>
<EasingDoubleKeyFrame KeyTime="0:0:0.6" Value="1"/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
<Storyboard x:Key="sbCollapse">
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(FrameworkElement.LayoutTransform).(ScaleTransform.ScaleY)" Storyboard.TargetName="ExpandSite">
<EasingDoubleKeyFrame KeyTime="0:0:0.5" Value="0">
<EasingDoubleKeyFrame.EasingFunction>
<CubicEase EasingMode="EaseOut"/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
</DoubleAnimationUsingKeyFrames>
<ThicknessAnimationUsingKeyFrames Storyboard.TargetProperty="(FrameworkElement.Margin)" Storyboard.TargetName="ExpandSite">
<EasingThicknessKeyFrame KeyTime="0:0:0.5" Value="6,2">
<EasingThicknessKeyFrame.EasingFunction>
<CubicEase EasingMode="EaseOut"/>
</EasingThicknessKeyFrame.EasingFunction>
</EasingThicknessKeyFrame>
</ThicknessAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Opacity)" Storyboard.TargetName="ExpandSite">
<EasingDoubleKeyFrame KeyTime="0:0:0.1" Value="0"/>
<EasingDoubleKeyFrame KeyTime="0:0:0.5" Value="0">
<EasingDoubleKeyFrame.EasingFunction>
<CubicEase EasingMode="EaseOut"/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</ControlTemplate.Resources>

Jeff Smith
Comment by matt

thanks for the code jeff!

Comment by phantasm

You forgot to mention that this improvement will only work for .NET Framework 4 or higher :)

phantasm