C# Disciples

my life in Avalon ….

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

June 13, 2009 - Posted by marlongrech | WPF, mvvm | | 12 Comments

12 Comments »

  1. Yeah baby

    Comment by sacha | June 13, 2009 | Reply

  2. :)

    Comment by marlongrech | June 13, 2009 | Reply

  3. Well done Marlon… As usual!

    Comment by Rudi Grobler | June 14, 2009 | Reply

  4. Thanks Rudi

    Comment by marlongrech | June 14, 2009 | Reply

  5. Well done Marlon!! proud of u and Im really happy to see you doing what u love :)

    Comment by Chris | June 14, 2009 | Reply

  6. 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.

    Comment by Josh Smith | June 15, 2009 | Reply

  7. good point Mr.Smith

    Comment by marlongrech | June 15, 2009 | Reply

  8. [...] Animations and MVVM (marlongrech) [...]

    Pingback by Dew Dump – June 13-15, 2009 | Alvin Ashcraft's Morning Dew | June 15, 2009 | Reply

  9. 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?

    Comment by DS | July 10, 2009 | Reply

  10. 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.

    Comment by johnhamm | September 22, 2009 | Reply

  11. 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.

    Comment by johnhamm | September 22, 2009 | Reply

  12. Read the please note part of the article …last sentence

    Comment by Marlon Grech | September 22, 2009 | Reply


Leave a comment