Windows Phone progress indicator

For a user there is nothing more exciting then filling out forms with tens of input fields. The joy of plowing your way down to the very last piece of information you have to give to someone before you hit register/submit/ok is something you need to experience, at least, on a weekly basis.

Joking aside, I don't really think that anyone in their right mind loves spending some of their precious time on this kind of things. I am not talking specifically about registration forms but about UI with a huge amount of data presented to the user at once.

What usually happens is that users quickly lose interest and just leave. This is exactly what we don't want to happen. Without users the million-dollar-app we have developed becomes just one more app in the depth of an application store.

One thing that we can do is to give the user some small "bite-size" chunks at once which are manageable while displaying the overall progress state. This was the user doesn't feel overwhelmed and is more likely to get to the last screen and that's the ultimate goal.

Displaying the user progress in Windows Phone apps is quite easy. All you need is a UserControl, some styling and a list of items you want the user to walk through. 

So let's get started!

We first need to create a UserControl with a single ListBox control in it inside our main Grid:

 <UserControl x:Class="ProgressIndicator.ProgressIndicator"
    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"
    FontFamily="{StaticResource PhoneFontFamilyNormal}"
    FontSize="{StaticResource PhoneFontSizeNormal}"
    Foreground="{StaticResource PhoneForegroundBrush}">
 <Grid x:Name="LayoutRoot" IsHitTestVisible="False" 
          Height="60"
          VerticalAlignment="Top"
          Background="Black">
        <ListBox x:Name="ListBox"/>
    </Grid>
</UserControl>

I have added some styling to the Grid just to make it look nice later.

What wee need to do now is to define how our grid will look like as well as how each item in our list will be displayed to the user. Let's create two styles for that (ProgressIndicatorStyle and ProgressIndicatorContainerStyle).

Our ListBox element looks now like this:

<ListBox x:Name="ListBox"
         ItemContainerStyle="{StaticResource ProgressIndicatorContainerStyle}"
         Style="{StaticResource ProgressIndicatorStyle}"/>

Now we need to define these styles and put them inside the UserControl.Resources element (you can place it everywhere you want as long as you can reference it later)

<UserControl.Resources>
  <Color x:Key="Blue">#FF00ACE7</Color>
  <Color x:Key="Pink">#F8046B</Color>
  <Style x:Key="ProgressIndicatorContainerStyle" TargetType="ListBoxItem">
    <Setter Property="Background" Value="Transparent"/>
    <Setter Property="Foreground" Value="White"/>
    <Setter Property="FontSize" Value="{StaticResource PhoneFontSizeNormal}"/>
    <Setter Property="FontFamily" Value="{StaticResource PhoneFontFamilyNormal}"/>
    <Setter Property="BorderThickness" Value="0"/>
    <Setter Property="BorderBrush" Value="Transparent"/>
    <Setter Property="Padding" Value="0"/>
    <Setter Property="HorizontalContentAlignment" Value="Left"/>
    <Setter Property="VerticalContentAlignment" Value="Center"/>
    <Setter Property="IsHitTestVisible" Value="False"></Setter>
    <Setter Property="Template">
      <Setter.Value>
        <ControlTemplate TargetType="ListBoxItem">
          <Border x:Name="LayoutRoot" BorderBrush="{TemplateBinding BorderBrush}"
                  BorderThickness="{TemplateBinding BorderThickness}" 
                  Background="{TemplateBinding Background}"
                  HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
                  VerticalAlignment="{TemplateBinding VerticalAlignment}">
             <VisualStateManager.VisualStateGroups>
                <VisualStateGroup x:Name="CommonStates">
                   <VisualState x:Name="Normal"/>
                   <VisualState x:Name="MouseOver"/>
                   <VisualState x:Name="Disabled"/>
                </VisualStateGroup>
                <VisualStateGroup x:Name="SelectionStates">
                   <VisualState x:Name="Unselected">
                     <Storyboard>
                        <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Background"
                                                       Storyboard.TargetName="Border">
                           <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource Blue}"/>
                        </ObjectAnimationUsingKeyFrames>
                     </Storyboard>
                   </VisualState>
                   <VisualState x:Name="Selected">
                     <Storyboard>
                        <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Background"
                                                       Storyboard.TargetName="Border">
                            <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource Pink}"/>
                        </ObjectAnimationUsingKeyFrames>
                     </Storyboard>
                   </VisualState>
                 </VisualStateGroup>
               </VisualStateManager.VisualStateGroups>
               <Border x:Name="Border" Width="35" Height="35" Margin="20,0,0,0">
                  <ContentControl x:Name="ContentContainer" HorizontalAlignment="Center"
                     Margin="{TemplateBinding Padding}"
                     ContentTemplate="{TemplateBinding ContentTemplate}"
                     Content="{TemplateBinding Content}"
                     FontFamily="{TemplateBinding FontFamily}"
                     Foreground="{TemplateBinding Foreground}" 
                     HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" 
                     VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"/>
               </Border>
            </Border>
         </ControlTemplate>
      </Setter.Value>
    </Setter>
  </Style>
  <ItemsPanelTemplate x:Key="ItemsPanelHorizontalTemplate">
    <StackPanel Orientation="Horizontal"/>
  </ItemsPanelTemplate>
  <Style x:Key="ProgressIndicatorStyle" TargetType="ListBox">
    <Setter Property="ItemsPanel" Value="{StaticResource ItemsPanelHorizontalTemplate}"/>
    <Setter Property="Foreground" Value="White"/>
    <Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Disabled"/>
    <Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Disabled"/>
    <Setter Property="BorderThickness" Value="0"/>
    <Setter Property="BorderBrush" Value="Transparent"/>
    <Setter Property="Padding" Value="0"/>
    <Setter Property="Template">
      <Setter.Value>
        <ControlTemplate TargetType="ListBox">
           <ScrollViewer x:Name="ScrollViewer" BorderBrush="{TemplateBinding BorderBrush}"
                         BorderThickness="{TemplateBinding BorderThickness}"
                         Background="{TemplateBinding Background}"
                         Foreground="{TemplateBinding Foreground}"
                         Padding="{TemplateBinding Padding}">
             <ItemsPresenter/>
           </ScrollViewer>
        </ControlTemplate>
     </Setter.Value>
    </Setter> 
  </Style>
</UserControl.Resources>

Most of the stuff above is just what you get out of the box when you edit a ListBox template. There are just two things you need to pay attention to.

The first thing is that we have defined the ItemsPanel in ProgressIndicatorStyle to be a StackPanel which is oriented horizontally. This way we will get each step of the progress displayed next to each other.

The second, and more important, thing is the state of each element in the list. Each step can either be the current (i.e. Selected) or a non-current step (i.e. Unselected). If you look closely into the ProgressIndicatorContainerStyle you will find the VisualStateGroup for SelectionStates. There we defined two distinct states our elements can be.

If the current element is selected we will set the background of the border surrounding each element to pink. If it's not selected the background will be set to blue (please keep in mind I am not a designer, and am somewhat colorblind :))

Now that we have the look and feel ready we just need to set up the UserControl and make it ready to be used elsewhere:

public partial class ProgressIndicator : UserControl
{
   public static readonly DependencyProperty SelectedIndexProperty = 
       DependencyProperty.Register("SelectedIndex",
                                   typeof(int),
                                   typeof(ProgressIndicator),
                                   new PropertyMetadata(-1, OnSelectedIndexPropertyChanged));
   public static readonly DependencyProperty ItemsCountProperty = 
       DependencyProperty.Register("ItemsCount",
                                   typeof(int),
                                   typeof(ProgressIndicator),
                                   new PropertyMetadata(1, OnItemsCountPropertyChanged));

   public int SelectedIndex
   {
       get { return (int)GetValue(SelectedIndexProperty); }
       set { SetValue(SelectedIndexProperty, value); }
   }

   public int ItemsCount
   {
       get { return (int)GetValue(ItemsCountProperty); }
       set { SetValue(ItemsCountProperty, value); }
   }


   private static void OnSelectedIndexPropertyChanged(DependencyObject d,
                                                      DependencyPropertyChangedEventArgs e)
   {
       var control = (ProgressIndicator)d;
       var value = (int)e.NewValue;

       var itemsCount = control.ListBox.Items.Count();
       control.ListBox.SelectedIndex = itemsCount == 0 ||
                                       value < 0 ||
                                       (itemsCount > 0 && value > itemsCount - 1) ? -1 : value;
   }
   private static void OnItemsCountPropertyChanged(DependencyObject d,
                                                   DependencyPropertyChangedEventArgs e)
   {
       var control = (ProgressIndicator)d;
       var itemsCount = (int)e.NewValue;
       itemsCount = itemsCount > 0 ? itemsCount : 0;

       var oldSelected = control.SelectedIndex;
       control.SelectedIndex = -1;
       control.ListBox.Items.Clear();
       for (var i = 0; i < itemsCount; i++)
       {
           control.ListBox.Items.Add(new ListBoxItem { Content = (i + 1).ToString() });
       }
       //reselect
       var newSelected = itemsCount == 0 ||
                         oldSelected < 0 || 
                         (itemsCount > 0 && oldSelected > itemsCount - 1) ? -1 : oldSelected;
       control.ListBox.SelectedIndex = newSelected;
       control.SelectedIndex = newSelected;
   }

   public ProgressIndicator()
   {
       InitializeComponent();
   }
}

Everything is pretty straightforward. We created dependency properties for the items count and currently selected index. One part here we need to pay a bit more attention to is the OnItemsCountProprtyChanged event handler. There we add elements to our progress indicator based on how many items (or steps) we have. After the elements are in place we only have to wait for the user to move through the steps and the progress indicator will select the appropriate element highlighting the current step.

Once this all is in place we are just one step away from having an awesome UI feature for our users:

<Grid x:Name="LayoutRoot" Background="Transparent">
  <Grid.RowDefinitions>
    <RowDefinition Height="Auto"/>
    <RowDefinition/>
  </Grid.RowDefinitions>

  <progressIndicator:ProgressIndicator
     Grid.Row="0"
     ItemsCount="6"
     SelectedIndex="{Binding ElementName=Pivot, Path=SelectedIndex}"/>
  <phone:Pivot x:Name="Pivot" Grid.Row="1">
     <phone:PivotItem Header="first"/>
     <phone:PivotItem Header="second"/>
     <phone:PivotItem Header="third"/>
     <phone:PivotItem Header="fourt"/>
     <phone:PivotItem Header="fifth"/>
     <phone:PivotItem Header="sixth"/>
  </phone:Pivot>
</Grid>

We have created a simple Grid and places our progress indicator along with the a Pivot control. We set the progress indicator ItemsCount to 6 and bound the SelectedIndex of our progress indicator to the currently selected index in the Pivot.

Now it's time to plug in your phone or run the emulator and enjoy the fruits of your labor.

 
 


Do you like my apps?

Let your ListBox items breath