Wpf – How to set WPF animation BeginTime based on properties (Top/Tabstop) of the control

animationexpression-blendwpfxaml

I've got a WPF app that has a series of buttons in a vertical stack panel, similar to what a DVR menu would look like. I figured out how to make an animation where when each new menu page is loaded, the buttons/controls all {unfade, fall, deblur, whatever} into existence, which is nice but a little too uniform. What I'd really like is to have that animation start on each button at a slightly different time, based on its location or tabstop properties. So for instance the top button would be the first to begin, then the next, etc, until the bottom button, maybe a 50 ms delay between each start, (but such that the first one doesn't have to finish before the 2nd one starts). Sure I could just make different Animation for each button, but I'm hoping for a slightly more elegant solution. I don't see a way to load any of the control's properties into the BeginTime of the storyboard. Is there a good way to do using only XAML, or will something like this require codebehind? If the latter, is it possible to package that into a Behavior and be able to load that declaratively later on, or am I stuck with imperative code forever?

Best Answer

The short answer is no (to the best of my knowledge), you cannot do exactly what you are asking with XAML only, and here is why.

While there are many ways to do animations in WPF, the only way to animate with XAML is by using Storyboards. The only way to share a Storyboard across elements is by packaging it in a Resource such as a Style, ControlTemplate, or DataTemplate. However, in order to use a Storyboard as a Resource it must be Freezable, which means it cannot contain any data binding expressions that would be required to associate the animation BeginTime with a property of the target of the animation (such as the TabIndex or some other property of your Buttons). Even if you could somehow bind the BeginTime of an animation to a property of the Button, you would still need to use codebehind to write a ValueConverter to convert the Button property value to a TimeSpan value (your desired 50ms cumulative delay).

So in order to do exactly what you want, you would need to use codebehind as shown in this example:

XAML:

<StackPanel HorizontalAlignment="Left" Width="100" Loaded="StackPanel_Loaded">
    <Button Content="Button1"/>
    <Button Content="Button2"/>
    <Button Content="Button3"/>
    <Button Content="Button4"/>
    <Button Content="Button5"/>
    <Button Content="Button6"/>
    <Button Content="Button7"/>
    <Button Content="Button8"/>
    <Button Content="Button9"/>
    <Button Content="Button10"/>
</StackPanel>

Codebehind:

private void StackPanel_Loaded(object sender, RoutedEventArgs e)
{
    StackPanel stackPanel = sender as StackPanel;
    DoubleAnimation fadeInAnimation = new DoubleAnimation(1.0, new Duration(TimeSpan.FromMilliseconds(200)));
    for (int i = 0; i < stackPanel.Children.Count; i++)
    {
        fadeInAnimation.BeginTime = TimeSpan.FromMilliseconds(i * 50);
        stackPanel.Children[i].Opacity = 0.0;
        stackPanel.Children[i].BeginAnimation(UIElement.OpacityProperty, (AnimationTimeline)fadeInAnimation.GetAsFrozen());
    }
}

That being said, there are ways to approximate your desired behavior using XAML only. For example, you could overlay the StackPanel with a LinearGradientBrush OpacityMask and animate that with a single Storyboard like this:

XAML:

<StackPanel HorizontalAlignment="Left" Width="100">
    <StackPanel.OpacityMask>
        <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
            <GradientStop Color="Black"/>
            <GradientStop x:Name="TransparentGradient" Color="Transparent"/>
        </LinearGradientBrush>
    </StackPanel.OpacityMask>
    <StackPanel.Triggers>
        <EventTrigger RoutedEvent="Loaded">
            <BeginStoryboard>
                <Storyboard>
                    <DoubleAnimation
                        Storyboard.TargetName="TransparentGradient" Storyboard.TargetProperty="Offset"
                        To="1.0" Duration="0:0:0.5"/>
                    <ColorAnimation
                        Storyboard.TargetName="TransparentGradient" Storyboard.TargetProperty="Color"
                        To="Black" BeginTime="0:0:0.5" Duration="0:0:0.5"/>
                </Storyboard>
            </BeginStoryboard>
        </EventTrigger>
    </StackPanel.Triggers>
    <Button Content="Button1"/>
    <Button Content="Button2"/>
    <Button Content="Button3"/>
    <Button Content="Button4"/>
    <Button Content="Button5"/>
    <Button Content="Button6"/>
    <Button Content="Button7"/>
    <Button Content="Button8"/>
    <Button Content="Button9"/>
    <Button Content="Button10"/>
</StackPanel>

That would give you a nice top to bottom fade in. Other effects could also be possible with a little creativity, but a word of advice: try to avoid spending too much time finding complex XAML solutions when you can acheive the desired effect with just a few lines of code.

Happy coding!

Related Topic