Here's some code which does validation using predicates.
There is a fair bit in the base class which is to do with other aspects but you or someone might find that interesting anyhow.
Base viewmodel:
public class BaseValidVM : BaseNotifyUI, INotifyDataErrorInfo, INotifyPropertyChanged
{
// From Validation Error Event
private RelayCommand<PropertyError> conversionErrorCommand;
public RelayCommand<PropertyError> ConversionErrorCommand
{
get
{
return conversionErrorCommand
?? (conversionErrorCommand = new RelayCommand<PropertyError>
(PropertyError =>
{
if (PropertyError.Added)
{
AddError(PropertyError.PropertyName, PropertyError.Error, ErrorSource.Conversion);
}
FlattenErrorList();
}));
}
}
// From Binding SourceUpdate Event
private RelayCommand<string> sourceUpdatedCommand;
public RelayCommand<string> SourceUpdatedCommand
{
get
{
return sourceUpdatedCommand
?? (sourceUpdatedCommand = new RelayCommand<string>
(Property =>
{
ValidateProperty(Property);
}));
}
}
private RelayCommand validateCommand;
public RelayCommand ValidateCommand
{
get
{
return validateCommand
?? (validateCommand = new RelayCommand
(() =>
{
bool isOk = IsValid;
RaisePropertyChanged("IsValid");
}));
}
}
private ObservableCollection<PropertyError> errorList = new ObservableCollection<PropertyError>();
public ObservableCollection<PropertyError> ErrorList
{
get
{
return errorList;
}
set
{
errorList = value;
RaisePropertyChanged();
}
}
protected Dictionary<string, List<AnError>> errors = new Dictionary<string, List<AnError>>();
protected bool isBusy = false;
public bool IsBusy
{
get { return isBusy; }
set { isBusy = value; RaisePropertyChanged("IsBusy"); }
}
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
public IEnumerable GetErrors(string property)
{
if (string.IsNullOrEmpty(property))
{
return null;
}
if (errors.ContainsKey(property) && errors[property] != null && errors[property].Count > 0)
{
return errors[property].Select(x => x.Text).ToList();
}
return null;
}
public bool HasErrors
{
get { return errors.Count > 0; }
}
public void NotifyErrorsChanged(string propertyName)
{
if (ErrorsChanged != null)
{
ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
}
}
public virtual Dictionary<string, List<PredicateRule>> ValiditionRules { get; set; }
private List<string> lastListFailures = new List<string>();
public bool IsValid {
get
{
// Clear only the errors which are from object Validation
// Conversion errors won't be detected here
RemoveValidationErrorsOnly();
var vContext = new ValidationContext(this, null, null);
List<ValidationResult> vResults = new List<ValidationResult>();
Validator.TryValidateObject(this, vContext, vResults, true);
TransformErrors(vResults);
// Iterating the dictionary allows you to check the rules for each property which has any rules
if(ValiditionRules != null)
{
foreach (KeyValuePair<string, List<PredicateRule>> ppty in ValiditionRules)
{
ppty.Value.Where(x => x.IsOK(this) == false)
.ToList()
.ForEach(x =>
AddError(ppty.Key, x.Message, ErrorSource.Validation)
);
}
}
var propNames = errors.Keys.ToList();
propNames.Concat(lastListFailures)
.Distinct()
.ToList()
.ForEach(pn => NotifyErrorsChanged(pn));
lastListFailures = propNames;
FlattenErrorList();
//foreach (var item in errors)
//{
// Debug.WriteLine($"Errors on {item.Key}");
// foreach (var err in item.Value)
// {
// Debug.WriteLine(err.Text);
// }
//}
if (propNames.Count > 0)
{
return false;
}
return true;
}
}
private void RemoveValidationErrorsOnly()
{
foreach (KeyValuePair<string, List<AnError>> pair in errors)
{
List<AnError> _list = pair.Value;
_list.RemoveAll(x => x.Source == ErrorSource.Validation);
}
var removeprops = errors.Where(x => x.Value.Count == 0)
.Select(x => x.Key)
.ToList();
foreach (string key in removeprops)
{
errors.Remove(key);
}
}
public void ValidateProperty(string propertyName)
{
errors.Remove(propertyName);
lastListFailures.Add(propertyName);
if(!propertyName.Contains("."))
{
var vContext = new ValidationContext(this, null, null);
vContext.MemberName = propertyName;
List<ValidationResult> vResults = new List<ValidationResult>();
Validator.TryValidateProperty(this.GetType().GetProperty(propertyName).GetValue(this, null), vContext, vResults);
TransformErrors(vResults);
}
// Apply Predicates
// ****************
if (ValiditionRules !=null && ValiditionRules.ContainsKey(propertyName))
{
ValiditionRules[propertyName].Where(x => x.IsOK(this) == false)
.ToList()
.ForEach(x =>
AddError(propertyName, x.Message, ErrorSource.Validation)
);
}
FlattenErrorList();
NotifyErrorsChanged(propertyName);
RaisePropertyChanged("IsValid");
}
private void TransformErrors(List<ValidationResult> results)
{
foreach (ValidationResult r in results)
{
foreach (string ppty in r.MemberNames)
{
AddError(ppty, r.ErrorMessage, ErrorSource.Validation);
}
}
}
private void AddError(string ppty, string err, ErrorSource source)
{
List<AnError> _list;
if (!errors.TryGetValue(ppty, out _list))
{
errors.Add(ppty, _list = new List<AnError>());
}
if (!_list.Any(x => x.Text == err))
{
_list.Add(new AnError { Text = err, Source = source });
}
}
private void FlattenErrorList()
{
ObservableCollection<PropertyError> _errorList = new ObservableCollection<PropertyError>();
foreach (var prop in errors.Keys)
{
List<AnError> _errs = errors[prop];
foreach (AnError err in _errs)
{
_errorList.Add(new PropertyError { PropertyName = prop, Error = err.Text });
}
}
ErrorList = _errorList;
}
public void ClearErrors()
{
List<string> oldErrorProperties = errors.Select(x => x.Key.ToString()).ToList();
errors.Clear();
ErrorList.Clear();
foreach (var p in oldErrorProperties)
{
NotifyErrorsChanged(p);
}
NotifyErrorsChanged("");
}
}
}
In a viewmodel which inherits from that, you may add your predicates which drive validation:
// Empty string is validation which is not at all property specific
// Otherwise.
// Add an entry per property you need validation on with the list of PredicateRule containing the validation(s)
// to apply.
public override Dictionary<string, List<PredicateRule>> ValiditionRules { get; set; }
public UserControl1ViewModel()
{
// Constructor of the inheriting viewmodel adds any rules which do not suit annotations
// Note
// Two alternative styles of writing rules:
ValiditionRules = new Dictionary<string, List<PredicateRule>>
{
{"Amount2",
new List<PredicateRule>
{
new PredicateRule
{
Message ="Amount2 must be greater than Amount",
IsOK = x => Amount2 > Amount
}
}
},
{"OrderDate",
new List<PredicateRule>
{
new PredicateRule
{
Message ="Amount must be greater than 1 if Order Date is in future",
IsOK = x =>
{
if(OrderDate.Date > DateTime.Now.Date)
{
if(Amount <= 1)
{
return false;
}
}
return true;
}
},
new PredicateRule
{
Message ="Order Date may only be a maximum of 31 days in the future",
IsOK = x => (OrderDate - DateTime.Now.Date).Days < 31
}
}
}
};
}
This is from a PoC for some real world code.