我正在尝试制作一个可重复使用的控件的小型 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();
}
这是节省大部分时间的神奇路线
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();
}