Mobile Corner

Using Visual States in Your Windows Phone User Controls

Nick Randolph demonstrates two techniques for using Visual States to change the layout of a Windows Phone User Control.

One of the most important aspects of building applications for any platform is to provide an intuitive, responsive interface. This of course raises a challenge when the user invokes a task that might take some time to complete; for example, refreshing a feed of data might take a couple of seconds for the data to be fetched.

Legacy desktop applications would most likely block at this point until the task has been completed, but this yields a poor UX, as there's little or no feedback to the user. Modern applications, such as those written for Windows 8/8.1 or Windows Phone, typically offload these actions to a background thread or task, allowing the application to continue to accept input from the user. Of course, it's still important to provide user feedback while this background task is being completed. This is a perfect opportunity to use Visual States to alter the contents on the screen to surface up information to the user. In this article, I'll demonstrate two ways you can use Visual States within a UserControl.

Let's set the scene with a simple Windows Phone application made up of a single page, MainPage, and its corresponding view model. In the codebehind file for the page, MainPage.xaml.cs, I've wired up a new instance of the MainViewModel as the DataContext for the page. As the user navigates into and away from the page, the OnNavigatedTo and OnNavigatedFrom methods are invoked, respectively, which in turn call the Start and Stop methods on the MainViewModel, like so:

public partial class MainPage
{
  public MainPage()
  {
    InitializeComponent();
    DataContext = new MainViewModel();
  }

  private MainViewModel ViewModel
  {
    get { return DataContext as MainViewModel; }
  }

  protected override void OnNavigatedTo(NavigationEventArgs e)
  {
    base.OnNavigatedTo(e);

    ViewModel.Start();
  }

  protected override void OnNavigatedFrom(NavigationEventArgs e)
  {
    base.OnNavigatedFrom(e);

    ViewModel.Stop();
  }
}

The MainViewModel class implements an interface, IStateChanged, which exposes a property, State, which reflects the current state of the view model, and an event, StateChanged, which is raised whenever the State property changes.

public interface IStateChanged
{
  string State { get; }
  event EventHandler StateChanged;
}

In the implementation of the MainViewModel, the possible states are defined using an enumeration (see Listing 1). This eliminates the use of string literals, making it less error-prone. The Start and Stop methods simply control the timer, which is used to periodically change between states. (This is purely for demonstrative purposes; in a real-world application you'd use the states to reflect the actual state of the view model. For example, whether data is being loaded or whether it has successfully loaded.)

Listing 1: Implementation of the MainViewModel
public class MainViewModel : IStateChanged
{
  private enum LoadingStates
  {
    Base,
    Loading,
    Loaded,
    LoadFailed
  }

  public event EventHandler StateChanged;

  private readonly DispatcherTimer startTimer;
  private LoadingStates currentState;

  public MainViewModel()
  {
    startTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(2) };
    startTimer.Tick += (s, et) =>
    {
      var state = CurrentState;
      switch (state)
      {
        case LoadingStates.Base:
        case LoadingStates.LoadFailed:
          CurrentState = LoadingStates.Loading;
          break;
        case LoadingStates.Loading:
          CurrentState = LoadingStates.Loaded;
          break;
        case LoadingStates.Loaded:
          CurrentState = LoadingStates.LoadFailed;
          break;
      }
        };

    }

    private LoadingStates CurrentState
    {
        get { return currentState; }
        set
        {
            if (CurrentState.Equals(value)) return;
            currentState = value;
            if (StateChanged != null)
            {
                StateChanged(this, EventArgs.Empty);
            }
        }
    }

    public string State
    {
        get { return CurrentState.ToString(); }
    }

    public void Start()
    {
        startTimer.Start();
    }

    public void Stop()
    {
        startTimer.Stop();
    }
}

At this point the application will run, but the state changes in the MainViewModel aren't reflected anywhere on the page. To demonstrate two ways you can incorporate these states into a UserControl, I'm going to add two new UserControls to the project: MainDataControl and SecondMainDataControl. In both methods I'll define three visual states within the UserControl which will toggle the visibility of the appropriate TextBlock. The only difference is that in the MainDataControl the names of the states have been prefixed with "Control" to illustrate that they're not directly related to the states defined in the MainViewModel. Here's the XAML for both user controls:

MainDataControl
<UserControl x:Class="StatesAndUserControls.MainDataControl"
             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:mc="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="ControlLoading">
                    <Storyboard>
                        <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="ControlLoaded">
                    <Storyboard>
                        <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)"
                                                       Storyboard.TargetName="textBlock1">
                            <DiscreteObjectKeyFrame KeyTime="0">
                                <DiscreteObjectKeyFrame.Value>
                                    <Visibility>Visible</Visibility>
                                </DiscreteObjectKeyFrame.Value>
                            </DiscreteObjectKeyFrame>
                        </ObjectAnimationUsingKeyFrames>
                    </Storyboard>
                </VisualState>
                <VisualState x:Name="ControlLoadFailed">
                    <Storyboard>
                        <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)"
                                                       Storyboard.TargetName="textBlock2">
                            <DiscreteObjectKeyFrame KeyTime="0">
                                <DiscreteObjectKeyFrame.Value>
                                    <Visibility>Visible</Visibility>
                                </DiscreteObjectKeyFrame.Value>
                            </DiscreteObjectKeyFrame>
                        </ObjectAnimationUsingKeyFrames>
                    </Storyboard>
                </VisualState>
            </VisualStateGroup>
        </VisualStateManager.VisualStateGroups>
        <TextBlock x:Name="textBlock"
                   Text="Loading..."
                   Style="{StaticResource PhoneTextLargeStyle}"
                   Visibility="Collapsed" />
        <TextBlock x:Name="textBlock1"
                   Text="Loaded"
                   Style="{StaticResource PhoneTextLargeStyle}"
                   Visibility="Collapsed" />
        <TextBlock x:Name="textBlock2"
                   Text="Load Failed"
                   Style="{StaticResource PhoneTextLargeStyle}"
                   Visibility="Collapsed" />

    </Grid>
</UserControl>
SecondMainDataControl
<UserControl x:Class="StatesAndUserControls.SecondMainDataControl"
             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:mc="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="textBlock">
                            <DiscreteObjectKeyFrame KeyTime="0">
                                <DiscreteObjectKeyFrame.Value>
                                    <Visibility>Visible</Visibility>
                                </DiscreteObjectKeyFrame.Value>
                            </DiscreteObjectKeyFrame>
                        </ObjectAnimationUsingKeyFrames>
                    </Storyboard>
                </VisualState>
                <VisualState x:Name="Loaded">
                    <Storyboard>
                        <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)"
                                                       Storyboard.TargetName="textBlock1">
                            <DiscreteObjectKeyFrame KeyTime="0">
                                <DiscreteObjectKeyFrame.Value>
                                    <Visibility>Visible</Visibility>
                                </DiscreteObjectKeyFrame.Value>
                            </DiscreteObjectKeyFrame>
                        </ObjectAnimationUsingKeyFrames>
                    </Storyboard>
                </VisualState>
                <VisualState x:Name="LoadFailed">
                    <Storyboard>
                        <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)"
                                                       Storyboard.TargetName="textBlock2">
                            <DiscreteObjectKeyFrame KeyTime="0">
                                <DiscreteObjectKeyFrame.Value>
                                    <Visibility>Visible</Visibility>
                                </DiscreteObjectKeyFrame.Value>
                            </DiscreteObjectKeyFrame>
                        </ObjectAnimationUsingKeyFrames>
                    </Storyboard>
                </VisualState>
            </VisualStateGroup>
        </VisualStateManager.VisualStateGroups>
        <TextBlock x:Name="textBlock"
                   Text="Loading..."
                   Style="{StaticResource PhoneTextLargeStyle}"
                   Visibility="Collapsed" />
        <TextBlock x:Name="textBlock1"
                   Text="Loaded"
                   Style="{StaticResource PhoneTextLargeStyle}"
                   Visibility="Collapsed" />
        <TextBlock x:Name="textBlock2"
                   Text="Load Failed"
                   Style="{StaticResource PhoneTextLargeStyle}"
                   Visibility="Collapsed" />

    </Grid>
</UserControl>

I'll add an instance of both these controls to the MainPage by dividing the ContentPanel Grid into two rows and adding a control to each row:

<Grid x:Name="ContentPanel"
        Grid.Row="1"
        Margin="12,0,12,0">
    <Grid.RowDefinitions>
        <RowDefinition />
        <RowDefinition />
    </Grid.RowDefinitions>
    <local:MainDataControl x:Name="mainDataControl" />
    <local:SecondMainDataControl x:Name="secondMainDataControl"
                                    Grid.Row="1" />
</Grid>

At this point, if the application is run there will be no information presented to the screen despite having two UserControls, both with visual states which could be used to display the current state of the MainViewModel. This is because neither UserControl knows when to change Visual State. I'll start with the MainDataControl, where I'll extend the class to include a LoadingState dependency property. When this property changes, the new value is used to trigger the change in Visual State by calling the GoToState on the VisualStateManager. It's important to note that the enum values should match the names of the VisualStates defined for the MainDataControl, rather than matching the states of the MainViewModel.

public enum ControlLoadingStates
{
    Base,
    ControlLoading,
    ControlLoaded,
    ControlLoadFailed
}

public sealed partial class MainDataControl
{
    public MainDataControl()
    {
        InitializeComponent();
    }

    public ControlLoadingStates LoadingState
    {
        get { return (ControlLoadingStates)GetValue(LoadingStateProperty); }
        set { SetValue(LoadingStateProperty, value); }
    }

    public static readonly DependencyProperty LoadingStateProperty =
        DependencyProperty.Register("LoadingState", typeof(ControlLoadingStates), 
        typeof(MainDataControl), new PropertyMetadata(ControlLoadingStates.Base, StateChanged));

    private static void StateChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        try
        {
            var uc = d as MainDataControl;
            if (uc == null) return;

            var state = (ControlLoadingStates)e.NewValue;

            VisualStateManager.GoToState(uc, state.ToString(), true);
        }
        catch (Exception ex)
        {
            Debug.WriteLine(ex.Message);
        }
    }
}

As the MainDataControl exposes the LoadingStates dependency property, I also need to add Visual States to the MainPage which will change the value of the LoadingState property. Here are the Visual States defined on the MainPage (note that the names of the Visual States match the states defined for the MainViewModel):

<VisualStateManager.VisualStateGroups>
    <VisualStateGroup x:Name="LoadingStates">
        <VisualState x:Name="Loading">
            <Storyboard>
                <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(MainDataControl.LoadingState)"
                                                Storyboard.TargetName="mainDataControl">
                    <DiscreteObjectKeyFrame KeyTime="0">
                        <DiscreteObjectKeyFrame.Value>
                            <local:ControlLoadingStates>ControlLoading</local:ControlLoadingStates>
                        </DiscreteObjectKeyFrame.Value>
                    </DiscreteObjectKeyFrame>
                </ObjectAnimationUsingKeyFrames>
            </Storyboard>
        </VisualState>
        <VisualState x:Name="Loaded">
            <Storyboard>
                <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(MainDataControl.LoadingState)"
                                                Storyboard.TargetName="mainDataControl">
                    <DiscreteObjectKeyFrame KeyTime="0">
                        <DiscreteObjectKeyFrame.Value>
                            <local:ControlLoadingStates>ControlLoaded</local:ControlLoadingStates>
                        </DiscreteObjectKeyFrame.Value>
                    </DiscreteObjectKeyFrame>
                </ObjectAnimationUsingKeyFrames>
            </Storyboard>
        </VisualState>
        <VisualState x:Name="LoadFailed">
            <Storyboard>
                <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(MainDataControl.LoadingState)"
                                                Storyboard.TargetName="mainDataControl">
                    <DiscreteObjectKeyFrame KeyTime="0">
                        <DiscreteObjectKeyFrame.Value>
                            <local:ControlLoadingStates>ControlLoadFailed</local:ControlLoadingStates>
                        </DiscreteObjectKeyFrame.Value>
                    </DiscreteObjectKeyFrame>
                </ObjectAnimationUsingKeyFrames>
            </Storyboard>
        </VisualState>
    </VisualStateGroup>
</VisualStateManager.VisualStateGroups>

The MainPage will then have to listen to the StateChanged event on the MainViewModel in order to invoke a change in its own state.

protected override void OnNavigatedTo(NavigationEventArgs e)
{
    base.OnNavigatedTo(e);

    ViewModel.StateChanged += StateChanged;
    ViewModel.Start();
}

protected override void OnNavigatedFrom(NavigationEventArgs e)
{
    base.OnNavigatedFrom(e);

    ViewModel.Stop();
    ViewModel.StateChanged -= StateChanged;
}

private void StateChanged(object sender, EventArgs e)
{
    var changer = sender as IStateChanged;
    if (changer == null) return;
    VisualStateManager.GoToState(this, changer.State, true);
}

Here's what happens with each state change:

  • The CurrentState property changes on the MainViewModel, which raises the StateChanged event.
  • This event is handled by the MainPage, where it triggers a change to the visual state on the page.
  • The visual state defines a new value for the LoadingState property on the MainDataControl.
  • The LoadingState property changes, in turn changing the Visual State of the MainDataControl to show the appropriate TextBlock.

The SecondMainDataControl will be wired up by data binding to the MainViewModel, allowing the control to react directly to changes in the state of the MainViewModel. In order to do this, the SecondMainDataControl needs to intercept when the DataContext changes so as to attach or detach event handlers for the StateChanged event on the MainViewModel.

Unfortunately, there's no way to override the DataContext property, nor is there an event raised when the DataContext changes. Instead, this can be achieved by exposing a dependency property, StateDataContext, which can be data bound. Here's the code for the SecondMainDataControl which exposes this dependency property and changes the VisualState based on the StateChanged event:

public sealed partial class SecondMainDataControl 
{
    public SecondMainDataControl()
    {
        InitializeComponent();
    }

    public object StateDataContext
    {
        get { return GetValue(StateDataContextProperty); }
        set { SetValue(StateDataContextProperty, value); }
    }
    public static readonly DependencyProperty StateDataContextProperty =
        DependencyProperty.Register("StateDataContext", typeof(object), typeof(SecondMainDataControl), new PropertyMetadata(null, DataContextChanged));

    private static void DataContextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var uc = d as SecondMainDataControl;
        if (uc == null) return;

        var old = e.OldValue as IStateChanged;
        if (old != null)
        {
            old.StateChanged -= uc.StateChanged;
        }

        var newVal = e.NewValue as IStateChanged;
        if (newVal != null)
        {

            newVal.StateChanged += uc.StateChanged;
        }
    }

    private void StateChanged(object sender, EventArgs e)
    {
        var changer = sender as IStateChanged;
        if (changer == null) return;
        VisualStateManager.GoToState(this, changer.State, true);
    }
}

The only required change to the MainPage.xaml is to data bind to the StateDataContext property (note that the data binding doesn't define a path (ie just "{Binding}"), which means it will simply pass through the DataContext of the SecondMainDataControl):

<local:SecondMainDataControl x:Name="secondMainDataControl"
                                StateDataContext="{Binding}"
                                Grid.Row="1" />

This mechanism is much simpler to summarize:

  • An instance of the MainViewModel is set as the DataContext on the page; this flows down and is set as the DataContext on the SecondMainDataControl.
  • The StateDataContext property will be set to the same value as the DataContext, and an event handler will be attached to the StateChanged event on the MainViewModel.
  • When the state on the MainViewModel changes, the Visual State on the SecondMainDataControl will change to display the appropriate TextBlock.

Running this application will cycle through Loading, Loaded and Load Failed TextBlocks on both user controls, as shown in Figure 1.

[Click on image for larger view.] Figure 1. Different Visual States

In this article you've seen how you can use Visual States within your UserControls. The first method allows the UserControl to be developed independently; the Visual States have no direct correlation to the page it will be used on or the view model to which it will be data bound. To second method is comparatively simpler and doesn't require visual states on the host page. However, it does mean that there's a direct mapping between the states on the view model and the Visual States of the UserControl. There is no right or wrong method, so it's a pragmatic decision as to which method you should use.

comments powered by Disqus

Featured

  • AI for GitHub Collaboration? Maybe Not So Much

    No doubt GitHub Copilot has been a boon for developers, but AI might not be the best tool for collaboration, according to developers weighing in on a recent social media post from the GitHub team.

  • Visual Studio 2022 Getting VS Code 'Command Palette' Equivalent

    As any Visual Studio Code user knows, the editor's command palette is a powerful tool for getting things done quickly, without having to navigate through menus and dialogs. Now, we learn how an equivalent is coming for Microsoft's flagship Visual Studio IDE, invoked by the same familiar Ctrl+Shift+P keyboard shortcut.

  • .NET 9 Preview 3: 'I've Been Waiting 9 Years for This API!'

    Microsoft's third preview of .NET 9 sees a lot of minor tweaks and fixes with no earth-shaking new functionality, but little things can be important to individual developers.

  • Data Anomaly Detection Using a Neural Autoencoder with C#

    Dr. James McCaffrey of Microsoft Research tackles the process of examining a set of source data to find data items that are different in some way from the majority of the source items.

  • What's New for Python, Java in Visual Studio Code

    Microsoft announced March 2024 updates to its Python and Java extensions for Visual Studio Code, the open source-based, cross-platform code editor that has repeatedly been named the No. 1 tool in major development surveys.

Subscribe on YouTube