A better Slider for Windows Phone 7

The Slider control as shipped with the Windows Phone Developer Tools has some problems, both due to how it was templated, and also because it was originally designed for the desktop, where mice are very good at hitting small targets.

The visible Thumb was removed, and the invisible Thumb, even when you know where it is, is quite difficult to touch and drag. This means that most often, you end up pressing one of the RepeatButtons that will slowly increment or decrement the Slider’s value. That may be what you intended, but the Slider on the phone shouldn’t be used for precision adjustments. That will just frustrate the user.

It is quite difficult to manipulate a horizontal Slider to get the maximum value, and if you somehow do manage to do that, you will find that you have moved the invisible Thumb off of the Slider, where it will be clipped, and you have to press the decrement button a while to get it back.

I have re-templated the Slider, and added a small tick mark in the thumb for some sort of a visual cue, as when the Slider is on its minimum value, the darkened track is sometimes hard to see. It is easy to remove the tick if your application provides enough context to make it clear that there is a Slider and how to use it. I made the touchable area much wider—in fact, the Thumb has been expanded to cover the entire Slider area, so that the user can drag anywhere on the Slider without having to hit a small, invisible target.

Note that the Slider should be used judiciously. Controls that are manipulated by dragging should not be placed in situations where they are on a parent that uses drags of the same orientation. So, for example, you should not put a horizontal Slider inside of a Panorama or Pivot control, and you should not put a vertical Slider inside of a ListBox (I have a hard time imagining why you’d want to do the latter.)

Here is the new Slider template. I started with the template that I found in

%ProgramFiles%\Microsoft SDKs\Windows Phone\v7.0\Design\System.Windows.xaml

although I could have used Blend, added a Slider, and then right-clicked on it and selected “Edit Template…” and then “Edit a Copy…”. I put my template in App.xaml:

Code Snippet

  1. <Application
  2.     x:Class="SliderApp.App"
  3.     xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"       
  4.     xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
  5.     xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"
  6.     xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone"
  7.     xmlns:local="clr-namespace:SliderApp">
  8.  
  9.     <!--Application Resources-->
  10.     <Application.Resources>
  11.  
  12.         <ControlTemplate x:Key="PhoneSimpleRepeatButton" TargetType="RepeatButton">
  13.             <Rectangle/>
  14.         </ControlTemplate>
  15.  
  16.         <ControlTemplate x:Key="PhoneSimpleThumbHorizontal" TargetType="Thumb">
  17.             <Border Background="Transparent" Margin="-480,-18">
  18.                 <Rectangle Width="2" Height="6" Fill="{StaticResource PhoneForegroundBrush}"/>
  19.             </Border>
  20.         </ControlTemplate>
  21.  
  22.         <ControlTemplate x:Key="PhoneSimpleThumbVertical" TargetType="Thumb">
  23.             <Border Background="Transparent" Margin="-6,-800">
  24.                 <Rectangle Width="6" Height="2" Margin="24,0,0,0" Fill="{StaticResource PhoneForegroundBrush}"/>
  25.             </Border>
  26.         </ControlTemplate>
  27.  
  28.         <Style x:Key="sliderStyle" TargetType="local:PhoneSlider">
  29.             <Setter Property="BorderThickness" Value="0"/>
  30.             <Setter Property="BorderBrush" Value="Transparent"/>
  31.             <Setter Property="Maximum" Value="10"/>
  32.             <Setter Property="Minimum" Value="0"/>
  33.             <Setter Property="Value" Value="0"/>
  34.             <Setter Property="Margin" Value="{StaticResource PhoneHorizontalMargin}"/>
  35.             <Setter Property="Background" Value="{StaticResource PhoneContrastBackgroundBrush}"/>
  36.             <Setter Property="Foreground" Value="{StaticResource PhoneAccentBrush}"/>
  37.             <Setter Property="Template">
  38.                 <Setter.Value>
  39.                     <ControlTemplate TargetType="local:PhoneSlider">
  40.                         <Grid Background="Transparent">
  41.                             <VisualStateManager.VisualStateGroups>
  42.                                 <VisualStateGroup x:Name="CommonStates">
  43.                                     <VisualState x:Name="Normal"/>
  44.                                     <VisualState x:Name="MouseOver"/>
  45.                                     <VisualState x:Name="Disabled">
  46.                                         <Storyboard>
  47.                                             <DoubleAnimation Duration="0" Storyboard.TargetName="HorizontalTrack" Storyboard.TargetProperty="Opacity" To="0.1" />
  48.                                             <DoubleAnimation Duration="0" Storyboard.TargetName="VerticalTrack" Storyboard.TargetProperty="Opacity" To="0.1" />
  49.                                             <ObjectAnimationUsingKeyFrames Storyboard.TargetName="HorizontalFill" Storyboard.TargetProperty="Fill">
  50.                                                 <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneDisabledBrush}" />
  51.                                             </ObjectAnimationUsingKeyFrames>
  52.                                             <ObjectAnimationUsingKeyFrames Storyboard.TargetName="VerticalFill" Storyboard.TargetProperty="Fill">
  53.                                                 <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneDisabledBrush}" />
  54.                                             </ObjectAnimationUsingKeyFrames>
  55.                                         </Storyboard>
  56.                                     </VisualState>
  57.                                 </VisualStateGroup>
  58.                             </VisualStateManager.VisualStateGroups>
  59.                             <Grid x:Name="HorizontalTemplate">
  60.                                 <Grid.ColumnDefinitions>
  61.                                     <ColumnDefinition Width="*"/>
  62.                                     <ColumnDefinition Width="0"/>
  63.                                     <ColumnDefinition Width="auto"/>
  64.                                 </Grid.ColumnDefinitions>
  65.                                 <Rectangle x:Name="HorizontalTrack" IsHitTestVisible="False" Fill="{TemplateBinding Background}" Opacity="0.2" Grid.ColumnSpan="3" Height="12" Margin="0,22,0,50"/>
  66.                                 <Rectangle x:Name="HorizontalFill" IsHitTestVisible="False" Fill="{TemplateBinding Foreground}" Grid.Column="0" Height="12" Margin="0,22,0,50"/>
  67.                                 <RepeatButton x:Name="HorizontalTrackLargeChangeDecreaseRepeatButton" IsTabStop="False" Template="{StaticResource PhoneSimpleRepeatButton}" Grid.Column="0" />
  68.                                 <RepeatButton x:Name="HorizontalTrackLargeChangeIncreaseRepeatButton" IsTabStop="False" Template="{StaticResource PhoneSimpleRepeatButton}" Grid.Column="2" />
  69.                                 <Thumb x:Name="HorizontalThumb" Width="1" Margin="-1,0,0,0" Grid.Column="1" Template="{StaticResource PhoneSimpleThumbHorizontal}" RenderTransformOrigin="0.5,0.5" CacheMode="BitmapCache"/>
  70.                             </Grid>
  71.                             <Grid x:Name="VerticalTemplate">
  72.                                 <Grid.RowDefinitions>
  73.                                     <RowDefinition Height="*"/>
  74.                                     <RowDefinition Height="0"/>
  75.                                     <RowDefinition Height="Auto"/>
  76.                                 </Grid.RowDefinitions>
  77.                                 <Rectangle x:Name="VerticalTrack" IsHitTestVisible="False" Fill="{TemplateBinding Background}" Opacity="0.2" Grid.RowSpan="3" Width="12" Margin="24,0"/>
  78.                                 <Rectangle x:Name="VerticalFill" IsHitTestVisible="False" Fill="{TemplateBinding Foreground}" Grid.Row="2" Width="12" Margin="24,0"/>
  79.                                 <RepeatButton x:Name="VerticalTrackLargeChangeDecreaseRepeatButton" IsTabStop="False" Template="{StaticResource PhoneSimpleRepeatButton}" Grid.Row="0"/>
  80.                                 <RepeatButton x:Name="VerticalTrackLargeChangeIncreaseRepeatButton" IsTabStop="False" Template="{StaticResource PhoneSimpleRepeatButton}" Grid.Row="2"/>
  81.                                 <Thumb x:Name="VerticalThumb" Height="1" Margin="0,-1,0,0" Grid.Row="1" Template="{StaticResource PhoneSimpleThumbVertical}" RenderTransformOrigin="0.5,0.5" CacheMode="BitmapCache"/>
  82.                             </Grid>
  83.                         </Grid>
  84.                     </ControlTemplate>
  85.                 </Setter.Value>
  86.             </Setter>
  87.         </Style>
  88.     </Application.Resources>
  89.  
  90.     <Application.ApplicationLifetimeObjects>
  91.         <!--Required object that handles lifetime events for the application-->
  92.         <shell:PhoneApplicationService
  93.             Launching="Application_Launching" Closing="Application_Closing"
  94.             Activated="Application_Activated" Deactivated="Application_Deactivated"/>
  95.     </Application.ApplicationLifetimeObjects>
  96.  
  97. </Application>

This could also be placed in your page’s resources or in generic.xaml. You will note that in lines 28 and 39, I have am not using TargetType=”Slider”, but I have used the name of my subclassed type. This is because I had to use some code-behind to handle the Clip area. Since I am using negative margins to make the Thumbs cover the entire Slider, and I had to make the Thumbs large enough to cover the entire Slider, no matter where it is positioned, I need code to handle the clipping, otherwise the Thumbs would extend way beyond the Slider’s bounds, and cover up other controls. This is particularly a problem for vertical Sliders.

Fortunately, the code-behind isn’t much:

Code Snippet

  1. using System.Windows;
  2. using System.Windows.Controls;
  3. using System.Windows.Media;
  4.  
  5. namespace SliderApp
  6. {
  7.     public class PhoneSlider : Slider
  8.     {
  9.         public PhoneSlider()
  10.         {
  11.             SizeChanged += new SizeChangedEventHandler(PhoneSlider_SizeChanged);
  12.         }
  13.  
  14.         void PhoneSlider_SizeChanged(object sender, SizeChangedEventArgs e)
  15.         {
  16.             if (e.NewSize.Width > 0 && e.NewSize.Height > 0)
  17.             {
  18.                 Rect clipRect = new Rect(0, 0, e.NewSize.Width, e.NewSize.Height);
  19.                 if (Orientation == Orientation.Horizontal)
  20.                 {
  21.                     clipRect.X -= 12;
  22.                     clipRect.Width += 24;
  23.                     object margin = Resources["PhoneHorizontalMargin"];
  24.                     if (margin != null)
  25.                     {
  26.                         Margin = (Thickness)margin;
  27.                     }
  28.                 }
  29.                 else
  30.                 {
  31.                     clipRect.Y -= 12;
  32.                     clipRect.Height += 24;
  33.                     object margin = Resources["PhoneVerticalMargin"];
  34.                     if (margin != null)
  35.                     {
  36.                         Margin = (Thickness)margin;
  37.                     }
  38.                 }
  39.  
  40.                 this.Clip = new RectangleGeometry() { Rect = clipRect };
  41.             }
  42.         }
  43.     }
  44. }

It just has to set the margins and clip correctly, but the values depend on the value of Slider.Orientation. If your application was going to use Sliders with only one orientation, you could simplify the code-behind and the template, but you’d still need the code-behind for the clip.

You can download a sample project that contains both horizontal and vertical Sliders, with the default and modified templates. Here’s what it looks like:

image

You really have to try this on an actual Phone to appreciate how hard it can be to use the default Slider. The emulator makes it easy—we have all been trained to use a relatively high-precision mouse to hit small targets on the screen, and the invisible Thumb is quite easy to hit when using the emulator and mouse.

SliderApp.zip