Wintellect  

Browse by Tags

All Tags » storyboard   (RSS)

We often trip over ourselves trying to minimize code behind and abstract behaviors in the UI from the models, etc. This is important for clean separation, but sometimes behaviors may add too much abstraction. The real fact is many applications require some sort of transition or animation based on events, and while we can try to put as many of those as possible into the VisualStateManager, there may be instances such as dynamically created animations or special triggers that end up involving the view model somehow.

What I'm proposing here is a very simple solution to allowing your view model to fire and respond to animations without having to know about those animations. The view model knows it has to fire some event, and may want notification when the event is done, but how that event is implemented is up to you. This way, the live version can contain Storyboard objects while the unit tests can contain mocks.

First, let's create a simple interface that our view model can talk to. It simply begins the animation and provides a callback when the animation is complete. (If you like, you can make it even more generic and call it a transition or an event).

public interface IAnimationDelegate
{
    void BeginAnimation(Action animationComplete);
}

That's pretty simple. Now, in my view model, I can fire the event and react. In this totally contrived example, whenever our first value changes, we fire a transition and only when the transition is complete, we move it with some changes to a second value.

public class MyViewModel : BaseNotify 
{
   public IAnimationDelegate Value1Transition { get; set; }

   private string _value1; 

   public string Value1 
   { 
      get { return _value1; } 
      set {
              _value1 = value;
              Value1Transition.BeginAnimation(_Value1Transitioned);
              RaisePropertyChanged("Value1"); 
          }
    }

    private string _value2; 

    public string Value2 
    {
       get { return _value2; }
       set {
             _value2 = value; 
             RaisePropertyChanged("Value2");
           }       
    }

    private void _Value1Transitioned()
    {
       Value2 = "This was the first value: " + Value1; 
    }
}    

The implementation of the animation delegate class might look like this:

public class AnimationDelegate : IAnimationDelegate
{
    private readonly Storyboard _storyboard;

    private Action _animationComplete;

    public AnimationDelegate(Storyboard storyboard)
    {
        _storyboard = storyboard;
        _storyboard.Completed += StoryboardCompleted;
    }

    void StoryboardCompleted(object sender, EventArgs e)
    {
        if (_animationComplete != null)
        {
            _animationComplete();
        }
    }

    public void BeginAnimation(Action animationComplete)
    {
        _animationComplete = animationComplete;
        _storyboard.Begin();
    }
}

We could unregister the event when completed, add other parameters to stop it when completed, etc, but this is a good staring point.

Now I simply need to wire the delegate. If my animation is called AnimateValue1 then with the Managed Extensibility Framework (MEF), I'd do something like this in the code behind for my control:

[Import]
public MyViewModel ViewModel
{
   get {  return LayoutRoot.DataContext as MyViewModel; }
   set {
          value.Value1Transition = new AnimationDelegate(AnimateValue1); 
          LayoutRoot.DataContext = value;          
       }
}

... and there you have it.

Jeremy Likness

OK, so we've been through quite a series of iterations and refactoring for something simple: triggering a story board animation based on an event somewhere else. We used dependency and attached properties, we used custom behaviors, and now we're going to do it completely "out of the box."

I figured this one would be easiest to show as a video, so this is the first one I've posted to my blog. I don't have good audio so I used the "notepad" technique. What I'm showing is how to take a storyboard and trigger it based on an event fired by something else, using only what comes built-in with Blend. There is no code-behind and in fact the only XAML I edit is for the list boxes (sorry, I know there's a data tab but I still code HTML in notepad).

Here it is:

Click to View Video

Jeremy Likness

One of the most powerful benefits of Silverlight is that it uses the DependencyProperty model. Using this model, you can create attached properties to describe reusable behaviors and attach those behaviors to certain elements.

An example of this is firing animations in the UI elements. One criticism of Silverlight has been the lack of support for the range of triggers that Windows Presentation Foundation (WPF) supports. The main "trigger" you can tap into is the Loaded event for a control. This makes it difficult to define triggers for events such as data binding and/or UI events.

It only takes a little bit of magic using the DependencyProperty system, however, to create those triggers yourself.

A behavior is a reusable "action" that can be attached to a control. A trigger is an event that causes something to happen. Imagine having a list that is bound to a list box. Your view model contains the list of top level entities. When a selection item is clicked on, a grid expands (using a Storyboard animation) that contains details for the selected item.

Using Silverlight's advanced databinding features, everything but the animation is straightforward. You can bind the ListBox directly to the list of objects:

...
<ListBox x:Name="ObjectListBox" ItemsSource="{Binding ObjectList}"
   DisplayMemberPath="Name"/>
...

The grid can then automatically databind to the selected object:

...
<Grid DataContext="{Binding ElementName=ObjectListBox,Path=SelectedItem}"/>
...

Now your UI will work like magic - provide your ObjectList, and then click to see the details "magically" appear in the grid. But how do we get the grid to explode? This is where I've seen a lot of attempts to do things like bind collections and triggers to the view model. While I understand the view model is a go-between data and the view, I still think knowing about animations in some cases is too much information for the view model.

I'm not really trying to drive anything from the data. I'm driving a UI behavior with a UI trigger, so why can't I keep all of this cleanly in the XAML, without involving a view model at all? As it turns out, I can.

To understand how to create this, we first need to understand the behavior and the trigger.

The behavior is to kick off a Storyboard. In our case, the storyboard will simply "explode" the grid using a scale transform:

<Grid.Resources>
    <Storyboard x:Name="GridExplode">
        <DoubleAnimation Storyboard.TargetName="TransformSetting" Storyboard.TargetProperty="ScaleX" 
   From="0" To="1.0" Duration="0:0:0.3"/>
        <DoubleAnimation Storyboard.TargetName="TransformSetting" Storyboard.TargetProperty="ScaleY" 
   From="0" To="1.0" Duration="0:0:0.3"/>
    </Storyboard>
</Grid.Resources>
<Grid.RenderTransform>
    <ScaleTransform x:Name="TransformSetting" ScaleX="1.0" ScaleY="1.0"/>
</Grid.RenderTransform>

Now we have a nice behavior, but short of the event trigger provided by Silverlight at load time, we have no easy way to fire it off. Our trigger is the SelectionChanged event on the ListBox. Normally, we would throw the event into the XAML:

...
<ListBox SelectionChanged="ObjectListBox_SelectionChanged"/>
...

Then go into our code behind and kick off the animation:

private void ObjectListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
   GridExplode.Begin();
}

So now that we know the behavior and the trigger, let's try a different way to accomplish it.

I'm going to create a host class for my storyboard triggers and call it, aptly, StoryboardTriggers. The class is static because it exists solely to help me manage my dependency properties. First, we'll want to keep a collection of storyboards that are participating in our new system. We will let the user assign a (hopefully globally unique) key to the Storyboard. This is different from the x:Name because it will be reused throughout the system.

public static class StoryboardTriggers
{
    private static readonly Dictionary<string, Storyboard> _storyboardCollection = new Dictionary<string, Storyboard>();
}

Two steps are required. First, we need to register the storyboard with our collection, so that it is available to manipulate. I like to go ahead and wire in the Completed event to stop the animation so that it can be reused.


public static string GetStoryboardKey(DependencyObject obj)
{
    return obj.GetValue(StoryboardKeyProperty).ToString();
}

public static void SetStoryboardKey(DependencyObject obj, string value)
{
    obj.SetValue(StoryboardKeyProperty, value);
}

public static readonly DependencyProperty StoryboardKeyProperty =
    DependencyProperty.RegisterAttached("StoryboardKey", typeof(string), typeof(StoryboardTriggers),
                                        new PropertyMetadata(null, StoryboardKeyChanged));

public static void StoryboardKeyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
    Storyboard storyboard = obj as Storyboard;
    if (storyboard != null)
    {
        if (args.NewValue != null)
        {
            string key = args.NewValue.ToString();
            if (!_storyboardCollection.ContainsKey(key))
            {
                _storyboardCollection.Add(key, storyboard);
                storyboard.Completed += _StoryboardCompleted;
            }
        }               
    }
}

static void _StoryboardCompleted(object sender, System.EventArgs e)
{
   ((Storyboard)sender).Stop();
}

This is the standard way to declare a new dependency property. The dependency property itself is registered and owned by our static class. Methods are provided to get and set the property. We also tap into the changed event (and we're assuming here that we'll only be attaching the property) and use that event to load the storyboard into our collection.

Now allowing a storyboard to participate in our trigger system is easy, we simply add a reference to the class (I'll call it Behaviors), and then attach the property. Our storyboard now looks like this:

...
<Storyboard x:Name="GridExplode" Behaviors:StoryboardTriggers.StoryboardKey="GridExplodeKey">
...

Note I've given it our "key" of GridExplodKey. Next, let's create a trigger! We want the selection change event to fire the grid. Instead of just writing it for our particular case, we can use the primitive Selector and make the trigger available to any control that exposes the SelectionChanged event. All we want to do is take one of those controls, and set a trigger to the storyboard we want to fire. We do this in our behavior class like this:

public static string GetStoryboardSelectionChangedTrigger(DependencyObject obj)
{
    return obj.GetValue(StoryboardSelectionChangedTriggerProperty).ToString();
}

public static void SetStoryboardSelectionChangedTrigger(DependencyObject obj, string value)
{
    obj.SetValue(StoryboardSelectionChangedTriggerProperty, value);
}

public static readonly DependencyProperty StoryboardSelectionChangedTriggerProperty =
    DependencyProperty.RegisterAttached("StoryboardSelectionChangedTrigger", typeof(string), typeof(StoryboardTriggers),
                                        new PropertyMetadata(null, StoryboardSelectionChangedTriggerChanged));

public static void StoryboardSelectionChangedTriggerChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
    Selector selector = obj as Selector;
    if (selector != null)
    {
        if (args.NewValue != null)
        {
            selector.SelectionChanged += _SelectorSelectionChanged;
        }
    }
}

static void _SelectorSelectionChanged(object sender, SelectionChangedEventArgs e)
{
    string key = GetStoryboardSelectionChangedTrigger((DependencyObject) sender);
    if (_storyboardCollection.ContainsKey(key))
    {
        _storyboardCollection[key].Begin();
    }
}

That's it. When the property is set, we set a handler for the SelectionChanged event. When the selection changed event fires, we get the key from the dependency property, look it up in the master list, and, if it exists, kick off the animation. The property is attached like this:

...
<ListBox x:Name="ServiceList" Behaviors:StoryboardTriggers.StoryboardSelectionChangedTrigger="GridExplodeKey"/>

That's all there is to it! Now we have bound the trigger (selection changed) to the behavior (kick off the animation) and can leave the code-behind and view models completely out of the equation.

For a more full implementation of this, you would want to also handle the event of clearing or detaching the property and remove the event handler from the bound object. (Don't forget your data contract as well ... the methods for attaching, getting, setting, etc should check the parameters and types; I've left that out for brevity here.) You can create other triggers as well and attach those just as easily and even parse multiple values to allow multiple bindings. Once you start thinking in terms of triggers and behaviors within Silverlight, anything truly is possible.

Jeremy Likness