可重用控件和 DependencyProperty 的奇怪行为

问题描述 投票:0回答:1

我正在尝试制作一个可重复使用的控件的小型 POC,但我遇到了奇怪的行为,看在上帝的份上,我无法弄清楚为什么。

所以我希望这里的任何人都可以对此有所了解。

我正在尝试制作一个可重复使用的数字输入,带有+/-按钮和可配置的最小/最大值。 如果设置了最小值/最大值并且用户输入了更大或更小的值,我需要控件将绑定属性更新为允许的最小值或最大值。

手动输入值时一切正常,但只要通过代码设置绑定属性(例如,在主视图模型中单击按钮),它就不起作用

这是我在手动填写太大的值时得到的输出:

Setting BindThis to 105
BindThis set to 105
UC OnPropertyChanged: 25 -> 105
UC forcing min/max value: 100)
Setting BindThis to 100
BindThis set to 100
UC OnPropertyChanged: 105 -> 100

这就是当我将 BindThis 设置为单击按钮时的值时得到的结果:

Setting BindThis to 1000,2345678
UC OnPropertyChanged: 100 -> 1000,2345678
UC forcing min/max value: 100)
UC OnPropertyChanged: 1000,2345678 -> 100
BindThis set to 1000,2345678

注意在设置视图模型上的属性时如何调用用户控件的 DependencyProperty 内容,以及在设置视图模型的属性后手动填写值时如何完成所有操作。

请注意,我正在使用 CommunityToolkit.Mvvm 并且该应用程序在 .NET 8 下运行:

<PackageReference Include="CommunityToolkit.Mvvm" Version="8.3.2" />

尽管我怀疑这对我正在经历的事情有任何影响。

给定以下用户控件:

<UserControl x:Class="WpfApp1.UserControl1"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:WpfApp1"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800"
             x:Name="MyControl">
    <StackPanel Orientation="Horizontal" DataContext="{Binding ElementName=MyControl}">
        <Button Command="{Binding Decrease}" Content="-" />
        <TextBox Width="300" Text="{Binding Value}" />
        <Button Command="{Binding Increase}" Content="+" />
    </StackPanel>
</UserControl>
namespace WpfApp1
{
    /// <summary>
    /// Interaction logic for UserControl1.xaml
    /// </summary>
    public partial class UserControl1 : UserControl
    {
        public UserControl1()
        {
            _increaseCommand = new RelayCommand(OnIncrease, CanIncrease);
            _decreaseCommand = new RelayCommand(OnDecrease, CanDecrease);

            InitializeComponent(); 
            
        }

        public static readonly DependencyProperty ValueProperty = DependencyProperty.Register(
            nameof(Value), typeof(double?), typeof(UserControl1), 
            new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, ValueProperty_OnPropertyChanged));

        // https://stackoverflow.com/questions/7267840/dependency-property-updating-the-source
        private static void ValueProperty_OnPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            Debug.WriteLine($"UC OnPropertyChanged: {e.OldValue} -> {e.NewValue}");
            UserControl1 instance = (UserControl1)d;
            double? newValue = (double?)e.NewValue;
            double? result = newValue;
            
            if(newValue != null)
            {
                if(newValue < instance.Min)
                {
                    result = instance.Min;
                }
                else if(newValue > instance.Max)
                {
                    result = instance.Max;
                }
            }

            if(newValue != result)
            {
                Debug.WriteLine($"UC forcing min/max value: {result})");
                d.SetCurrentValue(ValueProperty, result);
            }

            instance.Increase.NotifyCanExecuteChanged();
            instance.Decrease.NotifyCanExecuteChanged();
        }

        public double? Value
        {
            get => (double?)GetValue(ValueProperty);
            set => SetValue(ValueProperty, value);
        }

        public double Min { get; set; }
        public double Max { get; set; }

        private RelayCommand _increaseCommand;
        public RelayCommand Increase => _increaseCommand;
        
        private RelayCommand _decreaseCommand;
        public RelayCommand Decrease => _decreaseCommand;

        private bool CanDecrease() => Value - 1 >= Min;
        private void OnDecrease() => Value -= 1;

        private bool CanIncrease() => Value + 1 <= Max;
        private void OnIncrease() => Value += 1;
    }
}

正在这样使用:

<Window x:Class="WpfApp1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp1"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <StackPanel>
        <Label Content="{Binding BindThis}" />
        <local:UserControl1 Value="{Binding BindThis, Mode=TwoWay}" Min="-100" Max="100" />
        <Button Command="{Binding ButtonClick}" Content="Over max" />
        <Button Command="{Binding ButtonMinClick}" Content="Below min" />
    </StackPanel>
</Window>
namespace WpfApp1
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            DataContext = new MainWindowViewModel();
        }
    }

    public class MainWindowViewModel : ObservableObject
    {
        private double? _value;
        public double? BindThis
        {
            get => _value;
            set
            {
                Debug.WriteLine($"Setting BindThis to {value}");
                SetProperty(ref _value, value);
                Debug.WriteLine($"BindThis set to {_value}");
            }
        }
        private void OnButtonMinClick() => BindThis = -1000.12345;

        private void OnButtonClick() => BindThis = 1000.2345678;

        private RelayCommand _btnClick;
        public ICommand ButtonClick => _btnClick;

        private RelayCommand _btnMinClick;
        public ICommand ButtonMinClick => _btnMinClick;

        public MainWindowViewModel()
        {
            _btnClick = new RelayCommand(OnButtonClick);
            _btnMinClick = new RelayCommand(OnButtonMinClick);
        }
    }
}

更新1:
正如评论中所建议的,我应该使用 CoarceValueCallback 而不是在 OnPropertyChanged 中使用 SetCurrentValue。 然而,这会导致整个线上的行为相同。错误的值存储在视图模型的属性中,而有限的值显示在文本框中。

这些是变化:

 public static readonly DependencyProperty ValueProperty = DependencyProperty.Register(
     nameof(Value), typeof(double?), typeof(UserControl1), 
     new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, ValueProperty_OnPropertyChanged, ValueProperty_OnCoarceValue));

 private static object ValueProperty_OnCoarceValue(DependencyObject d, object baseValue)
 {
     UserControl1 instance = (UserControl1)d;
     double? newValue = (double?)baseValue;
     double? result = newValue;

     if (newValue != null)
     {
         if (newValue < instance.Min)
         {
             result = instance.Min;
         }
         else if (newValue > instance.Max)
         {
             result = instance.Max;
         }
     }

     return result;
 }

 // https://stackoverflow.com/questions/7267840/dependency-property-updating-the-source
 // https://blog.scottlogic.com/2012/02/06/a-simple-pattern-for-creating-re-useable-usercontrols-in-wpf-silverlight.html
 private static void ValueProperty_OnPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
 {
     //Debug.WriteLine($"UC OnPropertyChanged: {e.OldValue} -> {e.NewValue}");
     UserControl1 instance = (UserControl1)d;
     //double? newValue = (double?)e.NewValue;
     //double? result = newValue;
    
     //if(newValue != null)
     //{
     //    if(newValue < instance.Min)
     //    {
     //        result = instance.Min;
     //    }
     //    else if(newValue > instance.Max)
     //    {
     //        result = instance.Max;
     //    }
     //}

     //if(newValue != result)
     //{
     //    Debug.WriteLine($"UC forcing min/max value: {result})");
     //    d.Dispatcher.BeginInvoke(() => d.SetCurrentValue(ValueProperty, result));
     //}

     instance.Increase.NotifyCanExecuteChanged();
     instance.Decrease.NotifyCanExecuteChanged();
 }
c# wpf mvvm dependency-properties community-toolkit-mvvm
1个回答
0
投票

这是节省大部分时间的神奇路线

 await Task.Yield();

完整方法:

        private async static void ValueProperty_OnPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            Debug.WriteLine($"UC OnPropertyChanged: {e.OldValue} -> {e.NewValue}");
            UserControl1 instance = (UserControl1)d;
            double? newValue = (double?)e.NewValue;
            double? result = newValue;

            if (newValue != null)
            {
                if (newValue < instance.Min)
                {
                    result = instance.Min;
                }
                else if (newValue > instance.Max)
                {
                    result = instance.Max;
                }
            }

            if (newValue != result)
            {
                Debug.WriteLine($"UC forcing min/max value: {result})");
                d.SetCurrentValue(ValueProperty, result);
                await Task.Yield();
                instance._valueBindingExpression?.UpdateSource();
            }
     
            instance.Increase.NotifyCanExecuteChanged();
            instance.Decrease.NotifyCanExecuteChanged();
        }
    
© www.soinside.com 2019 - 2024. All rights reserved.