Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

WPF MultiRangeSlider Control

0.00/5 (No votes)
6 Aug 2013 1  
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:

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.

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.

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

But the value of slider is center of slider thumb:

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.

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).

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

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:

<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:

<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:

<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:

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:

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 InsertRangeCmd 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):

 <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:

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here