0

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

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

  • I agree that DataValidation in WPF is a terrible mess, but can't you just perform the validation of the whole collection in the ViewModel containing it? It can expose a property IsCollectionValid that control the state of the command. – Dtex Nov 29 '17 at 15:03
  • @Dtex the question is how to trigger this validation. Validating within an item is fine, but the item does not (and should not) know that it is a part of a collection in order to validate over that same collection. – Peter M Nov 29 '17 at 15:11
  • Well you should use an MVVM framework of course. I use ReactiveUI that comes with a ReactiveCollection that notifies you when a property of an item in the collection changes. Tha parent view model can subscribe to both collection changes and item changes and update the state of the command. – Dtex Nov 29 '17 at 15:14
  • @Dtex ReactiveUI (or similar) may be a solution but right now I'd prefer not to get pulled in another direction – Peter M Nov 29 '17 at 15:19
  • Can you provide some minimal example containing code? maybe exposing what you already have (eg validation using the datagrid). – Emerson Cardoso Nov 29 '17 at 16:27
0

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...
}
  • So basically you are saying that it's OK for a ProductViewModel item to know that it is a part of a collection. I am on the fence about that but tend to think that the collection level validation should be injected in somehow. BTW this is sort of related but having a strongly typed backing variable splits the data validation between the View and the ViewModel. EG if you type a non-numeric char into the Id edit cell, then the View will intercept that and supply its own error message. The only way around that is to change the backing variables to be strings. Which I loathe. – Peter M Nov 29 '17 at 18:17
  • Actually I am now wondering if all validation shouldn't be injected into the ProductViewModel – Peter M Nov 29 '17 at 18:20
  • In general, I think injecting the validator is a good thing. The validator could have a reference to the product Collection and then be able to perform the collection level validation. About the strongly typed backing variable, I see no problem in having only string properties in your VM, if your GUI enables the user to type text chars. By doing so, you would be able to implement everything in the ViewModel and also test stuff. I don't see this as a problem. You could have string properties, and in the end a method that generate the Product object (with correct types). – Emerson Cardoso Nov 29 '17 at 18:37
  • I have been trying to avoid string backing variables in order to keep as close as possible to the data model. But it is looking more and more like I can't avoid them. – Peter M Nov 29 '17 at 18:44
  • 1
    I took your code and ran with it adding my own spin – Peter M Nov 30 '17 at 14:23
0

If you really have to implement editing within a grid, then to perform validation both the data item and the main viewmodel have to implement INotifyPropertyChanged, with IDataErrorInfo handled by the data item.

This is the base model class that I use to handle validation

public abstract class ValidatingModelBase : INotifyPropertyChanged, IDataErrorInfo
{
    private readonly Dictionary<string, List<ValidationRule>> _validationRules = new Dictionary<string, List<ValidationRule>>();

    #region INotifyPropertyChanged
    public event PropertyChangedEventHandler PropertyChanged;

    public virtual void RaisePropertyChanged(string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
    #endregion

    #region IDataErrorInfo
    public string this[string columnName]
    {
        get
        {
            var result = string.Empty;

            if (_validationRules.ContainsKey(columnName))
                foreach (var errorCheck in _validationRules[columnName])
                {
                    if (errorCheck.IsValid.Invoke())
                        continue;

                    result = errorCheck.FailErrorMessage;
                    break;
                }

            var errorStateChanged = string.IsNullOrWhiteSpace(result) 
                ? _invalidProperties.Remove(columnName) 
                : _invalidProperties.Add(columnName);

            if (errorStateChanged && !nameof(IsValid).Equals(columnName))
                RaisePropertyChanged(nameof(IsValid));

            return result;
        }
    }

    public string Error => string.Empty;
    #endregion

    private readonly HashSet<string> _invalidProperties = new HashSet<string>();

    public bool IsValid => !_invalidProperties.Any();

    public bool HasError(string propertyName) => _invalidProperties.Contains(propertyName);

    public void AddValidationRule(string propertyName, Func<bool> isValid, string failErrorMessage)
    {
        if (!_validationRules.ContainsKey(propertyName))
            _validationRules[propertyName] = new List<ValidationRule>();

        _validationRules[propertyName].Add(new ValidationRule(isValid, failErrorMessage));
    }

    // =============================================================================== 

    private class ValidationRule
    {
        public ValidationRule(Func<bool> isValid, string failErrorMessage)
        {
            IsValid = isValid;
            FailErrorMessage = failErrorMessage;
        }

        public Func<bool> IsValid { get; }
        public string FailErrorMessage { get; }
    }
}

then, given a model class

public PersonModel: ValidatingModelBase
{
    public PersonModel()
    {
        AddValidationRule(nameof(FirstName), !string.IsNullorWhiteSpace(FirstName), "First Name can't be blank");
        ...
    }

    private string _firstName;
    public string FirstName
    {
        get { return _firstName; }
        set {
                _firstName = value;   
                RaisePropertyChanged(nameof(FirstName));
            }
    }

    ....
}

in your ViewModel you could write something like

public MainViewModel: INotifyPropertyChanged
{
    public MainViewModel()
    {
        GridData = _dataService.GetPeople();

        foreach(var person in PersonGridData)
            person.PropertyChanged += (s,e) => if (nameof(Person.IsValid).equals(e.PropertyName))
                                                   RaisePropertyChanged(nameof(PersonGridIsValid));
    }

    public ICollection<PersonModel> PersonGridData {get;}

    public bool PersonGridIsValid => PersonGridData.All(p => p.IsValid);
}

The PersonGridIsValid property can then be used to determine if the whole view is in a valid state.

  • OK It looks to me like you are simply adding an "IsValid" flag to each Person record, and then pulling out the overall status with PersonGridIsValid . The latter will be read as a part of the ICommand on the button. But how to you validate across multiple people? And do so at the time of editing? EG add a rule that says "FirstName" has to be unique. My initial guess is that you inject the collection itself when you assign the PropertyChanged handlers. – Peter M Nov 29 '17 at 17:50
  • Each item must be independent - the validation for an item must only be based on the current state of that item. If you want validation across the whole collection, than that must be handled at the main viewmodel level, e.g. by adding another propertychanged handler to each model item that does the required checking. It seems you're getting overly complicated now though - a better solution might be to make the grid read-only and to have a modal popup window to edit a single item at a time with the required validation. – Peregrine Nov 29 '17 at 18:19
  • Collection level validation is my main goal (along with individual property and item validation). I think your solution is headed in the direction of where I want to go if you could inject collection level rules to your AddValidationRule . (Or as I was musing in the other answer, inject all validation rules into the PersonModel) – Peter M Nov 29 '17 at 18:28
  • I had thought of a Modal dialog as well but in my current application I really want to keep the user as close as possible to the data, and felt that adding another layer would be detrimental to the UI – Peter M Nov 29 '17 at 18:29
  • BTW Putting on my Code Review hat, I think that your AddValidationRule et al is trying to reinvent both DataAnnotations and the INotifyDataErrorInfo interface. About the only good thing in that TechNet article I linked to is that it made me understand DataAnnotations as I had never really encountered it before. (and I only looked INotifyDataErrorInfo for the first time 2 weeks ago!) – Peter M Nov 29 '17 at 18:56

Your Answer

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Not the answer you're looking for? Browse other questions tagged or ask your own question.