简体   繁体   中英

Slider Value does not update after changing Minimum and Maximum

I was answering this question, and while doing so I found a lot of weird behavior. Since I'm pro-MVVM, I put together a solution to see if I would see the same behavior. What my solution uncovers is that even though I'm binding TwoWay to Slider.Value , it is not being updated in my ViewModel after Slider.Maximum and Slider.Minimum change; ie my view model's Value can be outside UpperLimit and LowerLimit , meanwhile Slider.Value (which my VM's Value property is bound to) is inside the range.

In the aforementioned question, changing Slider.Maximum or Slider.Minimum seems to always keep the Slider.Value in range, and sometimes "restores" Slider.Value to a previous value it used to be set to.

Microsoft's Slider Source Code

  1. Why does Slider.Value change/restore its value as seen in linked question even though the current value is within the Min/Max range?
  2. Why doesn't my view model's Value property, bound to Slider.Value match with a TwoWay binding after changing the UpperLimit and LowerLimit ?
    • Note that the bindings to Maximum & Minimum do work

MainWindow.xaml:

<DockPanel>
    <Slider Name="MySlider" DockPanel.Dock="Top" AutoToolTipPlacement="BottomRight" Value="{Binding Value, Mode=TwoWay}" Maximum="{Binding UpperLimit}" Minimum="{Binding LowerLimit}"/>
    <Button Name="MyButton1" Click="MyButton1_Click" DockPanel.Dock="Top" Content="shrink borders"/>
    <Button Name="MyButton2" Click="MyButton2_Click" DockPanel.Dock="Top" VerticalAlignment="Top" Content="grow borders"/>
    <Button Name="MyButton3" Click="MyButton3_Click" DockPanel.Dock="Top" VerticalAlignment="Top" Content="Print ItemVM Value"/>
</DockPanel>

MainWindow.xaml.cs:

public partial class MainWindow : Window
{
    private readonly ItemViewModel item;
    public MainWindow()
    {
        InitializeComponent();
        DataContext = item = new ItemViewModel(new Item(1, 20, 0.5));
    }

    private void MyButton1_Click(object sender, RoutedEventArgs e)
    {
        //MySlider.Minimum = 1.6;
        //MySlider.Maximum = 8;
        item.LowerLimit = 1.6;
        item.UpperLimit = 8;

    }

    private void MyButton2_Click(object sender, RoutedEventArgs e)
    {
        //MySlider.Minimum = 0.5;
        //MySlider.Maximum = 20;
        item.LowerLimit = 0.5;
        item.UpperLimit = 20;
    }

    private void MyButton3_Click(object sender, RoutedEventArgs e)
    {
        System.Diagnostics.Debug.WriteLine("Item Value: " + item.Value);
        System.Diagnostics.Debug.WriteLine("Slider Value: " + MySlider.Value);
    }
}

Item/ItemViewModel:

public class ItemViewModel : INotifyPropertyChanged
{
    private readonly Item _item;

    public event PropertyChangedEventHandler PropertyChanged;

    public ItemViewModel(Item item)
    {
        _item = item;
    }

    public double UpperLimit
    {
        get
        {
            return _item.UpperLimit;
        }
        set
        {
            _item.UpperLimit = value;
            NotifyPropertyChanged();
        }
    }
    public double LowerLimit
    {
        get
        {
            return _item.LowerLimit;
        }
        set
        {
            _item.LowerLimit = value;
            NotifyPropertyChanged();
        }
    }

    public double Value
    {
        get
        {
            return _item.Value;
        }
        set
        {
            _item.Value = value;
            NotifyPropertyChanged();
        }
    }

    private void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}
public class Item
{
    private double _value;
    private double _upperLimit;
    private double _lowerLimit;
    public double Value
    {
        get
        {
            return _value;
        }
        set
        {
            _value = value;
        }
    }
    public double UpperLimit
    {
        get
        {
            return _upperLimit;
        }
        set
        {
            _upperLimit = value;
        }
    }
    public double LowerLimit
    {
        get
        {
            return _lowerLimit;
        }
        set
        {
            _lowerLimit = value;
        }
    }

    public Item(double value, double upperLimit, double lowerLimit)
    {
        _value = value;
        _upperLimit = upperLimit;
        _lowerLimit = lowerLimit;
    }
}

Steps to reproduce:

  1. Click MyButton3

    • Item Value = 1

    • Slider Value = 1

  2. Move Slider/Thumb all the way to right

  3. Click MyButton3

    • Item Value = 20

    • Slider Value = 20

  4. Click MyButton1

  5. Click MyButton3

    • Item Value = 20

    • Slider Value = 8

If you put a break point in MyButton3_Click and execute the last step, you can see that MySlider.Value = 8

This is due to value coercion, you can read more about it here .

Generally speaking, WPF controls are designed to be used with loose data binding. Their get/set accessors and events etc were added to assist in the transition from Winforms but they add an additional layer of logic that doesn't always filter through to your bound properties. This is one of the many examples of problems that can arise when you mix "good" WPF code (data binding) with "bad" (accessing controls directly).

EDIT:

The Coercion callback handler for a dependency property is invoked whenever the current value needs to be determined. Think of it as the very last chance to modify the result; it doesn't change the binding itself, only the value of what's being returned. If you have an integer property in a view model (say) containing the value of 10 and you bind a textbox to it like this:

    <TextBlock Text="{Binding MyValue}" />

That value will obviously display as 10. Now let's say you create a user control with an integer dependency property called "MyProperty", and let's say that the coercion callback multiplies whatever the current value is by 2:

    <local:MyControl x:Name="myControl" MyProperty="{Binding MyValue}" />

This will do nothing whatsoever. We're binding MyProperty to the MyValue property, but it's just a DP. We're never actually invoking it. Now let's say we add a second TextBox but this time bind to MyControl.MyProperty:

    <TextBlock Text="{Binding Path=MyProperty, ElementName=myControl}" />

The first control will continue to display 10 (which is the value still in our view model) but the second will display 20, because the coercion call for the MyProperty DP modified the value that it got from it's own binding to MyValue. (Interestingly doing two-way binding also works, the coercion callback results in the value being doubled whenever the value is changed).

The important clue to all this is that the coercion callback is only called when the value needs to be resolved either by another dependency updating itself or by the code manually calling the getter. Obviously you calling the Value getter on Slider results in this happening, but simply changing the values of Minimum and Maximum does not. It's like changing the value of a property in your view model without invoking property change notification...you know what you've done but nothing else does.

Further reading: the RangeBase source code (specifically the ConstrainToRange coercion callback) and the Slider source code (namely UpdateValue which is only called when the slider or thumbnail are dragged).

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM