03 October 2012

A WinRT behavior to start a storyboard on an event

Sometimes I get a bit distracted. In this article I mention a behavior that I wrote which starts a storyboard on an event, I even put in in the win8nl library – and then totally forget to blog about it, or even to announce it. So anyway, when Danny van Neyghem asked me about a problem that was almost solved by my behavior, I kinda remembered it, and that I totally forgot about it - and decided a) to improve it and b) to talk about it (well, write).

World, meet StartStoryboardBehavior. It’s a very simple behavior that has the following properties:

Storyboard The name of the Storyboard to start. Mandatory.
StartImmediately true = don’t wait for an event to start, just start the storyboard when the behavior is loaded. Optional; default is false
EventName The event of the AttachedObject that will initiate the start of the storyboard (e.g. ‘Click’ on a button). Ignored when StartImmediately  == true, mandatory otherwise
SearchTopDown Optional, default is true. If true, the behavior will start to search for your storyboard from the top of the visual tree. This is the scenario in which for instance clicking a button will initiate a storyboard that sits somewhere in the top of the page. If this value is set to false, the behavior will search from the attached object up. Choose this setting when you use the behavior to start a storyboard that’s sitting inside a template.

“SearchTopDown” was added just yet, and I hope it solves Danny’s problem. For those who just want to use the behavior: Nuget Win8nl and you have the behavior armed and ready. I’ve updated the package with the improved behavior already. For those who want to learn – well, it’s not rocket science but read on.

The first part is not so very interesting – just your basic setting up of the behavior, the wiring up and a wiring down of the events. The only interesting things happen in AssociatedObjectLoaded:

using System;
using System.Linq;
using Windows.UI.Xaml;
using WinRtBehaviors;
using System.Reflection;
using Win8nl.External;
using Windows.UI.Xaml.Media.Animation;
using System.Reactive.Linq;

namespace Win8nl.Behaviors
{
  public class StartStoryboardBehavior : Behavior<FrameworkElement>
  {
    protected override void OnAttached()
    {
      base.OnAttached();
      AssociatedObject.Loaded += AssociatedObjectLoaded;
    }
    
    protected override void OnDetaching()
    {
      AssociatedObject.Loaded -= AssociatedObjectLoaded;
      base.OnDetaching();
    }

    private void AssociatedObjectLoaded(object sender, RoutedEventArgs e)
    {
      if (StartImmediately)
      {
        StartStoryboard();
      }

      if (!string.IsNullOrWhiteSpace(EventName))
      {
        var evt = AssociatedObject.GetType().GetRuntimeEvent(EventName);
        if (evt != null)
        {
          Observable.FromEventPattern<RoutedEventArgs>(AssociatedObject, EventName)
            .Subscribe(se => StartStoryboard());
        }
      }
    }
  }
}

If StartImmediately is true, the storyboard is fired immediately. If not, the behavior tries to find an event with the name supplied in the EventName property and tries to attach a dynamic handler to it using Rx. Nothing new here, I already described this technique earlier - in the article where I announced this forgotten behavior :-)

More interesting is this funny little method, that’s basically one big Linq statement:

private Storyboard GetStoryBoardInVisualDescendents(FrameworkElement f)
{
  return f.GetVisualDescendents()
    .Where(p => p.Resources.ContainsKey(Storyboard) && p.Resources[Storyboard] is Storyboard)
    .Select(p => p.Resources[Storyboard] as Storyboard).FirstOrDefault();
}

It checks in all visual children’s resources for a storyboard with the name supplied in “Storyboard”, and if so, it selects it. It uses the extension method GetVisualDescendents to generate a list of all the children – all the way to the bottom of the visual tree from the element in “f”.

As for the behavior’s default behavior (meh), it searches top-down for the storyboard, using this method:

private void StartStoryboardTopDown()
{
  var root = AssociatedObject.GetVisualAncestors().Last() ?? AssociatedObject;

  var storyboard = GetStoryBoardInVisualDescendents(root);
  if (storyboard != null)
  {
    storyboard.Begin();
  }
}

In a nutshell: find the very last top object in the visual tree – that is the root. If that’s null, apparently the current associated object already is the root. Then it starts to search downward for the storyboard and if it finds it, it will fire it.

For the bottom-up strategy, another method is employed, with a very original name:

private void StartStoryboardBottomUp()
{
  var root = AssociatedObject;
  Storyboard storyboard;
  do
  {
    storyboard = GetStoryBoardInVisualDescendents(root);
    if (storyboard == null)
    {
      root = root.GetVisualParent();
    }
  } while (root != null && storyboard == null);

  if (storyboard != null)
  {
    storyboard.Begin();
  }
}

This method starts by trying to find the storyboard in the visual tree below the current associated object. If it doesn’t find it, in moves one level up and tries again. And another one. Till it a) finds the storyboard or b) runs out of levels. If a storyboard is found, then it fires it. Okay, so that’s a little inefficient, but it only happens upon firing the event.

The final pieces of the puzzle is StartStoryboard, which simply selects the method based upon the SearchTopDown value.

private void StartStoryboard()
{
  if( SearchTopDown)
  {
    StartStoryboardTopDown();
  }
  else
  {
    StartStoryboardBottomUp();
  }
}

And that’s all there is to it. I’ve omitted the declaration of the four (attached dependency) properties as that only bulks up this post with no actual value, and the complete listing can be found here on CodePlex if that helps you to get overview.

Screenshot8For those who love to see the behavior in action I created a little sample app, which shows a few items in a ListBox. At the end of each row there’s a button, and if you click that the color of three texts to the left is animated from black to red in about a second. That’s a storyboard being fired by the behavior. Very exiting ;-) - but it proves the point.

Enjoy! I hope this helps you with your Windows 8 development! Global Availability is coming, get cracking to get your stuff in the store!

No comments: