Wintellect  

Browse by Tags

All Tags » behavior   (RSS)

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

The WeakReference is one of those fun interview topics. Some people have never heard of it and when they learn about it will say, "That's a textbook question ... why would I ever use a Weak Reference?"

A weak reference references an object while still allowing that object to be reclaimed by garbage collection.

Why would you want to do that?

A common example you might see on the web is for a cache. For example, consider implementing functionality that relies on a service call for some information. You wouldn't mind holding onto it for awhile and avoiding the network round trip, but you also don't want it to fill up memory. Something like this might do the trick:

public class MyContainer 
{
    private WeakReference _myCachedObject; 

    public MyObject GetMyObject() 
    {
        if (_myCachedObject.IsAlive) 
        {
           return _myCachedObject.Target as MyObject;
        }
        else 
        {
            MyObject retVal = _FetchObjectFromService(); 
            _myCachedObject = new WeakReference(retVal, false);
            return retVal;
        }
    }
}

Of course, I've left out the implemenation of fetching this, but you get the point. While that is an interesting example, how about a more practical one? One of the most common causes of memory leaks in Silverlight/WPF applications is related to the registering and unregistering of events. To learn more about this memory leak (and also learn how to troubleshoot/debug/and find the source of the leak), read this article about finding memory leaks in Silverlight.

What makes these types of issues so inviting is the entire dependency property system. It is tempting, for example, to do something like I demonstrated in an earlier post about Silverlight Behaviors and Triggers.

In the post, I demonstrated a way to register story boards and then fire them using behaviors. I stored the collection of storyboards to trigger in a dictionary, like this:


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

This creates a mapping between a key and the Storyboard instance. The problem is that this static class stays in scope through the duration of the Silverlight application. This means that the storyboards will never go out of scope! For a larger application, this could become a serious issue. When the control with the storyboard goes out of scope, the runtime should be allowed to release that memory. In this case, the reference in the dictionary will ensure that the storyboard object is never garbage collected (nor anything else in the object graph of the storyboard).

How do we fix this? This is where WeakReference comes to the rescue! Instead of keeping strong references to the Storyboard, we'll use WeakReferences instead. As long as the storyboard remains in scope, the WeakReference will contain a valid pointer to the class. If it goes out of scope, however, our weak reference won't keep it from being garbage collected and will allow it to gracefully "degrade" into the garbage compactor. The refactored code looks like this (focusing on the changes to the existing code as published in my previous post):


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

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, new WeakReference(storyboard,false));
                storyboard.Completed += _StoryboardCompleted;
            }
        }               
    }
}

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

And the trigger itself simply needs to check to see if the storyboard is still in scope:


static void _SelectorSelectionChanged(object sender, SelectionChangedEventArgs e)
{
    string key = GetStoryboardSelectionChangedTrigger((DependencyObject) sender);
    if (_storyboardCollection.ContainsKey(key))
    {
        Storyboard storyboard = _storyboardCollection[key].Target as Storyboard;
        if (storyboard != null)  
        {
           storyboard.Begin();
        }
        else 
        {
           _storyboardCollection.Remove(key);
        }                  
    }
}

Now we keep our application clean and allow objects to degrade as needed.

Jeremy Likness

We have explored using dependency properties and attached properties to abstract behaviors and triggers. In my last article, TextBox Magic, I showed how to create a dependency property to enable a textbox filter that would prevent anything but digits. In this article, we'll take it a step further and turn it into a "true" behavior.

A "true" behavior inherits from Behavior. The first step is to add a reference to System.Windows.Interactivity. There are a number of nice things about using the behavior system that make them more flexible to use than basic dependency properties. First, the behaviors can integrate with Expression Blend. You can define a behavior, import it into the tool, and literally drag it onto a control to attach the behavior. Second, behaviors automatically provide overrides to tap into the attach/detatch events. Third, behaviors are typed to their target controls: you may have a generic behavior that targets FrameworkElement or a specific behavior that targets TextBox. Finally, we can decorate behaviors with attributes that are discovered by Intellisense and available when attaching the behavior.

We are going to create a filter behavior. The first step is to simply inherit from the base behavior class and then override the pertinent events:

public class TextBoxFilterBehavior : Behavior<TextBox>
{
   protected override void OnAttached()
   {
       AssociatedObject.KeyDown += _TextBoxFilterBehaviorKeyDown;
   }

   protected override void OnDetaching()
   {
       AssociatedObject.KeyDown -= _TextBoxFilterBehaviorKeyDown; 
   }
}

As you can see, we have a convenient place to hook into the KeyDown event and unhook at the end. We also have a nice, typed AssociatedObject to use and manipulate.

We'll extend our filter to handle alpha (alphanumeric and spaces), positive numeric digits, and regular numeric digits. For the regular numeric digits, we'll allow the subtract/minus sign only if it's the very first character in the text. The "helper" methods look like this:

private static readonly List _controlKeys = new List<Key>
                                                     {
                                                         Key.Back,
                                                         Key.CapsLock,
                                                         Key.Ctrl,
                                                         Key.Down,
                                                         Key.End,
                                                         Key.Enter,
                                                         Key.Escape,
                                                         Key.Home,
                                                         Key.Insert,
                                                         Key.Left,
                                                         Key.PageDown,
                                                         Key.PageUp,
                                                         Key.Right,
                                                         Key.Shift,
                                                         Key.Tab,
                                                         Key.Up
                                                     };

private static bool _IsDigit(Key key)
{
    bool shiftKey = (Keyboard.Modifiers & ModifierKeys.Shift) != 0;
    bool retVal;
    if (key >= Key.D0 && key <= Key.D9 && !shiftKey)
    {
        retVal = true;
    }
    else
    {
        retVal = key >= Key.NumPad0 && key <= Key.NumPad9;
    }
    return retVal;
}

static bool _HandleNumeric(string text, Key key)
{
    bool handled = true;
    
    // if empty, allow minus - only can be first character
    if (string.IsNullOrEmpty(text.Trim()) && key.Equals(Key.Subtract))
    {
        handled = false;
    }
    else if (_controlKeys.Contains(key) || _IsDigit(key))
    {
        handled = false;
    }

    return handled;
}

static bool _HandlePositiveNumeric(Key key)
{
    bool handled = true;

    if (_controlKeys.Contains(key) || _IsDigit(key))
    {
        handled = false;
    }

    return handled;
}

static bool _HandleAlpha(Key key)
{
    bool handled = true;

    if (_controlKeys.Contains(key) || _IsDigit(key) || key.Equals(Key.Space) ||
        (key >= Key.A && key <= Key.Z))
    {
        handled = false;
    }

    return handled;
}

public enum TextBoxFilterType
{
    AlphaFilter,
    PositiveNumericFilter,
    NumericFilter
}

public TextBoxFilterType FilterType { get; set; }

As you can see, the familiar list of "friendly" keys is carried over. There are some helper methods for our different types of filters, as well as an enumeration. The enumeration gives us the flexibility of determining which filter to use and then a property is exposed to set that enumeration.

The last step is to wire in the actual event:

void _TextBoxFilterBehaviorKeyDown(object sender, KeyEventArgs e)
{
    switch(FilterType)
    {
        case TextBoxFilterType.AlphaFilter:
            e.Handled = _HandleAlpha(e.Key);
            break;

        case TextBoxFilterType.NumericFilter:
            e.Handled = _HandleNumeric(AssociatedObject.Text, e.Key);
            break;

        case TextBoxFilterType.PositiveNumericFilter:
            e.Handled = _HandlePositiveNumeric(e.Key);
            break;
    }
}

As you can see, it's as simple as checking the enumeration, calling the helper function and modifying the handled property.

Now we can reference the project with our behavior and reference the System.Windows.Interactivity. In XAML, you attach the behavior like this:

<TextBox>
   <Interactivity:Interaction.Behaviors>
      <Behaviors:TextBoxFilterBehavior FilterType="AlphaFilter"/>
   </Interactivity:Interaction.Behaviors>
</TextBox>

As you can see, easy to add, easy to read quickly what the behavior does, and the added bonus is that you can manipulate these in Expression Blend.

Jeremy Likness

Edit 10/16/2009: Thanks to all who pointed out the bug in the detach event ... it's been fixed.

So far we've explored how to use dependency properties and attached properties to create reusable behaviors and triggers. I showed you recently how to refactor an attached property to use the Behavior base class instead. Today, we'll look at the TriggerAction that is also available in System.Windows.Interactivity (either as a part of Expression Blend, or available through the Blend SDK).

If you recall in TextBox Magic, I used an attached property to define an attribute that, when set to true, would bind to the TextChanged event and force data-binding to allow validation and binding without having to lose focus from the text box.

Because the action is tied to an event, it makes sense to refactor as a trigger action.

The first step is simply to build a class based on TriggerAction which requires adding a reference to System.Windows.Interactivity. Like the Behavior class, the TriggerAction can be strongly typed, so we will type it to the TextBox:

public class TextBoxBindingTrigger : TriggerAction<TextBox>
{
}

The method you must override is the Invoke method. This will be called when the action/event the trigger is bound to fires. If you recall from the previous example, we simply need to grab a reference to the binding and force it to update:

protected override void Invoke(object parameter)
{
    BindingExpression binding = AssociatedObject.GetBindingExpression(TextBox.TextProperty);
    if (binding != null)
    {
        binding.UpdateSource();
    }
}

That's it! We now have a trigger action that is ready to force the data binding. Now we just need to implement it with a text box and bind it to the TextChanged event. In Expression Blend, the new action appears in the Behaviors section under assets.

Behavior in Expression Blend

You can click on the trigger and drag it onto an appropriate UI element. Because we typed our trigger to TextBox, Blend will only allow you to drag it onto a TextBox. Once you've attached the trigger to a UI action, you can view the properties and set the appropriate event. In this case, we'll bind to the TextChanged event.

Trigger Properties in Expression Blend

Notice how the associated element defaults to the parent, but can be changed in Blend to point to any other TextBox available as well.

Of course, all of this can be done programmatically or through XAML as well. To add the trigger in XAML, simply reference both System.Windows.Interactivity as well as the namespace for your trigger. Then, you can simply add to the TextBox lik this:

...
<TextBox 
    Text="{Binding Name, Mode=TwoWay, NotifyOnValidationError=true, ValidatesOnExceptions=true}" 
    Grid.Row="0" Height="30" Grid.Column="0" Margin="5" Width="200">
 <i:Interaction.Triggers>
  <i:EventTrigger EventName="TextChanged">
   <local:TextBoxBindingTrigger/>
  </i:EventTrigger>
 </i:Interaction.Triggers>
</TextBox>

As you can see, this is a much more elegant way to create behaviors attached to events as it allows you to easily attach the trigger as well as define the event that the trigger is coupled with.

Jeremy Likness

The TextBox control is popular in Silverlight but comes with a few nuances. For example, the default behavior is that the data is not bound until the control loses focus! This can be awkward when you have a form with a disabled save button. The save button won't enable until the text box contains content, but it won't "know" about the content until the box loses focus, so the user is forced to tab from the last field before they can save.

I will cover two topics specific to the TextBox: the first is a behavior and trigger that resolve the issue with the "late data binding." The second is a filter behavior that allows you to control exactly what goes into the text box.

Let's get started! As you recall, the recipe for creating a new attached property is to declare the properties on a static class and register them as an attached property. We're going to start with the filter. As you know, the TextBox control itself takes any type of character input. A numeric field can be validated which prevents it from binding to the object, but why let the user type anything but a digit in the first place? Prevention is the best medicine, so why not filter the field?

We'll start with a class called TextBoxFilters to hold our attached properties specific to the text box. To filter the text box, we want to hook into the KeyDown event, check to see if it's a key we like, and set the event to "handled" if we don't want it (this prevents it from bubbling up and making it into the form). Of course, we have to be careful because we're not just checking for digits. Users must be able to navigate into and out of the field using TAB or fix their input using BACK.

The first part of the class is here. We create a convenient list of key presses that should always be allowed, then provide a method that tells us if it is a digit. Notice that we need to check the presence of the SHIFT key because Key.D1 becomes an exclamation mark when the SHIFT key is pressed.

public static class TextBoxFilters
{
    private static readonly List<Key> _controlKeys = new List<Key>
                                                            {
                                                                Key.Back,
                                                                Key.CapsLock,
                                                                Key.Ctrl,
                                                                Key.Down,
                                                                Key.End,
                                                                Key.Enter,
                                                                Key.Escape,
                                                                Key.Home,
                                                                Key.Insert,
                                                                Key.Left,
                                                                Key.PageDown,
                                                                Key.PageUp,
                                                                Key.Right,
                                                                Key.Shift,
                                                                Key.Tab,
                                                                Key.Up
                                                            };

    private static bool _IsDigit(Key key)
    {
        bool shiftKey = (Keyboard.Modifiers & ModifierKeys.Shift) != 0;
        bool retVal;
        if (key >= Key.D0 && key <= Key.D9 && !shiftKey)
        {
            retVal = true;
        }
        else
        {
            retVal = key >= Key.NumPad0 && key <= Key.NumPad9;
        }
        return retVal; 
    }
}

Next, we need to build our dependency properties. We'll provide a getter and setter and the registration. When the property is set, we'll check to see if the control is a TextBox. If it is, we hook into the KeyDown event and suppress it. The code looks like this:


public static bool GetIsPositiveNumericFilter(DependencyObject src)
{
    return (bool) src.GetValue(IsPositiveNumericFilterProperty);
}

public static void SetIsPositiveNumericFilter(DependencyObject src, bool value)
{
    src.SetValue(IsPositiveNumericFilterProperty, value);
}

public static DependencyProperty IsPositiveNumericFilterProperty = DependencyProperty.RegisterAttached(
    "IsPositiveNumericFilter", typeof (bool), typeof (TextBoxFilters),
    new PropertyMetadata(false, IsPositiveNumericFilterChanged));

public static void IsPositiveNumericFilterChanged(DependencyObject src, DependencyPropertyChangedEventArgs args)
{
    if (src != null && src is TextBox)
    {
        TextBox textBox = src as TextBox; 
        
        if ((bool)args.NewValue)
        {
            textBox.KeyDown += _TextBoxPositiveNumericKeyDown;
        }
    }
}

static void _TextBoxPositiveNumericKeyDown(object sender, KeyEventArgs e)
{
    bool handled = true;

    if (_controlKeys.Contains(e.Key) || _IsDigit(e.Key))
    {
        handled = false;
    }

    e.Handled = handled;
}

That's it ... now we have a handy attached behavior. If you include the namespace in your XAML and refer to it as Behaviors, you'll be able to attach the property to a text box like this:



<TextBox DataContext="{Binding MyNumericField}" Behaviors:TextBoxFilters.IsPositiveNumericFilter="True"/>

That's it - just attach the behavior, fire up your application, and try to put anything but a digit into your text box!

Now that we know how to attach that behavior, wouldn't it be nice to have some instant validation and databinding as well? This isn't difficult at all with dependency properties. All you need to do is hook into the event that is fired when the text changes, and force an update to the binding. The code for that behavior looks like this:

public static bool GetIsBoundOnChange(DependencyObject src)
{
    return (bool) src.GetValue(IsBoundOnChangeProperty);
}

public static void SetIsBoundOnChange(DependencyObject src, bool value)
{
    src.SetValue(IsBoundOnChangeProperty, value);
}

public static DependencyProperty IsBoundOnChangeProperty = DependencyProperty.RegisterAttached(
    "IsBoundOnChange", typeof(bool), typeof(TextBoxFilters),
    new PropertyMetadata(false, IsBoundOnChangeChanged));

public static void IsBoundOnChangeChanged(DependencyObject src, DependencyPropertyChangedEventArgs args)
 {
     if (src != null && src is TextBox)
     {
         TextBox textBox = src as TextBox;

         if ((bool)args.NewValue)
         {
             textBox.TextChanged += _TextBoxTextChanged;
         }
     }
 }

static void _TextBoxTextChanged(object sender, TextChangedEventArgs e)
 {
     if (sender is TextBox)
     {
         TextBox tb = sender as TextBox;
         BindingExpression binding = tb.GetBindingExpression(TextBox.TextProperty);
         if (binding != null)
         {
             binding.UpdateSource();
         }
     }
 }

As you can see, we hook into the text changed event, grab the binding and force it to update. Now you will have instant feedback. Of course, all validation rules are followed so if the binding causes a validation error, your validation will display instantly and the underlying objects will not be databound.

The new "super" text box that is filtered and binds as the text changes looks like this:


<TextBox DataContext="{Binding MyNumericField}" Behaviors:TextBoxFilters.IsPositiveNumericFilter="True"
   Behaviors:TextBoxfilters.IsBoundOnChange="True"/>

That's all there is too it ... keep your users honest by preventing them from typing something they don't need.

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