Wednesday, January 5, 2011

Visual State Manager in WPF4

One of the questions I hear as a Silverlight Developer is why are there no Triggers in Silverlight?  This question has been answered a few times (one of which is in references below).  Mainly because Silverlight has always had the Visual State Manager (VSM) which is far more powerful than triggers.

Triggers were a great tool for developers in WPF for a while, and were really easy to implement in code. Developers loved them but designers hated them.  Designers had no way of applying easing animations or "softer" transitions rather than just a snap into another state.  In fact now, WPF4 has the VSM as well and combined with Blend 4 the VSM is easy to use and very fast to create different visual representations of a button for default and mouse over and animations in between for example.

If you are starting a new WPF application you would be wise to prefer VSM over triggers.  Even in the most basic state changes, there may be a time when someone will want to add animation or edit it in Blend. Blend doesn't play nicely  with triggers I have found.

I wrote a quick little demo to show how the VSM works and how to use it with the MVVM pattern.

Download the code here.
Screen shots:
You can't see the easing animation in the screenshots obviously, but its there in the code.  There are 2 states for this sample application, there is either an incoming call, or you are on a call.  Depending on the state the button color and text changes.

The challenge with MVVM is that you want the controller to own and control the state.  This is done by the magic of binding to an attached property.
Here's the State property on the controller:

        public CallCardState CurrentState
        {
            get
            {
                return this.currentState;
            }

            private set
            {
                this.currentState = value;
                NotifyPropertyChange("CurrentState");
                NotifyPropertyChange("MainActionLabel");
            }
        }


The return type is a simple custom enum (with 2 possible values).  The MainActionLabel is where the button gets its text from, which in my simple example is using a switch case to return appropriate text.
Here's the Xaml binding for Visual State:

<UserControl 
    x:Class="VSMTest.CallCard" 
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    d:DesignHeight="200" 
    d:DesignWidth="300" 
    local:VisualStateHelper.VisualStateName="{Binding CurrentState}" 
    mc:Ignorable="d" 
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
    xmlns:local="clr-namespace:VSMTest" >
...</UserControl>


Here you can see the binding to a custom attached property, this is done at the control level.  In my case the control is the entire panel showing a button on the right and some text on the left. In this example the state applies to the whole user control.

Here's the code for the attached property:

namespace VSMTest
{
    using System;
    using System.Windows;
    using System.Windows.Controls;

    public class VisualStateHelper : DependencyObject
    {
        public static readonly DependencyProperty VisualStateNameProperty = DependencyProperty.RegisterAttached(
            "VisualStateName", 
            typeof(string), 
            typeof(VisualStateHelper), 
            new PropertyMetadata(OnVisualStateNameChanged));

        public static string GetVisualStateName(DependencyObject target)
        {
            return target.GetValue(VisualStateNameProperty).ToString();
        }

        public static void SetVisualStateName(DependencyObject target, string visualStateName)
        {
            // This may throw an exception if an enum is used. However, it shouldn't be used as the user control
            // should not set its own state, the controller will always set it.
            try
            {
                target.SetValue(VisualStateNameProperty, visualStateName);
            } catch (Exception ex)
            {
                throw new NotSupportedException("Setting visual states from within the user control or from binding is not supported. It should be set by the controller", ex);
            }
        }

        private static void OnVisualStateNameChanged(object sender, DependencyPropertyChangedEventArgs args)
        {
            var visualStateName = args.NewValue.ToString();
            var control = sender as Control; // Must be a control, ie an input control.
            if (control == null)
            {
                throw new InvalidOperationException("This attached property only supports types derived from Control (ie UserControl).");
            }

            // Apply the visual state.
            VisualStateManager.GoToState(control, visualStateName, true);
        }
    }
}

The critical piece here is the OnVisualStateNameChanged, this is triggered when the controller changes the underlying value. When it does the VisualStateManager.GoToState static method is called, and the magic begins.


Summary
Using the VSM is even easier if you choose not to use MVVM, but even using MVVM it is pretty straight forward. Even for quick simple applications I find myself regretting using triggers and not using VSM straight off, just like when I think using code behind might be quicker than using MVVM.
It might be a little more code than using triggers but it is definitely more flexible and easier to get right. Designers can use Blend to perfect animations and colours leaving us developers to get the real work done ;-)




References:

No comments:

Post a Comment