RangeSelector: A new control for your XAML for WinRT application

While working on UrzaGatherer v3.0, I found myself in the need of a range selector control. Something like the slider control but with two thumbs.

Feel free to ping me on Twitter(@deltakosh) if you want to discuss about this article

because this control is not part of the default library, I decided to create one. Feel free to download the complete solution here.

For flexibility reason, I created a custom control named RangeSelector and based on this template:

<Style TargetType="local:RangeSelector" >
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="local:RangeSelector">
                <Grid Height="32">
                    <Grid.Resources>
                        <Style TargetType="Thumb">
                            <Setter Property="Template">
                                <Setter.Value>
                                    <ControlTemplate TargetType="Thumb">
                                        <Ellipse Width="32" Height="32" Fill="{TemplateBinding Background}" 
Stroke="{TemplateBinding Foreground}" StrokeThickness="4" RenderTransformOrigin="0.5 0.5"> <Ellipse.RenderTransform> <TranslateTransform X="-16"></TranslateTransform> </Ellipse.RenderTransform> </Ellipse> </ControlTemplate> </Setter.Value> </Setter> </Style> </Grid.Resources> <Rectangle Height="8" Fill="{TemplateBinding Background}" Margin="12,0"></Rectangle> <Canvas x:Name="ContainerCanvas" Margin="16,0"> <Rectangle x:Name="ActiveRectangle" Fill="{TemplateBinding Foreground}" Height="8" Canvas.Top="12"></Rectangle> <Thumb x:Name="MinThumb" Background="{TemplateBinding Background}" /> <Thumb x:Name="MaxThumb" Background="{TemplateBinding Background}"/> </Canvas> </Grid> </ControlTemplate> </Setter.Value> </Setter> </Style>

So we mainly have a canvas that hosts two thumbs and a rectangle.

The goal is to have this kind of rendering:

The C# control by itself is then based on a Control class with the following “plumbing” done to connect parts:

public sealed class RangeSelector : Control
{

    Rectangle ActiveRectangle;
    Thumb MinThumb;
    Thumb MaxThumb;
    Canvas ContainerCanvas;

    public RangeSelector()
    {
        DefaultStyleKey = typeof(RangeSelector);
    }

    protected override void OnApplyTemplate()
    {
        ActiveRectangle = GetTemplateChild("ActiveRectangle") as Rectangle;
        MinThumb = GetTemplateChild("MinThumb") as Thumb;
        MaxThumb = GetTemplateChild("MaxThumb") as Thumb;
        ContainerCanvas = GetTemplateChild("ContainerCanvas") as Canvas;

        MinThumb.DragCompleted += Thumb_DragCompleted;
        MinThumb.DragDelta += MinThumb_DragDelta;
        MinThumb.DragStarted += MinThumb_DragStarted;

        MaxThumb.DragCompleted += Thumb_DragCompleted;
        MaxThumb.DragDelta += MaxThumb_DragDelta;
        MaxThumb.DragStarted += MaxThumb_DragStarted;

        ContainerCanvas.SizeChanged += ContainerCanvas_SizeChanged;

        base.OnApplyTemplate();
    }

    private void ContainerCanvas_SizeChanged(object sender, SizeChangedEventArgs e)
    {
        SyncThumbs();
    }

Basically we need to connect to the drag events of our thumbs and provide a SyncThumbs() method to move the thumbs and the rectangle in sync with range values.

This range values are defined by regular dependencies properties:

public static readonly DependencyProperty MinimumProperty = 
DependencyProperty.Register("Minimum", typeof(double), typeof(RangeSelector), new PropertyMetadata(0.0, null));

public static readonly
DependencyProperty MaximumProperty =
DependencyProperty.Register("Maximum", typeof(double), typeof(RangeSelector), new PropertyMetadata(1.0, null));
public static readonly
DependencyProperty RangeMinProperty =
DependencyProperty.Register("RangeMin", typeof(double), typeof(RangeSelector), new PropertyMetadata(0.0, null));
public static readonly
DependencyProperty RangeMaxProperty =
DependencyProperty.Register("RangeMax", typeof(double), typeof(RangeSelector), new PropertyMetadata(1.0, null));

The SyncThumbs() method is just a simple translation between Maximum<->Minimum and the canvas’ width:

public void SyncThumbs()
{
    if (ContainerCanvas == null)
    {
        return;
    }

    var relativeLeft = ((RangeMin - Minimum) / (Maximum - Minimum)) * ContainerCanvas.ActualWidth;
    var relativeRight = ((RangeMax - Minimum) / (Maximum - Minimum)) * ContainerCanvas.ActualWidth;

    Canvas.SetLeft(MinThumb, relativeLeft);
    Canvas.SetLeft(ActiveRectangle, relativeLeft);

    Canvas.SetLeft(MaxThumb, relativeRight);

    ActiveRectangle.Width = Canvas.GetLeft(MaxThumb) - Canvas.GetLeft(MinThumb);
}

The drag events are then responsible for doing the opposite transformation:

private void MinThumb_DragDelta(object sender, DragDeltaEventArgs e)
{
    RangeMin = DragThumb(MinThumb, 0, Canvas.GetLeft(MaxThumb), e);
}

private void MaxThumb_DragDelta(object sender, DragDeltaEventArgs e)
{
    RangeMax = DragThumb(MaxThumb, Canvas.GetLeft(MinThumb), ContainerCanvas.ActualWidth, e);
}

private double DragThumb(Thumb thumb, double min, double max, DragDeltaEventArgs e)
{
    var currentPos = Canvas.GetLeft(thumb);
    var nextPos = currentPos + e.HorizontalChange;

    nextPos = Math.Max(min, nextPos);
    nextPos = Math.Min(max, nextPos);

    Canvas.SetLeft(thumb, nextPos);

    return (Minimum + (nextPos / ContainerCanvas.ActualWidth) * (Maximum - Minimum)); ;
}

private void MinThumb_DragStarted(object sender, DragStartedEventArgs e)
{
    Canvas.SetZIndex(MinThumb, 10);
    Canvas.SetZIndex(MaxThumb, 0);
}

private void MaxThumb_DragStarted(object sender, DragStartedEventArgs e)
{
    Canvas.SetZIndex(MinThumb, 0);
    Canvas.SetZIndex(MaxThumb, 10);
}

Please note that I also defined the ZIndex property to be sure to have the right thumb on top of the canvas

Using this control is then a piece of cake:

<StackPanel Orientation="Vertical" VerticalAlignment="Center">
    <TextBlock FontSize="20" Text="{Binding RangeMin, ElementName=RangeSelector}" HorizontalAlignment="Center" />
    <local:RangeSelector x:Name="RangeSelector" Background="Gray" Foreground="Red" BorderThickness="4" 
Minimum="0" Maximum="100" RangeMin="20" RangeMax="80"/> <TextBlock FontSize="20" Text="{Binding RangeMax, ElementName=RangeSelector}" HorizontalAlignment="Center" /> </StackPanel>

I hope you will find this control useful!