MVVM and animations…

Right now I am working on a Silverlight project for my company. In that project, as in most projects with Silverlight I need to run some animations. And since I’m working with MVVM this becomes a little cumbersome and complicated. I don’t want my view to be dependent on the viewmodel. So the view cant tell the model what storyboards to play. And I don’t want the viewmodel to be dependent on the view either. So i don’t want to give the viewmodel a referens to the view. I guess I could get some separation using interfaces, but it still felt a little off… So I thought a little about this, and then I Googled it. Do you know what I found when googling for “patterns mvvm animations”. Nothing really useful. A bunch of questions. I even tried to search for WPF and tried to leverage the WPF delelopers knowledge…no luck… So I had to figure something our myself. And I think I have actually found a pretty nice separation by using a Storyboard manager object.

The manager declares a attached property called ID. I actually wanted it to be Name, but adding an attached property with that name caused problems in VS apparently (2008 with SL3). The auto generated cs file all of the sudden started to register duplicate storyboards…I thought that that was the reason for placing the Name attribute in a separate namespace…well…apparently not… Anyhow. The manager then contains a dictionary that keeps track of all storyboards registered with an id. It also has a method called PlayAnimation. The play animation method takes an id, a callback and a state object. Calling that method will locate the correct storyboard, register for its Completed event, play it and then call the callback delegate when it is completed. It will also pass the state object to the callback as this might be helpful…

If it can’t find a storyboard with the specified id, it calls the callback straight away. So the viewmodel don’t have to bother if there is a storyboard with that name or not. I guess this sort of follows the “parts and states” pattern that Microsoft uses for their controls…

It is real simple, but gets the job done. I might extend it in the future to support stopping and querying the state of the storyboard as well. But for now, this is what I needed. The code for the manager looks something like this

public static class StoryboardManager
{
public static DependencyProperty IDProperty =
DependencyProperty.RegisterAttached("ID", typeof(string), typeof(StoryboardManager),
new PropertyMetadata(null, IDChanged));
static Dictionary<string, Storyboard> _storyboards = new Dictionary<string, Storyboard>();

private static void IDChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
Storyboard sb = obj as Storyboard;
if (sb == null)
return;

string key = e.NewValue as string;

if (_storyboards.ContainsKey(key))
_storyboards[key] = sb;
else
_storyboards.Add(key, sb);
}

public static void PlayStoryboard(string id, Callback callback, object state)
{
if (!_storyboards.ContainsKey(id))
{
callback(state);
return;
}
Storyboard sb = _storyboards[id];
EventHandler handler = null;
handler = delegate { sb.Completed -= handler; callback(state); };
sb.Completed += handler;
sb.Begin();
}

public static void SetID(DependencyObject obj, string id)
{
obj.SetValue(IDProperty, id);
}
public static string GetID(DependencyObject obj)
{
return obj.GetValue(IDProperty) as string;
}
}
public delegate void Callback(object state);

A nice little thing to look specifically at is this

EventHandler handler = null;
handler = delegate { sb.Completed -= handler; callback(state); };
sb.Completed += handler;

 

By creating an EventHandler explicitly makes it possible to disconnect it when it is called. If I had just added an anonymous method, it would have been called over and over again every time that storyboard played, which is not something I want. I want to have the callback called for this play only. And I also have to explicitly declare it as null before setting it to the anonymous method. If I don’t, I can’t reference it in my anonymous method. That’s the reason for the cumbersome, two line declaration…

Using it looks like this in Xaml

<UserControl x:Class="MyApplication"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:StoryboardManager="MyApplication.Utilities">
<Grid x:Name="LayoutRoot" Background="White">
<Grid.Resources>
<Storyboard x:Key="MyStoryboard" StoryboardManager:StoryboardManager.ID="MyAnimation"></Storyboard>
</Grid.Resources>
</Grid>
</UserControl>

 

And using it in my viewmodel looks like this

StoryboardManager.PlayStoryboard("MyStoryboard", (o => DoSomething()), myObject);

 

Kind of sweet right. No direct dependencies between the viewmodel and the view. Keeping the viewmodel unit testable. Well…I like it at least. It solved a bit of my problems. This with some ICommands and the CommandManager got my viewmodel to a place where I like it to be…

Cheers!

Update:
After a couple of comments about it not working, I have created a demo solution to test it in. The code works perfectly fine… After having thought about it a bit more, I am not 100% sure that this is the best way to handle it. But it does work… So anyhow… here is the code: MVVMAnimation.zip (16.71 kb)

Comments (20) -

Hiya,
Don't know what I am missing, I've tried to implement your solution verbatim but the storyboard collection is always zero.
I set the ID like this..
<Storyboard x:Key="HideMessageListStoryBoard1"
ViewModel:StoryboardManager.ID="MyAnimation">

Hi Abdul!
To be perfectly honest I have no clue why it isn't working for you. Unless you get any exception in the Xaml when compiling, it should work. If you want to, you can zip up the project and send it to me, and I will take a look at it for you

Yeah, I'm getting the same problem. The IDChanged handler never fires and the storyboard collection stays empty.

This code was just what I needed! Many thanks! Smile

RandomEngy: As mentioned in the update above, I have added a code sample to show how that it works...

Great piece of code! Thanks!

This was excellent.  It saved me a lot of time.

thanks for this. It works in Silverlight, but not in WPF. I had come up with a similar (although more complicated idea than this - learninggames.codeplex.com/.../30938#661240) for WPF. Just need to find something that works for both.

animation studio 11/14/2010 5:42:58 PM

Thanks for providing the code

You are more than welcome!

this doesn't work for controllers like ListBox Items. For example if you want to animate a items in listbox how could you make this work?

No it won't work in a listbox for example as it would register the same storybard id over and over again. One time for each of the items. In that case, and probably in most other, you should look at using Blend Actions.
To be honest, I would probably recommend doing that instead of this in most situations. Get a BeginStoryboard action and bind it to the VM using a DataTrigger. Then change the VM property when you need to run the animation...
This solution came out before I read up on actions and behaviors...
Cheers!

Thanks. I'm already using DataTriggers but i can't get them to fire before bounded property change for example i have stack panel visibility property bounded to bool IsVisible and when IsVisible changes i have to run the animation to this stack panel. This is possible with storyboardmanager how could i achieve this with DataTriggers because stackpanel go's invisible before animation is displayed.

in story board manager i could do something like

public bool? IsVisible
{
   get

   {
      if (_isVisible == null)
      {
          _isVisible = true;
      }

       return _isVisible;
    }

    set
    {
       _isVisible = value;

       if (value == true)
       {
          RaisePropertyChanged("IsVisible");
          StoryboardManager.PlayStoryboard("sbStackPanelShow",null,null);
       }
       else
       {

         StoryboardManager.PlayStoryboard("sbStackPanelHide", o => ColorChage(), this);

       }

    }

}

private ColorChange(){RaisePropertyChange("IsVisible")};

That's true AllSpark. The StoryboardManager is a little more powerfull when it comes to situations that. The other way to handle your problem would be to write a custom Action. Actions are quite easy to write, and it should be fairly easy to write one that runs an animation before changing the visibility...
That way, you will keep a bit more separation, and still get the desired behaviour...

Thanks for the input. I already did this with DataTriggers and ControlStoryboardAction but like you said its much more cleaner when done with Custom Trigger actions so here is the code so some one might find it useful

forums.silverlight.net/.../509194.aspx#509194

Come on - no need to bash Martin guys, I'm sure he wants to know what is happening as much as us. Jake <a href="www.pandorabraceletwholesaleion.com/">Pandora Bracelet Wholesale</a> said in his blog he didn't know about the news until the day it was announced.
I'm sure all of the bbc f1 team want to know the details just <a href="http://www.tiffanycojewelryu.com/">Tiffany Co Jewelry</a> like us - so don't <a href="www.pandorabraceletsoutletion.com/">Pandora Bracelets Outlet</a> blame them, it's the top bbc bosses that are responsible for what has happened.

I like it. It's a very nice trick.

For those of you who can't get it to work, because Storyboard Manager attached property never gets set it's because the Resource never gets initialized since it's not used anywhere.

Add something like:

var x = this.MainGrid.FindResource("ExitConfirmationStoryboard");

for example in the window's constructor.

I improved it a bit:

usage:
StoryboardManager.PlayStoryboard("MyStoryboard", () => DoSomethingWithMyState(state) );

code:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Media.Animation;

namespace WPF.Helpers
{
    //chris.59north.com/post/mvvm-and-animations.aspx

    public static class StoryboardManager
    {
        public static DependencyProperty IDProperty =
            DependencyProperty.RegisterAttached(
                "ID",
                typeof(string),
                typeof(StoryboardManager),
                new PropertyMetadata(null, IDChanged));

        private static Dictionary<string, Storyboard> _storyboards = new Dictionary<string, Storyboard>();
        private static Dictionary<string, Queue<Action>> _storyboardsCompletedHandlers = new Dictionary<string, Queue<Action>>();

        private static void IDChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
        {
            Storyboard sb = obj as Storyboard;
            
            if (sb == null)
                return;

            var key = e.NewValue as string;
            
            EventHandler completedHandler =
                (sender, args) =>
                {
                    if (!_storyboardsCompletedHandlers[key].Any())
                        return;

                    var action = _storyboardsCompletedHandlers[key].Dequeue();

                    if (action != null)
                        action();
                };

            if (!_storyboards.ContainsKey(key))
            {
                try
                {
                    sb.Completed += completedHandler;
                }
                catch (Exception ex)
                {
                    throw new ApplicationException("Cannot register completed event handler for the story board. Is it frozen?", ex);
                }

                _storyboards.Add(key, sb);
                _storyboardsCompletedHandlers.Add(key, new Queue<Action>());
            }
        }

        public static void PlayStoryboard(string id, Action onCompleted = null)
        {
            if (!_storyboards.ContainsKey(id))
            {
                throw new ApplicationException("Storyboard with the given ID is not registered.");
            }

            _storyboardsCompletedHandlers[id].Enqueue(onCompleted);
            _storyboards[id].Begin();
        }

        public static void SetID(DependencyObject obj, string id)
        {
            obj.SetValue(IDProperty, id);
        }
        public static string GetID(DependencyObject obj)
        {
            return obj.GetValue(IDProperty) as string;
        }
    }
}

Comments are closed