简体   繁体   中英

WPF MVVM Validation DataGrid and disable CommandButton

I have created a sample MVVM application attached.

  • I use a datagrid
  • I have a button bound to a command
  • I have a some custom validation rules applied to certain cells and a textbox.

What I want to achieve is:

  • I like validating while typing (this is already working with validation rules and UpdateSourceTrigger=PropertyChanged).
  • I'd like to validate single cells/rows (this is already working, too)

  • I'd like to do "form" validation. Eg Cross-row validation to validate that no duplicate strings are in the first column of the datagrid.

  • I'd like to disable the command if any validation rule has or the viewmodels form validation has an error.
  • I'd like to enable the command if the form is valid.

How would you do this? I have no clue how to implement the form validation in the view model.

My first idea was just to call a validation method on the viewmodel from the code behind every time anything changes. But doing so, I still don't know how to inform the viewmodel about an validation error in the view's validation rule (eg if someone enters text to the ID column). The viewmodel would simply not know about it and eventually validate successfully, just because the wrong value never reaches it. Ok, I could use strings and do the whole conversion in the viewmodel - but I don't like this idea, because I would like to use the whole power of the converters/validators in WPF.

Has anybody already done something like that?

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

We need to handle actually 3 types of errors.

  1. Error generated by Binding engine of WPF when we enter String where Int is needed. Using UpdateSourceExceptionFilter solves this problem.
  2. Custom UI level validation. Using our own Interface and following notification pattern like INotifyPropertyChanged solves this problem.
  3. Custom back-end level validation. Handling PropertyChanged event in our ViewModel solves this problem.

One by one solutions

  1. Error generated by Binding engine of WPF when we enter String where Int is needed.

      <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

object ReturnExceptionHandler(object bindingExpression, Exception exception)
        {
            vm.CanHello = false;

            return "This is from the UpdateSourceExceptionFilterCallBack.";
       }
  1. Custom UI level validation

To enable Button respond properly we need to glue 4 things together viz; ViewModel, Button, ValidationRules, and DataGrid's template column's textbox. Otherwise ViewModel.CanHello property can't be set properly thus making RelayCommand of no use. Right now ValidationRules : CustomValidRule and NegValidRule are not glued to ViewModel. To make them notify ViewModel about their validation result, they need to fire some event. We will make use of notification pattern which WPF follows using InotifyPropertyChanged. We will create an interface IViewModelUIRule for UI level validation rules to interact with ViewModel.

ViewModelUIRuleEvent.cs

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; }
        }
    }

Our validation rules will now implement this interface.

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;
    }

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

    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;
}

Now, we need to update our ViewModel class to maintain validation rules collection. And to handle ValidationDone event fired by our custom validation rules.

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;
        }
        ……

Now that we have added Rules collection, we need to add our validation rules to it. For this we need to have reference to our validation rules. We are now adding these rules using XAML, so we will use TexBox's Loaded event for the TextBox binded to ID field to get access to these like so,

<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>

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

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);
        }
  1. Custom back-end level validation. This is done by handling PropertyChanged event of Class1's objects. See ViewModel.cs listing above.

     void ViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e) { // if all back-end last level validations run good here // canHello = true; } 

Note : We can use reflection to avoid handling of TextBox Loaded event. So merely adding validation rules to the will do the work.

I dont believe it is possible to validate a row using multiple columns in a DataGrid . But, as you mentioned, you can do it using the viewmodel.

You would have to store the rows of the DataGrid in the ViewModel (but I expect you are doing that already). The you need to implement INotifyDataErrorInfo . This interface allows you to notify the view if some errors changed.

Then, every time the name property is changed, validate if there are any duplicates.

Your save button should use an ICommand to invoke the save action. In the CanExecute method you can check the HasErrors property of the object that implements INotifyDataErrorInfo and return the appropriate boolean . This disables the button accordingly.

Kinda brute force approach. I kinda did the following design in my project. It's kinda hard to explain in text so hope you'll understand what I typed here

I would have the following design

  1. FormLevelViewModel - Which contains a collection of InnerViewModels (DataRowViewModel - ie Each row is an viewmodel) and the buttom command
  2. DataRowLevelViewModel - contains a collection of InnerViewModels (ie CellViewModel)
  3. CellLevelViewModel

    • For CellViewModel, can perform property level validation over there and populate the error to control accordingly
    • For DataRowViewModel, can perform object level validation and perform validation from all the InnerViewModels
    • Similarly FormViewModel, can perform validation in recurrsive method to trigger validation from all InnerViewModels and obtained the aggregated results.

With the above design setup, all you need is having a EventHandler at your ViewModelBase that fired after an ViewModel performed Validation. Use this event to Trigger parent ViewModel to perform it's own level of validation and keep populate the error result back to the root view model.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM