Click here to Skip to main content
15,867,756 members
Articles / Desktop Programming / WPF

WPF MultiRangeSlider Control

Rate me:
Please Sign up or sign in to vote.
4.92/5 (10 votes)
6 Aug 2013CPOL5 min read 54.8K   3.1K   22   6
MultiRangeSlider control to specify not intersecting ranges

Introduction

From time to time it is necessary to specify not intersecting ranges. The last case in my practice is control to define different settings for displaying map for different zoom levels. You can solve this by using DatGrid with two columns: From and To. But in this case you have to keep track of changing values to ensure that the ranges are not intersected. Herewith you have to signal when user input wrong data, for example by coloring grid cell in red, or by silent discarding changes. This generates complicated logic and confuses user. Much more simple for developer and for user is specifying not intersecting ranges by multirange control which is physically does not allow to set wrong values:

Image 1

Unfortunately, there is no such element in standard visual studio controls. There are many articles about controls to specifying single range (with two thumbs). Common approaches - put one slider on another (same as first) and track the values.

I summarize this approach to expand it to unlimited number of sliders and to add interaction with user, to let him possibility to add or remove new range in runtime.

Main idea

Main idea is that each inner slider is connected with two neighboring and operates in concert with them.

Inner slider contains four properties:

  1. LeftValue – left value of range, associated with slider (which is equal to Value property of common Slider, i.e. position of thumb on slider axis).
  2. RightValue – right value of range, associated with slider.
  3. MinimumValue
    – the minimal boundary to LeftValue, which is equal to RightValue of previous slider.
  4. MaximumValue
    – the maximum boundary to RightValue, which is equal to LeftValue of next slider.

When I move the slider to the right, I modify the RightValue of previous slider (so the previous range grows), the MaximumValue of previous slider (so I can move the previous slider thumb onward), the LeftValue of this slider itself and the MinimumValue of next slider.

Image 2

As can be seen from the picture, to describe N ranges you need N+1 slider (slider thumbs), because the last slider thumb defines the Right value of last range (RightValue of penult slider).

Naturally, for first and last sliders you have to consider that theirs left and right boundaries is Minimum and Maximum of slider respectively.

MinimumValue
and MaximumValue properties are needed in order to sliders not overlap one to another, but there is a catch.

First thought – to check that slider value (LeftValue or Value of common slider) inside the boundaries.

C#
if (value > MinimumValue && value < MaximumValue)
       return true;
return false;

But the value of slider is center of slider thumb:

Image 3

My way to solve this problem – set different scales for different sliders, so that the values of sliders just fall into the border of thumb.

The scale of the slider will be depending on the visual size of thumb, and you will have to change the scale while resizing the control, but it is the easiest way.

Summing up, the process of generation inner sliders in my control has three steps:

1. Creation of sliders, based on specified settings with binding to specified ranges.

C#
private void CreateSliders()
{
   foreach (var item in ItemsSource)
        Items.Add(CreateSlider(item));
            
   InitSliders();
            
}

2. Initialization of sliders with the creation of binding to the values of neighboring sliders and creation of last slider (that is not bound to any range object).

C#
private void InitSliders()
{
   Items.First().IsFirst = true;  
   for(int i = 0; i < Items.Count; i++)
   {
        InitSliderMinimum(i > 0? Items[i-1] : null, Items[i]);
        InitSliderMaximum(Items[i], i < Items.Count - 1 ? Items[i + 1] : null);
   } 
   Items.Add(CreateLastSliderFromItem(Items.Last()));
 
   ArrangeSliders();
}
 
private void InitSliderMaximum(WitMultiRangeSliderItem slider, 
    WitMultiRangeSliderItem nextSlider)
{
   slider.SetBinding(WitMultiRangeSliderItem.MaximumValueProperty, nextSlider == null ? 
        GetBinding(slider, x => x.RightValue) : 
        GetBinding(nextSlider, x => x.LeftValue));
}
 
private void InitSliderMinimum(WitMultiRangeSliderItem previousSlider, 
    WitMultiRangeSliderItem slider)
{
   if (previousSlider == null) 
        slider.MinimumValue = Minimum;
   else 
        slider.SetBinding(WitMultiRangeSliderItem.MinimumValueProperty, 
            GetBinding(previousSlider, x => x.LeftValue));
}
 
private WitMultiRangeSliderItem CreateLastSliderFromItem(WitMultiRangeSliderItem lastItem)
{
   var slider = new WitMultiRangeSliderItem
   {
        Item = null,
        IsLast = true,
        MaximumValue = Maximum
   };
 
   slider.SetBinding(WitMultiRangeSliderItem.LeftValueProperty, 
        GetBinding(lastItem, x => x.RightValue));
   slider.SetBinding(WitMultiRangeSliderItem.MinimumValueProperty,
        GetBinding(lastItem, x => x.LeftValue));
 
   return slider;
 
} 

3. Rescaling sliders axes

C#
private void ArrangeSliders()
{
   var nValues = Items.Count - 1;
 
   for (int i = 0; i < nValues; i++)
   {
        Items[i].Maximum = Maximum + ThumbValue * (nValues - i);
        Items[i].Minimum = Minimum - ThumbValue * i;
   }
 
   Items.Last().Minimum = Minimum - ThumbValue * nValues;
}
 
private double ThumbValue
{
   get { return ActualWidth > 0? m_thumbWidth * (Maximum - Minimum)/ActualWidth : 0; }
}

Usage

My solution contains two classes:

WitMultiRangeSliderItem
and WitMultiRangeSlider. Frist class – WitMultiRangeSliderItem, represents common slider and inherits Slider class. Second – WitMultiRangeSlider, is container which manage collection of WitMultiRangeSliderItem.

Usage of these controls is quiet simple and there are two ways to use WitMultiRangeSlider control: bound and unbound.

Bound way

You can bind ItemsSource property of WitMultiRangeSlider to collection of your objects, which represents ranges. Additionally you have to specify bindings to left value of range (LeftValue property of WitMultiRangeSlider), right value of range (RightValue property of WitMultiRangeSlider) in your object and Minimum/Maximum values for ranges:

XML
<InWit:WitMultiRangeSlider ItemsSource="{Binding RangeItems}" 
   SelectedItem="{Binding SelectedRange, Mode=TwoWay}" 
   LeftValueBinding="{Binding From, Mode=TwoWay}" 
   RightValueBinding="{Binding To, Mode=TwoWay}" 
   Minimum="0.0" Maximum="22.0"/>  

You can specify binding for selected item (your object from ItemsSource collection). Also you can set TickFrequency and IsSnapToTickEnabled, these values will be transferred to inner sliders (WitMultiRangeSliderItem).

WitMultiRangeSlider contains MultiRangeSliderBarClicked event. It passed position where user clicked. So you can implement behavior when user clicking on slider bar, automatically adding new slider with click position as from value.

Unbound way

When you bind ItemsSource to collection of your object control automatically creates WitMultiRangeSliderItem elements and sets their bindings. You can manually add WitMultiRangeSliderItem elements to WitMultiRangeSlider control by using Items property:

XML
<InWit:WitMultiRangeSlider Minimum="0.0"Maximum="2200.0">
    <InWit:WitMultiRangeSlider.Items>
        <InWit:WitMultiRangeSliderItem 
           LeftValue="{Binding UnboundRange1.From, Mode=TwoWay}"
           RightValue="{Binding UnboundRange1.To, Mode=TwoWay}" />
        <InWit:WitMultiRangeSliderItem 
           LeftValue="{Binding UnboundRange2.From, Mode=TwoWay}"
           RightValue="{Binding UnboundRange2.To, Mode=TwoWay}"/>
        <InWit:WitMultiRangeSliderItem 
           LeftValue="{Binding UnboundRange3.From, Mode=TwoWay}"
           RightValue="{Binding UnboundRange3.To, Mode=TwoWay}"/>
        <InWit:WitMultiRangeSliderItem LeftValue="{Binding UnboundRange4.From, Mode=TwoWay}"
           RightValue="{Binding UnboundRange4.To, Mode=TwoWay}"/>
    </InWit:WitMultiRangeSlider.Items>
</InWit:WitMultiRangeSlider> 

Or even without any bindings:

XML
<InWit:WitMultiRangeSlider Minimum="0.0" Maximum="2200.0">
    <InWit:WitMultiRangeSlider.Items>
        <InWit:WitMultiRangeSliderItem LeftValue="500" RightValue="700"/>
        <InWit:WitMultiRangeSliderItem LeftValue="700" RightValue="1200"/>
        <InWit:WitMultiRangeSliderItem LeftValue="1200" RightValue="1600"/>
    </InWit:WitMultiRangeSlider.Items>
</InWit:WitMultiRangeSlider>

In this case you have to use ValueChanged event of WitMultiRangeSliderItem (of Slider) to track range changes.

Note, you can only use ItemsSource or Items, and not simultaneously.

Simple example of implementation

The task – to create a control for managing the set of nonintersecting ranges with ability to add new ranges and modify user data.

You have some class to represent range with user data:

C#
public class RangeItem : INotifyPropertyChanged
{
   public event PropertyChangedEventHandler PropertyChanged = delegate { };
   
   private int m_from;
   private int m_to;
   private string m_name; 
 
   public int From
   {
        get { return m_from; }
        set
        {
            m_from = value;
            this.FirePropertyChanged();
        }
   }

   public int To
   {
        get { return m_to; }
        set
        {
            m_to = value;
            this.FirePropertyChanged();
        }
   }
 
   public string Name
   {
        get { return m_name; }
        set
        {
            m_name = value;
            this.FirePropertyChanged();
        }
    } 
} 

This class contains following fields:

  1. From – left bound of range
  2. To – right bound of range
  3. Name – user data

You should create view model for set of ranges with command to add new range:

C#
public class RangesViewModel : INotifyPropertyChanged
{
 
   public event PropertyChangedEventHandler PropertyChanged = delegate { };
 
   private readonly ObservableContentCollection<RangeItem> m_rangeItems;
   private RangeItem m_selectedRange;
 
   private readonly Command m_insertRangeCmd;
 
   public RangesViewModel()
   {
        m_rangeItems = new ObservableContentCollection<RangeItem>
                            {
                                new RangeItem {From = 0, To = 13, Name = "BoundRange0"},
                                new RangeItem {From = 13, To = 17, Name = "BoundRange1"},
                            };
 
        m_insertRangeCmd = new DelegateCommand(x => InsertRange((int)(double)x));
   } 
 
   private void InsertRange(int level)
   {
        if (level > m_rangeItems.Last().To)
            InsertRightRange(level);
        else if (level < m_rangeItems.First().From)
            InsertLeftRange(level);
        else
        {
            var previousRange = m_rangeItems.First(x => x.To >= level);
 
            var newRange = new RangeItem
                               {
                                   From = level, 
                                   To = previousRange.To, 
                                   Name = string.Format("BoundRange{0}", m_rangeItems.Count)
                               };
 
           m_rangeItems.Insert(m_rangeItems.IndexOf(previousRange) + 1, newRange);
 
            previousRange.To = level;
        }
 
   }
 
   private void InsertRightRange(int level)
   {
        var rightRange = new RangeItem
                             {
                                 From = m_rangeItems.Last().To, 
                                 To = level, 
                                 Name = string.Format("BoundRange{0}", m_rangeItems.Count)
                             };
 
        m_rangeItems.Add(rightRange);
   }
 
   private void InsertLeftRange(int level)
   {
        var leftRange = new RangeItem
                            {
                                From = level, 
                                To = m_rangeItems.First().From, 
                                Name = string.Format("BoundRange{0}", m_rangeItems.Count)
                            };
 
        m_rangeItems.Insert(0, leftRange);
   }
 
    
   public ObservableContentCollection<RangeItem> RangeItems
   {
        get { return m_rangeItems; }
   }
 
   public RangeItem SelectedRange
   {
        get { return m_selectedRange; }
        set
        {
            m_selectedRange = value;
            this.FirePropertyChanged();
        }
   }
 
    
   public Command InsertRangeCmd
   {
        get { return m_insertRangeCmd; }
   }
} 

As

MultiRangeSliderBarClicked
event of WitMultiRangeSlider pass a slider value where user clicked as parameter, you should create new range with passed parameter as From value of new range and next slider From value as To value of new range. So you should split existing range at the clicked point.

Now you can bind RangeItems from RangesViewModel to ItemsSource of

WitMultiRangeSlider
to manage
ranges and <span style="font-family: Calibri, sans-serif; font-weight: normal;">InsertRangeCmd</span><span style="font-family: Calibri, sans-serif;"> </span>to MultiRangeSliderBarClicked event of WitMultiRangeSlider to handle double click on WitMultiRangeSlider. Also you can bind RangeItems from RangesViewModel to
ItemsSource
of common DataGrid to modify user data (Name property):

XML
 <InWit:WitMultiRangeSlider ItemsSource="{Binding RangeItems}" 
        SelectedItem="{Binding SelectedRange, Mode=TwoWay}" 
        LeftValueBinding="{Binding From, Mode=TwoWay}" 
    	RightValueBinding="{Binding To, Mode=TwoWay}" 
    	Minimum="0.0" Maximum="22.0">
   <i:Interaction.Triggers>
        <i:EventTrigger EventName="MultiRangeSliderBarClicked">
            <U:InvokeCommandActionWithParam Command="{Binding InsertRangeCmd}" 
             	CommandParameter="{Binding RelativeSource={RelativeSource Self}, 
                	Path=InvokeParameter, Converter={StaticResource EventArgsToDouble}}"/>
        </i:EventTrigger>
   </i:Interaction.Triggers>
</InWit:WitMultiRangeSlider>
<DataGrid ItemsSource="{Binding RangeItems}" 
	SelectedItem="{Binding SelectedRange, Mode=TwoWay}" 
	CanUserAddRows="False" CanUserDeleteRows="False" 
	CanUserReorderColumns="False" 
	AutoGenerateColumns="False">
   <DataGrid.Columns>
        <DataGridTextColumn Width="*" Header="Name" Binding="{Binding Name, Mode=TwoWay}" 
		SortMemberPath="Name"/>
        <DataGridTextColumn Width="Auto" MinWidth="40" Header="From" Binding="{Binding From}" 
		IsReadOnly="True" SortMemberPath="From"/>
        <DataGridTextColumn Width="Auto" MinWidth="40" Header="To" Binding="{Binding To}" 
		IsReadOnly="True" SortMemberPath="To"/>
   </DataGrid.Columns>
</DataGrid>

In attached example you will see both ways of using WitMultiRangeSlider with interaction:

Image 4

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Technical Lead
Russian Federation Russian Federation
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionThanks! Pin
Member 1447994415-Mar-21 3:50
Member 1447994415-Mar-21 3:50 
QuestionLogarithmic scale? Pin
devilliantest7-Jan-18 2:34
devilliantest7-Jan-18 2:34 
QuestionHow modify template of thumb Pin
peppemath30-Jun-17 2:17
peppemath30-Jun-17 2:17 
QuestionAdding Ticks to the multi range slider Pin
Swaminathan_K10-Sep-15 1:13
Swaminathan_K10-Sep-15 1:13 
QuestionHow to remove a range Pin
JOTRIC13-Jan-14 3:43
JOTRIC13-Jan-14 3:43 
QuestionПодскажите пожалуйста решение. Pin
Member 964074317-Dec-13 23:01
Member 964074317-Dec-13 23:01 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.