DiscreteSlider - Adding Functionality with a Simple Control Subclass

How do you prevent the Slider from returning "ugly" values, and get it to "snap" to only the values that you want, such as integers, or multiples of a certain step value such as 0.125? One way to do this is to override OnValueChanged, but it helps to understand how that mechanism works. Below is an example of subclassing Slider to add that functionality. It overrides OnValueChanged to alter the behavior of Slider to return only multiples of the SmallChange value, and the Thumb will snap to those values.

When the Value property changes, whether by the user dragging the Thumb or programmatically, the OnValueChanged virtual method is called. The Slider's implementation of OnValueChanged will raise the ValueChanged event, which is how most apps respond to the user moving the Slider's Thumb. The OnValueChanged method has the old and new values in its parameters. To get the default Slider behavior, if you override OnValueChanged you pass those two parameters back to the base implementation of OnValueChanged. But what if you don't call base.OnValueChanged, and what if you change the values that you pass back?

For starters, if you don't call the base implementation, the ValueChanged event will not get fired. The Value property will change, but there will no event raised. It turns out that passing different values in base.OnValueChanged is only partially useful--if you pass different values, those are the values are used to make the RoutedPropertyChangedEventArgs, but it has no effect on the Value property. So if you want to modify the Value property, you'll have to set it yourself, but doing so inside of the ValueChanged override will cause reentrancy

By overriding the OnValueChanged, changing the values that you pass back to base.OnValueChanged, and changing the Value property (provided you take care of reentrancy), you can determine when the ValueChanged event is raised, and the values that the Value property will take.

Let's take a look at some of the code. This snippet does the work of converting the new value to a multiple of SmallChange:

double newDiscreteValue = (int)(Math.Round(newValue / SmallChange)) * SmallChange;

After we've done that, we check to see if the new discrete value is different from our old one. In other words, the Slider may think that the Value has changed from 4.0 to 4.2, but if our SmallChange value is .5, then we want to stay at 4.0. We set the new Value (this is what causes the reentrancy), call base.OnValueChanged, then save the discrete value for next time:

if (newDiscreteValue != m_discreteValue)

{

    Value = newDiscreteValue;

    base.OnValueChanged(m_discreteValue, newDiscreteValue);

    m_discreteValue = newDiscreteValue;

}

Notice that if the discrete value did not change, we do not call base, so the event is not fired, and the Thumb does not move.

When we set the Value property, the OnValueMethod will get called again while we're still in it, so we mitigate against this by using the m_busy flag.

Here's the code:

using System;
using System.Windows;
using System.Windows.Controls;

namespace TransitionApp

{

    public class DiscreteSlider : Slider

    {

        protected override void OnValueChanged(double oldValue, double newValue)

        {

            if (!m_busy)

            {

                m_busy = true;

                if (SmallChange != 0)

                {

                    double newDiscreteValue = (int)(Math.Round(newValue / SmallChange)) * SmallChange;

                    if (newDiscreteValue != m_discreteValue)

                    {

                        Value = newDiscreteValue;

                        base.OnValueChanged(m_discreteValue, newDiscreteValue);

                        m_discreteValue = newDiscreteValue;

                    }

          }

                else

                {

                    base.OnValueChanged(oldValue, newValue);

                }

                m_busy = false;

            }

        }

        bool m_busy;

        double m_discreteValue;

    }

}