Silverlight, MVVM and Animations

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.

6 Comments

  1. 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.

Leave a Reply to Anonymous Cancel reply

Your email address will not be published. Required fields are marked *