Click here to Skip to main content
Click here to Skip to main content

Creating an Animated ContentControl

By , 15 Dec 2010
 

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, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

isaks
Software Developer ABB
Sweden Sweden
Member
My name is Isak Savo and I work as a Research Engineer at ABB Corporate Research in Västerås, Sweden. My work is focused around user experience which includes a lot of prototyping of new solutions for user interfaces and user interaction.
 
While I have a background in C programming in a Linux environment, my daily work is mostly spent in Windows using C# and WPF.

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
QuestionGreat control, I added ContentTemplateSelector support [modified]memberdamianom8 May '13 - 0:37 
Your control is great but doesn't support ContentTemplateSelector.
 
I simply pass the reference to PART_MainContent
 
        public override void OnApplyTemplate()
        {
            _paintArea = Template.FindName("PART_PaintArea", this) as Shape;
            _mainContent = Template.FindName("PART_MainContent", this) as ContentPresenter;
            _mainContent.ContentTemplateSelector = this.ContentTemplateSelector;
 
            base.OnApplyTemplate();
        }
 
<AnimatedContentControl:AnimatedContentControl Content="{Binding something}" ContentTemplateSelector="StaticResource ViewSelector}"/>
 
Hope this help.

modified 8 May '13 - 6:49.

QuestionGreat examplememberrealhabs2 Feb '13 - 23:05 
i'm wondering how to add button each for one direction ? left to rigt and right to left ?
perhaps adding new property like direction ?
like in flipview winRT control
Question.NET 3.5 Easing FunctionsmemberMember 884920820 Aug '12 - 22:37 
Hi ,great tutorial!
 
However, I wasn't able to get this to work for a .NET 3.5 project though, as easing functions doesn't seem to be supported. Any way around this?
 
Thanks!
Ken

AnswerRe: .NET 3.5 Easing Functionsmemberisaks21 Aug '12 - 5:36 
If you don't care about the easing effect (the "bounce" effect) you can just uncomment that piece of code. The control will still work.
 
If you do want that effect on .net 3.5 you can take a look at the thriple project on codeplex: http://thriple.codeplex.com/[^] it contains an EasingDoubleAnimation which you can use instead of the one I had in my code.
QuestionRe: .NET 3.5 Easing FunctionsmemberMember 884920822 Aug '12 - 23:08 
Thanks! I'll check out the thriple project that you suggestedSmile | :)
 
I have another quick question if you don't mind: When is OnContentChanged fired?
 
I tried placing the animated content control as one of the tabItems in a tabControl (it works perfectly fine on its own). The trouble comes when I try to select another tab, and also when I try to select back the original tab housing the animated control. On both events, the sliding animation was strangely triggered, which is not desired.
 
On further step-debugging, I found that whenever tab switching occurs, OnContentChanged is fired. To me, it appears that this event handler is firing whenever the content becomes visible or invisible. Any insights as to why this is occuring?
 
Thanks in advance!
AnswerRe: .NET 3.5 Easing FunctionsmemberKen Toh23 Aug '12 - 17:32 
I solved this by checking to see if either oldContent or newContent is null before animating. Anyway thanks!
GeneralMy vote of 5memberDušan Suchoň25 Jul '12 - 22:23 
Simple, easy to follow. Great work.
GeneralMy vote of 5memberGeert van Horrik7 Jul '11 - 4:17 
Excellent, easy to use, simple, just the way an article should be!
GeneralVery nicememberKarl Shifflett7 Feb '11 - 19:40 
isaks,
 
Very nice article and code, thanks for sharing!
Cheers, Karl
 
My Blog | Mole's Home Page |
XAML Power Toys Home Page

Just a grain of sand on the worlds beaches.


GeneralRe: Very nicememberKarl Shifflett7 Feb '11 - 20:28 
I added a Fade transition and two additional dependency properties for selecting the transition and setting the duration.
 
This way developers can databind a users preference for the animation at runtime.
 
Great article. Tried to vote but kept getting an error, will try later. 5 for sure.
 
Best,
 
Karl
 


 
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
 
namespace Controls {
 
public enum TransitionType { None, Slide, Fade }
 
///
/// A ContentControl that animates the transition between content
///

[TemplatePart(Name = "PART_PaintArea", Type = typeof(Shape))]
[TemplatePart(Name = "PART_MainContent", Type = typeof(ContentPresenter))]
public class AnimatedContentControl : ContentControl {
 
#region Properties
 
public TransitionType TransitionType {
get { return (TransitionType)GetValue(TransitionTypeProperty); }
set { SetValue(TransitionTypeProperty, value); }
}
 
public static readonly DependencyProperty TransitionTypeProperty =
DependencyProperty.Register("TransitionType", typeof(TransitionType), typeof(AnimatedContentControl), new PropertyMetadata(TransitionType.Slide));
 
public Int32 Duration {
get { return (Int32)GetValue(DurationProperty); }
set { SetValue(DurationProperty, value); }
}
 
public static readonly DependencyProperty DurationProperty =
DependencyProperty.Register("Duration", typeof(Int32), typeof(AnimatedContentControl), new PropertyMetadata(500));
 
#endregion // Properties
 
#region Generated static constructor
 
static AnimatedContentControl() {
DefaultStyleKeyProperty.OverrideMetadata(typeof(AnimatedContentControl), new FrameworkPropertyMetadata(typeof(AnimatedContentControl)));
}
 
#endregion //Generated static constructor
 
#region Declarations
 
Shape _paintArea;
ContentPresenter _mainContent;
 
#endregion //Declarations

#region Methods
 
///
/// This gets called when the template has been applied and we have our visual tree
///

public override void OnApplyTemplate() {
_paintArea = Template.FindName("PART_PaintArea", this) as Shape;
_mainContent = Template.FindName("PART_MainContent", this) as ContentPresenter;
 
base.OnApplyTemplate();
}
 
///
/// This gets called when the content we're displaying has changed
///

/// The content that was previously displayed
/// The new content that is displayed
protected override void OnContentChanged(object oldContent, object newContent) {
if (_paintArea != null && _mainContent != null) {
_paintArea.Fill = CreateBrushFromVisual(_mainContent);
switch (this.TransitionType) {
case TransitionType.Slide:
BeginAnimateContentReplacement();
break;
case TransitionType.Fade:
FadeTransition();
break;
default:
break;
}
}
base.OnContentChanged(oldContent, newContent);
}
 
void FadeTransition() {
_paintArea.Visibility = Visibility.Visible;
_paintArea.Opacity = 1;
_paintArea.InvalidateVisual();
_paintArea.BeginAnimation(OpacityProperty, new DoubleAnimation(1, 0, new Duration(TimeSpan.FromMilliseconds(this.Duration))));
var animation = new DoubleAnimation(0, 1, new Duration(TimeSpan.FromMilliseconds(this.Duration)));
animation.Completed += (s, e) => _paintArea.Visibility = Visibility.Hidden;
_mainContent.BeginAnimation(OpacityProperty, animation);
}
 
///
/// Starts the animation for the new content
///

void BeginAnimateContentReplacement() {
var newContentTransform = new TranslateTransform();
var oldContentTransform = new TranslateTransform();
_paintArea.RenderTransform = oldContentTransform;
_mainContent.RenderTransform = newContentTransform;
_paintArea.Visibility = Visibility.Visible;
 
newContentTransform.BeginAnimation(TranslateTransform.XProperty, CreateAnimation(this.ActualWidth, 0));
oldContentTransform.BeginAnimation(TranslateTransform.XProperty, CreateAnimation(0, -this.ActualWidth, (s, e) => _paintArea.Visibility = Visibility.Hidden));
}
 
///
/// Creates the animation that moves content in or out of view.
///

/// The starting value of the animation.
/// The end value of the animation.
/// (optional) A callback that will be called when the animation has completed.
AnimationTimeline CreateAnimation(Double from, Double to, EventHandler whenDone = null) {
IEasingFunction ease = new BackEase { Amplitude = 0.5, EasingMode = EasingMode.EaseOut };
var duration = new Duration(TimeSpan.FromMilliseconds(this.Duration));
var anim = new DoubleAnimation(from, to, duration) { EasingFunction = ease };
if (whenDone != null) {
anim.Completed += whenDone;
}
anim.Freeze();
return anim;
}
 
///
/// Creates a brush based on the current appearnace of a visual element. The brush is an ImageBrush and once created, won't update its look
///

/// The visual element to take a snapshot of
Brush CreateBrushFromVisual(Visual visual) {
if (visual == null) {
throw new ArgumentNullException("visual");
}
var target = new RenderTargetBitmap((int)this.ActualWidth, (int)this.ActualHeight, 96, 96, PixelFormats.Pbgra32);
target.Render(visual);
var brush = new ImageBrush(target);
brush.Freeze();
return brush;
}
 
#endregion //Methods
 
}
}

Cheers, Karl
 
My Blog | Mole's Home Page |
XAML Power Toys Home Page

Just a grain of sand on the worlds beaches.


General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Permalink | Advertise | Privacy | Mobile
Web04 | 2.6.130523.1 | Last Updated 16 Dec 2010
Article Copyright 2010 by isaks
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid