MVVM – Validating DataGrid in C# While Adhering to MVVM

cmvvmvalidationwpf

I am struggling to find a good architecture for what must be a common problem

Enable/Disable a button in a View that contains a datagrid, and has
validation requirements on the cell, row and collection levels. And to perform this cleanly in a MVVM architecture.

If it wasn't for the datagrid then this would be easy, I'd just check the overall result of either an INotifyDataErrorInfo or IDataErrorInfo implementation on the ViewModel and use existence of any errors to disable the button via the ICommand interface to the button, that resides in the ViewModel.

However the data grid complicates things, and I find myself going back and forth between doing data validation in the ViewModel or in the View itself. EG:

  1. Validation in the ViewModel keeps the validation closer to the datasource and I can implement INotifyDataErrorInfo on each item in the collection. This enables me to validate on a cell and row level, but there appears to be no support for validating against the overall collection itself – and I have yet to find a decent solution that can do this (see link below). (And I don't want to pollute the items in the collection by adding some reference to an external entity to them).

  2. Validation in the View itself (using validation rules on the datagrid) allows me to validate on the cell, row and collection level1, but I can't see how to communicate the presence of errors to the ViewModel as nothing is passed back to the ViewModel unless it is valid.

The closest solution I have seen online that solves all of my problems is this 2014 TechNet one – Data Validation in MVVM

But it is as ugly as ugly can get2. To quote from that page (emphasis mine):

The Most Important thing is to disable the Save button when the form
is invalid. For this we had a static property in our view model named
Errors. When the Error is occurred the Error count is increased by 1.
When the error is corrected the error count is decreased by 1. So when
the Count is 0 the save will be enabled or disabled.

The author's solution is put a static counter on the ViewModel and set a call back in the View itself. When an error is detected in the ViewModel the IDataErrorInfo interface passes that information up to the View which then increments the counter on the ViewModel, and the value of the counter is use to disable the button.

In addition, to perform validation on a collection level, they implement another static variable on the ViewModel which is set to the instance of the collection in the current ViewModel. Thus an item validates itself against the entire collection by finding the collection from this static variable.

This code makes me cringe.

So what am I missing? How can I easily, cleanly, and keeping to strict MVVM guidelines, perform validation of an overall ViewModel that requires validation of a data grid at the cell, row and collection levels? And communicate the results of that validation back to the ViewModel itself?


1 In order to perform validation at the collection level using ValidationRules I created a custom rule as per this SO question https://stackoverflow.com/a/10555571/31326 that injects the binding source of the data grid into the rule. This cleanly allows you to do validation at the collection level.

2 According to the comments at the bottom of that link, this article won prizes!


Update

I am going with a modified version of Emerson's answer. I am going to keep the internal validation within the object view model, but inject in a validation test for doing collection level validation. The following code lays out my ideas. This code seems to work (and probably could be optimised), but I am not sure if I am going to hell for it 😀

// Base of all view models 
// Handles property notification and error notification
public abstract class ViewModelBase<T> INotifyPropertyChanged, INotifyDataErrorInfo :  where T : class
{
    // Validate view model against external sources
    // Return a list of errors
    protected Func<T, List<String>> ExternalValidate = null;

    // Rest of INotifyPropertyChanged and INotifyDataErrorInfo implementation etc.
    protected void AddError(string error, [CallerMemberName] string propertyName = null)
    {
      INotifyDataErrorInfo.AddError(error, propertyName);
    }

    protected void AddError(List<string> errors, [CallerMemberName] string propertyName = null)
    {
      foreach(var error in errors)
      {
        INotifyDataErrorInfo.AddError(error, propertyName);
      }
    }
}

// The Product data model
public class Product
{
  public int Number { get; set; }
}

// Adapts a Product Data/Domain model to a view model
// Does all the error checking for the view model both within and external to this one object
public class ProductViewModel : ViewModelBase<ProductViewModel>
{
  public int Number
  {
    get { return _Number; }
    set 
    {
      // Validate "value" against other properties in *this* view model 
      if (value<0) AddError("can't be negative"); 

      // Validate against **External** values
      if (ExternalValidate != null)
      {
        var errors = ExternalValidate(this);
        if (errors != null) AddError(errors);
      }
    }
  }
  protected int _Number;

  public ProductViewModel(Func<ProductViewModel,List<string> externalValidate, Product product)
  {
    // The external validation check
    ExternalValidate = externalValidate

    // Initialize PVM from product
    _Number  = product.Number;
  }
}

// Holds the collection of ProductViewModel objects
// Defines the collection level validation rules
// Injects those rules into the ProductViewModel objects
// The key thing is capturing the values in the Func.
public class MainVM()
{
  public int TheAnswer {get; set; } = 42;

  public List<ProductViewModel> PVMS = new List<ProductViewModel>();

  Func<ProductViewModel, List<string>> ExternalTest = (ProductViewModel pvm) =>
  {
    var result = new List<string>();
    foreach(var pvms in PVMS)
    {
      if (!Object.ReferenceEquals(ppm,pvms)
      {
        if (ppm.Number == pvms.Number)
        {
          result.Add("Can't have duplicates");
        }
      }
    }

    if (pvm.Number == TheAnswer) 
    {          
      result.Add("Can't equal the answer");
    }

    return result.Count>0 ? result : null;

  }

  List<Product> products = GetProductsFromSource();

  foreach(var product in products)
  {
    PVMS.Add( new ProductViewModel(ExternalTest, product) );
  }
}

In the above code, whenever the Number property of the ProductViewModel is set, the class will validate within itself and then call the external validation. If the Number is duplicated or set to the current value of TheAnswer then an error message will be generated.

The jumping through hoops to get to this means that my ProductViewModel does not know that it is a part of a collection and simply validates itself against some externally defined set of conditions. These external conditions could be anything, such as iterating over the collection and looking for duplicate values, or in this case checking that the Number property doesn't equal some particular value.

And by placing the conditional check in an generic base class this code is reusable.

Best Answer

Supposing you have a Product class, each product shouldn't know about other products in the list.

In your GUI, I suggest you use a ProductViewModel, responsible to validate all info. Therefore, there's no problem in making the ProductViewModel to have knowledge of other instances of the same type in its parent collection. After all validation succeeds, you could generate a collection of Product if necessary.

Here's some code trying to illustrate my suggestion:

Your data entity class

public class Product {
    public int Id {get;set;}
    public string Name {get;set;}
}

Your view model (responsible to validate everything)

//Represents the view model for the Product.
//This view model is responsible for validating information of the product.
//As part of  this validation, it is necessary that this instance know
//details of the collection it belongs.
public class ProductViewModel : INotifyDataErrorInfo {

    //This could be injected via constructor or property.
    //Represents the collection of products, that requires validation
    //See its definition below
    private ProductViewModelCollection parentCollection;

    public int Id { 
        get { return _id; }
        set { if (ValidateId(value))) _id = value; }
    }
    public string Name {
        get { return _name; }
        set { if (ValidateName(value)) _name = value; }
    }

    //The methods below validate all info.
    //Alternatively, you could implement the validation somewhere else, 
    //and inject the necessary validators into this viewModel instance.
    private bool ValidateId(int id) { 
        //... validate the id...

        foreach (ProductViewModel otherProduct in parentCollection) {
            //...if necessary, perform additional validations comparing
            //with other products in the parent collection
        }
    }
    private bool ValidateName(string name) { 
        //... validate the name...

        foreach (ProductViewModel otherProduct in parentCollection) {
            //...if necessary, perform additional validations comparing
            //with other products in the parent collection
        }
    }

    //...implementation of INotifyDataErrorInfo...
}

Your collection of products, it is the source for the datagrid

public class ProductViewModelCollection : ObservableCollection<ProductViewModel>, IINotifyDataErrorInfo {
    //...implementation of INotifyDataErrorInfo...
}

After this, you won't need to use built-in validation of UI components; you can implement everything in the view model, being able to unit test it accordingly. The knowledge across collection is implemented in the ViewModel (because your GUI requires it), not within your data entity.

UPDATE: Second approach (via injection):

//Represents the view model for the Product.
//This view model is responsible for validating information of the product.
//As part of  this validation, it is necessary that this instance know
//details of the collection it belongs.
public class ProductViewModel : INotifyDataErrorInfo {

    //Injected via constructor or property.
    private IProductValidator validator;

    //String properties, IF YOUR GUI ALLOWS user to type
    //text in the fields
    public string Id { 
        get { return _id; }
        set { if (validator.ValidateId(value))) _id = value; }
    }
    public string Name {
        get { return _name; }
        set { if (validator.ValidateName(value)) _name = value; }
    }

    public Product GetProduct() {
        //...generate Product object with valid info...
    }
}

public class ProductValidator : IProductValidator {

    //This could be injected via constructor or property.
    //Represents the collection of products, that requires validation
    //See its definition below
    private ProductViewModelCollection parentCollection;

    //The methods below validate all info.
    //Alternatively, you could implement the validation somewhere else, 
    //and inject the necessary validators into this viewModel instance.
    private bool ValidateId(string id) { 
        //... validate the id...

        foreach (ProductViewModel otherProduct in parentCollection) {
            //...if necessary, perform additional validations comparing
            //with other products in the parent collection
        }
    }
    private bool ValidateName(string name) { 
        //... validate the name...

        foreach (ProductViewModel otherProduct in parentCollection) {
            //...if necessary, perform additional validations comparing
            //with other products in the parent collection
        }
    }

    //...implementation of INotifyDataErrorInfo...
}