Yesterday I was asked about a way to play animations in MVVM environment.
So, I gave this some thought today, and came up with a few ideas.
First, and the easiest way is to separate ViewModel from animation by attaching an animation to a behavior. Here is how we would write the behavior:
public class MVVMSimpleAnimationBehavior : TargetedTriggerAction<UIElement>
{
protected override void Invoke(object parameter)
{
RunAnimation();
}
public Storyboard AnimationStoryBoard
{
get { return (Storyboard)GetValue(AnimationStoryBoardProperty); }
set { SetValue(AnimationStoryBoardProperty, value); }
}
public static readonly DependencyProperty AnimationStoryBoardProperty =
DependencyProperty.Register("AnimationStoryBoard", typeof(Storyboard), typeof(MVVMSimpleAnimationBehavior), new PropertyMetadata(null));
private void RunAnimation()
{
if (AnimationStoryBoard != null)
{
AnimationStoryBoard.Begin();
}
}
}
Pretty simple approach. Here is how would use animation in the XAML:
<Button Content="Behavior Test" Grid.Column="1">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Click">
<local:MVVMAnimationBehavior AnimationStoryBoard="{StaticResource TestStoryboard}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</Button>
Now, I want to complicate the story a little bit. I really want to trigger an animation in the UI via an event in my model. I am going to start with a sample ViewModel. I want to have an event there that I will later use as a trigger for my animation. Here is what my ViewModel would look like:
public class SampleModel: INotifyPropertyChanged
{
public event EventHandler StartAnimation;
protected virtual void OnStartAnimation()
{
if (StartAnimation != null)
{
StartAnimation(this, EventArgs.Empty);
}
}
public void RaiseEvent()
{
OnStartAnimation();
}
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
}
My event is called StartAnimation. Now it is time to develop a more complicate behavior to support my event driven animation from ViewModel. I am going to actually combine new functionality with the simple behavior about and have multi-functional useful object. Here is what the final product looks like:
using System;
using System.Windows.Interactivity;
using System.Windows;
using System.Windows.Media.Animation;
using System.ComponentModel;
using System.Reflection;
namespace SLMVVMAnimationBehavior
{
public class MVVMAnimationBehavior : TargetedTriggerAction<UIElement>
{
private EventInfo info;
protected override void Invoke(object parameter)
{
RunAnimation(this, EventArgs.Empty);
}
public Storyboard AnimationStoryBoard
{
get { return (Storyboard)GetValue(AnimationStoryBoardProperty); }
set { SetValue(AnimationStoryBoardProperty, value); }
}
public static readonly DependencyProperty AnimationStoryBoardProperty =
DependencyProperty.Register("AnimationStoryBoard", typeof(Storyboard), typeof(MVVMAnimationBehavior), new PropertyMetadata(null));
public string EventName
{
get { return (string)GetValue(EventNameProperty); }
set { SetValue(EventNameProperty, value); }
}
public static readonly DependencyProperty EventNameProperty =
DependencyProperty.Register("EventName", typeof(string), typeof(MVVMAnimationBehavior), new PropertyMetadata(HandlModelChanged));
public object Model
{
get { return GetValue(ModelProperty); }
set { SetValue(ModelProperty, value); }
}
public static readonly DependencyProperty ModelProperty =
DependencyProperty.Register("Model", typeof(object), typeof(MVVMAnimationBehavior), new PropertyMetadata(HandlModelChanged));
private static void HandlModelChanged(object sender, DependencyPropertyChangedEventArgs e)
{
MVVMAnimationBehavior behavior = sender as MVVMAnimationBehavior;
object model = behavior.Model;
if (behavior.info != null)
{
behavior.info.RemoveEventHandler(model, Delegate.CreateDelegate(typeof(EventHandler), behavior, "RunAnimation"));
}
if (model != null && !string.IsNullOrEmpty(behavior.EventName))
{
behavior.info = model.GetType().GetEvent(behavior.EventName);
behavior.info.AddEventHandler(model, Delegate.CreateDelegate(typeof(EventHandler), behavior, "RunAnimation"));
}
}
private void RunAnimation(object sender, EventArgs e)
{
if (AnimationStoryBoard != null)
{
AnimationStoryBoard.Begin();
}
}
}
}
As you can see, I added two mode dependency properties β one for event name that will trigger the animation, the other one for ViewModel ( I actually called it model). Now the tricky part is to use a bit of reflection to attach an event handler to the event on my model. This is done in HandlModelChanged routine that is called when Model or Event name is set. I am getting an event, and attaching a handler to it. The actual handler is located inside the behavior itself β RunAnimation method. Here is what my fianl XAML looks like that is testing both animation approaches:
<UserControl x:Class="SLMVVMAnimationBehavior.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:SLMVVMAnimationBehavior"
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
mc:Ignorable="d" d:DesignWidth="640" d:DesignHeight="480">
<UserControl.Resources>
<Storyboard x:Name="TestStoryboard">
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="textBlock" Storyboard.TargetProperty="(TextBlock.FontSize)">
<EasingDoubleKeyFrame KeyTime="00:00:00.1000000" Value="24"/>
<EasingDoubleKeyFrame KeyTime="00:00:00.2000000" Value="21.333"/>
<EasingDoubleKeyFrame KeyTime="00:00:00.3000000" Value="18.667"/>
<EasingDoubleKeyFrame KeyTime="00:00:00.4000000" Value="16"/>
<EasingDoubleKeyFrame KeyTime="00:00:00.5000000" Value="13.333"/>
<EasingDoubleKeyFrame KeyTime="00:00:00.6000000" Value="16"/>
<EasingDoubleKeyFrame KeyTime="00:00:00.7000000" Value="18.667"/>
<EasingDoubleKeyFrame KeyTime="00:00:00.8000000" Value="21.333"/>
<EasingDoubleKeyFrame KeyTime="00:00:00.9000000" Value="24"/>
<EasingDoubleKeyFrame KeyTime="00:00:01" Value="26.667"/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
<local:SampleModel x:Key="CurrentModel" />
<local:MVVMAnimationBehavior AnimationStoryBoard="{StaticResource TestStoryboard}" Model="{StaticResource CurrentModel}" x:Key="Animation" EventName="StartAnimation"/>
</UserControl.Resources>
<Grid x:Name="LayoutRoot" Background="#FFF3EEEE" DataContext="{Binding Source={StaticResource CurrentModel}}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<Button x:Name="TestButton" Content="Click To Start Animation" d:LayoutOverrides="Height" HorizontalAlignment="Left" Click="TestButton_Click"/>
<Button Content="Behavior Test" Grid.Column="1">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Click">
<local:MVVMAnimationBehavior AnimationStoryBoard="{StaticResource TestStoryboard}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</Button>
<TextBlock x:Name="textBlock" Margin="0,0,0,6" Text="Sample Text" TextWrapping="Wrap" d:LayoutOverrides="Width, Height" Grid.Row="1" Grid.ColumnSpan="3" HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="26.667"/>
</Grid>
</Grid>
</UserControl>
To summarize, I illustrate a couple of approaches to trigger animations in MVVM environment. Of course, there is always code-behind, but many developers frown at this approach..
Please let me know if you have any questions.
Another way to do MVVM based animations is something I call Reverse Commands – they are basically ICommands that you execute in your ViewModel and have them trigger a set of specified action(s) (like manipulating storyboard or view-states) in the View. See for some examples and code http://www.orktane.com/Blog/post/2010/01/07/Reverse-ICommands-for-MVVM.aspx
Cheers,
Rishi
Rishi,
As always, there is a million ways to skin a cat in Microsoft world π
Thanks for sharing yours.
Rishi,
Your recommendation requires downloading nRoute.Toolkit. I donβt understand why Microsoft itself does not have a library that supports MVVM and animation using Silverlight.
I have a Silverlight app that receives messages from a third party messaging platform. My app is using MVVM pattern. Is there a good way to blink the UI (the user control) when a message is received without using code behind.
http://www.basarat.com/2011/05/expression-blend-starting-storyboard.html#comment-form this explains how to do this using expression blend
@D.
I am not certain I understand the question.