关于 c#:WPF MVVM Validation DataGrid 并禁用 CommandButton

WPF MVVM Validation DataGrid and disable CommandButton

我创建了一个附加的示例 MVVM 应用程序。

  • 我使用数据网格
  • 我有一个绑定到命令的按钮
  • 我有一些自定义验证规则应用于某些单元格和一个文本框。

我想要实现的是:

  • 我喜欢在输入时进行验证(这已经在使用验证规则和 UpdateSourceTrigger=PropertyChanged)。
  • 我想验证单个单元格/行(这也已经在工作了)

  • 我想进行"表单"验证。例如。跨行验证以验证数据网格的第一列中没有重复的字符串。

  • 如果任何验证规则有或 viewmodels 表单验证有错误,我想禁用该命令。
  • 如果表单有效,我想启用该命令。

你会怎么做呢?我不知道如何在视图模型中实现表单验证。

我的第一个想法是在每次发生任何变化时从背后的代码中调用视图模型上的验证方法。但是这样做,我仍然不知道如何通知视图模型有关视图验证规则中的验证错误(例如,如果有人在 ID 列中输入文本)。视图模型根本不知道它并最终成功验证,只是因为错误的值永远不会到达它。好的,我可以使用字符串并在视图模型中进行整个转换 - 但我不喜欢这个想法,因为我想在 WPF 中使用转换器/验证器的全部功能。

有没有人做过类似的事情?

https://www.dropbox.com/s/f3a1naewltbl9yp/DataGridValidationTest.zip?dl=0


我们实际上需要处理 3 种类型的错误。

  • 当我们在需要 Int 的地方输入 String 时,WPF 的 Binding 引擎会产生错误。
    使用 UpdateSourceExceptionFilter 解决了这个问题。
  • 自定义 UI 级别验证。
    使用我们自己的接口并遵循 INotifyPropertyChanged 之类的通知模式可以解决这个问题。
  • 自定义后端级别验证。
    在我们的 ViewModel 中处理 PropertyChanged 事件可以解决这个问题。
  • 一一解决

  • 当我们在需要Int的地方输入String时,WPF的Binding引擎产生错误。

    1
    2
    3
    4
    5
    6
    7
    8
    9
        <TextBox VerticalAlignment="Stretch" VerticalContentAlignment="Center" Loaded="TextBox_Loaded">
          <TextBox.Text>
            <Binding Path="ID" UpdateSourceExceptionFilter="ReturnExceptionHandler" UpdateSourceTrigger="PropertyChanged" ValidatesOnDataErrors="True" ValidatesOnExceptions="True">
                  <Binding.ValidationRules>
                     <CustomValidRule ValidationStep="ConvertedProposedValue"></CustomValidRule>
                  </Binding.ValidationRules>
            </Binding>
           </TextBox.Text>
          </TextBox>
  • MainWindow.xaml.cs

    1
    2
    3
    4
    5
    6
    object ReturnExceptionHandler(object bindingExpression, Exception exception)
            {
                vm.CanHello = false;

                return"This is from the UpdateSourceExceptionFilterCallBack.";
           }
  • 自定义 UI 级别验证
  • 要使 Button 正确响应,我们需要将 4 个东西粘合在一起,即; ViewModel、Button、ValidationRules 和 DataGrid 的模板列和文本框。否则 ViewModel.CanHello 属性无法正确设置,从而使 RelayCommand 无用。
    现在 ValidationRules : CustomValidRule 和 NegValidRule 没有粘在 ViewModel 上。为了让他们通知 ViewModel 他们的验证结果,他们需要触发一些事件。
    我们将利用 WPF 使用 InotifyPropertyChanged 遵循的通知模式。
    我们将为 UI 级别验证规则创建一个接口 IViewModelUIRule 以与 ViewModel 交互。

    ViewModelUIRuleEvent.cs

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    using System;

        namespace BusinessLogic
        {
            public interface IViewModelUIRule
            {
                event ViewModelValidationHandler ValidationDone;
            }

            public delegate void ViewModelValidationHandler(object sender, ViewModelUIValidationEventArgs e);

            public class ViewModelUIValidationEventArgs : EventArgs
            {
                public bool IsValid { get; set; }

                public ViewModelUIValidationEventArgs(bool valid) { IsValid = valid; }
            }
        }

    我们的验证规则现在将实现这个接口。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    public class CustomValidRule : ValidationRule, IViewModelUIRule
        {

            bool _isValid = true;
            public bool IsValid { get { return _isValid; } set { _isValid = value; } }

            public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
            {

                int? a = value as int?;
                ValidationResult result = null;

                if (a.HasValue)
                {
                    if (a.Value > 0 && a.Value < 10)
                    {
                        _isValid = true;
                        result = new ValidationResult(true,"");
                    }
                    else
                    {
                        _isValid = false;
                        result = new ValidationResult(false,"must be > 0 and < 10");
                    }
                }

                OnValidationDone();

                return result;
            }

            private void OnValidationDone()
            {
                if (ValidationDone != null)
                    ValidationDone(this, new ViewModelUIValidationEventArgs(_isValid));
            }

            public event ViewModelValidationHandler ValidationDone;
        }

    ////////////////////////

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
        public class NegValidRule : ValidationRule, IViewModelUIRule
    {
        bool _isValid = true;
        public bool IsValid { get { return _isValid; } set { _isValid = value; } }

        ValidationResult result = null;

        public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
        {
            int? a = value as int?;
            if (a.HasValue)
            {
                if (a.Value < 0)
                {
                    _isValid = true;
                    result = new ValidationResult(true,"");
                }
                else
                {
                    _isValid = false;
                    result = new ValidationResult(false,"must be negative");
                }
            }

            OnValidationDone();

            return result;
        }

        private void OnValidationDone()
        {
            if (ValidationDone != null)
                ValidationDone(this, new ViewModelUIValidationEventArgs(_isValid));
        }

        public event ViewModelValidationHandler ValidationDone;
    }

    现在,我们需要更新 ViewModel 类来维护验证规则集合。并处理由我们的自定义验证规则触发的 ValidationDone 事件。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    namespace BusinessLogic
    {
        public class ViewModel : INotifyPropertyChanged
        {
            private ObservableCollection<ValidationRule> _rules;
            public ObservableCollection<ValidationRule> Rules { get { return _rules; } }

            public ViewModel()
            {
                _rules = new ObservableCollection<ValidationRule>();

                Rules.CollectionChanged += Rules_CollectionChanged;

                MyCollection.CollectionChanged += MyCollection_CollectionChanged;            

                MyCollection.Add(new Class1("Eins", 1));
                MyCollection.Add(new Class1("Zwei", 2));
                MyCollection.Add(new Class1("Drei", 3));
            }

            void Rules_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
            {
                foreach (var v in e.NewItems)
                    ((IViewModelUIRule)v).ValidationDone += ViewModel_ValidationDone;
            }

            void ViewModel_ValidationDone(object sender, ViewModelUIValidationEventArgs e)
            {
                canHello = e.IsValid;
            }

            void MyCollection_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
            {
                foreach (var v in e.NewItems)
                    ((Class1)v).PropertyChanged += ViewModel_PropertyChanged;
            }

            void ViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
            {            
                // if all validations runs good here
                // canHello = true;
            }
            a€|a€|

    现在我们已经添加了规则集合,我们需要向它添加我们的验证规则。为此,我们需要参考我们的验证规则。我们现在使用 XAML 添加这些规则,因此我们将使用 TexBoxa€?s Loaded 事件为绑定到 ID 字段的 TextBox 来访问这些,像这样,

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <TextBox VerticalAlignment="Stretch" VerticalContentAlignment="Center" Loaded="TextBox_Loaded">
                                <TextBox.Text>
                                        <Binding Path="ID" UpdateSourceExceptionFilter="ReturnExceptionHandler" UpdateSourceTrigger="PropertyChanged" ValidatesOnDataErrors="True" ValidatesOnExceptions="True">
                                            <Binding.ValidationRules>
                                                <b:CustomValidRule ValidationStep="ConvertedProposedValue"></b:CustomValidRule>
                                            </Binding.ValidationRules>
                                        </Binding>
                                </TextBox.Text>
                            </TextBox>

    ////////////////////

    1
    2
    3
    4
    5
    6
    7
    private void TextBox_Loaded(object sender, RoutedEventArgs e)
            {
                Collection<ValidationRule> rules= ((TextBox)sender).GetBindingExpression(TextBox.TextProperty).ParentBinding.ValidationRules;

                foreach (ValidationRule rule in rules)
                    vm.Rules.Add(rule);
            }
  • 自定义后端级别验证。
    这是通过处理 Class1a 对象的 PropertyChanged 事件来完成的。请参阅上面的 ViewModel.cs 列表。

    1
    2
    3
    4
    5
    void ViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {            
        // if all back-end last level validations run good here
        // canHello = true;
    }
  • 注意:我们可以使用反射来避免处理 TextBox Loaded 事件。因此,只需向意志中添加验证规则即可。


    有点蛮力的方法。我在我的项目中做了以下设计。
    用文字解释有点困难,所以希望你能明白我在这里输入的内容

    我会有以下设计

  • FormLevelViewModel - 其中包含 InnerViewModels 的集合(DataRowViewModel - 即每一行都是一个视图模型)和按钮命令
  • DataRowLevelViewModel - 包含 InnerViewModels 的集合(即 CellViewModel)
  • CellLevelViewModel

    • 对于 CellViewModel,可以在那里执行属性级别验证并填充错误以进行相应控制
    • 对于 DataRowViewModel,可以执行对象级验证并从所有 InnerViewModel 执行验证
    • 同理FormViewModel,可以通过递归的方式进行验证,从所有的InnerViewModel中触发验证,得到聚合结果。
  • 通过上述设计设置,您所需要的只是在 ViewModelBase 中拥有一个在 ViewModel 执行验证后触发的 EventHandler。使用此事件触发父 ViewModel 执行它自己的验证级别并将错误结果填充回根视图模型。


    我不相信使用 DataGrid 中的多个列来验证一行是可能的。但是,正如您所提到的,您可以使用 viewmodel 来做到这一点。

    您必须将 DataGrid 的行存储在 ViewModel 中(但我希望您已经这样做了)。您需要实现 INotifyDataErrorInfo。此界面允许您在某些错误发生更改时通知视图。

    然后,每次更改 name 属性时,验证是否有任何重复项。

    您的保存按钮应该使用 ICommand 来调用保存操作。在 CanExecute 方法中,您可以检查实现 INotifyDataErrorInfo 的对象的 HasErrors 属性并返回适当的 boolean。这会相应地禁用按钮。