Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Creating an Animated ContentControl

0.00/5 (No votes)
15 Dec 2010 1  
Shows how to build a content control which animates transitions between content.

AnimatedContentControl.png

Introduction

WPF has very powerful animation capabilities, but in some cases, these are quite hard to use in combination with data driven content. One example is when a ContentControl is dynamically rendering a View based on a bound object in its ViewModel.

This article shows a solution where a standard ContentControl is enhanced to animate the transitions between content while still maintaining its familiar functionality and behavior.

The AnimatedContentControl

The control provided in the sample is a standalone control inheriting from ContentControl which will apply a right-to-left fly-out animation whenever it detects that its content has changed. It doesn't matter if content comes from databinding, from code-behind, or from XAML. In fact, apart from the animation, it behaves just like a normal ContentControl.

The control is created as a "Custom Control", which differs from a User control in that it:

  • can inherit from any WPF control, instead of just UserControl
  • doesn't have a backing .xaml file and thus cannot make hard assumptions about its visual tree (it does have a default style though, which is what we'll use in this article)

It's outside the scope of this article to discuss the differences between these two approaches, but since we wanted to create a ContentControl, we had to go with the "Custom Control" approach.

To handle the animation, the AnimatedContentControl needs something to temporarily draw the old content to. And, in the default style for the control, we add a rectangle for this purpose. The rectangle is the same size as the content, and occupies the same space in the layout. We will control the positioning and visibility from code later, of course.

Here is the default style for the AnimatedContentControl:

<Style TargetType="{x:Type local:AnimatedContentControl}">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type local:AnimatedContentControl}">
                <Grid>
                    <ContentPresenter 
                        Content="{TemplateBinding Content}" 
                        x:Name="PART_MainContent" />
                    <Rectangle x:Name="PART_PaintArea" />
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

Notice the naming used for the controls we need to use in our code. This follows the naming convention for custom controls, and gives us a way to get hold of these controls in our C# code. This is discussed more in detail in this CodeProject article, but in short, we can use the GetName method after the template has been applied to get references to the controls.

/// <summary>
/// This gets called when the template has been applied and we have our visual tree
/// </summary>
public override void OnApplyTemplate()
{
    m_paintArea = Template.FindName("PART_PaintArea", this) as Shape;
    m_mainContent = Template.FindName("PART_MainContent", this) as ContentPresenter;
 
    base.OnApplyTemplate();
}

Reacting to content changes

The base class provides an overload called OnContentChanged that we can use to perform some work when the content has changed. One thing to note is that when this is called, the actual property has changed (so if we look at this.Content, it would be equal to the newContent parameter), but the visual appearance has not yet been updated. We exploit this by capturing the current visual appearance and paint it on top of our temporary rectangle.

/// <summary>
/// This gets called when the content we're displaying has changed
/// </summary>
/// <param name="oldContent">The content that was previously displayed</param>
/// <param name="newContent">The new content that is displayed</param>
protected override void OnContentChanged(object oldContent, object newContent)
{
    if (m_paintArea != null && m_mainContent != null)
    {
        m_paintArea.Fill = CreateBrushFromVisual(m_mainContent);
        BeginAnimateContentReplacement();
    }
    base.OnContentChanged(oldContent, newContent);
}

The CreateBrushFromVisual method simply takes a snapshot of the current appearance and stores it into an ImageBrush:

/// <summary>
/// Creates a brush based on the current appearance of a visual element. 
/// The brush is an ImageBrush and once created, won't update its look
/// </summary>
/// <param name="v">The visual element to take a snapshot of</param>
private Brush CreateBrushFromVisual(Visual v)
{
    if (v == null)
        throw new ArgumentNullException("v");
    var target = new RenderTargetBitmap((int)this.ActualWidth, (int)this.ActualHeight, 
                                        96, 96, PixelFormats.Pbgra32);
    target.Render(v);
    var brush = new ImageBrush(target);
    brush.Freeze();
    return brush;
}

With the rectangle now having the look of the old content, it's time to start the animation:

/// <summary>
/// Starts the animation for the new content
/// </summary>
private void BeginAnimateContentReplacement()
{
    var newContentTransform = new TranslateTransform();
    var oldContentTransform = new TranslateTransform();
    m_paintArea.RenderTransform = oldContentTransform;
    m_mainContent.RenderTransform = newContentTransform;
    m_paintArea.Visibility = Visibility.Visible;
            
    newContentTransform.BeginAnimation(TranslateTransform.XProperty, 
                                  CreateAnimation(this.ActualWidth, 0));
    oldContentTransform.BeginAnimation(TranslateTransform.XProperty, 
                                  CreateAnimation(0, -this.ActualWidth, 
                                    (s,e) => m_paintArea.Visibility = Visibility.Hidden));
}

In the above method, we create two new TranslateTransforms. These are responsible for moving the content to the left, side by side:

  • The animation for our temporary rectangle starts from the location where the original content was shown, and will be moved left until it is completely outside the visible area of the control.
  • The animation for our new content, which is applied to the ContentPresenter (the one holding the new visual appearance by the time the animation starts), will start off screen to the right, and moves in until it occupies its final location.

To make the transition feel more alive, we're using an easing function with a BackEase algorithm. This will make the animated content travel slightly too far and then bounce back to its resting place. The code used to create the animations is wrapped in a simple method:

/// <summary>
/// Creates the animation that moves content in or out of view.
/// </summary>
/// <param name="from">The starting value of the animation.</param>
/// <param name="to">The end value of the animation.</param>
/// <param name="whenDone">(optional)
///   A callback that will be called when the animation has completed.</param>
private AnimationTimeline CreateAnimation(double from, double to, 
                          EventHandler whenDone = null)
{
    IEasingFunction ease = new BackEase 
                                { Amplitude = 0.5, EasingMode = EasingMode.EaseOut };
    var duration = new Duration(TimeSpan.FromSeconds(0.5));
    var anim = new DoubleAnimation(from, to, duration) 
                                { EasingFunction = ease };
    if (whenDone != null)
        anim.Completed += whenDone;
    anim.Freeze();
    return anim;
}

The final result can be seen in this low-res GIF animation:

AnimatedContentControl.gif

DataBinding

The downloadable source code shows how this control is used in an MVVM (Model-View-ViewModel) architecture, with content being changed from ViewModels, which is completely unaware of the animations done in the View layer.

The ViewModel for the main window, MainWindowViewModel, contains a property called Content and a command called ChangeContentCommand. These are bound from the MainWindow like so:

<Grid>
        <Grid.RowDefinitions>
                <RowDefinition Height="Auto" />
        <RowDefinition Height="*" />
    </Grid.RowDefinitions>
    <Button Command="{Binding ChangeContentCommand}" 
        Content="Change Content" FontSize="20" 
        HorizontalAlignment="Center" 
        VerticalAlignment="Center" Margin="10" 
        Padding="10,5" />
    <local:AnimatedContentControl 
        Content="{Binding Content}" Grid.Row="1" />
</Grid>

When the command is executed in the ViewModel, it just sets its Content property to a new instance of a MyContentViewModel. The property notification system will then notify our control of the new content, and it will in turn trigger the animation.

Points of Interest

  • The animations in this example are very simple. It's only animating a translation of the X-axis and, thanks to the easing function, still provides a visually appealing effect. That said though, a visual designer could quite easily enhance this to provide an even richer experience, and the good thing is that the programming interface towards the control doesn't change at all.
  • The sample code attached to this article contains the RelayCommand class from Laurient Bugnion's MVVM Light toolkit. This particular class is obviously his copyright, and is licensed under the MIT License.

History

  • v1.0 (Dec 15, 2010) - Initial release.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here