Playing Storyboards on DataTriggers to animate a Path

In the application I am working on, we had to display an image accordingly to the value of a field in our ViewModel represented by an Enum. The Enum had 3 possible values: normal, up or down. An arrow pointing up, right or down was to be drawn as a result of the bounded field’s value.WpfAnim-ss001

 

Download Sample Source code

You can download the sample source code for this blog post by following this link.

 

Initial sample setup

The plan was to draw an arrow using a Path. Then use animations to rotate the arrow accordingly to what was set on the ViewModel.

I went ahead and created a sample application that draws all this stuff on the UI. It also adds three buttons at the bottom which allow us to manipulate the value on the bounded field to control the tendency that should be displayed:

WpfAnim-ss002

As mentioned before, a Path was used to create the arrow. For us to be able to animate the arrow’s rotation we had to create a render transform with the initial value to animate from:

<Path x:Name="path"
      Fill="#FFCC0707" Stretch="Fill" Stroke="#FF000000"
      Height="32" Width="64" RenderTransformOrigin="0.5,0.5"
      Data="M78,88.333333 L141.5,87.500336 142.16667,65.833664 175.83283,96.833545 143.49989,127.16709 142.16656,109.16693 78.166666,108.16694 z">
    <Path.RenderTransform>
        <TransformGroup>
            <ScaleTransform ScaleX="1" ScaleY="1"/>
            <SkewTransform AngleX="0" AngleY="0"/>
            <RotateTransform Angle="0"/>
            <TranslateTransform X="0" Y="0"/>
        </TransformGroup>
    </Path.RenderTransform>
</Path>

Next I added the three Storyboards: OnTendencyUp, OnTendencyNormal and OnTendencyDown. Each storyboard animates the rotation of the arrow to its corresponding Enum value.

<Storyboard x:Key="OnTendencyUp">
    <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" 
                                   Storyboard.Target="{Binding TemplatedParent}" 
                                   Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[2].(RotateTransform.Angle)">
        <SplineDoubleKeyFrame KeyTime="00:00:00.3000000" Value="-45"/>
    </DoubleAnimationUsingKeyFrames>
</Storyboard>
<Storyboard x:Key="OnTendencyNormal">
    <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" 
                                   Storyboard.Target="{Binding TemplatedParent}" 
                                   Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[2].(RotateTransform.Angle)">
        <SplineDoubleKeyFrame KeyTime="00:00:00.3000000" Value="0"/>
    </DoubleAnimationUsingKeyFrames>
</Storyboard>
<Storyboard x:Key="OnTendencyDown">
    <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" 
                                   Storyboard.Target="{Binding TemplatedParent}"
                                   Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[2].(RotateTransform.Angle)">
        <SplineDoubleKeyFrame KeyTime="00:00:00.3000000" Value="45"/>
    </DoubleAnimationUsingKeyFrames>
</Storyboard>

Everything is now in place for us to start playing with the animation. We have a path and three animations that change its rotation.

 

Controlling Storyboards with DataTriggers based on the Enum value

The next step was to create the DataTriggers. The plan was that whenever the tendency changes we kick off the consequent storyboard. For this we would use DataTrigger.EnterActions to initiate the storyboard. The DataTriggers should be added to the style of the Path:

<Path.Style>
    <Style>
        <Style.Triggers>
            <DataTrigger Binding="{Binding Tendency}" Value="Down">
                <DataTrigger.EnterActions>
                    <BeginStoryboard Storyboard="{StaticResource OnTendencyDown}"  x:Name="OnTendencyDown_BeginStoryboard"/>
                </DataTrigger.EnterActions>
            </DataTrigger>
            <DataTrigger Binding="{Binding Tendency}" Value="Up">
                <DataTrigger.EnterActions>
                    <BeginStoryboard Storyboard="{StaticResource OnTendencyUp}" x:Name="OnTendencyUp_BeginStoryboard"/>
                </DataTrigger.EnterActions>
            </DataTrigger>
            <DataTrigger Binding="{Binding Tendency}" Value="Normal">
                <DataTrigger.EnterActions>
                    <BeginStoryboard Storyboard="{StaticResource OnTendencyNormal}" x:Name="OnTendencyNormal_BeginStoryboard"/>
                </DataTrigger.EnterActions>
            </DataTrigger>
        </Style.Triggers>
    </Style>
</Path.Style>

In theory, whenever a value changes the DataTrigger fires the storyboard thus animating the rotation either way. The strange thing is that no animation is played, none whatsoever, zip, rien, niente, nenhuma! This happens because we have competing DataTriggers over the same binding making the last one the winner! (For additional information check this link)

If you try and change the order by which the DataTriggers are displayed you will notice that a single animation will be played (the last one on the list), and when the rotation gets to its target value no other animation will play.

 

Controlling the Storyboards through Buttons and EventTriggers

Now if you trigger the animations via normal event triggers all animations will play appropriately. I went ahead and produced a sample that fires the storyboards based on the clicking event on each button:

<EventTrigger RoutedEvent="ButtonBase.Click" SourceName="btnDown">
    <BeginStoryboard Storyboard="{StaticResource OnTendencyDown}"/>
</EventTrigger>
<EventTrigger RoutedEvent="ButtonBase.Click" SourceName="btnNormal">
    <BeginStoryboard Storyboard="{StaticResource OnTendencyNormal}"/>
</EventTrigger>
<EventTrigger RoutedEvent="ButtonBase.Click" SourceName="btnUp">
    <BeginStoryboard Storyboard="{StaticResource OnTendencyUp}"/>
</EventTrigger>

In order for the animations to play correctly we have to change the bindings on the storyboards that lead to the rotation of the Path. Instead of using TemplatedParent we will use a binding to a specific element. For each storyboard I changed the binding to:

(…)
Storyboard.Target="{Binding ElementName=path}"
(…)

Now, all the animations play appropriately every time you click one of the buttons.

 

How to enable controlling the Storyboards with DataTriggers

The solution to the problem with the DataTriggers is for us to remove the Storyboard on each DataTrigger whenever DataTrigger.ExitActions is invoked, likewise:

<DataTrigger Binding="{Binding Tendency}" Value="Down">
    <DataTrigger.EnterActions>
        <BeginStoryboard Storyboard="{StaticResource OnTendencyDown}"
                         x:Name="OnTendencyDown_BeginStoryboard"/>
    </DataTrigger.EnterActions>
    <DataTrigger.ExitActions>
        <RemoveStoryboard BeginStoryboardName="OnTendencyDown_BeginStoryboard" />
    </DataTrigger.ExitActions>
</DataTrigger>
<DataTrigger Binding="{Binding Tendency}" Value="Up">
<DataTrigger.EnterActions>
<BeginStoryboard Storyboard="{StaticResource OnTendencyUp}"
x:Name="OnTendencyUp_BeginStoryboard"/>
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<RemoveStoryboard BeginStoryboardName="OnTendencyUp_BeginStoryboard" />
</DataTrigger.ExitActions>
</DataTrigger> <DataTrigger Binding="{Binding Tendency}" Value="Normal"> <DataTrigger.EnterActions> <BeginStoryboard Storyboard="{StaticResource OnTendencyNormal}" x:Name="OnTendencyNormal_BeginStoryboard"/> </DataTrigger.EnterActions> <DataTrigger.ExitActions> <RemoveStoryboard BeginStoryboardName="OnTendencyNormal_BeginStoryboard" /> </DataTrigger.ExitActions> </DataTrigger>

By removing the Storyboards on ExitActions we guarantee that animations will play correctly every time the Storyboard is initiated.

 

Conclusion

This solution feels a bit hacky but the truth is that it fixed our issue allowing our animations to play smoothly!

You can download a sample application for this post by following this link.

 

Updates

2009/07/10: I have updated the order by which the DataTriggers are defined on the ‘How to enable controlling the Storyboards with DataTriggers’. The sample source code was updated as well. An issue remains however: when you click up and then click down there will be a jump in the animation from the up to normal and then animate to down. I am still trying to figure this one out…

Thank you to Carlos for spotting this glitch!

 

Shout it kick it on DotNetKicks.com

2 thoughts on “Playing Storyboards on DataTriggers to animate a Path

  1. In your sample application, when using data triggers, the arrow jumps when moving from Up to Normal position. I don’t understand why. Have you looked at this?
    Thanks for the example.

    1. Hi Carlos, thanks for the feedback and for spotting this out! I have updated both the sample and the article with a fix for this.

      The problem still persists however. When you move from being Up directly to Down there is still a jump from Up to Normal although I am not sure on why this happens! For certain this has to do with the order by which the storyboards get removed and then play.

Leave a Reply

Your email address will not be published. Required fields are marked *