0
votes

I am learning WPF animation, and I created a simple demo app with a pretty straightforward animation. I have divided the main grid into three rows; a Buttons Row at the top, and two content rows that for the remainder of the screen, one red and one blue. Complete XAML is below.

There are two buttons, Show Red and Show Blue. When each button is pressed, I want the area below the Buttons Row to change color with a slow top-to-bottom wipe. The Storyboard sets the height of both rows to 0, then animates the desired row to a height of 1*, like this:

<Storyboard>
    <Utility:GridLengthAnimation Storyboard.TargetName="RedRow" Storyboard.TargetProperty="Height" To="0" Duration="0:0:0" />
    <Utility:GridLengthAnimation Storyboard.TargetName="BlueRow" Storyboard.TargetProperty="Height" To="0" Duration="0:0:0" />
    <Utility:GridLengthAnimation Storyboard.TargetName="BlueRow" Storyboard.TargetProperty="Height" From="0" To="1*" Duration="0:0:5" />
</Storyboard>

The colors change as expected, but there is no animation. So, my question is simple: Why isn't the animation working?

I am using a custom animation class, GridLengthAnimation (adapted from this CodeProject article) to animate the grid lengths. I have reproduced the class below.

To recreate the demo project: To recreate my demo project, create a new WPF project (I used VS 2010) and replace the XAML in MainWindow.xaml with the following:

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:Utility="clr-namespace:Utility" Title="MainWindow" Height="350" Width="525">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Name="Buttons" Height="35" />
            <RowDefinition Name="RedRow" Height="0.5*" />
            <RowDefinition Name="BlueRow" Height="0.5*" />
        </Grid.RowDefinitions>

        <!-- Buttons -->
        <StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
            <Button Content="Show Red" Width="100" Margin="5" >
                <Button.Triggers>
                    <EventTrigger RoutedEvent="Button.Click">
                        <EventTrigger.Actions>
                            <BeginStoryboard>
                                <Storyboard>
                                    <Utility:GridLengthAnimation Storyboard.TargetName="RedRow" Storyboard.TargetProperty="Height" To="0" Duration="0:0:0" />
                                    <Utility:GridLengthAnimation Storyboard.TargetName="BlueRow" Storyboard.TargetProperty="Height" To="0" Duration="0:0:0" />
                                    <Utility:GridLengthAnimation Storyboard.TargetName="RedRow" Storyboard.TargetProperty="Height" From="0" To="1*" Duration="0:0:5" />
                                </Storyboard>
                            </BeginStoryboard>
                        </EventTrigger.Actions>
                    </EventTrigger>
                </Button.Triggers>
            </Button>
            <Button Content="Show Blue" Width="100" Margin="5" >
                <Button.Triggers>
                    <EventTrigger RoutedEvent="Button.Click">
                        <EventTrigger.Actions>
                            <BeginStoryboard>
                                <Storyboard>
                                    <Utility:GridLengthAnimation Storyboard.TargetName="RedRow" Storyboard.TargetProperty="Height" To="0" Duration="0:0:0" />
                                    <Utility:GridLengthAnimation Storyboard.TargetName="BlueRow" Storyboard.TargetProperty="Height" To="0" Duration="0:0:0" />
                                    <Utility:GridLengthAnimation Storyboard.TargetName="BlueRow" Storyboard.TargetProperty="Height" From="0" To="1*" Duration="0:0:5" />
                                </Storyboard>
                            </BeginStoryboard>
                        </EventTrigger.Actions>
                    </EventTrigger>
                </Button.Triggers>
            </Button>
        </StackPanel>

        <!-- Grid Fills-->
        <Border Grid.Row="1" Background="Red" />
        <Border Grid.Row="2" Background="Blue" />

    </Grid>
</Window>

There is no code-behind added to MainWindow.xaml.

Add a C# class to the project named GridLengthAnimation.cs. Replace the code in that class with the following:

using System;
using System.Windows.Media.Animation;
using System.Windows;

namespace Utility
{
    /// <summary>
    /// Enables animation of WPF Grid row heights and column widths.
    /// </summary>
    /// <remarks>Adapted from Graus & Sivakumar, "WPF Tutorial - Part 2 : Writing a custom animation class",
    /// http://www.codeproject.com/KB/WPF/GridLengthAnimation.aspx, retrieved 08/12/2010.</remarks>
    internal class GridLengthAnimation : AnimationTimeline
    {
        static GridLengthAnimation()
        {
            FromProperty = DependencyProperty.Register("From", typeof(GridLength),
                typeof(GridLengthAnimation));

            ToProperty = DependencyProperty.Register("To", typeof(GridLength), 
                typeof(GridLengthAnimation));
        }

        public override Type TargetPropertyType
        {
            get 
            {
                return typeof(GridLength);
            }
        }

        protected override Freezable CreateInstanceCore()
        {
            return new GridLengthAnimation();
        }

        public static readonly DependencyProperty FromProperty;
        public GridLength From
        {
            get
            {
                return (GridLength)GetValue(FromProperty);
            }
            set
            {
                SetValue(FromProperty, value);
            }
        }

        public static readonly DependencyProperty ToProperty;
        public GridLength To
        {
            get
            {
                return (GridLength)GetValue(ToProperty);
            }
            set
            {
                SetValue(ToProperty, value);
            }
        }

        public override object GetCurrentValue(object defaultOriginValue, object defaultDestinationValue, AnimationClock animationClock)
        {
            double fromVal = ((GridLength)GetValue(FromProperty)).Value;
            double toVal = ((GridLength)GetValue(ToProperty)).Value;

            if (animationClock.CurrentProgress != null)
            {
                if (fromVal > toVal) 
                {
                    return new GridLength((1 - animationClock.CurrentProgress.Value) * (fromVal - toVal) + toVal, GridUnitType.Star);
                }
                else 
                {
                    return new GridLength(animationClock.CurrentProgress.Value * (toVal - fromVal) + fromVal, GridUnitType.Star);
                }
            }
            else
            {
                return null;
            }
        }
    }
}
1

1 Answers

0
votes

I found my answer in this blog post. It turns out there is a problem animating height or width properties. I worked around the problem by using a dissolve effect, instead of a wipe. To animate a dissolve, declare both controls in the same Grid row and column, which will load them on top of each other. Declare the default control last, which will make it the visible control. Then, animate the Opacity value of the default control to zero to hide it, and back to 1 to show it.

If the controls being animated are UserControls or other controls you need to click on, you need to take one more step. That's because setting the Opacity of a control to zero simply makes it invisible. It will still prevent a click on the control beneath it. So, declare a Render.Transform on the default control, then animate the ScaleY property to set it to 0 when invisible and 1 when showing.

Here is an example from the production app I am working on. It switches between a note list and a calendar (two different UserControls) in the Navigator pane of an Explorer-style interface. Here is the declaration of the two controls:

<!-- ClientArea: Navigator -->
<Grid x:Name="Navigator">
    <View:CalendarNavigator x:Name="Calendar"  />
    <View:NoteListNavigator x:Name="NoteList">
        <View:NoteListNavigator.RenderTransform>
            <ScaleTransform ScaleX="1" ScaleY="1" />
        </View:NoteListNavigator.RenderTransform>
    </View:NoteListNavigator>
</Grid>

Note the declaration of the ScaleTransform on the note list. I use a couple of Ribbon buttons to switch between the two UserControls:

<ribbon:RibbonToggleButton x:Name="NoteListViewButton" LargeImageSource="..\Images\ListViewLarge.png" SmallImageSource="..\Images\ListViewSmall.png" Label="Note List" Click="OnViewButtonClick">
    <ribbon:RibbonToggleButton.Triggers>
        <EventTrigger RoutedEvent="ribbon:RibbonToggleButton.Checked">
            <EventTrigger.Actions>
                <BeginStoryboard>
                    <Storyboard>
                        <DoubleAnimation Storyboard.TargetName="NoteList" Storyboard.TargetProperty="(View:NoteListNavigator.RenderTransform).(ScaleTransform.ScaleY)" To="1" Duration="0:0:0" />
                        <DoubleAnimation Storyboard.TargetName="NoteList" Storyboard.TargetProperty="Opacity" To="1" Duration="0:0:1" />
                    </Storyboard>
                </BeginStoryboard>
            </EventTrigger.Actions>
        </EventTrigger>
        </ribbon:RibbonToggleButton.Triggers>
</ribbon:RibbonToggleButton>
                    
<ribbon:RibbonToggleButton x:Name="CalendarViewButton" LargeImageSource="..\Images\CalendarViewLarge.png" SmallImageSource="..\Images\CalendarViewSmall.png" Label="Calendar" Click="OnViewButtonClick">
    <ribbon:RibbonToggleButton.Triggers>
        <EventTrigger RoutedEvent="ribbon:RibbonToggleButton.Checked">
            <EventTrigger.Actions>
                <BeginStoryboard>
                    <Storyboard>
                        <DoubleAnimation Storyboard.TargetName="NoteList" Storyboard.TargetProperty="Opacity" To="0" Duration="0:0:1" />
                        <DoubleAnimation Storyboard.TargetName="NoteList" Storyboard.TargetProperty="(View:NoteListNavigator.RenderTransform).(ScaleTransform.ScaleY)" To="0" Duration="0:0:0" />
                    </Storyboard>
                </BeginStoryboard>
            </EventTrigger.Actions>
        </EventTrigger>
    </ribbon:RibbonToggleButton.Triggers>
</ribbon:RibbonToggleButton>

The ScaleY transforms get the invisible note list out of the way when the Calendar is showing so that I can click on my calendar controls. Note that I needed fully-qualified references to the ScaleY properties in my Storyboards. That's why the references are enclosed in parentheses.

Hope that helps someone else down the road! It's likely to be me, since I'll probably forget how I did this...