Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Slider.Value changes on adjusting Minimum when it shouldn't

Tags:

c#

wpf

slider

In short:
Under certain circumstances setting Slider.Minimum will adjust Slider.Value although the current Value is bigger then the new Minimum.

Code: (should be reproducible)
MainWindow.xaml:

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <DockPanel>
        <Slider Name="MySlider" DockPanel.Dock="Top" AutoToolTipPlacement="BottomRight" />
        <Button Name="MyButton1" DockPanel.Dock="Top" Content="shrink borders"/>
        <Button Name="MyButton2" DockPanel.Dock="Top" VerticalAlignment="Top" Content="grow borders"/>
    </DockPanel>
</Window>

MainWindow.xaml.cs:

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

namespace WpfApplication1
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            MySlider.ValueChanged += (sender, e) =>
            {
                System.Diagnostics.Debug.WriteLine("Value changed: " + e.NewValue);
            };

            System.ComponentModel.DependencyPropertyDescriptor.FromProperty(Slider.MinimumProperty, typeof(Slider)).AddValueChanged(MySlider, delegate
            {
                System.Diagnostics.Debug.WriteLine("Minimum changed: " + MySlider.Minimum);
            });

            System.ComponentModel.DependencyPropertyDescriptor.FromProperty(Slider.MaximumProperty, typeof(Slider)).AddValueChanged(MySlider, delegate
            {
                System.Diagnostics.Debug.WriteLine("Maximum changed: " + MySlider.Maximum);
            });

            MySlider.Value = 1;

            MySlider.Minimum = 0.5;
            MySlider.Maximum = 20;

            MyButton1.Click += (sender, e) =>
            {
                MySlider.Minimum = 1.6;
                MySlider.Maximum = 8;
            };

            MyButton2.Click += (sender, e) =>
            {
                MySlider.Minimum = 0.5;
                MySlider.Maximum = 20;
            };
        }
    }
}

Steps to reproduce:
1. Run in Debug mode.
2. Move Slider to the far right.
3. Press "shrink borders" Button.
4. Press "grow borders" Button.

Expected Output:

Value changed: 1
Minimum changed: 0,5
Maximum changed: 20
//Multiple times "Value changed"
Value changed: 20
Minimum changed: 1,6
Value changed: 8
Maximum changed: 8
Minimum changed: 0,5
Maximum changed: 20

Slider stays at 8.

Actual Output:

Value changed: 1
Minimum changed: 0,5
Maximum changed: 20
//Multiple times "Value changed"
Value changed: 20
Minimum changed: 1,6
Value changed: 8
Maximum changed: 8
Value changed: 1
Minimum changed: 0,5
Maximum changed: 20

(Note the additional "Value changed: 1")
Slider jumps to 1.

Sidenote:
Binding Slider.Value OneWayToSource (or TwoWay) to a double property, fixes the issue.

Question:
Why is this happening?

Theory:
I'm pretty sure it has something to do with Value Coercion. It seems like the following happens:
1. Setting Value to 1 programmatical sets Value's "base value". It is between the default values for Minimum and Maximum, so the "effective value" is exactly the same.
2. Setting Minimum and Maximum does not change anything, because Value is still in between them.
3. Manually pulling the Slider to the right apparently changes the "effective value" but for some weird reason not the "base value".
4. Increasing the borders again, calls the coercion callback, which realizes, that the (wrong) "base value" is between the new Minimum and Maximum and changes the "effective value" back to it.

Assuming this is indeed what is happening, leads us to the following question:
Why is manually pulling the Slider (3.) not affecting the "base value"?

like image 993
Tim Pohlmann Avatar asked Aug 17 '15 14:08

Tim Pohlmann


1 Answers

I'm not sure what the use case is, but this issue could probably be avoided if you were using MVVM and everything was bound. If that's what you're doing and this is just another way you're reproducing the issue, good find.

Take a look at the Slider source code. I couldn't pinpoint the issue, but I have a better understanding that there is an internal value being used.

Add 2 buttons, and only change Minimum or Maximum in the event handlers for all buttons:

MyButton1.Click += (sender, e) =>
{
    MySlider.Minimum = 1.6;
};

MyButton2.Click += (sender, e) =>
{
    MySlider.Minimum = 0.5;
};

MyButton3.Click += (sender, e) =>
{
    MySlider.Maximum = 20;
};

MyButton4.Click += (sender, e) =>
{
    MySlider.Maximum = 8;
};

At startup, click MyButton1 and MyButton2:

Value changed: 1.6
Minimum changed: 1.6
Value changed: 1
Minimum changed: 0.5
Value changed: 1.6
Minimum changed: 1.6
Value changed: 1
Minimum changed: 0.5

You can see, internally, it stores the original start value and restores it when the range is capable of showing it, it sets it back to what it was. If you change only the maximum's at startup (without moving slider bar), the Value property isn't changed because the Value is in the current range:

Maximum changed: 8
Maximum changed: 20
Maximum changed: 8
Maximum changed: 20

However, when you change the slider bar maximum to 20, then change the Maximum = 8 and Minimum = 1.6, the Minimum is now out of range from (internal) Value (1) and uses the Maximum value for it's Value. When you grow it again, you set Minimum = 0.5 and Maximum = 20, and since the internal value = 1 and is between 0.5 and 20, it sets the Value back to 1.

I found one workaround, and that is resetting the internal value every time you change the range. To reset, you just set the Slider.Value again. If you do this after you change the Maximum and Minimum it will persist the value in the event the old value isn't in the range of the new range. So taking your initial implementation:

MyButton1.Click += (sender, e) =>
{
    MySlider.Minimum = 1.6;
    MySlider.Maximum = 8;
    MySlider.Value = MySlider.Value;
};

MyButton2.Click += (sender, e) =>
{
    MySlider.Minimum = 0.5;
    MySlider.Maximum = 20;
    MySlider.Value = MySlider.Value;
};

Output (matches your expected):

Value changed: 1
Minimum changed: 0.5
Maximum changed: 20
//Multiple times "Value changed"
Value changed: 20
Minimum changed: 1.6
Value changed: 8
Maximum changed: 8
Minimum changed: 0.5
Maximum changed: 20

Edit

The link you posted about value coercion is very helpful. I'm not sure if your question would be considered a second question, but I believe I found the answer.

When you use the Slider (technically the Thumb with the Track), realize that the Thumb sliding around is bound to the Slider.Valuethrough Slider.UpdateValue gets called. This issue now is that SetCurrentValueInternal is not open source :( What we can conclude is that the mystery function isn't coercing ValueProperty, instead it's only setting the base ("desired") value. Try:

private void MyButton1_Click(object sender, RoutedEventArgs e)
{
    MySlider.Minimum = 1.6;
    MySlider.Maximum = 8;
    MySlider.CoerceValue(Slider.ValueProperty); //Set breakpoint, watch MySlider.Value before and after breakpoint.
}

Doing the above you will see that even though Value will go from 20 down to 8, the second you actually Coerce ValueProperty when it drops down to 8, the value will then change to 1.6. In fact, that's what happens when you grow the range. After you set Max or Min you will see the Value change since it is coercing value property again. Try flipping Max and Min:

private void MyButton2_Click(object sender, RoutedEventArgs e)
{
    MySlider.Maximum = 20; //Will change Value to 1.6 after executing
    MySlider.Minimum = 0.5; //Will change Value to 1 after executing
}

Still, this makes you think, how can it remember to go back to 1 when possible, is there an effective, base, and initial values? I'm not sure.

Matter of fact try this.

  1. Start application
  2. Move thumb all the way to right (Value = 20)
  3. Shrink borders (Value = 8)
  4. Take thumb and move it to the left, then all the way to the right (Value = 8)
  5. Then grow borders

After step 5 you will observe that Value still equals 8. This is how I found out that its something to do with that mystery function.

Note: My brain is dead again, so if this isn't clear, I apologize, I'll have to come back and edit it.

Edit 2

For one, both SetCurrentValueInteral and SetValue both call SetValueCommon. The difference is SetCurrentValueInternal re-evaluates the current value with the base value since the coerce flag is set to true. (keeps the value in-bounds of Min/Max), ref.

Through my digging, I found that SetCurrentValue and SetValue have two completely different results, and that is: specifying the coercion (IsInternal seems to be unused in the case where the bound property is of value type).

My proof is documentation that states:

"The SetCurrentValue method is another way to set a property, but it is not in the order of precedence. Instead, SetCurrentValue enables you to change the value of a property without overwriting the source of a previous value. You can use SetCurrentValue any time that you want to set a value without giving that value the precedence of a local value..."

With that being said, moving the Thumb of the Slider doesn't effect the base value because it is calling SetCurrentValueInternal.

Moreover, changing the Value manually changes the base value because it calls SetValue. The ValueProperty dependency property defines how to coerce using the CoerceValueCallback as stated in the last paragraph here. In more detail of what's going on in Slider.Value,

Coercion interacts with the base value in such a way that the constraints on coercion are applied as those constraints exist at the time, but the base value is still retained. Therefore, if constraints in coercion are later lifted, the coercion will return the closest value possible to that base value, and potentially the coercion influence on a property will cease as soon as all constraints are lifted.

Of course I read further, Dependency Property Callbacks and Validation - Advanced Coercion and Callback Scenarios and found this:

For instance, in the Min/Max/Current scenario, you could choose to have Minimum and Maximum be user settable. If so, you might need to coerce that Maximum is always greater than Minimum and vice versa. But if that coercion is active, and Maximum coerces to Minimum, it leaves Current in an unsettable state, because it is dependent on both and is constrained to the range between the values, which is zero. Then, if Maximum or Minimum are adjusted, Current will seem to "follow" one of the values, because the desired value of Current is still stored and is attempting to reach the desired value as the constraints are loosened.

In conclusion, I think this is as-designed in Slider because it is coded in such a way that Value must always be in between Minimum and Maximum. The Value will follow the Maximum property, and when constraints are lifted (the current value is in range of the base value) the value property will return to its original value.

Lastly, IMO WPF designed the slider this way because WPF goes hand in hand with data binding. I assume they designed under the assumption that developers would take full advantage of data-binding and would implement the logic to prevent invalid values from being used.

like image 99
Kcvin Avatar answered Oct 20 '22 09:10

Kcvin