Wednesday, November 10, 2010

Jounce Part 6: Visual States and Multiple Views per ViewModel

I knocked out quite a few items for the next release of Jounce, the primary being explicit support for multiple views bound to the same view model. In the previous version, the visual state binding would only reflect the most recent view (and a bug prevented the actual binding of the view model, but that's been fixed).

The changes are targeted for release 1.0, but you can download the latest now simply by navigating to the Source Code tab and then clicking "download" in the upper right under "Latest Version."

That same download addresses several items I received requests for.

Refactoring the Simple Navigation to use Regions

While I've provided examples of navigation with regions and without, the region version was very different than the simple navigation example that was without regions. Someone suggested that I build an example to show the same solution, but using regions. I decided to do that and a little more ... but first, let's focus on how I refactored the SimpleNavigation example to demonstrate SimpleNavigationWithRegion.

First, I was able to take out the bindings and code that were explicitly placing the controls because the region management takes it over. If you recall, creating a region is as simple as tagging it:

   <ContentControl Grid.Row="0" Regions:ExportAsRegion.RegionName="NavigationRegion"/>
        <ItemsControl Grid.Row="1" Regions:ExportAsRegion.RegionName="ShapeRegion">
            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <Grid/>
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
        </ItemsControl>

Notice what I did for the shape region: I used an items control instead of a content control, but overrode the panel to be a Grid. This will force the views routed to the region to take up the same space, but there is a method to my madness ... I'll explain in a bit.

In the views, we place a simple tag to route the region:

[ExportViewToRegion("RedSquare", "ShapeRegion")]
public partial class RedSquare ...

The ShellViewModel no longer has to manage the navigation region or maintain state. In fact, the only thing it needs to do is raise a navigation event for the navigation region so the navigation controls will show. Here is what it ends up looking like:

[ExportAsViewModel("Shell")]
public class ShellViewModel : BaseViewModel, IPartImportsSatisfiedNotification
{       
    public void OnImportsSatisfied()
    {           
        EventAggregator.Publish(new ViewNavigationArgs("Navigation"));
    }               
}

The old navigation view used a command to fire navigation. The latest Jounce features a NavigationTrigger that you can use to trigger navigation from XAML. In this case, we bind it the view name that we build from the meta data for the views:

<ItemsControl.ItemTemplate>
    <DataTemplate>
        <Button Margin="5" Content="{Binding Item2}" ToolTipService.ToolTip="{Binding Item3}">
            <Interactivity:Interaction.Triggers>
                <Interactivity:EventTrigger EventName="Click">
                    <Services:NavigationTrigger Target="{Binding Item1,FallbackValue=Navigation}"/>
                </Interactivity:EventTrigger>
            </Interactivity:Interaction.Triggers>
        </Button>
    </DataTemplate>
</ItemsControl.ItemTemplate>

The "item1" etc comes from the tuple I create by parsing meta data to get the button names and tool tips. The behavior now handles firing the navigation event for me, and because the views are wired to the region, the region manager will place them on the control surface.

Multiple Views per ViewModel

The original Jounce supported binding multiple view models to a view, but a bug prevented the second instance from being bound. Not only is this fixed, but I also improved the support for the visual state manager. We'll cover that in a second. First, I wanted to make a view model called ShapeViewModel to handle some transitions. The same view model will be bound to all of the shape controls. To do this, I simply created a simple Binding class and exported all of the bindings:

public class Bindings
{
    [Export]
    public ViewModelRoute Circle
    {
        get { return ViewModelRoute.Create("ShapeViewModel", "GreenCircle"); }
    }

    [Export]
    public ViewModelRoute Square
    {
        get { return ViewModelRoute.Create("ShapeViewModel", "RedSquare"); }
    }

    [Export]
    public ViewModelRoute Text
    {
        get { return ViewModelRoute.Create("ShapeViewModel", "TextView"); }
    }
}

Jounce handles this with no problem and simply uses the same view model (still only calling Initialize the first time it is created) and binds it to each view (calling Activate whenever a view is navigated to, and passing in the view name).

So what can we do with this?

Page Transitions

Another question I received was, "How do I do transitions between pages, i.e. fade one out, the next in, etc?" Obviously with a content control this is impossible because the new view replaces the old view.

But there is a solution!

Instead of replacing views, you can take the approach I did in the sample application, and use an items control with a grid for a panel. This allows you to stack the views on the same surface, but manipulate their visibility using visual states.

For visual states, I used the same set of groups and transitions on every control. There is a visible and a hidden state. The hidden state slowly expands and fades the old control.

<VisualStateManager.VisualStateGroups>
            <VisualStateGroup x:Name="ShowAndHide">
                <VisualStateGroup.States>
                    <VisualState x:Name="ShowState">
                        <Storyboard>
                            <ObjectAnimationUsingKeyFrames BeginTime="0:0:0" Storyboard.TargetName="LayoutRoot"
                                                       Storyboard.TargetProperty="(UIElement.Visibility)">
                                <DiscreteObjectKeyFrame KeyTime="0:0:0">
                                    <DiscreteObjectKeyFrame.Value>
                                        <Visibility>Visible</Visibility>
                                    </DiscreteObjectKeyFrame.Value>
                                </DiscreteObjectKeyFrame>
                            </ObjectAnimationUsingKeyFrames>
                            <DoubleAnimation 
                                             Duration="0:0:0"
                                             Storyboard.TargetName="LayoutRoot"
                                             Storyboard.TargetProperty="(UIElement.RenderTransform).(ScaleTransform.ScaleX)"
                                             From="0.3" To="1.0"/>
                            <DoubleAnimation Duration="0:0:0" Storyboard.TargetName="LayoutRoot"
                                             Storyboard.TargetProperty="(UIElement.RenderTransform).(ScaleTransform.ScaleY)"
                                             From="0.3" To="1.0"/>
                            <DoubleAnimation Duration="0:0:0" Storyboard.TargetName="LayoutRoot"
                                             Storyboard.TargetProperty="(UIElement.Opacity)"
                                             From="0.3" To="1.0"/>
                        </Storyboard>
                    </VisualState>
                    <VisualState x:Name="HideState">
                        <Storyboard>
                            <DoubleAnimation Duration="0:0:0" Storyboard.TargetName="LayoutRoot"
                                             Storyboard.TargetProperty="(UIElement.RenderTransform).(ScaleTransform.ScaleX)"
                                             From="1.0" To="1.2"/>
                            <DoubleAnimation Duration="0:0:0" Storyboard.TargetName="LayoutRoot"
                                             Storyboard.TargetProperty="(UIElement.RenderTransform).(ScaleTransform.ScaleY)"
                                            From="1.0" To="1.2"/>
                            <DoubleAnimation Duration="0:0:0" Storyboard.TargetName="LayoutRoot"
                                             Storyboard.TargetProperty="(UIElement.Opacity)"
                                             From="1.0" To="0.3"/>
                            <ObjectAnimationUsingKeyFrames BeginTime="0:0:0" Storyboard.TargetName="LayoutRoot"
                                                       Storyboard.TargetProperty="(UIElement.Visibility)">
                                <DiscreteObjectKeyFrame KeyTime="0:0:0.0">
                                    <DiscreteObjectKeyFrame.Value>
                                        <Visibility>Collapsed</Visibility>
                                    </DiscreteObjectKeyFrame.Value>
                                </DiscreteObjectKeyFrame>
                            </ObjectAnimationUsingKeyFrames>
                        </Storyboard>
                    </VisualState>
                </VisualStateGroup.States>
                <VisualStateGroup.Transitions>                    
                    <VisualTransition To="HideState">
                        <Storyboard>
                            <DoubleAnimation 
                                Duration="0:0:0.2"
                                Storyboard.TargetName="LayoutRoot"
                                             Storyboard.TargetProperty="(UIElement.RenderTransform).(ScaleTransform.ScaleX)"
                                             From="1.0" To="1.2"/>
                            <DoubleAnimation Duration="0:0:0.2" Storyboard.TargetName="LayoutRoot"
                                             Storyboard.TargetProperty="(UIElement.RenderTransform).(ScaleTransform.ScaleY)"
                                             From="1.0" To="1.2"/>
                            <DoubleAnimation Duration="0:0:0.2" Storyboard.TargetName="LayoutRoot"
                                             Storyboard.TargetProperty="(UIElement.Opacity)"
                                             From="1.0" To="0.3"/>
                            <ObjectAnimationUsingKeyFrames BeginTime="0:0:0" Storyboard.TargetName="LayoutRoot"
                                                       Storyboard.TargetProperty="(UIElement.Visibility)">
                                <DiscreteObjectKeyFrame KeyTime="0:0:0.2">
                                    <DiscreteObjectKeyFrame.Value>
                                        <Visibility>Collapsed</Visibility>
                                    </DiscreteObjectKeyFrame.Value>
                                </DiscreteObjectKeyFrame>
                            </ObjectAnimationUsingKeyFrames>
                        </Storyboard>
                    </VisualTransition>
                </VisualStateGroup.Transitions>
            </VisualStateGroup>
        </VisualStateManager.VisualStateGroups>

I placed this in each control. So what now? Because Jounce supports multiple views for the view model, and because Jounce passes in the view information, the view model can trigger state changes without knowing what the views are or even how to interact with the visual state manager - it only needs to know the name of the state.

Take a look at the ShapeViewModel:

[ExportAsViewModel("ShapeViewModel")]
public class ShapeViewModel : BaseViewModel
{
    private string _lastView = string.Empty;

    public override void _Activate(string viewName)
    {
        if (string.IsNullOrEmpty(_lastView))
        {
            _lastView = viewName;
        }
        else if (!_lastView.Equals(viewName))
        {
            EventAggregator.Publish(new ViewNavigationArgs(_lastView) {Deactivate = true});
            _lastView = viewName;
        }

        GoToVisualStateForView(viewName, "ShowState", true);
    }

    public override void _Deactivate(string viewName)
    {
        GoToVisualStateForView(viewName, "HideState", true);
    }
}

Basically, it simply remembers the last view name. When an activation occurs, it raises the deactivate event for the old view. The activation then sets the visual state to the ShowState for the view being activated. On the deactivation call, it sets the visual state to the HideState for the view being deactivated. The net result is that as you navigate, the old views seem to pop and disappear as the new views appear.

Also note that Jounce automatically wires the visual state transitions to execute on the UI thread for you.

Jeremy Likness