You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 04:17:24