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"?
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
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.Value
through 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.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With