Skip Navigation LinksHome > View Post

Creating a Range Slider in WPF (and other cool tips and tricks for UserControls)

I've had to create a RangeSlider (where you can select a range within a range, not just a value) several times now and there's a couple of neat tricks I use to compose such controls that I thought I'd share with you.

Basic Slider

The idea is simple, the control should look like a slider but with two 'thumbs':

Range Slider

The way I chose to tackle this was, as always, to leverage existing controls wherever possible. Composition is king in WPF and UserControls provide a very lightweight, cheap angle of attack here.

I decided to have a UserControl composed, primarily, of two Sliders stack on top of each other.

Logically, the RangeSlider would have four properties:

  • Minimum (double)
  • LowerValue (double)
  • UpperValue (double)
  • Maximum (double)
So the first thing to do is add these as dependency properties to our new UserControl (called RangeSlider):

public double Minimum
{
    get { return (double)GetValue(MinimumProperty); }
    set { SetValue(MinimumProperty, value); }
}

public static readonly DependencyProperty MinimumProperty =
    DependencyProperty.Register("Minimum", typeof(double), typeof(RangeSlider), new UIPropertyMetadata(0d));

public double LowerValue
{
    get { return (double)GetValue(LowerValueProperty); }
    set { SetValue(LowerValueProperty, value); }
}

public static readonly DependencyProperty LowerValueProperty =
    DependencyProperty.Register("LowerValue", typeof(double), typeof(RangeSlider), new UIPropertyMetadata(0d));

public double UpperValue
{
    get { return (double)GetValue(UpperValueProperty); }
    set { SetValue(UpperValueProperty, value); }
}

public static readonly DependencyProperty UpperValueProperty =
    DependencyProperty.Register("UpperValue", typeof(double), typeof(RangeSlider), new UIPropertyMetadata(0d));

public double Maximum
{
    get { return (double)GetValue(MaximumProperty); }
    set { SetValue(MaximumProperty, value); }
}

public static readonly DependencyProperty MaximumProperty =
    DependencyProperty.Register("Maximum", typeof(double), typeof(RangeSlider), new UIPropertyMetadata(1d));

Properties Done.

Big trick/tip - using binding to get control data into your composition

I now need to get these properties into the controls within my UserControl. I use ElementName binding to this and it looks like this:

<UserControl x:Class="UserControlFun.RangeSlider"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    x:Name="root">
    <Slider x:Name="LowerSlider"
        Minimum="{Binding ElementName=root, Path=Minimum}"
        Maximum="{Binding ElementName=root, Path=Maximum}"
        Value="{Binding ElementName=root, Path=LowerValue}" />
    <Slider x:Name="UpperSlider"
        Minimum="{Binding ElementName=root, Path=Minimum}"
        Maximum="{Binding ElementName=root, Path=Maximum}"
        Value="{Binding ElementName=root, Path=UpperValue}" />

</UserControl>

Notice how we set a name on the UserControl which allows us to bind to him and access all his properties - inluding our juicy new ones. Even, better, because the Bindings are two way any updates propagate between the UserControl and the composed controls. Awesome. Functionally, we already have most of our control!

Some rules need applying, e.g. the UpperValue should always be greater than or equal to the LowerValue and I'd even like one slider to drag the other along if it bumps into it.

However, the most pressing problem at the moment is the fact that my control looks like this:

ooops!

Which is clearly wrong and, worse still, I can't click through to the LowerSlider control.

No problem - just time to re-template! Remembering how templates work and need to be composed for each control is a nightmare.

little tip - steal templates from Blend

Another trick I use is to get Expression Blend to reveal the internal ControlTemplates of controls. Just drag a Slider onto the design surface, right click, choose 'Edit Control Templates' > 'Edit a copy' and you're away. You can now head off to Xaml view to get a good look at the ControlTemplate.

Edit A Copy

Be warned, some of the ControlTemplates in WPF are extremely complicated. Don't be put off though - in our case the required template is very simple. To keep the post short, I'm going to go for a very simple looking slider, here's the ControlTemplate for the individual sliders:

<ControlTemplate x:Key="simpleSlider" TargetType="{x:Type Slider}">
    <Border SnapsToDevicePixels="true"
            BorderBrush="{TemplateBinding BorderBrush}"
            BorderThickness="{TemplateBinding BorderThickness}">
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto" MinHeight="{TemplateBinding MinHeight}"/>
                <RowDefinition Height="Auto"/>
            </Grid.RowDefinitions>

            <Rectangle x:Name="PART_SelectionRange"/>

            <Track x:Name="PART_Track" Grid.Row="1">
                <Track.Thumb>
                    <Thumb x:Name="Thumb">
                        <Thumb.Template>
                            <ControlTemplate TargetType="Thumb">
                                <Rectangle Fill="Red"
                                    Stroke="Black"
                                    StrokeThickness="1"
                                    Width="10"
                                    Height="18"
                                    SnapsToDevicePixels="True"/>
                            </ControlTemplate>
                        </Thumb.Template>
                    </Thumb>
                </Track.Thumb>
            </Track>
        </Grid>
    </Border>
</ControlTemplate>

And here's the two Sliders (and a border to create the line that represents the range):

<Grid VerticalAlignment="Top">
    <Border BorderThickness="0,1,0,0"
        BorderBrush="Black"
        VerticalAlignment="Center"
        Height="1"
        Margin="5,0,5,0"/>
    
    <Slider x:Name="LowerSlider"
        Minimum="{Binding ElementName=root, Path=Minimum}"
        Maximum="{Binding ElementName=root, Path=Maximum}"
        Value="{Binding ElementName=root, Path=LowerValue}"
        Template="{StaticResource simpleSlider}"
        Margin="0,0,10,0"
    />
    <Slider x:Name="UpperSlider"
        Minimum="{Binding ElementName=root, Path=Minimum}"
        Maximum="{Binding ElementName=root, Path=Maximum}"
        Value="{Binding ElementName=root, Path=UpperValue}"
        Template="{StaticResource simpleSlider}"
        Margin="10,0,0,0"
    />
</Grid>

The Margins stop the two thumbs overlapping (so it looks like one literally pushes the other along).

Finally, I had to add some code-behind (boo!) to monitor for changes in the sliders. I only want these hooked up after the control has loaded so here's the UserControl's constructor and some subsequent delegates:

public RangeSlider()
{
    InitializeComponent();
    this.Loaded += RangeSlider_Loaded;
}

void RangeSlider_Loaded(object sender, RoutedEventArgs e)
{
    LowerSlider.ValueChanged += LowerSlider_ValueChanged;
    UpperSlider.ValueChanged += UpperSlider_ValueChanged;
}

private void LowerSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
{
    UpperSlider.Value = Math.Max(UpperSlider.Value, LowerSlider.Value);
}

private void UpperSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
{
    LowerSlider.Value = Math.Min(UpperSlider.Value, LowerSlider.Value);
}

Which altogether looks like (with some added bound textboxes to show the values):

Finished Slider

There's plenty that I haven't accounted for in the control so far:
  • Vertical Orientation
  • Visualising Disabled State, Focussed State etc
  • No doubt much, much, more...
Nonetheless it works reasonably well as a starter and was very easy to implement. Get the source code here: Download Source (9KB).

Enjoy - usual demoware disclaimers apply.

UPDATE Why not check out the Silverlight example in my follow up post.

Tags: WPF MVVM

 
Josh Post By Josh Twist
2:34 PM
15 Jul 2009

» Next Post: Creating a Range Slider in Silverlight (and some of the binding gotchas in SL3)
« Previous Post: Animating when Data Changes - Part II

Comments are closed for this post.

Posted by Pa @ 05 Aug 2009 12:13 PM
Hey, thanks for the gread control... very useful! Greetings

Posted by Brenda @ 25 Aug 2009 12:13 PM
Thank you so much for the post - exactly what I needed and am sure will need again!

Posted by Dave @ 29 Oct 2009 5:56 PM
Nice control! I'm going to have to look at it under the hood and see if it's possible to also make the sub range draggable, by dragging the region between the two thumbs. Unless you can add it for me. ;)

Posted by josh @ 30 Oct 2009 12:26 AM
Yes, this is very possible and I've already done it 2-3 times on projects. Sadly, I don't have time to type it up right now.

There's a number of ways to skin this cat, so I'd be interested which route you choose :)

Posted by Eeee @ 29 Dec 2009 9:07 AM
Pretty cool! Thanks for sharing this. I was wondering if you'd follow the same approach if you needed to have multiple (indeterminate) intervals on the same track? Any advice?

Posted by landofthesamp @ 26 Apr 2010 3:55 PM
Thanks - a very useful control! You state that you haven't accounted for vertical orientation - and that's exactly what I need... Any hints on how to accomplish this?

Posted by Josh @ 27 Apr 2010 12:03 PM
Hi, It's actually pretty easy but I don't have time to post the details in full. Instead, I'd suggest you take a look at how the Slider itself is implemented in WPF (use Blend to create a copy of the template). This will be a good exercise and give you some ideas.

Posted by Lukas @ 24 Jul 2011 10:45 PM
One question regarding the template: What part makes the lower thumb appear above the line? I have copied the original data and cannot make it work. The simple one works. My assumption was that is the Name vs. x:Name, but that does not seem to be the case...

© 2005 - 2014 Josh Twist - All Rights Reserved.