다음을 통해 공유


A Custom Storyboard Component in Xaml

In WPF & Silverlight, a Storyboard is a collection of animations running in parallel.  Not everyone likes the name “Storyboard” though.  The idea behind the name was that that list of timelines running in parallel are like a list of plot lines in the plan for a movie.

In any case, just like you can package up a tree of elements into a re-usable UserControl component, it’s nice to be able to package up a tree of timelines into a re-usable Storyboard component.  Here’s an approach to do just that, using Xaml, and made simpler by using a new Xaml feature in WPF4.

For example, here’s a Storyboard that puts three animations on a Rectangle:

 <Grid>

    <Grid.Resources>
        <BackEase Amplitude="0.5" x:Key="backEase" />
    </Grid.Resources>

    <Grid.Triggers>
        <EventTrigger RoutedEvent="Grid.Loaded">
            <BeginStoryboard>
                <Storyboard TargetName="Rectangle1">
                    <DoubleAnimation Storyboard.TargetProperty="Width"
                                     From="0"
                                     EasingFunction="{StaticResource backEase}"
                                     Duration="0:0:1" />

                    <DoubleAnimation Storyboard.TargetProperty="Height"
                                     From="0"
                                     EasingFunction="{StaticResource backEase}"
                                     Duration="0:0:1" />

                    <ColorAnimation Storyboard.TargetProperty="Fill.Color"
                                    From="LightGray"
                                    Duration="0:0:1" />
                </Storyboard>

            </BeginStoryboard>
        </EventTrigger>
    </Grid.Triggers>

    <Rectangle Name="Rectangle1" Width="100" Height="100" Fill="Red" />

</Grid>    

The goal is to package up this Storyboard into a re-usable component that I can use on any Rectangle that has a SolidColorBrush Fill.  To do that, I’ll just move that <Storyboard> markup into a separate Xaml file and parameterize it.

Unfortunately there’s no single menu item for this e.g. in Visual Studio, but it’s not hard to do.  What I do is right click on the project, “Add”, “User Control”, name it something like “MyRectangleStoryboard.xaml”, and get something like this:

 <UserControl x:Class="CustomStoryboard.MyRectangleStoryboard"
    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml" >
    <Grid>
            
    </Grid>
</UserControl>

… and this:

 public partial class MyRectangleStoryboard : UserControl
{
    public MyRectangleStoryboard()
    {
        InitializeComponent();
    }
}

Now, just change “UserControl” to “Storyboard (and remove the <Grid> from the Xaml), and you’re all set:

 <Storyboard x:Class="CustomStoryboard2.MyRectangleStoryboard"
    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml" >
    
</Storyboard> 
 public partial class MyRectangleStoryboard : Storyboard
{
    public MyRectangleStoryboard()
    {
        InitializeComponent();
    }
} 

The goal is to use this component instead of the storyboard/animation markup:

 <Grid>
    <Grid.Resources>
        <BackEase Amplitude="0.5" x:Key="backEase" />
    </Grid.Resources>

    <Grid.Triggers>
        <EventTrigger RoutedEvent="Grid.Loaded">
            <BeginStoryboard>
                <l:MyRectangleStoryboard 
                             TargetName="Rectangle1" 
                             Duration="0:0:1" 
                             EasingFunction="{StaticResource backEase}"/>
            </BeginStoryboard>
        </EventTrigger>
    </Grid.Triggers>

    <Rectangle Name="Rectangle1" Width="100" Height="100" Fill="Red" />

</Grid>

As for the implementation of the MyRectangleStoryboard here’s the markup, which is cut/paste from the original with a couple of modifications for parameterization (which I’ll explain):

 <Storyboard x:Class="CustomStoryboard.MyRectangleStoryboard"
             xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:l="clr-namespace:CustomStoryboard" >

    <DoubleAnimation Storyboard.TargetProperty="Width"
                     From="0"
                     Duration="{Binding Duration, Source={l:XRoot}}"
                     EasingFunction="{Binding EasingFunction, Source={l:XRoot}}" />

    <DoubleAnimation Storyboard.TargetProperty="Height"
                     From="0"
                     Duration="{Binding Duration, Source={l:XRoot}}"
                     EasingFunction="{Binding EasingFunction, Source={l:XRoot}}" />

    <ColorAnimation Storyboard.TargetProperty="Fill.Color"
                    From="LightGray"
                    Duration="{Binding Duration, Source={l:XRoot}}" />

    
</Storyboard>

Notice that this custom storyboard is parameterized for the setting of the Duration and the EasingFunction properties on the animations.  (Recall that these two properties were set when the MyRectangleStoryboard was used above).

Ordinarily, if you set the Duration on the storyboard it doesn’t inherit down to its children timelines, instead it clips its children timelines.  But I want the MyRectangleStoryboard.Duration to actually be the Double/ColorAnimation.Duration, so I bound the Animation.Duration properties to the root’s Duration property. 

The source of this binding is a custom “XRoot” markup extension that I wrote.  You could equivalently establish this binding in code, and in fact you pretty much have to today.  But in WPF4 (which is now in beta), you can write a custom MarkupExtension to access the root of a Xaml document, using the new IRootObjectProvider service.  Here’s the implementation:

 public class XRoot : MarkupExtension
{
    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        var rootProvider = serviceProvider.GetService(typeof(IRootObjectProvider)) as IRootObjectProvider;
        if (rootProvider == null)
            return null;

        return rootProvider.RootObject;
    }
}

The other parameterization on this MyRectangleStoryboard component is the EasingFunction.  This is a custom property on MyRectangleStoryboard so that I can set it any time I use it.  The implementation is just a DependencyProperty:

 public partial class MyRectangleStoryboard : Storyboard
{
    public MyRectangleStoryboard()
    {
        InitializeComponent();
    }

    public EasingFunctionBase EasingFunction
    {
        get { return (EasingFunctionBase)GetValue(EasingFunctionProperty); }
        set { SetValue(EasingFunctionProperty, value); }
    }

    public static readonly DependencyProperty EasingFunctionProperty =
        DependencyProperty.Register("EasingFunction", typeof(EasingFunctionBase), typeof(MyRectangleStoryboard));

}

So in the end there’s three takeaways from this:

  • You can create powerful custom animation sets by subclassing Storyboard.
  • Xaml is a useful component definition language, it’s not just for controls.
  • In WPF4, Xaml markup extensions can get explicit access to the lexical root of the Xaml document.