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:
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).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.