Triggering Storyboards with data binding

Triggering Storyboards with data binding

A clean way for your viewmodel's properties to trigger storyboard animations

Creating animations in XAML is pretty easy using Storyboards, but after you create them, you have to trigger them somehow. There are a bunch of ways to do this, but each has its own shortcomings if you want to trigger your animation based on viewmodel changes:

DataTrigger & Trigger - Not supported in WinRT apps so they are non-starters.

EventTrigger - Triggers your animation in response to an event. These are still supported in WinRT but they can only hook up to the Loaded event so it is effectively inaccessible from a viewmodel.

Storyboard.Begin() method - straight to the point, but not easy to databind to since it is a method.

VisualStateManager - This is essentially the replacement for DataTriggers, but again, it requires calling a method (VisualStateManager.GoToState()) to trigger it.

Behaviors - Code like this can trigger a storyboard using a custom behavior, but (and maybe this is just me) I rarely use behaviors and find them kind of clunky for some reason. Just my opinion.

So where does that leave us? How about this:

<Storyboard common:StoryboardHelper.BeginIf="{Binding IsLoggedIn}">
    <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.RenderTransform).(CompositeTransform.TranslateY)" Storyboard.TargetName="StartSessionOverlay">
        <EasingDoubleKeyFrame KeyTime="0:0:0" Value="640"/>
        <EasingDoubleKeyFrame KeyTime="0:0:0.3" Value="0"/>
    </DoubleAnimationUsingKeyFrames>
</Storyboard>

Using a custom attached property, we can set up a mechanism that allows declarative databinding in the XAML and yet runs arbitrary code behind the scenes. The implementation is straight-forward:

using System;
using System.Collections.Generic;
using System.Text;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media.Animation;

namespace Demo
{
    public class StoryboardHelper : DependencyObject
    {
        public static bool GetBeginIf(DependencyObject obj)
        {
            return (bool)obj.GetValue(BeginIfProperty);
        }

        public static void SetBeginIf(DependencyObject obj, bool value)
        {
            obj.SetValue(BeginIfProperty, value);
        }

        public static readonly DependencyProperty BeginIfProperty = DependencyProperty.RegisterAttached("BeginIf", typeof(bool), typeof(StoryboardHelper), new PropertyMetadata(false, BeginIfPropertyChangedCallback));

        private static void BeginIfPropertyChangedCallback(DependencyObject s, DependencyPropertyChangedEventArgs e)
        {
            var storyboard = s as Storyboard;
            if (storyboard == null)
                throw new InvalidOperationException("This attached property only supports Storyboards.");

            var begin = (bool)e.NewValue;
            if (begin) storyboard.Begin();
            else storyboard.Stop();
        }
    }
}

Attached properties have been solving a lot of little things like this for me lately so I will probably write about them some more in the future.