Skip navigation links
Home
Document Center
Blog
Videos
Forum
PhillyXAML.org > Blog > Categories
Extending FrameworkElement 3: Animating Fades on User Interface Elements
The third installment of this series will cover creating an overloaded method that will allow us to fade a control to a specific opacity, as well as give us the ability to fade in or out. A scenario for this might be to fade a window out, open a dialog, and after the dialog call, fade the window back in.
 
This is a scenario we see on the Web all the time, and more recently in client applications to indicate synchronous, blocking processing where the application needs to "pause" its UI layer, indicating it is disabled, and more importantly creating focal emphasis on the dialog element. We visit a page, click Login, the window disables and fades, a new window opens, we log in, the login window closes, and the parent window fades back in and its back to business as usual.
 
Another use for this method would be to facilitate some simple animations of UI elements that are displayed dynamically based on application context. An example might be a client application that has a datagrid that appears or disappears depending on whether or not there is data. While still allowing the grid to occupy the same screenspace whether it is hidden or not, we can provide some enhanced interactivity by fading that grid in or out when there is data instead of instantaneously popping it on or off the screen. This creates a more dynamic feel to the application and really works to richen our user experience.
 
We will program the option to fade in or out with several overrides, comprised of combinations of the items below:
  • Fade to a specific opacity
  • Fade over a fixed duration of time
  • Fade based on direction (In, Out)

The first thing we need to do is add a new item to our library. Create an empty code file named Enums.cs and add the following code. We do not need any using statements since this file will only contain an enumerator used for determining fade directionality.

namespace PX.Samples.FrameworkElementExtender
{
    public enum Fade { In, Out };
}

With our enumerator in place, we add the following method overrides to FrameworkElementExtender.cs. The comments for each method indicate the specific combination of the above features the method provides. Also note the string constant fadeDuration, which is used as a default for some of the overrides.

        #region FadeInOut Extension Method
        private const string fadeDuration = "0:0:0.5";
        /// <summary>
        /// Fades the FrameworkElement in or out over a specified duration
        /// </summary>
        /// <param name="elUI">The FrameworkElement being extended</param>
        /// <param name="FadeDirection">Direction to fade, In or Out</param>
        /// <param name="Duration">Valid TimeSpan string in the format 
        /// d.h:m:s.sss where h:m:s is required</param>
        public static void FadeInOut(this UIElement elUI, Fade FadeDirection, string Duration)
        {
            TimeSpan duration;
            if (!TimeSpan.TryParse(Duration, out duration))
            {
                throw new Exception(string.Format("{0} is not a valid value for Duration", Duration));
            }
            if (FadeDirection == Fade.Out)
            {
                FadeInOut(elUI, .03, Duration);
            }
            else
            {
                FadeInOut(elUI, 1, Duration);
            }
        }
        /// <summary>
        /// Fades the FrameworkElement in or out over the default duration
        /// </summary>
        /// <param name="elUI">The FrameworkElement being extended</param>
        /// <param name="FadeDirection">Direction to fade, In or Out</param>
        public static void FadeInOut(this UIElement elUI, Fade FadeDirection)
        {
            FadeInOut(elUI, FadeDirection, fadeDuration);
        }
        /// <summary>
        /// Fades the FrameworkElement to a specific opacity over a specified duration
        /// </summary>
        /// <param name="elUI">The FrameworkElement being extended</param>
        /// <param name="Opacity">The normalized opacity value between 0.0 and 1.0</param>
        /// <param name="Duration">Valid TimeSpan string in the format 
        /// d.h:m:s.sss where h:m:s is required</param>
        public static void FadeInOut(this UIElement elUI, double Opacity, string Duration)
        {
            TimeSpan duration;
            if (!TimeSpan.TryParse(Duration, out duration))
            {
                throw new Exception(string.Format("{0} is not a valid value for Duration", Duration));
            }
            DoubleAnimation da;
            da = new DoubleAnimation(Opacity, duration);
            elUI.BeginAnimation(Window.OpacityProperty, da);
        }
        /// <summary>
        /// Fades the FrameworkElement to a specific opacity over the default duration
        /// </summary>
        /// <param name="elUI">The FrameworkElement being extended</param>
        /// <param name="Opacity">The normalized opacity value between 0.0 and 1.0</param>
        public static void FadeInOut(this UIElement elUI, double Opacity)
        {
            FadeInOut(elUI, Opacity, fadeDuration);
        } 
        #endregion

Worth noting is the usage of the Fade enumerator to provide our logic switch for fading in or out. Additionally, the default fade out value is to an opacity of .03 or 3%. This provides a nice barely visible ghost of our control executing the method. This would the object elUI in the above code.

Lets take a look at the one override that really does all of the work, the override that takes a specific opacity and a specific duration, which is third in our code above. This override creates a DoubleAnimation object, and sets its To value and Duration using the respective constructor overload. We are then calling the BeginAnimation invoker on the extended element, passing in the DependencyProperty we wish to animate (UIElement.OpacityProperty) and the AnimationTimeline object that defines the animation (our DoubleAnimation object).

Also worth noting is that the additional method overrides are really just recursive versions of one another, using the fadeDuration string constant as a default when omitted. This combination of overrides covers all possible combinations of usage, which we will take a look at in just a moment.

First off, lets fix those ugly red errors in our code, by adding a using statement for System.Windows.Media.Animation so we can get to the DoubleAnimation object. Lets build to make sure we have made it this far without any errors. Build Succeeded! My two favorite words!

Now that we have a working library, lets modify our test harness application to use it. First open winMain.xaml in either Visual Studio or Expression Blend and double-click the top button to add an event handler. If you have been following along, the handler should be named btnSample_Click.

We are going to use the rectangle in the middle of our screen to test the different overrides. We are going to create a simple counter based loop so that each time we click the button we can step through the method calls and increment our counter. Add the counter variable indicated below as well.

        private int counter = 0;
        private void btnSample_Click(object sender, RoutedEventArgs e)
        {
            switch (counter)
            {
                case 0:
                    counter++;
                    rectSample.FadeInOut(Fade.Out, "0:0:5");
                    break;
                case 1:
                    counter++;
                    rectSample.FadeInOut(Fade.In, "0:0:2.5"); 
                    break;
                case 2:
                    counter++;
                    rectSample.FadeInOut(Fade.Out);
                    break;
                case 3:
                    counter++;
                    rectSample.FadeInOut(Fade.In);
                    break;
                case 4:
                    counter++;
                    rectSample.FadeInOut(.5, "0:0:5");
                    break;
                case 5:
                    counter++;
                    rectSample.FadeInOut(1, "0:0:5");
                    break;
                case 6:
                    counter++;
                    rectSample.FadeInOut(.25);
                    break;
                case 7:
                    counter = 0;//Reset our counter
                    rectSample.FadeInOut(1);
                    break;
                default:
                    throw new Exception("This should never happen");
            }
        }

Now lets run our application by hitting F5 or Ctrl-F5. Now each time we click the button labeled Test Button 1 we will see each call as we click. We do not have any type of animation flow control in place in this simple sample so be sure to let the animations finish completely to avoid any unintended effect. Pay attention to which calls are being made in terms of fade direction and duration.

Creating Animations as Resources for Reuse
You can place animations (ColorAnimation, DoubleAnimationUsingKeyFrames, etc.) in a resource for reuse. I would place them in App.xaml for app level scope availability if need be. The sample below shows 4 ellipses with separate storyboards. The storyboards reference the animations using StaticResourceExtension. The storyboards all have separate start times and target properties. The animations can be reused due to their low level scope (animating a generic double, color, etc.) so they can be used anywhere where these types are used, not just among similar objects.
 
App.xaml:
<Application x:Class="AnimationResources.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    StartupUri="Window1.xaml">
    <Application.Resources>
        <DoubleAnimationUsingKeyFrames x:Key="aniX">
            <LinearDoubleKeyFrame KeyTime="0:0:0" Value="0"/>
            <LinearDoubleKeyFrame KeyTime="0:0:0.9" Value="210"/>
            <LinearDoubleKeyFrame KeyTime="0:0:1" Value="200"/>
        </DoubleAnimationUsingKeyFrames>
        <DoubleAnimationUsingKeyFrames x:Key="aniOpacity">
            <LinearDoubleKeyFrame KeyTime="0:0:0" Value="0"/>
            <LinearDoubleKeyFrame KeyTime="0:0:0.6" Value="1"/>
            <LinearDoubleKeyFrame KeyTime="0:0:1" Value="1"/>
        </DoubleAnimationUsingKeyFrames>
    </Application.Resources>
</Application>
 
Window1.xaml:
<Window 
    x:Class="AnimationResources.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Animation Resource Sample" 
    Height="163" 
    Width="267">
    <Canvas>
        <Ellipse 
            Width="24" 
            Height="24" 
            Fill="PaleGreen" 
            Stroke="Black" 
            StrokeThickness="1" 
            Opacity=".1">
            <Ellipse.Resources>
                <Storyboard 
                    RepeatBehavior="5x" 
                    BeginTime="0:0:1" 
                    x:Key="sbLeft" 
                    Storyboard.TargetProperty="(Canvas.Left)">
                    <StaticResourceExtension ResourceKey="aniX"/>
                </Storyboard>
                <Storyboard 
                    RepeatBehavior="5x" 
                    BeginTime="0:0:1" 
                    x:Key="sbOpacity" 
                    TargetProperty="Opacity">
                    <StaticResourceExtension ResourceKey="aniOpacity"/>
                </Storyboard>
            </Ellipse.Resources>
            <Ellipse.Triggers>
                <EventTrigger RoutedEvent="FrameworkElement.Loaded">
                    <BeginStoryboard Storyboard="{StaticResource sbLeft}"/>
                    <BeginStoryboard Storyboard="{StaticResource sbOpacity}"/>
                </EventTrigger>
            </Ellipse.Triggers>
        </Ellipse>

        <Ellipse 
            Width="24" 
            Height="24" 
            Fill="PaleVioletRed" 
            Stroke="Black" 
            StrokeThickness="1" 
            Canvas.Top="32" 
            Opacity=".1">
            <Ellipse.Resources>
                <Storyboard 
                    RepeatBehavior="5x" 
                    BeginTime="0:0:2" 
                    x:Key="sbLeft" 
                    Storyboard.TargetProperty="(Canvas.Left)">
                    <StaticResourceExtension ResourceKey="aniX"/>
                </Storyboard>
                <Storyboard 
                    RepeatBehavior="5x" 
                    BeginTime="0:0:2" 
                    x:Key="sbOpacity" 
                    TargetProperty="Opacity">
                    <StaticResourceExtension ResourceKey="aniOpacity"/>
                </Storyboard>
            </Ellipse.Resources>
            <Ellipse.Triggers>
                <EventTrigger RoutedEvent="FrameworkElement.Loaded">
                    <BeginStoryboard Storyboard="{StaticResource sbLeft}"/>
                    <BeginStoryboard Storyboard="{StaticResource sbOpacity}"/>
                </EventTrigger>
            </Ellipse.Triggers>
        </Ellipse>

        <Ellipse 
            Width="24" 
            Height="24" 
            Fill="PaleGoldenrod" 
            Stroke="Black" 
            StrokeThickness="1" 
            Canvas.Top="64" 
            Opacity=".1">
            <Ellipse.Resources>
                <Storyboard 
                    RepeatBehavior="5x" 
                    BeginTime="0:0:3" 
                    x:Key="sbLeft" 
                    Storyboard.TargetProperty="(Canvas.Left)">
                    <StaticResourceExtension ResourceKey="aniX"/>
                </Storyboard>
                <Storyboard 
                    RepeatBehavior="5x" 
                    BeginTime="0:0:3" 
                    x:Key="sbOpacity" 
                    TargetProperty="Opacity">
                    <StaticResourceExtension ResourceKey="aniOpacity"/>
                </Storyboard>
            </Ellipse.Resources>
            <Ellipse.Triggers>
                <EventTrigger RoutedEvent="FrameworkElement.Loaded">
                    <BeginStoryboard Storyboard="{StaticResource sbLeft}"/>
                    <BeginStoryboard Storyboard="{StaticResource sbOpacity}"/>
                </EventTrigger>
            </Ellipse.Triggers>
        </Ellipse>

        <Ellipse 
            Width="24" 
            Height="24" 
            Fill="PaleTurquoise" 
            Stroke="Black" 
            StrokeThickness="1" 
            Canvas.Top="96" 
            Opacity=".1">
            <Ellipse.Resources>
                <Storyboard 
                    RepeatBehavior="5x" 
                    BeginTime="0:0:4" 
                    x:Key="sbLeft" 
                    Storyboard.TargetProperty="(Canvas.Left)">
                    <StaticResourceExtension ResourceKey="aniX"/>
                </Storyboard>
                <Storyboard 
                    RepeatBehavior="5x" 
                    BeginTime="0:0:4" 
                    x:Key="sbOpacity" 
                    TargetProperty="Opacity">
                    <StaticResourceExtension ResourceKey="aniOpacity"/>
                </Storyboard>
            </Ellipse.Resources>
            <Ellipse.Triggers>
                <EventTrigger RoutedEvent="FrameworkElement.Loaded">
                    <BeginStoryboard Storyboard="{StaticResource sbLeft}"/>
                    <BeginStoryboard Storyboard="{StaticResource sbOpacity}"/>
                </EventTrigger>
            </Ellipse.Triggers>
        </Ellipse>
    </Canvas>
</Window>