Nick's .NET Travels

Continually looking for the yellow brick road so I can catch me a wizard....

Taking Visual States Cross Platform to iOS and Android with Xamarin

Last Friday at the Windows Phone 8.1 developer training, run by Microsoft, Nokia Microsoft and Built to Roam, we asked the room how many people used Blend and knew about Visual States. I was shattered to see very few people even knew what a Visual State was, let alone use them in their code. Visual States have to be one of the most useful aspects of the XAML system allowing you to define different states for not only your pages but also controls and user controls that you create.

On Tuesday night before the Sydney Mobile .NET Developer Meetup I was talking with Lewis Benge about how developers on other platforms control aspects of their user interface. The consensus is that it’s very reactive in that as elements of their data model, or in the case of mvvmcross elements of the view model, change elements of the UI are adjusted. For example if you were displaying some form of a loading indicator whilst some data was being retrieved from a service, when the call was complete, the progress indicator would be hidden. In this simple case you might think that it’s just one line of code to show or hide the progress indicator but now repeat this two or three times and all of a sudden you have UI control code littered throughout your application logic. In the XAML world we’d simply define these as visual states and we’d move between those states as required. In fact, as you’ll see we can even represent these states within our view model so that the state, not the visual representation of the states, form part of our application logic (and can be isolated and tested independently as it can sit nicely within a PCL).

Ok, now for a more concrete example:

- File –> New Project –> Windows Phone application

- Add New Project –> Android application

(you can repeat for iOS if you want)

- Add New Project – Portable Class Library

- Manage NuGet –> Add Mvvmcross to all projects, and follow “ToDos” as required for each project

>> At this point, you want to make sure that both your Windows Phone and Android applications build and can be run. They should both launch FirstView and contain a textbox and label with databinding working so that you can change the text in the textbox and it is updated in the label.

Blending some Visual States

We’re going to add a progress bar and a piece of text to say “Loading….” whilst data is being loaded.

- Right-click the Windows Phone project and select Open in Blend

- Add a progress bar and texblock to the contentpanel element to give you the following xaml – note that both elements are currently collapsed

<Grid x:Name="ContentPanel"
        Grid.Row="1"
        Margin="12,0,12,0">
    <ProgressBar x:Name="progressBar"
                    Visibility="Collapsed" />
    <TextBlock x:Name="textBlock"
                TextWrapping="Wrap"
                Text="Loading..."
                VerticalAlignment="Center"
                HorizontalAlignment="Center"
                Margin="0,-25,0,0"
                Visibility="Collapsed" />
</Grid>

- In the States tool windows, click the “Add state group” button in the icon bar. Call the state group “LoadingStates” (this name is somewhat irrelevant as it’s never used other than in Blend to allow you to differentiate if you have multiple state groups)

- Click the “Add state” button twice to generate states “Loading” and “Loaded”

- With the Loading state selected (there should be a red border around the main design surface indicating that you’re in state editing mode) set both textblock and progressbar to visible, and set the isindeterminate property to true. If you look at the xaml you should see the following states defined:

<VisualStateManager.VisualStateGroups>
    <VisualStateGroup x:Name="LoadingStates">
        <VisualState x:Name="Loading">
            <Storyboard>
                <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)"
                                                Storyboard.TargetName="progressBar">
                    <DiscreteObjectKeyFrame KeyTime="0">
                        <DiscreteObjectKeyFrame.Value>
                            <Visibility>Visible</Visibility>
                        </DiscreteObjectKeyFrame.Value>
                    </DiscreteObjectKeyFrame>
                </ObjectAnimationUsingKeyFrames>
                <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(ProgressBar.IsIndeterminate)"
                                                Storyboard.TargetName="progressBar">
                    <DiscreteObjectKeyFrame KeyTime="0">
                        <DiscreteObjectKeyFrame.Value>
                            <System:Boolean>True</System:Boolean>
                        </DiscreteObjectKeyFrame.Value>
                    </DiscreteObjectKeyFrame>
                </ObjectAnimationUsingKeyFrames>
                <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)"
                                                Storyboard.TargetName="textBlock">
                    <DiscreteObjectKeyFrame KeyTime="0">
                        <DiscreteObjectKeyFrame.Value>
                            <Visibility>Visible</Visibility>
                        </DiscreteObjectKeyFrame.Value>
                    </DiscreteObjectKeyFrame>
                </ObjectAnimationUsingKeyFrames>
            </Storyboard>
        </VisualState>
        <VisualState x:Name="Loaded" />
    </VisualStateGroup>
</VisualStateManager.VisualStateGroups>

- If you switch between the Loading and Loaded states in Blend you should see the elements show and hide as expected. Now we need to wire up some logic to switch between these states at runtime.

- Return to Visual Studio and go to the code-behind file for the FirstView (Windows Phone project).

- Add the following code to the OnNavigatedTo method:

protected async override void OnNavigatedTo(NavigationEventArgs e)
{
    base.OnNavigatedTo(e);
    VisualStateManager.GoToState(this, "Loading", true);
    await Task.Delay(2000);
    VisualStateManager.GoToState(this, "Loaded", true);
}

- Run the Windows Phone application and observe the application switching between the two states

- Now we need to move this logic one step further away from the UI layer and have it contained within our ViewModel. We’ll also try to eliminate those pesky string literals. To do this we’ll define an enumeration in code so that we can refer to our VisualStates – the name of the enumeration doesn’t matter but the name of the enumeration values needs to match the names of the states defined in Blend

public enum FirstStates
{
    Base,
    Loading,
    Loaded
}

- To move our state logic into the ViewModel we’ll need a way to trigger the actual state change in the UI layer (ie the view). For this we’ll expose an event on the ViewModel which will include the name of the state that we want the view to transition to. We’ll start by defining a StateEventsArgs class which we’ll use to wrap the name of the state. This class includes a couple of helper methods to make it easy to generate an instance from an enumeration value, and to return an enumeration value. You might ask why we’re converting to and from strings – we’ll come back to this!

public class StateEventArgs : EventArgs
{
    public string StateName { get; private set; }

    public static StateEventArgs Create<TState>(TState state) where TState : struct
    {
        return new StateEventArgs {StateName = state.ToString()};
    }

    public TState AsState<TState>() where TState : struct
    {
        TState state = default(TState);
        Enum.TryParse(StateName, true, out state);
        return state;
    }
}

- Update the FirstViewModel to raise a StateChanged event as follows:

public void OnStateChanged(FirstStates state)
{
    if (StateChanged != null)
    {
        StateChanged(this, StateEventArgs.Create(state));
    }
}
public override async void Start()
{
    base.Start();
    await Task.Yield(); // Attach state changed event handler

    OnStateChanged(FirstStates.Loading);
    await Task.Delay(5000);
    OnStateChanged(FirstStates.Loaded);
}
public event EventHandler<StateEventArgs> StateChanged;

- Now, in the code behind for our FirstView we can amend the OnNavigatedTo method to wire up a StateChanged event handler:

protected async override void OnNavigatedTo(NavigationEventArgs e)
{
    base.OnNavigatedTo(e);
    (ViewModel as FirstViewModel).StateChanged += FirstView_StateChanged;
}

void FirstView_StateChanged(object sender, StateEventArgs e)
{
    VisualStateManager.GoToState(this, e.StateName, true);
}

- When you run this, there is a 5 second delay during the Start method in the FirstViewModel during which the Loading state should be visible. There after it should revert to the Loaded state.

- Let’s flip across to our Android project and amend the FirstView to include the following xml.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="
http://schemas.android.com/apk/res/android"
    xmlns:local="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
    <EditText
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:textSize="40dp"
        local:MvxBind="Text Hello" />
    <TextView
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:textSize="40dp"
        local:MvxBind="Text Hello" />
    <Button
        android:id="@+id/MyButton"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="@string/Hello" />
    <ProgressBar
        style="?android:attr/progressBarStyleHorizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/progressBar1" />
</LinearLayout>

- Note that unlike in XAML for Windows Phone we can’t define the visual states in xml. Instead we define them as a set of Actions which will be invoked when transitioning to a particular state. In this case we’re setting the progress bar to indeterminate and changing the button color for Loading state. You’ll notice that unlike in XAML where we could rely on the state manager to revert state changes (ie Loaded state simply reverses any changes applied by the Loading state), here we have to be explicit about all changes we want to occur.

private IDictionary<FirstStates, Action> states;
protected override void OnCreate(Bundle bundle)
{
    base.OnCreate(bundle);
    SetContentView(Resource.Layout.FirstView);

    var button = FindViewById<Button>(Resource.Id.MyButton);
    button.Click += (s,e) => ViewModel.Start();

    var progress = FindViewById<ProgressBar>(Resource.Id.progressBar1);

    states = new Dictionary<FirstStates, Action>()
    {
        {FirstStates.Loading, () =>
        {
            progress.Indeterminate = true;
            button.SetTextColor(new Color(0,255,0));
        }},
        {FirstStates.Loaded, () =>
        {
            progress.Indeterminate = false;
            button.SetTextColor(new Color(255,0,0));
        }}
    };
}

- The final piece is to wire up an event handler to the StateChanged event, the same as we did for Windows Phone:

protected override void OnViewModelSet()
{
    base.OnViewModelSet();

    (ViewModel as FirstViewModel).StateChanged += FirstView_StateChanged;
}

void FirstView_StateChanged(object sender, StateEventArgs e)
{
    var state = e.AsState<FirstStates>();
    var change = states[state];
    change();
}

- When the StateChanged event is trigger, we find the appropriate Action by looking in the state Dictionary. The Action is then invoked to apply the appropriate visual changes.

In this post I’ve walked you through the basics of how you can use visual states across both Android and Windows Phone, controlled from within your view models. This same model can be applied to iOS and of course the code for wiring up the state changed event can be abstracted into base view models and base views so you don’t need to add it to every page.

Pingbacks and trackbacks (1)+

Comments are closed