MVVM模式下如何检测ListView内指定控件验证错误以禁用按钮
解决ListView内指定控件验证错误时禁用按钮的问题
这是WPF MVVM里很常见的场景,我给你两种可行方案,优先推荐符合MVVM分离原则的ViewModel层面处理方式,另一种是纯View层的实现,你可以根据现有代码结构选择:
方案一:MVVM友好的ViewModel验证(推荐)
这种方式把验证逻辑完全放在ViewModel层,避免View和ViewModel的耦合,更符合MVVM的设计思想:
1. 为列表项ViewModel实现INotifyDataErrorInfo接口
首先,你的每个ListView项对应的子ViewModel(比如叫TestItemViewModel)需要实现这个接口,用来跟踪指定属性的验证错误:
using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Runtime.CompilerServices; public class TestItemViewModel : INotifyPropertyChanged, INotifyDataErrorInfo { private string _name; private int _value; private readonly Dictionary<string, List<string>> _errors = new Dictionary<string, List<string>>(); // 需要验证的第一个属性:Name public string Name { get => _name; set { _name = value; OnPropertyChanged(); ValidateName(); } } // 需要验证的第二个属性:Value(替换成你实际的属性) public int Value { get => _value; set { _value = value; OnPropertyChanged(); ValidateValue(); } } // 验证Name属性 private void ValidateName() { ClearErrors(nameof(Name)); if (string.IsNullOrWhiteSpace(Name)) { AddError(nameof(Name), "名称不能为空"); } } // 验证Value属性 private void ValidateValue() { ClearErrors(nameof(Value)); if (Value <= 0) { AddError(nameof(Value), "值必须大于0"); } } // 以下是INotifyDataErrorInfo的实现细节 private void AddError(string propertyName, string errorMessage) { if (!_errors.ContainsKey(propertyName)) { _errors[propertyName] = new List<string>(); } _errors[propertyName].Add(errorMessage); ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName)); OnPropertyChanged(nameof(HasErrors)); } private void ClearErrors(string propertyName) { if (_errors.ContainsKey(propertyName)) { _errors.Remove(propertyName); ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName)); OnPropertyChanged(nameof(HasErrors)); } } public bool HasErrors => _errors.Any(); public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged; public IEnumerable GetErrors(string propertyName) { return _errors.TryGetValue(propertyName, out var errors) ? errors : Enumerable.Empty<string>(); } // INotifyPropertyChanged实现 public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } }
2. 在父ViewModel中监控所有子项的验证状态
你的父ViewModel(比如MainViewModel)需要维护SelectedActiveTests集合,并且监控每个子项的错误变化,计算按钮是否可启用:
using System.Collections.ObjectModel; using System.ComponentModel; using System.Runtime.CompilerServices; public class MainViewModel : INotifyPropertyChanged { private ObservableCollection<TestItemViewModel> _selectedActiveTests; public ObservableCollection<TestItemViewModel> SelectedActiveTests { get => _selectedActiveTests; set { if (_selectedActiveTests != null) { // 取消旧集合的事件订阅 _selectedActiveTests.CollectionChanged -= SelectedActiveTests_CollectionChanged; foreach (var item in _selectedActiveTests) { item.ErrorsChanged -= Item_ErrorsChanged; } } _selectedActiveTests = value; OnPropertyChanged(); if (_selectedActiveTests != null) { // 订阅新集合的事件 _selectedActiveTests.CollectionChanged += SelectedActiveTests_CollectionChanged; foreach (var item in _selectedActiveTests) { item.ErrorsChanged += Item_ErrorsChanged; } } OnPropertyChanged(nameof(CanSave)); } } // 按钮绑定的属性:只有所有子项的指定属性都无错误时才为true public bool CanSave { get { if (SelectedActiveTests == null || SelectedActiveTests.Count == 0) return false; // 检查每个子项的Name和Value属性是否都没有错误 return SelectedActiveTests.All(item => !item.GetErrors(nameof(TestItemViewModel.Name)).Any() && !item.GetErrors(nameof(TestItemViewModel.Value)).Any()); } } private void SelectedActiveTests_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { // 处理集合项的添加/移除时的事件订阅 if (e.OldItems != null) { foreach (TestItemViewModel item in e.OldItems) { item.ErrorsChanged -= Item_ErrorsChanged; } } if (e.NewItems != null) { foreach (TestItemViewModel item in e.NewItems) { item.ErrorsChanged += Item_ErrorsChanged; } } OnPropertyChanged(nameof(CanSave)); } private void Item_ErrorsChanged(object sender, DataErrorsChangedEventArgs e) { // 只关心我们指定的两个属性的错误变化 if (e.PropertyName == nameof(TestItemViewModel.Name) || e.PropertyName == nameof(TestItemViewModel.Value)) { OnPropertyChanged(nameof(CanSave)); } } public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } }
3. XAML中绑定按钮和ListView
现在按钮可以直接绑定到父ViewModel的CanSave属性,ListView的ItemTemplate也简化了:
<!-- 按钮部分 --> <Button Style="{StaticResource MaterialDesignRaisedButton}" IsEnabled="{Binding CanSave}" Content="保存" /> <!-- ListView部分 --> <ListView Name="ListViewTests" HorizontalAlignment="Center" Grid.Row="1" ItemsSource="{Binding SelectedActiveTests}" ScrollViewer.HorizontalScrollBarVisibility="Disabled" ScrollViewer.VerticalScrollBarVisibility="Auto" ScrollViewer.CanContentScroll="False"> <ListView.ItemTemplate> <DataTemplate> <StackPanel Orientation="Horizontal" Margin="0,4"> <!-- 第一个需要验证的控件 --> <TextBox m:HintAssist.Hint="Title" m:HintAssist.IsFloating="True" Width="150" Text="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" /> <!-- 第二个需要验证的控件 --> <TextBox m:HintAssist.Hint="Value" m:HintAssist.IsFloating="True" Width="100" Margin="8,0" Text="{Binding Value, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" /> <!-- 其他4个控件 --> <TextBox m:HintAssist.Hint="Other 1" Width="80" /> <TextBox m:HintAssist.Hint="Other 2" Width="80" /> <TextBox m:HintAssist.Hint="Other 3" Width="80" /> <TextBox m:HintAssist.Hint="Other 4" Width="80" /> </StackPanel> </DataTemplate> </ListView.ItemTemplate> </ListView>
方案二:纯View层的附加属性+转换器方案(适合不想修改ViewModel的场景)
如果你不想改动现有的ViewModel结构,可以用附加属性跟踪ListView内指定控件的验证状态,再通过转换器判断是否启用按钮:
1. 创建跟踪验证状态的附加属性
这个附加属性会监控控件的Validation.HasError变化,并同步到自定义属性:
using System; using System.Windows; using System.Windows.Controls; using System.Windows.Data; public static class ValidationTracker { public static readonly DependencyProperty TrackedHasErrorProperty = DependencyProperty.RegisterAttached("TrackedHasError", typeof(bool), typeof(ValidationTracker), new PropertyMetadata(false)); public static bool GetTrackedHasError(DependencyObject obj) => (bool)obj.GetValue(TrackedHasErrorProperty); public static void SetTrackedHasError(DependencyObject obj, bool value) => obj.SetValue(TrackedHasErrorProperty, value); public static readonly DependencyProperty TrackValidationProperty = DependencyProperty.RegisterAttached("TrackValidation", typeof(bool), typeof(ValidationTracker), new PropertyMetadata(false, OnTrackValidationChanged)); public static bool GetTrackValidation(DependencyObject obj) => (bool)obj.GetValue(TrackValidationProperty); public static void SetTrackValidation(DependencyObject obj, bool value) => obj.SetValue(TrackValidationProperty, value); private static void OnTrackValidationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is FrameworkElement element) { if ((bool)e.NewValue) { element.Loaded += OnElementLoaded; element.Unloaded += OnElementUnloaded; UpdateTrackedHasError(element); } else { element.Loaded -= OnElementLoaded; element.Unloaded -= OnElementUnloaded; if (element.Tag is DependencyPropertyListener listener) { listener.Dispose(); element.Tag = null; } } } } private static void OnElementLoaded(object sender, RoutedEventArgs e) { if (sender is FrameworkElement element) { var listener = new DependencyPropertyListener(); listener.Listen(element, Validation.HasErrorProperty); listener.PropertyChanged += (s, args) => UpdateTrackedHasError(element); element.Tag = listener; } } private static void OnElementUnloaded(object sender, RoutedEventArgs e) { if (sender is FrameworkElement element && element.Tag is DependencyPropertyListener listener) { listener.Dispose(); element.Tag = null; } } private static void UpdateTrackedHasError(FrameworkElement element) { SetTrackedHasError(element, Validation.GetHasError(element)); } private class DependencyPropertyListener : IDisposable { private WeakReference<DependencyObject> _weakObj; private DependencyProperty _property; private EventHandler _handler; public event EventHandler PropertyChanged; public void Listen(DependencyObject obj, DependencyProperty property) { _weakObj = new WeakReference<DependencyObject>(obj); _property = property; _handler = (s, e) => PropertyChanged?.Invoke(this, EventArgs.Empty); DependencyPropertyDescriptor.FromProperty(property, obj.GetType()).AddValueChanged(obj, _handler); } public void Dispose() { if (_weakObj.TryGetTarget(out var obj)) { DependencyPropertyDescriptor.FromProperty(_property, obj.GetType()).RemoveValueChanged(obj, _handler); } } } }
2. 创建转换器检查所有列表项的验证状态
这个转换器会遍历ListView的所有项,检查指定控件的验证状态:
using System; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Media; public class ListViewValidationConverter : IMultiValueConverter { public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture) { // 第一个参数是ListView,第二个参数是外部控件的Validation.HasError(可选) if (values[0] is not ListView listView) return false; // 先检查外部控件是否有错误(对应你原来的TextBoxName) if (values.Length > 1 && (bool)values[1]) return false; // 遍历所有列表项 foreach (var item in listView.Items) { if (listView.ItemContainerGenerator.ContainerFromItem(item) is not ListViewItem listViewItem) continue; // 找到你指定的两个控件(注意要给控件加Name属性) var nameTextBox = FindVisualChild<TextBox>(listViewItem, "TargetNameTextBox"); var valueTextBox = FindVisualChild<TextBox>(listViewItem, "TargetValueTextBox"); if (nameTextBox != null && ValidationTracker.GetTrackedHasError(nameTextBox)) return false; if (valueTextBox != null && ValidationTracker.GetTrackedHasError(valueTextBox)) return false; } return true; } public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture) { throw new NotImplementedException(); } // 递归查找VisualTree中的子控件 private T FindVisualChild<T>(DependencyObject parent, string name) where T : FrameworkElement { for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++) { var child = VisualTreeHelper.GetChild(parent, i); if (child is T element && element.Name == name) return element; var result = FindVisualChild<T>(child, name); if (result != null) return result; } return null; } }
3. XAML中配置附加属性和绑定
给ListView内的指定控件加Name和附加属性,然后按钮用MultiBinding绑定:
<Window.Resources> <local:ListViewValidationConverter x:Key="ListViewValidationConverter" /> </Window.Resources> <!-- 按钮部分 --> <Button Style="{StaticResource MaterialDesignRaisedButton}"> <Button.IsEnabled> <MultiBinding Converter="{StaticResource ListViewValidationConverter}"> <Binding ElementName="ListViewTests" /> <!-- 绑定原来的外部控件验证状态 --> <Binding Path="(Validation.HasError)" ElementName="TextBoxName" /> </MultiBinding> </Button.IsEnabled> </Button> <!-- ListView部分 --> <ListView Name="ListViewTests" HorizontalAlignment="Center" Grid.Row="1" ItemsSource="{Binding SelectedActiveTests}" ScrollViewer.HorizontalScrollBarVisibility="Disabled" ScrollViewer.VerticalScrollBarVisibility="Auto" ScrollViewer.CanContentScroll="False"> <ListView.ItemTemplate> <DataTemplate> <StackPanel Orientation="Horizontal" Margin="0,4"> <!-- 第一个需要验证的控件:加Name和跟踪属性 --> <TextBox x:Name="TargetNameTextBox" m:HintAssist.Hint="Title" m:HintAssist.IsFloating="True" Width="150"> <TextBox.Text> <Binding Mode="TwoWay" Path="Test.Item1.Name" UpdateSourceTrigger="PropertyChanged"> <Binding.ValidationRules> <local:NotEmptyValidationRule ValidatesOnTargetUpdated="True" /> </Binding.ValidationRules> </Binding> </TextBox.Text> <local:ValidationTracker.TrackValidation>True</local:ValidationTracker.TrackValidation> </TextBox> <!-- 第二个需要验证的控件:加Name和跟踪属性 --> <TextBox x:Name="TargetValueTextBox" m:HintAssist.Hint="Value" m:HintAssist.IsFloating="True" Width="100" Margin="8,0"> <TextBox.Text> <Binding Mode="TwoWay" Path="Test.Item1.Value" UpdateSourceTrigger="PropertyChanged"> <Binding.ValidationRules> <local:PositiveNumberValidationRule ValidatesOnTargetUpdated="True" /> </Binding.ValidationRules> </Binding> </TextBox.Text> <local:ValidationTracker.TrackValidation>True</local:ValidationTracker.TrackValidation> </TextBox> <!-- 其他4个控件 --> <TextBox m:HintAssist.Hint="Other 1" Width="80" /> <TextBox m:HintAssist.Hint="Other 2" Width="80" /> <TextBox m:HintAssist.Hint="Other 3" Width="80" /> <TextBox m:HintAssist.Hint="Other 4" Width="80" /> </StackPanel> </DataTemplate> </ListView.ItemTemplate> </ListView>
注意事项
- 方案二需要确保ListView的ItemContainer已经生成,否则可能找不到控件,你可以监听
ListView.ItemContainerGenerator.StatusChanged事件来刷新按钮绑定; - 方案一的扩展性更好,后续新增验证规则或属性只需要修改ViewModel,不需要改动View;
内容的提问来源于stack exchange,提问作者BAndrei
相关产品推荐
相关产品推荐

