Silverlight 4: using the VisualStateManager for state animations with MVVM

A recurring problem with MVVM is how to incorporate animations in a nice MVVM way. The problem is that StoryBoards need to be started from the view, since they are usually configured in XAML. This means that the ViewModel needs a reference to the view, in order to tell it when the animation should start. Keeping a reference from the ViewModel to the View is obviously “not done” in the MVVM pattern. In this post I’m going to show how you can use the VisualStateManager to start animations and incorporate it in the MVVM pattern, without keeping a reference from the ViewModel to the View. 

The VisualStateManager

When using animations in business applications, it’s usually to tell your user that the application is going from one state to another. For example, going from the “NotLoggedIn” state to the “LoggedIn” state. States in Silverlight are handled by the VisualStateManager. The VisualStateManager is probably well known under Silverlight developers; it’s used to go from state to state in controls. What a lot of developers don’t know, is that the VisualStateManager can also be used in other situations than custom control development or templating controls. For example, you can use the VisualStateManager to manually add states to a UserControl, let Blend record the user interface in the different states and use the “VisualStateManager.GoToState()” method to let your UserControl switch states. This way, you can declaratively tell in XAML, how your UserControl should look in each state. Blend handles the animations and the XAML part, all you have to do is tell the VisualStateManager when to switch states. I’m going to demonstrate this with a very simple application:

image

The idea is that when the user moves the mouse over the image, it becomes slightly enlarged, using an animation in the process. Obviously, MVVM is overkill for this very simple application, but it demonstrates the point quite well. We can define two states in this application, namely “Enlarged” and “Normal”.

In order to make this work in a MVVM way, we have to do a couple of things:

  • Create a ViewModel, named “MainPageViewModel”.
  • Use a command for the “MouseEnter” event of the image, to switch to the “Enlarged” state.
  • Use a command for the “MouseLeave” event of the image, to switch to the “Normal” state.
  • Bind a “CurrentState” property of the main UserControl, to a “CurrentState” property of the ViewModel. Every time the “CurrentState” property of the ViewModel changes, the View should switch states. The two commands for the “MouseLeave” and “MouseEnter” events should modify the “CurrentState” property of the ViewModel and the View should update itself through data binding. This way, the ViewModel doesn’t need to have a reference to the View.

When implementing the scenario outlined above you’ll encounter a couple of problems:

  • The Image component doesn’t have support for any ICommand objects.
  • A UserControl class doesn’t have a “CurrentState” property which you can bind to the ViewModel.
  • There isn’t a generic ICommand implementation in Silverlight.

To tackle these problems I’ve created a separate reusable Silverlight class library, named “MVVMSupport”. 

The MVVMSupport Library

This library contains three classes, one to fix each problem outlined above. First up is the DelegateCommand<T> class:

   1: using System;

   2: using System.Net;

   3: using System.Windows;

   4: using System.Windows.Controls;

   5: using System.Windows.Documents;

   6: using System.Windows.Ink;

   7: using System.Windows.Input;

   8: using System.Windows.Media;

   9: using System.Windows.Media.Animation;

  10: using System.Windows.Shapes;

  11:  

  12: namespace MVVMSupport

  13: {

  14:     public class DelegateCommand<T> : ICommand

  15:     {

  16:         public event EventHandler CanExecuteChanged;

  17:         private Action<T> _toExecute;

  18:         private Func<T, bool> _condition;

  19:  

  20:  

  21:         public DelegateCommand(Action<T> toExecute) : this(toExecute, (param)=>true)

  22:         {

  23:  

  24:         }

  25:  

  26:         public DelegateCommand(Action<T> toExecute, Func<T, bool> condition)

  27:         {

  28:             if (toExecute == null)

  29:             {

  30:                 throw new ArgumentException("There must be an Action to execute");

  31:             }

  32:             if (condition == null)

  33:             {

  34:                 throw new ArgumentException("Condition must have a value, or use the other constructor instead");

  35:             }

  36:             _toExecute = toExecute;

  37:             _condition = condition;

  38:         }

  39:  

  40:  

  41:         protected virtual void OnCanExecuteChanged(EventArgs args)

  42:         {

  43:             EventHandler temp = CanExecuteChanged;

  44:             if (temp != null)

  45:             {

  46:                 CanExecuteChanged(this, args);

  47:             }

  48:         }

  49:  

  50:         public void FireCanExecuteChanged(EventArgs args)

  51:         {

  52:             OnCanExecuteChanged(args);

  53:         }

  54:  

  55:         public bool CanExecute(object parameter)

  56:         {

  57:             return _condition((T)parameter);

  58:         }

  59:  

  60:  

  61:  

  62:         public void Execute(object parameter)

  63:         {

  64:             _toExecute((T)parameter);

  65:         }

  66:     }

  67: }

This is a slightly modified implementation of the DelegateCommand class wandering around on the Internet. It’s a generic ICommand implementation with two constructors, one which supports a condition that determines whether the command can be executed at the current time (to let a Button disable itself for example), the other supplies a condition that always returns true, so the command can always be executed. Because the implementation of the action and the condition to execute is supplied by Action and Func delegates coming from outside this class, the outside (usually the ViewModel) must also determine when the command can be executed. This means that the ViewModel is responsible for firing the “CanExecuteChanged” event. This is where the “FireCanExecuteChanged” method comes in. I don’t use these features in the sample application above, but a generic ICommand implementation should support them. This class tackled the “No generic command implementation in Silverlight” problem outlined above.

Next up is the static “Commands” class:

   1: using System;

   2: using System.Net;

   3: using System.Windows;

   4: using System.Windows.Controls;

   5: using System.Windows.Documents;

   6: using System.Windows.Ink;

   7: using System.Windows.Input;

   8: using System.Windows.Media;

   9: using System.Windows.Media.Animation;

  10: using System.Windows.Shapes;

  11:  

  12: namespace MVVMSupport

  13: {

  14:     public static class Commands

  15:     {

  16:  

  17:         public static readonly DependencyProperty MouseEnterCommandProperty =

  18:          DependencyProperty.RegisterAttached("MouseEnterCommand", typeof(ICommand), typeof(Commands),

  19:          new PropertyMetadata(new PropertyChangedCallback(AttachOrRemoveMouseEnterEvent)));

  20:  

  21:         public static readonly DependencyProperty MouseLeaveCommandProperty =

  22:             DependencyProperty.RegisterAttached("MouseLeaveCommand", typeof(ICommand), typeof(Commands),

  23:             new PropertyMetadata(new PropertyChangedCallback(AttachOrRemoveMouseLeaveEvent)));

  24:  

  25:  

  26:  

  27:         public static ICommand GetMouseEnterCommand(DependencyObject obj)

  28:         {

  29:             return (ICommand)obj.GetValue(MouseEnterCommandProperty);

  30:         }

  31:  

  32:         public static void SetMouseEnterCommand(DependencyObject obj, ICommand value)

  33:         {

  34:             obj.SetValue(MouseEnterCommandProperty, value);

  35:         }

  36:  

  37:         public static ICommand GetMouseLeaveCommand(DependencyObject obj)

  38:         {

  39:             return (ICommand)obj.GetValue(MouseLeaveCommandProperty);

  40:         }

  41:  

  42:         public static void SetMouseLeaveCommand(DependencyObject obj, ICommand value)

  43:         {

  44:             obj.SetValue(MouseLeaveCommandProperty, value);

  45:         }

  46:  

  47:         private static void AttachOrRemoveMouseEnterEvent(DependencyObject obj, DependencyPropertyChangedEventArgs args)

  48:         {

  49:  

  50:             FrameworkElement element = obj as FrameworkElement;

  51:             if(element != null)

  52:             {

  53:                 ICommand command = (ICommand)args.NewValue;

  54:  

  55:                 if (args.OldValue == null && args.NewValue != null)

  56:                 {

  57:                     element.MouseEnter += ExecuteMouseEnterCommand;

  58:                 }

  59:                 else if(args.NewValue == null && args.OldValue != null)

  60:                 {

  61:                     element.MouseEnter -= ExecuteMouseEnterCommand;

  62:                 }

  63:             }

  64:             else

  65:             {

  66:                 throw new ArgumentException("MouseEnterCommand is only supported on FrameworkElement");

  67:             }

  68:         }

  69:  

  70:         private static void AttachOrRemoveMouseLeaveEvent(DependencyObject obj, DependencyPropertyChangedEventArgs args)

  71:         {

  72:             FrameworkElement element = obj as FrameworkElement;

  73:             if (element != null)

  74:             {

  75:                 ICommand command = (ICommand)args.NewValue;

  76:                 if (args.OldValue == null && args.NewValue != null)

  77:                 {

  78:                     element.MouseLeave += ExecuteMouseLeaveCommand;

  79:                 }

  80:                 else if (args.NewValue == null && args.OldValue != null)

  81:                 {

  82:                     element.MouseLeave -= ExecuteMouseLeaveCommand;

  83:                 }

  84:             }

  85:             else

  86:             {

  87:                 throw new ArgumentException("MouseLeaveCommand is only supported on FrameworkElement");

  88:             }

  89:           

  90:         }

  91:  

  92:         private static void ExecuteMouseEnterCommand(object sender, MouseEventArgs args)

  93:         {

  94:             DependencyObject dSender = (DependencyObject)sender;

  95:             ICommand toExecute = (ICommand)dSender.GetValue(MouseEnterCommandProperty);

  96:             if (toExecute.CanExecute(args))

  97:             {

  98:                 toExecute.Execute(args);

  99:             }

 100:         }

 101:  

 102:         private static void ExecuteMouseLeaveCommand(object sender, MouseEventArgs args)

 103:         {

 104:             DependencyObject dSender = (DependencyObject)sender;

 105:             ICommand toExecute = (ICommand)dSender.GetValue(MouseLeaveCommandProperty);

 106:             if (toExecute.CanExecute(args))

 107:             {

 108:                 toExecute.Execute(args);

 109:             }

 110:         }

 111:         

 112:     }

 113: }

I’m not covering every bit of code in this class. The idea is that the lack of ICommand support of most controls in Silverlight, can easily be solved by using attached properties. I define two attached properties in the class above, a “MouseEnterCommand” and a “MouseLeaveCommand”. When set, this class registers event listeners for the “MouseEnter” and “MouseLeave” events, when these events occur, the correct command is executed, supplying the MouseEvent for extra information to the command. For every command you miss in Silverlight, you can use an attached property to provide support for it. This class obviously tackles the “No command support” problem outlined above. 

And finally, the static “VisualStates” class:

   1: using System;

   2: using System.Net;

   3: using System.Windows;

   4: using System.Windows.Controls;

   5: using System.Windows.Documents;

   6: using System.Windows.Ink;

   7: using System.Windows.Input;

   8: using System.Windows.Media;

   9: using System.Windows.Media.Animation;

  10: using System.Windows.Shapes;

  11:  

  12: namespace MVVMSupport

  13: {

  14:     public static class VisualStates

  15:     {

  16:  

  17:        public static readonly DependencyProperty CurrentStateProperty =

  18:            DependencyProperty.RegisterAttached("CurrentState", typeof(String), typeof(VisualStates), new PropertyMetadata(TransitionToState));

  19:  

  20:         public static string GetCurrentState(DependencyObject obj)

  21:         {

  22:             return (string)obj.GetValue(CurrentStateProperty);

  23:         }

  24:  

  25:         public static void SetCurrentState(DependencyObject obj, string value)

  26:         {

  27:             obj.SetValue(CurrentStateProperty, value);

  28:         }

  29:  

  30:         private static void TransitionToState(object sender, DependencyPropertyChangedEventArgs args)

  31:         {

  32:  

  33:             Control c = sender as Control;

  34:             if(c != null)

  35:             {

  36:                 VisualStateManager.GoToState(c, (string)args.NewValue, true);

  37:             }

  38:             else

  39:             {

  40:                 throw new ArgumentException("CurrentState is only supported on the Control type");

  41:             }

  42:         }

  43:     }

  44: }

This class provides an attached property for every Control. when this property is set, the control transitions to the new state. Because transitioning to states can now be done by setting properties instead of calling the “VisualStateManager.GoToState()” method, you can easily bind this property to a property in the ViewModel.

The ViewModel

Let’s take look at the ViewModel for the sample application:

   1: using System;

   2: using System.Net;

   3: using System.Windows;

   4: using System.Windows.Controls;

   5: using System.Windows.Documents;

   6: using System.Windows.Ink;

   7: using System.Windows.Input;

   8: using System.Windows.Media;

   9: using System.Windows.Media.Animation;

  10: using System.Windows.Shapes;

  11: using MVVMSupport;

  12: using System.ComponentModel;

  13:  

  14: namespace ImageApp

  15: {

  16:     public class MainPageViewModel : INotifyPropertyChanged

  17:     {

  18:         private string _currentState;

  19:  

  20:         public event PropertyChangedEventHandler PropertyChanged;

  21:         public ICommand ToLargeState { get; private set; }

  22:         public ICommand ToNormalState { get; private set; }

  23:         public string CurrentState

  24:         {

  25:             get

  26:             {

  27:                 return _currentState;

  28:             }

  29:             set

  30:             {

  31:                 if (value != _currentState)

  32:                 {

  33:                     _currentState = value;

  34:                     OnPropertyChanged(new PropertyChangedEventArgs("CurrentState"));

  35:                 }

  36:             }

  37:  

  38:         }

  39:  

  40:         public MainPageViewModel()

  41:         {

  42:             ToLargeState = new DelegateCommand<MouseEventArgs>((args) => CurrentState = "Enlarged");

  43:             ToNormalState = new DelegateCommand<MouseEventArgs>((args) => CurrentState = "Normal");

  44:         }

  45:  

  46:         protected virtual void OnPropertyChanged(PropertyChangedEventArgs propertyChangedEventArgs)

  47:         {

  48:             PropertyChangedEventHandler temp = PropertyChanged;

  49:             if (temp != null)

  50:             {

  51:                 temp(this, propertyChangedEventArgs);

  52:             }

  53:         }

  54:        

  55:     }

  56: }

The ViewModel is actually quite small. This is because all it has to do is, set the current state to the correct state when the “ToLargeState” command or the “ToNormalState” command is executed. The ViewModel uses the DelegateCommand<T> class covered earlier. The commands are bound to the “MouseEnterCommand” and the “MouseLeaveCommand” attached properties (also covered earlier) in the view, that’s why the lambda’s accept a “MouseEventArgs”. When the “CurrentState” property in the ViewModel changes, the View is automatically notified because of the INotifyPropertyChanged implementation.

 

The View

The whole View is implemented in XAML:

   1: <UserControl

   2:     >="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

   3:     >="http://schemas.microsoft.com/winfx/2006/xaml"

   4:     >="http://schemas.microsoft.com/expression/blend/2008"

   5:     >="http://schemas.openxmlformats.org/markup-compatibility/2006"

   6:     >="clr-namespace:ImageApp"

   7:     >="clr-namespace:MVVMSupport;assembly=MVVMSupport"

   8:     >="http://schemas.microsoft.com/expression/2010/interactions" >="http://schemas.microsoft.com/expression/2010/effects" x:Class="ImageApp.MainPage"

   9:     mc:Ignorable="d"

  10:     d:DesignHeight="300" d:DesignWidth="400"

  11:     DataContext="{StaticResource mainPageViewModel}"

  12:     mvvm:VisualStates.CurrentState="{Binding CurrentState}"

  13:     >

  14:  

  15:     <Grid x:Name="LayoutRoot" Background="White">

  16:         <VisualStateManager.VisualStateGroups>

  17:             <VisualStateGroup x:Name="States">

  18:                 <VisualStateGroup.Transitions>

  19:                     <VisualTransition GeneratedDuration="0:0:0.3">

  20:                         <ei:ExtendedVisualStateManager.TransitionEffect>

  21:                             <ee:CircleRevealTransitionEffect/>

  22:                         </ei:ExtendedVisualStateManager.TransitionEffect>

  23:                     </VisualTransition>

  24:                 </VisualStateGroup.Transitions>

  25:                 <VisualState x:Name="Enlarged">

  26:                     <Storyboard>

  27:                         <DoubleAnimation Duration="0" To="1.25" Storyboard.TargetProperty="(UIElement.RenderTransform).(CompositeTransform.ScaleX)" Storyboard.TargetName="image" d:IsOptimized="True"/>

  28:                         <DoubleAnimation Duration="0" To="1.25" Storyboard.TargetProperty="(UIElement.RenderTransform).(CompositeTransform.ScaleY)" Storyboard.TargetName="image" d:IsOptimized="True"/>

  29:                     </Storyboard>

  30:                 </VisualState>

  31:                 <VisualState x:Name="Normal"/>

  32:             </VisualStateGroup>

  33:         </VisualStateManager.VisualStateGroups>

  34:           <VisualStateManager.CustomVisualStateManager>

  35:             <ei:ExtendedVisualStateManager/>

  36:         </VisualStateManager.CustomVisualStateManager>

  37:         

  38:         

  39:         <Image x:Name="image" Source="parrot.jpg" Height="300" Width="300"

  40:                 mvvm:Commands.MouseEnterCommand="{Binding ToLargeState}"

  41:                 mvvm:Commands.MouseLeaveCommand="{Binding ToNormalState}" RenderTransformOrigin="0.5,0.5">

  42:             <Image.RenderTransform>

  43:                 <CompositeTransform/>

  44:             </Image.RenderTransform>

  45:         </Image>

  46:  

  47:     </Grid>

  48: </UserControl>

I’ll cover the different parts:

  • Line 11: “The DataContext” property is bound to the ViewModel, as usual with MVVM.
  • Line 12: The attached “CurrentState” property is bound to the “CurrentState” property of the ViewModel. This means that when the “CurrentState” property in the ViewModel changes, the UserControl also transitions to another state.
  • Line 16 – 36: This XAML is all generated by Blend 4, and tells the VisualStateManager how the control should look in each state. You can generate this XAML in Blend by first opening your UserControl and then switching to the “States” panel:

image

You can add states as much as you like and define transitions and animations by clicking on a state and then simply modify the properties in the designer. You only have to make sure that the added states have the same names as the states in the ViewModel.

  • Line 40 – 41: Here the attached “MouseEnterCommand” and the “MouseLeaveCommand” properties are bound to the commands in the ViewModel. They’ll execute when the mouse enters or leaves the image.

Conclusion

That’s it! This is all that is needed to wire everything up. By splitting the application up in states, using attached properties for the missing commands and using the VisualStateManager to transition between states in a declarative way, all responsibilities are neatly where they belong, without needing a reference from the ViewModel to the View. The ViewModel determines to which state is switched and remains testable and the View knows how it should look in each state. The beauty of the View is that all that it needs to know is in XAML, and you can easily tweak the appearance of the different states with Blend, without ever touching the ViewModel. The solution above also works for DataTemplates in an ItemsControl, DataTemplates also support the manually adding of states in Blend. You can download the whole working solution here. Creating nice interactive states with Blend is easy, especially with Blend’s fluid layout features. Those features have been expanded in Blend 4. Enjoy!