I've been working with Silverlight 3 in a number of LoB (Line of Business) scenarios for a while now and I'm consistently running in to a few dead ends with respect to Validation. Given I've posted twice before on Silverlight, MVVM and Validation:
However, the only way to get a control into an 'invalid state' is to throw an exception in a bound setter. This is the key problem and leads to a number of inconsistencies as to how one should deal with validation, e.g.:
Based on the example in the previous post in this 'series': Silverlight, Validation and MVVM - Part II, here's an example of how you might implement this interface. I've chosen to do it this way because I'm a big ViewModel fan and already have a base class to which adding some validation logic seems to make sense. Additionally, I want to leverage the ValidationAttributes available in RIA services (even if I'm not using RIA services themselves). Ideally, the attributes could just be added to the 'model' properties without anymore work. And that's what I've shot for with my new base class.
Firstly, to manage this and return the results in the appropriate format I've created a ValidationManager class: public class ValidationManager { private readonly INotifyPropertyChanged _instance; private bool _isDirty = true; private readonly ValidationResultCollection _results = new ValidationResultCollection(); public ValidationManager(INotifyPropertyChanged instance) { if (instance == null) throw new ArgumentNullException("instance"); _instance = instance; _instance.PropertyChanged += delegate { _isDirty = true; }; } public ValidationResultCollection Results { get { if (_isDirty) { Validate(); _isDirty = false; } return _results; } } public ValidationResultCollection ResultsForMemberName(string memberName) { var results = Results.Where(r => r.MemberNames.Contains(memberName)); return new ValidationResultCollection(results); } private void Validate() { _results.Clear(); Validator.TryValidateObject(_instance, new ValidationContext(_instance, null, null), _results, true); } } public class ValidationResultCollection : List<ValidationResult> { public ValidationResultCollection() : base() { } public ValidationResultCollection(IEnumerable<ValidationResult> results) : base(results) { } public override string ToString() { if (this.Count == 0) { return null; } StringBuilder sb = new StringBuilder(); for (int i = 0; i < this.Count; i++) { sb.Append(this[i]); if (i < this.Count - 1) { sb.AppendLine(); } } return sb.ToString(); } } This class simply helps me to implement IDataErrorInfo by storing validation results and ensuring they're only re-generated if the object is 'dirtied' (we observe the INotifyPropertyChanged instance so we can know this). The validation results are then created on demand for the whole object (to ensure any class-level validators are invoked).
Next, I created my base class (based on my BaseViewModel from my snippets). public class ValidatingViewModelBase : INotifyPropertyChanged, IDataErrorInfo { private readonly ValidationManager _validationManager; public ValidatingViewModelBase() { _validationManager = new ValidationManager(this); } public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged(string propertyName) { PropertyChangedEventHandler pceh = PropertyChanged; if (pceh != null) { pceh(this, new PropertyChangedEventArgs(propertyName)); } } protected virtual bool SetValue<T>(ref T target, T value, params string[] changedProperties) { if (Object.Equals(target, value)) { return false; } target = value; foreach (string property in changedProperties) { OnPropertyChanged(property); } return true; } public string Error { get { return _validationManager.Results.ToString(); } } public string this[string columnName] { get { return _validationManager.ResultsForMemberName(columnName).ToString(); } } } Now I just have to implement my model by inheriting from this class. And the best bit is I can just use attributes to apply validation logic to my type. Nice: public class Person : ValidatingViewModelBase { private string _name; [Required(ErrorMessage = "Name is required")] public string Name { get { return _name; } set { SetValue(ref _name, value, "Name"); } } private string _salutation; [Required(ErrorMessage = "Salutation is required")] public string Salutation { get { return _salutation; } set { SetValue(ref _salutation, value, "Salutation"); } } private int _age; [Required(ErrorMessage = "Age is required")] [Range(18, int.MaxValue, ErrorMessage = "Must be over 18")] public int Age { get { return _age; } set { SetValue(ref _age, value, "Age"); } } } And here's a demo for you to try:
And, even to my own surprise, this works remarkably well. You can even add new objects and the validation invokes nicely!
If the binding engine can't convert your input to the ViewModel type (for example, try inputting some nonsense string into the Age field above); then the view will display a notification (thank to ValidatesOnExceptions=true) but the ViewModel will be unaware of this failing and you won't be able to prevent the update going ahead. For this reason, I'd strongly recommend a hybrid approach utilising the ValidationScope from the previous post to capture Binding Errors and report them to the ViewModel. Note, I haven't included this in the sample above and, instead, have left that as an exercise for the reader.
The other problem to be aware of is silent conversion success. That is, if you enter 21.322 into the Age field then the binding will succeed. But, because that field is bound to an Int32, some of your data will be lost. As far as I know, there is now way to know this is happening so you'd have to implement something at the view to deal with this in a more sophisticated way (a new, derived TextBox control maybe?).
I've always felt IDataErrorInfo to be an 'immature' interface - it feels as though it's from a bygone era. Somebody in Redmond obviously feels the same and Silverlight 4 introduces a new INotifyDataErrorProperty which feels more elegant. The code above should modify to support this new interface with relative ease. Have a read of Mike Taulty's excellent posts on both interface for more information:
Originally posted by Josh Twist on 15 January 2009 here.