Animations and MVVM

This week I was asked from a colleague of mine, ‘How do I fire a command once a storyboard has completed?’. Do we have to break the MVVM here and write code behind. The answer is simple, HELL NO….

Attached Behavior to the rescue. Basically we can do this task by using an attached property, that will hook up to the Completed event of the storyboard and fire the command for us on the ViewModel. Here’s how to do this

   1: var timeline = d as Timeline;

   2: if (timeline != null && !timeline.IsFrozen)

   3: {

   4:    timeline.Completed += delegate

   5:    {

   6:        ICommand command = GetCommand(d);

   7:        object param = GetCommandParameter(d);

   8:  

   9:        if(command != null && command.CanExecute( param ))

  10:            GetCommand(d).Execute(GetCommandParameter(d));

  11:    };

  12: }

and here is how to use this in XAML

   1: <EventTrigger RoutedEvent="ButtonBase.Click">

   2:     <BeginStoryboard>

   3:         <Storyboard cmds:CommandStoryboard.Command="{Binding Close}" >

   4:             <DoubleAnimation Storyboard.TargetName="border" Storyboard.TargetProperty="Height" 

   5:                          DecelerationRatio="1.0" Duration="0:0:0.5" To="0"/>

   6:         </Storyboard>

   7:     </BeginStoryboard>

   8: </EventTrigger>

In the sample application (that is provided below with this article) shows how to animate some item in an items control and when the animation is completed the Storyboard executes a command that actually removes the item from the binding list.

I also created another simple class so that you can execute a Command from an EventTrigger. This is very handy since sometimes we want to execute a Command on a VM when some arbitrary RoutedEvent is raised. Here is the code for that

   1: public class CommandTimeline : Storyboard

   2: {

   3:     protected override Duration GetNaturalDurationCore(Clock clock)

   4:     {

   5:         return new Duration(TimeSpan.FromTicks(1));

   6:     }

   7:  

   8:     protected override Freezable CreateInstanceCore()

   9:     {

  10:         return new CommandTimeline();

  11:     }

  12: }

and here is how to consume it

   1: <EventTrigger RoutedEvent="FrameworkElement.Loaded">

   2:     <BeginStoryboard>

   3:         <cmds:CommandTimeline cmds:CommandStoryboard.Command="{Binding Test}" />

   4:     </BeginStoryboard>

   5: </EventTrigger>

As you can see this reuses the attached behavior that is used on normal storyboards. The only difference between a Storyboard and a CommandTimeline is that a CommandTimeline returns a Duration of 0 so the Completed event gets raised immediately.

PLEASE NOTE: This can be only used in an event trigger. When this is used with DataTriggers etc you will get an exception saying that the Trigger cannot be sealed. This happens because you cannot hook up to the Completed event of a storyboard in a DataTrigger/Trigger. In order to workaround this I use AttacedRoutedEvents so that I can have EventTriggers executing my commands.

Download the sample app + source code

15 thoughts on “Animations and MVVM

  1. Maybe a Debug.Fail(…) would be helpful if the storyboard is frozen and you can’t hook the Completed event, just so the developer would know why the command isn’t being executed.

  2. Pingback: Dew Dump – June 13-15, 2009 | Alvin Ashcraft's Morning Dew

  3. Nice example..Just have one thought, I see that you are subscribing to the Completed event of the storyboard but i don’t see anywhere that you are unsubscribing..Will this not be a memory leak?

  4. Very strange. When I change:

    to:

    I get:

    Cannot convert the value in attribute ‘Style’ to object of type ‘System.Windows.Style’. Cannot freeze this Storyboard timeline tree for use across threads. Error at object ‘System.Windows.Controls.Button’ in markup file ‘EnhancedDashboard;component/view/dashboardview.xaml’ Line 862 Position 18.

  5. Apparently my xaml was interpreted as HTML in my last comment. here it is again:

    I change my working eventtrigger from:

    <EventTrigger RoutedEvent=”Button.Click”>
    <BeginStoryboard>
    <Storyboard >

    to use the attached property:

    <EventTrigger RoutedEvent=”Button.Click”>
    <BeginStoryboard>
    <Storyboard views:Attachments.DelayedCommand=”{Binding UpdateCommand}” >

    And I get the following error:
    Cannot convert the value in attribute ‘Style’ to object of type ‘System.Windows.Style’. Cannot freeze this Storyboard timeline tree for use across threads. Error at object ‘System.Windows.Controls.Button’ in markup file ‘EnhancedDashboard;component/view/dashboardview.xaml’ Line 862 Position 18.

    Basically, all I’m trying to do is make my button not execute its command until after the click animation is finished! As a workaround, I’m using Thread.Sleep(2000) in my command’s handler since teh animation is 2 seconds long! But I hate that solution.

  6. Hi,
    I’m new to WPF.
    Can you explain how to attach the property to StoryBoard? [ cmds:CommandStoryboard.Command=”{Binding Close}” ].
    I could not understand…

  7. The link to download source code is not valid any more.Is It possible to update source code’s link?
    Thanks in advance

Leave a comment