WPF: Tips - Animating a ViewModel
This article explains how you can animate the value of properties in a ViewModel.
Background
In any MVVM project you will routinely bind properties in the View to public properties exposed from a ViewModel. Often you will change the values of these in the ViewModel and of course the View will respond accordingly. Occasionally, you don't really just want to change that value in a single step. You want to change the value gradually. Animating this via a Storyboard is the obvious solution. There's a bit of complication with using a Storyboard from a ViewModel though. Since these will be in the View and of course with MVVM you won't have a reference to that. (Unless you're really naughty). You need to use something like a Datatrigger on something bound from View to ViewModel to run them and this can be quite inconvenient. Sometimes it's nigh on impossible to get an animation to run without complication or compromise.
Animations where you can't tell what various values will be until you run them are a particular challenge because Storyboards must be freezable and you'll perhaps need to generate them dynamically.
What would be much more elegant, in some circumstances, would be if you could just do your animation in the ViewModel.
You want to change that property there in the ViewModel from the ViewModel so this is kind of an obvious place to run an animation. If only it was that simple.
Well if you ever thought that might be a good idea then you're in luck because this article is going to explain how.
Overview
Animations only work with Dependency Properties, so you'll be animating a Dependency Property. These are a bit wordy compared to regular properties most MVVM devs will be used to, but you have that propdp snippet to help reduce the pain.
When you use that and generate a Dependency Property in your ViewModel you will notice a compilation error. You need to make your ViewModel inherit from DependencyObject in order to use Dependency Properties in it.
That's pretty easily done.
The really tricky bit is the Storyboard.
The following explanation will use a working sample to illustrate the approach.
It's not incredibly complicated, but there are some tricky bits.
The Sample
The sample is available from the Technet Gallery here.
The sample animates the rotation of a 3d object to rotate it left or right by 10 degrees as the user clicks the left or right button. This is done by animating a Rotation Dependency Property which is exposed from the Viewmodel and of course bound in the View.
In the above screen shot the ( admittedly rather flat looking ) 3d object has been rotated several increments of 10 degrees from the initial flat-on.
As an alternative you can also use the left and right arrows which are bound to the same commands as the buttons.
Window Markup
Most of the markup is the 3d model which is that panel with the yellow to red gradient on it. We won't spend much time on how the 3d model works because it's only there to demonstrate the animation working.
<Window x:Class="wpf_Animate_ViewModel.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525"
xmlns:local="clr-namespace:wpf_Animate_ViewModel"
>
<Window.DataContext>
<local:MainWindowViewModel/>
</Window.DataContext>
<Window.InputBindings>
<KeyBinding Key="Left" Command="{Binding LeftRotateCommand}" />
<KeyBinding Key="Right" Command="{Binding RightRotateCommand}" />
</Window.InputBindings>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="100"/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="1">
<Button Command="{Binding LeftRotateCommand}">Left</Button>
<Button Command="{Binding RightRotateCommand}">Right</Button>
</StackPanel>
<Viewbox>
<Canvas Width="321" Height="250">
<!-- The Viewport3D provides a rendering surface for 3-D visual content. -->
<Viewport3D Name="MyAnimatedObject"
ClipToBounds="True" Width="250" Height="250"
Canvas.Left="0" Canvas.Top="10">
<!-- Defines the camera used to view the 3D object. -->
<Viewport3D.Camera>
<PerspectiveCamera x:Name="myPerspectiveCamera" Position="0,0,2" LookDirection="0,0,-1"
FieldOfView="60" />
</Viewport3D.Camera>
<!-- The ModelVisual3D children contain the 3D models -->
<Viewport3D.Children>
<!-- Two ModelVisual3D define the lights cast in the scene. Without light, the
3D object cannot be seen. Also, the direction of the lights affect shadowing. -->
<ModelVisual3D>
<ModelVisual3D.Content>
<DirectionalLight Color="#FFFFFF" Direction="-0.612372,-0.5,-0.612372" />
</ModelVisual3D.Content>
</ModelVisual3D>
<ModelVisual3D>
<ModelVisual3D.Content>
<DirectionalLight Color="#FFFFFF" Direction="0.612372,-0.5,-0.612372" />
</ModelVisual3D.Content>
</ModelVisual3D>
<ModelVisual3D>
<ModelVisual3D.Content>
<GeometryModel3D>
<!-- The geometry specifes the shape of the 3D plane. In this case, a flat sheet is created. -->
<GeometryModel3D.Geometry>
<MeshGeometry3D
TriangleIndices="0,1,2 3,4,5 "
Normals="0,0,1 0,0,1 0,0,1 0,0,1 0,0,1 0,0,1 "
TextureCoordinates="0,0 1,0 1,1 1,1 0,1 0,0 "
Positions="-0.5,-0.5,0.5 0.5,-0.5,0.5 0.5,0.5,0.5 0.5,0.5,0.5 -0.5,0.5,0.5 -0.5,-0.5,0.5 " />
</GeometryModel3D.Geometry>
<!-- The material specifies the material applied to the plane. In this case it is a linear gradient.-->
<GeometryModel3D.Material>
<MaterialGroup>
<DiffuseMaterial>
<DiffuseMaterial.Brush>
<LinearGradientBrush StartPoint="0,0.5" EndPoint="1,0.5">
<LinearGradientBrush.GradientStops>
<GradientStop Color="Yellow" Offset="0" />
<GradientStop Color="Red" Offset="1" />
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</DiffuseMaterial.Brush>
</DiffuseMaterial>
</MaterialGroup>
</GeometryModel3D.Material>
<!-- The Transform specifies how to transform the 3D object. The properties of the
Rotation object are animated causing the 3D object to rotate and "wobble" (see Storyboard below).-->
<GeometryModel3D.Transform>
<RotateTransform3D>
<RotateTransform3D.Rotation>
<AxisAngleRotation3D x:Name="myAngleRotation" Axis="0,3,0" Angle="{Binding Rotation, Mode=TwoWay}" />
</RotateTransform3D.Rotation>
</RotateTransform3D>
</GeometryModel3D.Transform>
</GeometryModel3D>
</ModelVisual3D.Content>
</ModelVisual3D>
</Viewport3D.Children>
</Viewport3D>
</Canvas>
</Viewbox>
</Grid>
</Window>
The particularly interesting parts of that are:
The bound AxisAngleRotation 3d. As it's name suggests, this controls the apparent angle of rotation of the object.
<GeometryModel3D.Transform>
<RotateTransform3D>
<RotateTransform3D.Rotation>
<AxisAngleRotation3D x:Name="myAngleRotation" Axis="0,3,0" Angle="{Binding Rotation, Mode=TwoWay}" />
</RotateTransform3D.Rotation>
</RotateTransform3D>
</GeometryModel3D.Transform>
The angle is bound to the Rotation property in the ViewModel.
Those buttons over on the top right:
<StackPanel Grid.Column="1">
<Button Command="{Binding LeftRotateCommand}">Left</Button>
<Button Command="{Binding RightRotateCommand}">Right</Button>
</StackPanel>
As well as two InputBindings for the left and right keys:
<Window.InputBindings>
<KeyBinding Key="Left" Command="{Binding LeftRotateCommand}" />
<KeyBinding Key="Right" Command="{Binding RightRotateCommand}" />
</Window.InputBindings>
Which are bound to two RelayCommands exposed from the MainWindowViewModel.
That's set as the datacontext of the view in xaml:
<Window.DataContext>
<local:MainWindowViewModel/>
</Window.DataContext>
Storyboard
The Storyboard which is going to animate that rotation is defined in a Resource Dictionary called Dictionary1.
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Storyboard x:Key="XRotateStoryboard"
Storyboard.TargetName="{Binding}"
TargetProperty="Rotation"
Duration="0:0:2">
<DoubleAnimation By="10"/>
</Storyboard>
</ResourceDictionary>
It's going to use "By" to increase the value on a double. Actually, that value is substituted later but more about that later.
The property is set to Rotation, which has already been mentioned above as the property the 3d object's RotateTransform uses.
The somewhat strange but absolutely critical part of the above is that the Storyboard.TargetName is set to "{Binding}", without this the Storyboard cannot be connected up to the ViewModel.
You could alternatively create the Storyboard in code ( which can be challenging) or by using a loose xaml template and XamlReader in a similar manner to the samples here.
App.xaml
Dictionary1 is merged into Application.Current.Resources by App.xaml as the app starts:
<Application x:Class="wpf_Animate_ViewModel.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="MainWindow.xaml">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Dictionary1.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
MainWindowViewModel
The sample only does the rotation so there's not much code in the ViewModel. As mentioned earlier this is a DependencyObject so we can have that Rotation Dependency Property.
public class MainWindowViewModel : DependencyObject
{
private Storyboard XRotate;
private DoubleAnimation XAnimation;
public double Rotation
{
get { return (double)GetValue(RotationProperty); }
set { SetValue(RotationProperty, value); }
}
public static readonly DependencyProperty RotationProperty =
DependencyProperty.Register("Rotation", typeof(double), typeof(MainWindowViewModel), new PropertyMetadata(0D)
);
public RelayCommand RightRotateCommand { get; set; }
public RelayCommand LeftRotateCommand { get; set; }
public MainWindowViewModel()
{
XRotate = Application.Current.Resources["XRotateStoryboard"] as Storyboard;
XAnimation = XRotate.Children[0] as DoubleAnimation;
Storyboard.SetTarget(XAnimation, this);
RightRotateCommand = new RelayCommand(RightRotateExecute);
LeftRotateCommand = new RelayCommand(LeftRotateExecute);
}
private void LeftRotateExecute()
{
XAnimation.By = -10;
XRotate.Begin();
}
private void RightRotateExecute()
{
XAnimation.By = 10;
XRotate.Begin();
}
}
If you take a look at the constructor, that goes and gets the storyboard which was merged in app.xaml.
public MainWindowViewModel()
{
XRotate = Application.Current.Resources["XRotateStoryboard"] as Storyboard;
XAnimation = XRotate.Children[0] as DoubleAnimation;
Storyboard.SetTarget(XAnimation, this);
RightRotateCommand = new RelayCommand(RightRotateExecute);
LeftRotateCommand = new RelayCommand(LeftRotateExecute);
}
In this case the storyboard will only have one child, which is the DoubleAnimation and it sets a private variable to a reference to that.
You could foreach if there were multiple animations in the one storyboard.
It then uses Storyboard.SetTarget to point the storyboard at this which is of course this instance of MainViewModel.
The two relaycommands are then connected up to their methods.
If you take a look at the Rotation Dependency Property:
public double Rotation
{
get { return (double)GetValue(RotationProperty); }
set { SetValue(RotationProperty, value); }
}
public static readonly DependencyProperty RotationProperty =
DependencyProperty.Register("Rotation", typeof(double), typeof(MainWindowViewModel), new PropertyMetadata(0D)
);
That's a double type dependency property and it's set to a default of zero using the technique mentioned here.
The two methods which the commands run:
private void LeftRotateExecute()
{
XAnimation.By = -10;
XRotate.Begin();
}
private void RightRotateExecute()
{
XAnimation.By = 10;
XRotate.Begin();
}
Both use the private variables set up in the constructor to set the value the animation will change the value BY, before calling Begin on the storyboard to set it working.
Conclusion
And that's it, not a huge amount of code but a few things to watch out for. Precisely what to set on the storyboard is the tricky bit. Partly to prove you can, values are set dynamically on that animation from code and you might have rather more complicated logic than switching between a plus and minus.
This is unlikely to be something you will want to use in every viewmodel nor in every project but can be quite a handy approach.
See Also
WPF Resources on the Technet Wiki
This article is part of the WPF Tips Series.