Thursday, March 18, 2010

Introducing the Visual State Aggregator

The visual state aggregator. What is it?

How many times have you found yourself adding freak methods and commands to your Silverlight projects just to manage some animations? It's a common issue, and I've even built solutions like the IAnimationDelegate to help solve the problem. While it works, one thing always bothered me.

I know it's a ViewModel, so it does do things with the view, but should I really be that concerned with what's going on with the UI? In other words, while it's nice that I can abstract the firing of an animation behind an Action call and use a command to set it off and then unit test without a real Storyboard object, it has always felt just a little bit dirty to do it that way.

Download the source code for this post

I also am a big fan of the VisualStateManager and got to thinking about whether I ever really need a named story board outside of VSM other than programmatic or complex ones that have to be built at runtime. Looking at the current application I'm working with, which uses some very heavy animated transitions, I realized that all of these could be boiled down to the VSM.

Consider this scenario: we have two panels. Panel A is active, Panel B is collapsed and off the screen. These are independent controls that probably don't even share the same view model.

In this case, I'm going to give Panel A a visual state of "Foreground" and Panel B a visual state of "Offscreen". If you're scratching your head about the visual state manager, take a look at Justin Angel's excellent post on the topic. You can create your own groups and states and have both "final" states and transition states.

Quick Tip: Visibility

Probably more people abuse visibility by wiring in delegates or commands that set visibility to visible or collapsed (I'm guilty as well) when it's not needed. Stop thinking in terms of visible, collapsed, etc, and start thinking in terms of states. With the visual state manager, this is what your states might look like:

<VisualStateGroup x:Name="TransitionStates">
    <VisualStateGroup.States>
        <VisualState x:Name="Show">
            <Storyboard>
                <ObjectAnimationUsingKeyFrames BeginTime="00:00:00" Duration="00:00:00.00000" Storyboard.TargetName="LayoutRoot" Storyboard.TargetProperty="(UIElement.Visibility)">
                    <DiscreteObjectKeyFrame KeyTime="00:00:00">
                        <DiscreteObjectKeyFrame.Value>
                            <Visibility>Visible</Visibility>
                        </DiscreteObjectKeyFrame.Value>
                    </DiscreteObjectKeyFrame>
                </ObjectAnimationUsingKeyFrames>
            </Storyboard>
        </VisualState>
        <VisualState x:Name="Hide">
            <Storyboard>
                <ObjectAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="LayoutRoot" Storyboard.TargetProperty="(UIElement.Visibility)">                                
                    <DiscreteObjectKeyFrame KeyTime="00:00:00">
                        <DiscreteObjectKeyFrame.Value>
                            <Visibility>Collapsed</Visibility>
                        </DiscreteObjectKeyFrame.Value>
                    </DiscreteObjectKeyFrame>
                </ObjectAnimationUsingKeyFrames>
             </Storyboard>
        </VisualState>
    </VisualStateGroup.States>
</VisualStateGroup>

Now you can simply go to a hide or show state, and if you want transitions, you add them to the VSM and voilĂ ! It works ... no need to hard code the transition from collapsed to visible. Now back to our regularly scheduled program ...

When an event happens on Panel A, it goes to the "Background" state and the Panel B goes to the "Onscreen" state. It looks like this:

Then, when we close Panel B, it goes back to "Offscreen" and the Panel A comes back to foreground. By using visual states, we can manipulate the plane projection and scale transform properties as well as the visibility to achieve our results, and use animations.

But here's the rub: how can I fire the event in "A" and then let "B" know it's time to change states?

The most common way I've seen goes something like this:

Put an Action or delegate of some sort in a view model. In the code behind or using a behavior (more clever than code behind), attach the desired visual state to the action. Make a command object, and while the command does it's thing, go ahead and call the delegate which in turn flips the visual state. For example, my view model might have:

...
public Action SwapPanelA { get; set; }
...

And in the code-behind, I can wire it up like this:

...
ViewModel.SwapPanelA = () => VisualStateManager.GoToState(this, "Foreground", true);
...

And so on and so on. While this certainly decouples it a bit, it also forces us to change the design of our view model to accommodate the transitions. What if we have four controls participating in the same event? Do we wire up a bunch of references for the view model? Does a control bring in a view model only for the sake of wiring in it's animations?

No, no, no. All wrong (but again, let me elaborate these are things I've tried and worked with in the past, so don't feel bad if you've been doing the same things).

It hit me that I really needed to decouple view actions from business actions. When I click Panel A, the business action might be to call out to a web service to get data to populate in Panel B. I shouldn't care about any transitions.

The view action would be the movement of the panels.

So how do we decouple this? Enter the Visual State Aggregator. What I decided to do was build an aggregator that could respond to published events by flipping view states. This includes a behavior to participate in an event, and a trigger to push the event out. The first piece is a subscription. A subscription contains a weak reference to a control, an event the control is listening for, and the state the control should go to when the event fires (including whether or not to use transitions). Here is VisualStateSubscription:

public class VisualStateSubscription
{
    private readonly WeakReference _targetControl;
    private readonly string _state;
    private readonly bool _useTransitions;
    private readonly string _event;
    private Guid _id = Guid.NewGuid();

    public VisualStateSubscription(Control control, string vsmEvent, string state, bool useTransitions)
    {
        _targetControl = new WeakReference(control);
        _event = vsmEvent;
        _state = state;
        _useTransitions = useTransitions; 
    }

    public bool IsExpired
    {
        get
        {
            return _targetControl == null || !_targetControl.IsAlive || _targetControl.Target == null; 
        }
    }

    public void RaiseEvent(string eventName)
    {
        if (!IsExpired && _event.Equals(eventName))
        {
            var control = _targetControl.Target as Control;
            VisualStateManager.GoToState(control, _state, _useTransitions); 
        }
    }

    public override bool Equals(object obj)
    {
        return obj is VisualStateSubscription && ((VisualStateSubscription) obj)._id.Equals(_id);
    }

    public override int GetHashCode()
    {
        return _id.GetHashCode();
    }
}

Next comes the aggregator. It contains the list of subscriptions. It will provide a means to subscribe, as well as a means to publish the event. I decided to use the Managed Extensibility Framework (MEF) to wire it in, so it also gets exported:

[Export]
public class VisualStateAggregator
{
    private readonly List<VisualStateSubscription> _subscribers = new List<VisualStateSubscription>();

    public void AddSubscription(Control control, string eventName, string stateName, bool useTransitions)
    {
        _subscribers.Add(new VisualStateSubscription(control, eventName, stateName, useTransitions));
    }

    public void PublishEvent(string eventName)
    {
        var expired = new List<VisualStateSubscription>();
        
        foreach(var subscriber in _subscribers)
        {
            if (subscriber.IsExpired)
            {
                expired.Add(subscriber);
            }
            else
            {
                subscriber.RaiseEvent(eventName);
            }
        }

        expired.ForEach(s=>_subscribers.Remove(s));
    }
}

Notice that I take advantage of the event to iterate the subscriptions and remove any that have expired (this means the control went out of scope, and we don't want to force it to hang on with a reference).

Now there is a behavior to subscribe. First, I want to indicate when a control should participate. This behavior can be attached to any FrameworkElement. Because the visual state manager only operates on controls, I'll walk the visual tree to find the highest containing control for that element. This way, if you have a grid (which is not a control) inside a user control, it will find the user control and use that as the target. If you don't want to use MEF, just give the aggregator a singleton pattern and reference it here instead of using the import.

public class VisualStateSubscriptionBehavior : Behavior<FrameworkElement> 
{        
    public VisualStateSubscriptionBehavior()
    {
        CompositionInitializer.SatisfyImports(this);
    }

    [Import]
    public VisualStateAggregator Aggregator { get; set; }

    public string EventName { get; set; }

    public string StateName { get; set; }

    public bool UseTransitions { get; set; }

    protected override void OnAttached()
    {
        AssociatedObject.Loaded += AssociatedObject_Loaded;
    }

    void AssociatedObject_Loaded(object sender, RoutedEventArgs e)
    {
        Control control = null;

        if (AssociatedObject is Control)
        {
            control = AssociatedObject as Control;
        }
        else
        {
            DependencyObject parent = VisualTreeHelper.GetParent(AssociatedObject);
            while (!(parent is Control) && parent != null)
            {
                parent = VisualTreeHelper.GetParent(parent);
            }
            if (parent is Control)
            {
                control = parent as Control; 
            }
        }

        if (control != null)
        {
            Aggregator.AddSubscription(control, EventName, StateName, UseTransitions);
        }
    }
}

Notice we hook into the loaded event so the visual tree is built prior to walking the tree for our hooks.

Of course, this implementation gives us a weak or "magic string" event, you could wire that to an enumeration to make that strongly typed. Let's call the event of swapping out Panel A "ActivatePanelB" and the event of bring it back "DeactivatePanelB". Panel A subscribes like this:

<UserControl>
   <Grid x:Name="LayoutRoot">
   <i:Interaction.Behaviors>
      <vsm:VisualStateSubscriptionBehavior EventName="ActivatePanelB" StateName="Background" UseTransitions="True"/>
      <vsm:VisualStateSubscriptionBehavior EventName="DeactivatePanelB" StateName="Foreground" UseTransitions="True"/>
   </i:Interaction.Behaviors>
   </Grid>
</UserControl>

Then Panel B may subscribe like this:

<UserControl>
   <Grid x:Name="LayoutRoot">
   <i:Interaction.Behaviors>
      <vsm:VisualStateSubscriptionBehavior EventName="ActivatePanelB" StateName="Onscreen" UseTransitions="True"/>
      <vsm:VisualStateSubscriptionBehavior EventName="DeactivatePanelB" StateName="Offscreen" UseTransitions="True"/>
   </i:Interaction.Behaviors>
   </Grid>
</UserControl>

I'm assuming the visual states are wired up with those names.

Now we need something to trigger an event. We use a trigger action for that. The trigger class is very simple, as it simply needs to publish the event. Because we are using a trigger action, we can bind to any routed event in order for the transition to fire, so this would work for a mouseover that had to animate a separate control for example.

public class VisualStateTrigger : TriggerAction<FrameworkElement> 
{
    public VisualStateTrigger()
    {
        CompositionInitializer.SatisfyImports(this);
    }

    [Import]
    public VisualStateAggregator Aggregator { get; set; }

    public string EventName { get; set; }

    protected override void Invoke(object parameter)
    {
        Aggregator.PublishEvent(EventName);
    }
}

To trigger this, let's say we have a fancy grid with information and clicking on the grid itself fires the event. In Panel A, we'd wire the event like this:

<Grid>
   <i:Interaction.Triggers>
      <i:EventTrigger EventName="MouseLeftButtonUp">
         <vsm:VisualStateTrigger EventName="ActivatePanelB"/>
      </i:EventTrigger>
   </i:Interaction.Triggers>
</Grid>

In Panel B, we might have a close button:

<Button Content="Close">
   <i:Interaction.Triggers>
      <i:EventTrigger EventName="Click">
         <vsm:VisualStateTrigger EventName="DeactivatePanelB"/>
      </i:EventTrigger>
   </i:Interaction.Triggers>
</Button>

And that's it! Without any view models involved, entirely in the UI with the help of our behaviors, we've aggregated the visual state transitions. When you click on the grid, panel A will flip to the "Background" state and animate away, while panel B will flip to the "Onscreen" state and slide in.

Here is an example for you to have fun with:

When you grab the source, you'll find there are absolutely no code-behinds whatsoever other than what is automatically generated. While Panel A and Panel B are completely separate controls that know nothing about each other, the common subscription to a aggregate event coordinates their actions when Panel A is clicked or the close button on Panel B is clicked.

The standalone project for this is included in the download. Enjoy!

P.S. implementing this in my project has been a boon. Was able to eliminate tons of methods and delegates that were there simply to facilitate the VM/View glue. Here's what's nice as well: you can type the events with an enum. If you need to fire an event within your view model, you don't have to find a delegate or grab anything special. Instead, you just import the aggregator and fire your event, like this:

[Import]
IAggregator Aggregator { get; set; }

private void SomeCode()
{
   Aggregator.RaiseEvent(Events.ActivatePanelB); 
}

That's it!

Download the source code for this post

Jeremy Likness

16 comments:

  1. This looks interesting. But I agree with your suspicion that you are overloading the vm's responsibility.

    I think the obsession with zero code behind creates some very convoluted implementations. Personally I accept that an animation is a job for the view, sometimes it can't be managed / created in xaml, so just do it in the code behind. job done.

    ReplyDelete
  2. I can't say I agree here. Yes, there is, in my opinion, an unwarranted obsession with "no code behind" (let's face it, XAML is code, albeit a different way to represent it). However, I believe VSM is intended for transitions and states, and the it is more convuluted to change the design of a view model simply to accommodate an animation when you can keep it all in the UI.

    Changing a state when a service call completes or business logic fires is one thing ... then it makes sense to have a delegate or other mechanism to fire off an animation ... but if it is truly for transitions based on events, I don't see any valid reason to involve more layers than necessary.

    ReplyDelete
  3. Interesting approach Jeremy. One question I have is why not just have a State property on your VM which throws a PropertyChanged notification when it is changed, and then have an attached behavior that litens to the State property change and then moves the VM state accordingly?


    Glenn

    ReplyDelete
  4. I like that idea as well, Glenn. The only issue I had with that approach is the affinity to the view model. I'm looking at a state transition that possibly coordinates between multiple views/controls and they each might have their own VM, so which VM would "own" the state property? All three? This way allows the various fews to subscribe to the state change regardless of their VM affinity, and allows any VM to publish the state change.

    ReplyDelete
  5. Are you saying that any of the 3 vms could fire the transition? Or is merely that the transition originates from a single VM but multiple partiers are interested in knowing about it?

    ReplyDelete
  6. Any can fire it. Think of this scenario: I have a panel with a list of customers. When I click on a customer, a panel slides in with the order details for the customer.

    There is an event "show detail" that causes the grid to slide away and the panel to slide in. That is two different views and the click command is on the customer list view model, so it publishes "show detail" and the grid "listens" by sliding out of the way and the panel "listens" by coming into focus.

    I'm separating the action of grabbing orders, etc, which is straightforward, from the UI element (the slides). My VM doesn't know or care about a grid or a panel or anything else: it simply publishes "show detail" and then we can wire up whatever set of actions we want.

    Now on the panel, I *could* have a close action. This is an example where I dirty my view model with a command to facilitate a UI-behavior. Closing has nothing to do with the data, it is a UI activity that says, "I'm done with this." So I can eliminate a command on the view model on instead bind the click of the "close" to the view model event "hide detail". The panel listens by sliding out of view and the grid listens by popping back.

    Again, the goal is to separate the UI-specific actions from the view models and have a single event able to coordinate it all.

    ReplyDelete
  7. Hi jeremy I am using your visual state aggregator for a silverlight 4 RIA project and it is working great , the only issue I have is I cannot view the design of any of the XAML pages that usee the visual state aggregator I get errors like :

    A value of type 'VisualStateSubscriptionBehavior' cannot be added to a collection or dictionary of type 'BehaviorCollection'.

    ReplyDelete
  8. I get this error

    Cannot create an instance of "VisualStateSubscriptionBehavior" VSMAggregatorExample

    ReplyDelete
  9. In the constructor for the trigger, change the code to this:

    public VisualStateTrigger()
    {
    if (!DesignerProperties.IsInDesignTool) {
    CompositionInitializer.SatisfyImports(this); }
    }

    That will make it designer-friendly. :)

    ReplyDelete
  10. Thanks jeremy

    that is a pretty sweet solution however when

    I tried to change the constructor for the trigger in your VisualAgrregator Source
    I now get :

    The name 'DesignerProperties' does not exist in the current context

    I feel like I am missing something very simple

    ReplyDelete
  11. I was able to resolve my issues by rebuilding your visualstate aggregator into vb.net since i am alot more proficent with vb.net that c#, after i did that I rubuilt your library and it all works.

    Thanks Jeremy

    ReplyDelete
  12. Hi Jeremy,
    it is a great article!
    I've only one question> with this solution how can you achieve that the second starts only after the first finished? Currently both storyboard play in the same time.

    so as you wrote this: "how can I fire the event in "A" and then let "B" know it's time to change states?"

    what I need is: after firing the even "A" and playing storyboard for "A", fire event "B" to play storyboard "B" but just after the storyboard of "A" finished.

    For example: I've a login screen. When the user push the login button I check its password. If it's invalid I would like to play a storyboard.

    It's easy to do it without MVVM but it's hard with MVVM - for me.
    I posted my issue here:
    http://forums.silverlight.net/t/255230.aspx/1?MVVM+error+handling+with+storyboard+annimation

    What do you think? Could you recommend something to me or this is the weak point of MVVM?

    ReplyDelete
    Replies
    1. All you'd have to do then is simply extend the model so that it hooks into the completed events to fire them in sequence. I wouldn't call this a weak point of MVVM at all.

      Delete
  13. Download Links is not working .. gives error database login failed ..

    Can you check this please

    regards

    ReplyDelete
  14. Hello Jeremy,

    Great post indeed, It helped me so much.
    I took the VisualStateSubscription, VisualStateAggregator, VisualStateSubscriptionBehavior, VisualStateTrigger and VisualStateAggregatorTrigger and was able to trigger the animation from within the XAML.
    Now I want to do this from my MVVM ... how can this be done, without the use whole Jounce ...

    Best regards

    ReplyDelete
  15. Hello Jeremy,

    By the way, in your post at the end you mention the MVVM implementation using IAggregator, which I can't locate !?

    I found the VisualStateAggregator and use it as follows :
    [Import]
    VisualStateAggregator Aggregator { get; set; }

    But is doesn't work ..
    any comments.

    ReplyDelete