在更改最小值和最大值后,滑块数值没有更新

4
我正在回答这个问题,但在回答过程中发现了很多奇怪的行为。由于我支持MVVM,我组合了一个解决方案,以查看是否会出现相同的行为。我的解决方案揭示了一个问题,即使我将TwoWay绑定到Slider.Value,在Slider.MaximumSlider.Minimum更改后,它不会在我的ViewModel中更新;也就是说,我的ViewModel的Value可能在UpperLimitLowerLimit之外,同时Slider.Value(我的VM的Value属性绑定到)在范围内。

在上述问题中,更改Slider.MaximumSlider.Minimum似乎总是保持Slider.Value在范围内,并有时“恢复”Slider.Value到先前设置的值。

Microsoft的Slider源代码

  1. 为什么Slider.Value会像链接的问题中看到的那样更改/恢复其值,即使当前值在Min/Max范围内?
  2. 为什么更改UpperLimitLowerLimit后,我的绑定到Slider.Value的视图模型的Value属性不匹配TwoWay绑定?
    • 请注意,最大值和最小值的绑定有效

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);
    }
}

项目/项目视图模型:

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;
    }
}

重现步骤:

  1. 点击 MyButton3

    • 项目值 = 1

    • 滑块值 = 1

  2. 将滑块/拇指移至最右侧

  3. 点击 MyButton3

    • 项目值 = 20

    • 滑块值 = 20

  4. 点击 MyButton1

  5. 点击 MyButton3

    • 项目值 = 20

    • 滑块值 = 8

如果在 MyButton3_Click 中设置断点并执行最后一步,您会发现 MySlider.Value = 8

1个回答

2
这是由于值强制转换,你可以在这里阅读更多相关信息。
一般来说,WPF控件被设计成与宽松的数据绑定一起使用。它们的get/set访问器和事件等是为了帮助过渡从Winforms过来的,但它们添加了一个额外的逻辑层,不总是过滤到你的绑定属性。这是混合“好”的WPF代码(数据绑定)和“坏”的(直接访问控件)时可能出现问题的众多例子之一。
编辑:
当需要确定当前值时,依赖属性的强制转换回调处理程序将被调用。将其视为修改结果的最后机会;它不会改变绑定本身,只会改变返回值的值。如果您在视图模型中有一个整数属性(例如包含值10),并像这样将文本框绑定到它:
    <TextBlock Text="{Binding MyValue}" />

那个值显然会显示为10。现在假设你创建了一个名为“MyProperty”的整数依赖属性的用户控件,并且假设强制回调将当前值乘以2:
    <local:MyControl x:Name="myControl" MyProperty="{Binding MyValue}" />

这将完全没有任何作用。我们将MyProperty绑定到MyValue属性,但它只是一个DP。我们从未实际调用它。现在假设我们添加了第二个文本框,但这次绑定到MyControl.MyProperty:
    <TextBlock Text="{Binding Path=MyProperty, ElementName=myControl}" />

第一个控件将继续显示10(这是我们视图模型中的值),但第二个控件将显示20,因为对于MyProperty DP的强制调用修改了它从自己的绑定到MyValue获取的值。(有趣的是,双向绑定也可以工作,当值改变时,强制回调会导致值加倍)。
所有这一切的重要线索是,只有在其他依赖项更新自身或代码手动调用getter时,才会调用强制回调来解析值。显然,您调用Slider上的Value getter会导致发生这种情况,但仅更改Minimum和Maximum的值不会。这就像在视图模型中更改属性的值而不调用属性更改通知...你知道你做了什么,但没有别的东西知道。
进一步阅读:RangeBase源代码(特别是ConstrainToRange强制转换回调函数)和Slider源代码(即仅在拖动滑块或缩略图时调用的UpdateValue函数)。

我已经基本上将其分解了,但我无法确定故障在哪里。你是否清楚行为如何改变代码中的值 - behind和绑定之间的区别; 对我来说并不明显。我还质疑强制转换是向上冒泡还是向下隧道。当我试图弄清楚SelectionStart/End在Value和Max/Min之前是否被强制执行时,我的思维过程变得混乱了。我需要阅读关于强制转换的资料。如果您对如何找出逻辑或发生了什么有更多见解,我全神贯注地听着。谢谢! - Kcvin
@MarkFeldman 我已经查看了WPF源代码,并且设置最小值明显调用了最大值和值的强制回调。我是否误解了您的帖子? - Tim Pohlmann
@MarkFeldman P.S. 欢迎来到2015年。http://referencesource.microsoft.com/#PresentationFramework/src/Framework/System/Windows/Controls/Primitives/RangeBase.cs 可能比你回答中提供的链接更可靠。我现在将审查你的回答和Tim的问题。 - Kcvin
@MarkFeldman 嗯,奇怪的是,对我来说它确实做到了: https://dev59.com/AI7da4cB1Zd3GeqP9ixu Minimum或Maximum的setter确实可以改变Value属性的有效值。 - Tim Pohlmann
@MarkFeldman 我在我的回答这里中对此进行了更深入的研究,如果你想进一步了解的话。不过你是正确的,这归结于强制转换,并且听起来像是按设计来的。 - Kcvin
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接