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.
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.
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
:
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:
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 TranslateTransform
s. 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:
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:
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.