Visual State Management with BuildIt.States and Uno

I’ve posted previously on using visual states in Uno and how they can be used to effectively manage the different visual layouts a page can take on. These may be changes in layout due to the application being resized, or perhaps due to different data loading states. I’ve recently created a Uno build of the BuildIt.States library, BuildIt.States.Uno. In this post I’m going to walk through using this library to help manage visual states from within your view model.

One of the topics that’s quite hot at the moment is whether XAML has had its day and whether the new coded UI techniques provide a better solution. I’m not going to go too far into this debate but one of the things I really like about XAML is the separation of the UI (declared in XAML) from its data representation (the ViewModel).

Whilst the data binding framework of both UWP and Xamarin.Forms works well for connecting properties on a ViewModel to attributes of a UI element, there is no way to connect visual states on the page, with some aspect of the ViewModel. This is where the BuildIt.States library kicks in.

Thinking about Visual States

Before we get into using the BuildIt.States library, lets start by thinking through the different states that our page can go through. For the purpose of this post we’re going to build a simple app with the following spec:

  • The app has a single page that has a button, “Load Data”
  • When the button is pressed, the button will be hidden and a loading indicator will be shown.
  • The app will attempt to load some data.
  • Loading data will randomly succeed or fail
  • When loading is complete (either succeed or fail) the loading indicator will be hidden, a message will be displayed indicating success or failure, and the Load Data button will be displayed again.
  • If the Load Data button is pressed again, the status of the previous attempt will remain on screen until the new attempt to load data has been completed.

An initial read of this spec would seem to indicate that there is a single set of states:

  • Not Loading
  • Loading
  • Loaded – Success
  • Loaded – Failed

However, when the button is pressed for the second time, there are two more states that need to be included : Loading (Loaded – Success) and Loading (Loaded – Failed).

An alternative way of thinking about the states is that there is a group of states that pertain to whether data is being loaded, and a different group of states that pertain to whether the page has data:

LoadingStates

  • NotLoading
  • Loading

These states control whether the button or the loading indicator is visible

DataStates

  • NoData
  • Data
  • DataFailedToLoad

These states control the visibility of the data, or in this case the message indicating success or failure of the data loading.

Visual States in XAML

Since the focus of this post isn’t on how to design a page in XAML I’ll skip over the steps involved in laying out the page. In summary, after creating a new Uno project (using the Uno project templates), I opened the MainPage.xaml in Visual Studio Blend. Whilst Blend is a long way from being the design-first tool it was once envisaged as, it does still have support for defining visual states, which is sorely missing in Visual Studio (and probably the only reason I still use Blend).

Using the design surface in Blend I’m able to layout the various TextBlock, Button and ProgressRing to build the simple UI for the app. I then use the States tool window to create two Visual State Groups (LoadingStates and DataStates) and the associated Visual States for showing the appropriate elements. The resulting XAML looks like the following:

<Page x:Class="BuildItWithStatesForUno.MainPage"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
      xmlns:local="using:BuildItWithStatesForUno"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      mc:Ignorable="d">

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <VisualStateManager.VisualStateGroups>
            <VisualStateGroup x:Name="LoadingStates">
                <VisualState x:Name="NotLoading" />
                <VisualState x:Name="Loading">
                    <VisualState.Setters>
                        <Setter Target="LoadDataButton.(UIElement.Visibility)" Value="Collapsed" />
                        <Setter Target="LoadingProgress.(UIElement.Visibility)" Value="Visible" />
                        <Setter Target="LoadingProgress.(ProgressRing.IsActive)" Value="True" />
                    </VisualState.Setters>
                </VisualState>
            </VisualStateGroup>
            <VisualStateGroup x:Name="DataStates">
                <VisualState x:Name="NoData" />
                <VisualState x:Name="Data">
                    <VisualState.Setters>
                        <Setter Target="DataSuccessTextBlock.(UIElement.Visibility)" Value="Visible" />
                    </VisualState.Setters>
                </VisualState>
                <VisualState x:Name="DataFailedToLoad">
                    <VisualState.Setters>
                        <Setter Target="DataFailedTextBlock.(UIElement.Visibility)" Value="Visible" />
                    </VisualState.Setters>
                </VisualState>
            </VisualStateGroup>
        </VisualStateManager.VisualStateGroups>
        <StackPanel HorizontalAlignment="Center"
                    VerticalAlignment="Center">
            <TextBlock x:Name="DataSuccessTextBlock"
                       Margin="20"
                       FontSize="40"
                       Foreground="Green"
                       Text="Data Loaded Successfully!"
                       TextAlignment="Center"
                       Visibility="Collapsed" />
            <TextBlock x:Name="DataFailedTextBlock"
                       Margin="20"
                       FontSize="40"
                       Foreground="Red"
                       Text="Data Failed to Load"
                       TextAlignment="Center"
                       Visibility="Collapsed" />
            <Grid Margin="50">
                <Button x:Name="LoadDataButton"
                        HorizontalAlignment="Center"
                        Click="LoadDataClick"
                        Content="Load Data"
                        FontSize="30" />
                <ProgressRing x:Name="LoadingProgress"
                              Width="50"
                              Height="50"
                              Foreground="Blue"
                              IsActive="False"
                              Visibility="Collapsed" />
            </Grid>
        </StackPanel>
    </Grid>
</Page>

Testing the Visual States

Before moving on I wanted to test that the Visual States work, so I wired up the Load Data button with an event handler so I could switch the visual states:

private async void LoadDataClick(object sender, RoutedEventArgs e)
{
    var loadTimeInMilliseconds = rnd.Next(1000, 10000);
    var success = loadTimeInMilliseconds % 2 > 0;
    VisualStateManager.GoToState(this, "Loading", true);
    await Task.Delay(loadTimeInMilliseconds);
    VisualStateManager.GoToState(this, "NotLoading", true);
    VisualStateManager.GoToState(this, success ? "Data" : "DataFailedToLoad", true);
}

Clearly this code isn’t production ready (string literals, codebehind, no error handling etc) but when I run the app, I can see the desired state changes. Here I’m just showing Android but since it’s Uno, it should work nicely on iOS, UWP and WASM too.

Visual States in Action (Gif captured and generated using Snagit from TechSmith)

ViewModel States

Now that we’ve defined the visual states, we need a way to both control and track the states in our ViewModel. In the same way that we can use data binding to update attributes of the visual elements on the page, we need a way to mirror visual states within our ViewModel. This is where we can make use of the StateManger from the BuildIt.States library.

In the following MainViewModel, a StateManager is created and setup using two different state groups. Rather than using string literals, we use an enum to define each state group. Note that each enum has a Base value, which reflects the default, or unset, state.

public enum LoadingStates
{
    Base,
    NotLoading,
    Loading
}

public enum DataStates
{
    Base,
    Data,
    DataFailedToLoad
}

public class MainViewModel : IHasStates
{
    private readonly Random rnd = new Random();

    public IStateManager StateManager { get; } = new StateManager();

    public MainViewModel()
    {
        StateManager
            .Group<LoadingStates>()
            .DefineAllStates()
            .Group<DataStates>()
            .DefineAllStates();
    }

    public async Task LoadData()
    {
        var loadTimeInMilliseconds = rnd.Next(1000, 10000);
        var success = loadTimeInMilliseconds % 2 > 0;

        await StateManager.GoToState(LoadingStates.Loading);
        await Task.Delay(loadTimeInMilliseconds);
        await StateManager.GoToState(LoadingStates.NotLoading);
        await StateManager.GoToState(LoadingStates.Loading);
        await StateManager.GoToState(success ? DataStates.Data : DataStates.DataFailedToLoad);
    }
}

I also need to update the codebehind in MainPage to create an instance of MainViewModel and then invoke the LoadData method when the Load Data button is clicked:

public sealed partial class MainPage : Page
{
    public MainViewModel ViewModel => DataContext as MainViewModel;

    public MainPage()
    {
        this.InitializeComponent();

        DataContext = new MainViewModel();
    }
    private async void LoadDataClick(object sender, RoutedEventArgs e)
    {
        await ViewModel.LoadData();
    }
}

Connecting ViewModel to Visual States

Of course, if we run the code at this point, the state tracked within the MainViewModel don’t update the UI. To complete the loop, we need to attach another StateManager to the visual states defined on MainPage, and then bind the two StateManagers so that they can remain in sync. This is all done in the MainPage codebehind.

public sealed partial class MainPage : Page
{
    public MainViewModel ViewModel => DataContext as MainViewModel;
    private IStateManager StateManager { get; } = new StateManager();

    public MainPage()
    {
        this.InitializeComponent();

        DataContext = new MainViewModel();

        StateManager
            .Group<LoadingStates>()
            .DefineAllStates(this,this.LoadingStates)
            .Group<DataStates>()
            .DefineAllStates(this, this.DataStates);

        StateManager.Bind(ViewModel.StateManager);
    }
    private async void LoadDataClick(object sender, RoutedEventArgs e)
    {
        await ViewModel.LoadData();
    }
}

And there we go – now when you run the application and press the Load Data button the LoadData method on the MainViewModel will be invoked. As the states of the MainViewModel change, the StateManager and subsequently the visual states on the MainPage are updated.

Leave a Reply

Your email address will not be published. Required fields are marked *