Click here to Skip to main content
15,891,677 members
Articles / Programming Languages / C#

Silverlight Behaviors and Triggers: Storyboard Trigger Example

Rate me:
Please Sign up or sign in to vote.
3.67/5 (2 votes)
6 Oct 2009CPOL5 min read 24.6K   13  
One of the most powerful benefits of Silverlight is that it uses the DependencyProperty model. Using this model, you can have great attached properties to describe reusable behaviors and attach those behaviors to certain elements.

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:

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

The grid can then automatically databind to the selected object:

XML
...
<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:

XML
<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:

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

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

C#
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.

C#
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.

C#
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:

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

Note I've given it our "key" of GridExplodeKey. 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:

C#
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:

XML
...
<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 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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Program Manager Microsoft
United States United States
Note: articles posted here are independently written and do not represent endorsements nor reflect the views of my employer.

I am a Program Manager for .NET Data at Microsoft. I have been building enterprise software with a focus on line of business web applications for more than two decades. I'm the author of several (now historical) technical books including Designing Silverlight Business Applications and Programming the Windows Runtime by Example. I use the Silverlight book everyday! It props up my monitor to the correct ergonomic height. I have delivered hundreds of technical presentations in dozens of countries around the world and love mentoring other developers. I am co-host of the Microsoft Channel 9 "On .NET" show. In my free time, I maintain a 95% plant-based diet, exercise regularly, hike in the Cascades and thrash Beat Saber levels.

I was diagnosed with young onset Parkinson's Disease in February of 2020. I maintain a blog about my personal journey with the disease at https://strengthwithparkinsons.com/.


Comments and Discussions

 
-- There are no messages in this forum --