Silverlight Slider that snaps to rounded value when dragged

The other day I had a problem where a slider had five distinct values and the user would drag the slider to their selection. An example of this would be a question like “From 1 to 5, please select how much you like puppies”. Of course, everyone would answer 5 to that question but that’s not the point… When a user clicked the slider (as opposed to dragging the thumb), the slider change would work fine, and the slider would go to the right place. The problem was that when the dragged the slider to change the value they could drag it to an intermediary value. Small change was 1 and large change was 1, but the could select 1.1393938… which would leave the slider sitting in a position between 1 and 2 somewhere.

The answer was to jump in to Silverlight Spy and have a bit of a hunt around the Slider code. I pulled out some of the code and created a new class inherited from Slider, and now when I drag it snaps to the right positions!

The code here is not complete, it doesn’t properly support IsDirectionReversed or Vertical sliders, but you could quite easily fix it for your own purposes.

Basically, just create a new control and inherit from Slider, then override the method as below.

Keep in mind, this to me is a fairly hacky approach πŸ™‚

public class MySlider : Slider
    {
        protected override void OnValueChanged(double oldValue, double newValue)
        {
            int val = Convert.ToInt32(Math.Round(newValue));        

            Thumb ElementHorizontalThumb = GetTemplateChild("HorizontalThumb") as Thumb;

            double maximum = base.Maximum;
            double minimum = base.Minimum;

            double num3 = val;
            double num4 = 1.0 - ((maximum - num3) / (maximum - minimum));

            RepeatButton ElementHorizontalLargeDecrease = GetTemplateChild("HorizontalTrackLargeChangeDecreaseRepeatButton") as RepeatButton;
            RepeatButton ElementHorizontalLargeIncrease = GetTemplateChild("HorizontalTrackLargeChangeIncreaseRepeatButton") as RepeatButton;

            Grid grid = GetTemplateChild("HorizontalTemplate") as Grid;

            if (grid != null)
            {
                
                if ((grid.ColumnDefinitions != null) && (grid.ColumnDefinitions.Count == 3))
                {
                    grid.ColumnDefinitions[0].Width = new GridLength(1.0, GridUnitType.Auto);
                    grid.ColumnDefinitions[2].Width = new GridLength(1.0, GridUnitType.Star);

                    if (ElementHorizontalLargeDecrease != null)
                    {
                        ElementHorizontalLargeDecrease.SetValue(Grid.ColumnProperty, 0);
                    }
                    if (ElementHorizontalLargeIncrease != null)
                    {
                        ElementHorizontalLargeIncrease.SetValue(Grid.ColumnProperty, 2);
                    }
                }
                if ((ElementHorizontalLargeDecrease != null) && (ElementHorizontalThumb != null))
                {
                    ElementHorizontalLargeDecrease.Width = Math.Max(0.0, num4 * (base.ActualWidth - ElementHorizontalThumb.ActualWidth));
                }
            }
        }

    }

The main crux of this is that I first round the value, then pass it in as “num3” which is then used to calculate the real position of the thumb slider.

As I said, a bit hacky, but it works nice!

I checked the Slider for a property that might turn it in to this mode, but couldn’t see anything… I think it would be a nice addition to the in built slider control.

13 thoughts on “Silverlight Slider that snaps to rounded value when dragged

  1. I just did this in the ValueChanged handler:
    ////
    void Slider3_ValueChanged(object sender, RoutedPropertyChangedEventArgs e)
    {
    Slider3.Value = Math.Round(e.NewValue);
    }
    ////

    Seems to work fine πŸ™‚

  2. Hey Martin,

    Thanks for your input πŸ™‚

    Yes your method works, but doesn’t take a couple if things in to account:

    Firstly by setting Slide.Value manually you’ll break any data binding on the Value property -> my designs are usually MVVM, so this isnt a great idea.

    Second – if you put a Debug.WriteLine in your value changed handler, you’ll see a non rounded change, then a rounded change – i.e. this method is calling Value twice. (0.951156812339332 then 1)

    Third – your method assumes you have access to code behind, which is usually only the case in UserControls, not so much in data templates or CustomControls πŸ™‚

    I normally really hate inhertiting off controls to do something so simple, but unfortunately I was stuck with the restrictions I listed here.

    Once again thanks for sharing your thoughts!

    • Thanks for this article. I ultimately went with one of your commenter’s suggestions.

      A few questions for you. How does limiting the value from the view break mvvm? Maybe you have two views with one allowing for more granular control than the other. I don’t think it breaks mvvm to limit view-settable values. Your 2nd point, I agree with, but it’s not a show stopper if you value maintaining some level of simplicity. On your third point, can’t you register an event handler on a control defined within a template?

  3. Hi,

    First of all: nice work, this override works like a charm.
    Although i have an issue, because of the override the ValueChanged event is not called anymore. How can I fix this?

    Thanks in advance!

    Thomas

  4. Well guys it seems to be good solution provided by Martin.

    I guess for this given problem I tried something and it works perfect for me and i guess it fits perfect in Jordan’s case as well i.e. “Not Breaking any Data Binding”

    Well steps are

    1. Set “TickFrequency” property = 1
    2. Set “isSnapToTickEnabled” property = true (Select check box in Blend)

    I hope it must work perfect for you case too πŸ™‚

    Cheers!
    Nishant

  5. public class RounderValueSlider:Slider
    {

    protected override void OnValueChanged(double oldValue, double newValue)
    {
    base.OnValueChanged(oldValue, newValue);
    int val = Convert.ToInt32(Math.Round(newValue));
    if (val != newValue) Value = val;
    }
    }
    work fine!
    and won’t break data binding

  6. This is getting a bit more subjective, but I much prefer the Zune Marketplace. The interface is colorful, has more flair, and some cool features like ‘Mixview’ that let you quickly see related albums, songs, or other users related to what you’re listening to. Clicking on one of those will center on that item, and another set of “neighbors” will come into view, allowing you to navigate around exploring by similar artists, songs, or users. Speaking of users, the Zune “Social” is also great fun, letting you find others with shared tastes and becoming friends with them. You then can listen to a playlist created based on an amalgamation of what all your friends are listening to, which is also enjoyable. Those concerned with privacy will be relieved to know you can prevent the public from seeing your personal listening habits if you so choose.

  7. older post but this code may be help someone;
    ValueChanged event filtered and only fires when integer changes. And by setting SmallChange=1 keyboard left right arrows are working success. But, as a result of setting Value manually, setter Bindings are not working like Jordan says..

    public class IntSlider : Slider
    {
    public IntSlider()
    {
    SmallChange = 1;
    }

    private double m_LastValue;

    protected override void OnValueChanged(double oldValue, double newValue)
    {
    double newIntValue = Math.Round(newValue);

    if (newIntValue == newValue)
    {
    if (newIntValue != m_LastValue)
    {
    base.OnValueChanged(oldValue, newValue);
    }
    }
    else
    {
    Value = newIntValue;
    }

    m_LastValue = newIntValue;
    }

    }

Comments are closed.