Windows 8.1, the Hub Control and Visual States

Windows 8.1, the Hub Control and Visual States

The more I use the Windows 8.1 Hub control the more I get frustrated by it. In my previous post Databinding with the Windows 8.1 Hub control, I talked about getting data binding to work. This is useful if you’re going to have a series of panes all with similar structure. However, you could probably get the same effect using grouping in a GridView. A more common scenario is to have a number of different panes, which hold content coming from different sources. In this case, each pane is going to be loading data independently and you’ll probably want each pane to have different visual states representing loading, data loaded successfully, data load failed, data loaded successfully but no data available etc. If we were building this using the Panorama control for Windows Phone, or using the deprecated method of a horizontal stack panel inside a scrollviewer, then we could simply drop in a couple of extra elements and then control which ones are visible using visual states. As the view model for the page is typically responsible for controlling the loading of content across each of the panes (it simply loads the data, and data binding does the rest), it is also responsible for controlling the visual states across the page. Unfortunately this doesn’t work with the Hub control – the contents

As a starting point for this post I’m going to use the source code sample from the article States, Navigation and Testing with Portable Class Libraries for Windows Phone, as this will give us the basic infrastructure for raising state changed events from our view model. I won’t go through the details of wiring up the Windows application to use the locator  pattern – it’s essentially the same as for Windows Phone. What we will be focussing on is the hub control and how we can use state change events to control the content for each pane. The Panorama control used in Windows Phone applications, is made up of PanoramaItems and the contents of each pane is simply added as content. The Hub control is made up of HubSections but the contents for each pane is added into a data template, similar to the following:

<Page >http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      >http://schemas.microsoft.com/winfx/2006/xaml"
      >http://schemas.microsoft.com/expression/blend/2008"
      >http://schemas.openxmlformats.org/markup-compatibility/2006"
      >http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             >http://schemas.microsoft.com/winfx/2006/xaml"
             >http://schemas.microsoft.com/expression/blend/2008"
             >http://schemas.openxmlformats.org/markup-compatibility/2006"
             mc_Ignorable="d"
             d_DesignHeight="300"
             d_DesignWidth="400">

    <Grid>
        <VisualStateManager.VisualStateGroups>
            <VisualStateGroup x_Name="LoadingStates">
                <VisualState x_Name="Loading">
                    <Storyboard>
                        <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)"
                                                       Storyboard.TargetName="SP_Loading">
                            <DiscreteObjectKeyFrame KeyTime="0">
                                <DiscreteObjectKeyFrame.Value>
                                    <Visibility>Visible</Visibility>
                                </DiscreteObjectKeyFrame.Value>
                            </DiscreteObjectKeyFrame>
                        </ObjectAnimationUsingKeyFrames>
                        <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(ProgressRing.IsActive)"
                                                       Storyboard.TargetName="progressRing">
                            <DiscreteObjectKeyFrame KeyTime="0">
                                <DiscreteObjectKeyFrame.Value>
                                    <x:Boolean>True</x:Boolean>
                                </DiscreteObjectKeyFrame.Value>
                            </DiscreteObjectKeyFrame>
                        </ObjectAnimationUsingKeyFrames>
                    </Storyboard>
                </VisualState>
                <VisualState x_Name="Loaded">
                    <Storyboard>
                        <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)"
                                                       Storyboard.TargetName="GD_Loaded">
                            <DiscreteObjectKeyFrame KeyTime="0">
                                <DiscreteObjectKeyFrame.Value>
                                    <Visibility>Visible</Visibility>
                                </DiscreteObjectKeyFrame.Value>
                            </DiscreteObjectKeyFrame>
                        </ObjectAnimationUsingKeyFrames>
                    </Storyboard>
                </VisualState>
                <VisualState x_Name="NotAbleToLoad">
                    <Storyboard>
                        <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)"
                                                       Storyboard.TargetName="GD_NotAbleToLoad">
                            <DiscreteObjectKeyFrame KeyTime="0">
                                <DiscreteObjectKeyFrame.Value>
                                    <Visibility>Visible</Visibility>
                                </DiscreteObjectKeyFrame.Value>
                            </DiscreteObjectKeyFrame>
                        </ObjectAnimationUsingKeyFrames>
                    </Storyboard>
                </VisualState>
            </VisualStateGroup>
        </VisualStateManager.VisualStateGroups>
        <StackPanel x_Name="SP_Loading"
                    Visibility="Collapsed">
            <TextBlock Text="Loading…."
                       FontSize="48" />
            <ProgressRing x_Name="progressRing"
                          Width="50"
                          Height="50" />

        </StackPanel>
        <Grid x_Name="GD_Loaded"
              Visibility="Collapsed">
            <TextBlock Text="All data loaded"
                       FontSize="48" />
        </Grid>
        <Grid x_Name="GD_NotAbleToLoad"
              Visibility="Collapsed">
            <TextBlock Text="Not able to load"
                       FontSize="48" />
        </Grid>
    </Grid>
</UserControl>

This usercontrol would then be added to the datatemplate for the hubsection:

<HubSection>
    <DataTemplate>
        <userControls:MainFirstPaneUserControl />
    </DataTemplate>
</HubSection>

The issue is that we need some mechanism for triggering the visual state change from the view model for the page. In the same way as the page wires an event handler for the StateChanged event on the viewmodel, the usercontrol also has to wire up to this event. As the usercontrol inherits the DataContext from the page, the usercontrol can listen to the DataContextChanged event and wire up to the StateChanged event at that point.

public sealed partial class MainFirstPaneUserControl
{
    public MainFirstPaneUserControl()
    {
        InitializeComponent();

        DataContextChanged += MainFirstPaneUserControl_DataContextChanged;
    }

    void MainFirstPaneUserControl_DataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
    {
        var ctrl = sender as MainFirstPaneUserControl;
        ctrl.WireStateChanged();

    }

    private void WireStateChanged()
    {
        ViewModel.StateChanged += ViewModelStateChanged;
    }

    private void ViewModelStateChanged(object sender, StateChangedEventArgs e)
    {
        VisualStateManager.GoToState(this, e.StateName, e.UseTransitions);
    }

    private BaseStateChange ViewModel
    {
        get
        {
            return DataContext as BaseViewModel;
        }
    }
}

This appears to work, except the initial state changed event is skipped. The Hub, by design, lazy loads the hubsections, meaning that the OnNavigatedTo for the page gets run before the hubsection has been created and wired up. To resolve this problem the view model needs to track the current visual state:

public event EventHandler<StateChangedEventArgs> StateChanged;
private readonly Dictionary<Type,object> currentState = new Dictionary<Type, object>();
protected void OnStateChanged<T>(T state, bool useTransitions = true) where T : struct
{
    currentState[typeof (T)] = state;

    if (StateChanged != null)
    {
        StateChanged(this, new StateChangedEventArgs { StateName = state.ToString(), UseTransitions = useTransitions });
    }
}

public T CurrentState<T>()
{
    object current;
    currentState.TryGetValue(typeof (T), out current);
    return (T) current;
}

And the usercontrol needs to query it at the point the state changed event is wired up:

private void WireStateChanged()
{
    var current = ViewModel.CurrentState<MainViewModel.LoadingStates>();
    if (current != MainViewModel.LoadingStates.Base)
    {
        VisualStateManager.GoToState(this, current.ToString(), false);
    }
    ViewModel.StateChanged += ViewModelStateChanged;
}

When we run this, we’ll see the visual state for the hubsection switch between loading and loaded states.

image     image

Download the code