using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Linq.Expressions; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; namespace Stylet { /// /// Base for ViewModels which require property validation /// [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable", Justification = "According to Albahari and Albahari, relying on the GC to tidy up WaitHandles is arguably acceptable, since they're so small.")] public class ValidatingModelBase : PropertyChangedBase, INotifyDataErrorInfo { /// /// Occurs when the validation errors have changed for a property or for the entire entity. /// public event EventHandler ErrorsChanged; private readonly SemaphoreSlim propertyErrorsLock = new SemaphoreSlim(1, 1); private readonly Dictionary propertyErrors = new Dictionary(); private IModelValidator _validator; /// /// Gets or sets the IModelValidator to use to validate properties. You're expected to write your own, using your favourite validation library /// protected virtual IModelValidator Validator { get { return this._validator; } set { this._validator = value; if (this._validator != null) this._validator.Initialize(this); } } /// /// Gets or sets a value indicating whether to run validation for a property automatically every time that property changes /// protected bool AutoValidate { get; set; } /// /// Initialises a new instance of the class, without using an /// public ValidatingModelBase() { this.AutoValidate = true; } /// /// Initialises a new instance of the class, using the specifies /// /// Validator adapter to use to perform validations public ValidatingModelBase(IModelValidator validator) : this() { // Can't set this.validator, as it's virtual, and FxCop complains this._validator = validator; if (this._validator != null) this._validator.Initialize(this); } private bool ErrorsEqual(string[] e1, string[] e2) { if (e1 == null && e2 == null) return true; if (e1 == null || e2 == null) return false; return e1.SequenceEqual(e2); } /// /// Validate all properties, synchronously /// /// True if all properties validated successfully protected bool Validate() { try { return this.ValidateAsync().Result; } catch (AggregateException e) { // We're only ever going to get one InnerException here - let's be nice and unwrap it throw e.InnerException; } } /// /// Validate all properties. /// /// True if all properties validated successfully /// If you override this, you MUST fire ErrorsChanged as appropriate, and call ValidationStateChanged protected virtual async Task ValidateAsync() { if (this.Validator == null) throw new InvalidOperationException("Can't run validation if a validator hasn't been set"); // We need the ConfigureAwait(false), as we might be called synchronously // However this means that the stuff after the await can be run in parallel on multiple threads // Therefore, we need the lock // However, we can't raise PropertyChanged events from within the lock, otherwise deadlock var results = await this.Validator.ValidateAllPropertiesAsync().ConfigureAwait(false); if (results == null) results = new Dictionary>(); var changedProperties = new List(); await this.propertyErrorsLock.WaitAsync().ConfigureAwait(false); try { foreach (var kvp in results) { var newErrors = kvp.Value == null ? null : kvp.Value.ToArray(); if (!this.propertyErrors.ContainsKey(kvp.Key)) this.propertyErrors[kvp.Key] = newErrors; else if (this.ErrorsEqual(this.propertyErrors[kvp.Key], newErrors)) continue; else this.propertyErrors[kvp.Key] = newErrors; changedProperties.Add(kvp.Key); } // If they haven't included a key in their validation results, that counts as no validation error foreach (var removedKey in this.propertyErrors.Keys.Except(results.Keys).ToArray()) { this.propertyErrors[removedKey] = null; changedProperties.Add(removedKey); } } finally { this.propertyErrorsLock.Release(); } if (changedProperties.Count > 0) this.OnValidationStateChanged(changedProperties); return !this.HasErrors; } /// /// Record a property error (or clear an error on a property). You can use this independently of the validation done by /// /// Name of the property to change the errors for (or to change the errors for the whole model) /// The new errors, or null to clear errors for this property protected virtual void RecordPropertyError(Expression> property, string[] errors) { this.RecordPropertyError(property.NameForProperty(), errors); } /// /// Record a property error (or clear an error on a property). You can use this independently of the validation done by /// /// Name of the property to change the errors for (or to change the errors for the whole model) /// The new errors, or null to clear errors for this property protected virtual void RecordPropertyError(string propertyName, string[] errors) { if (propertyName == null) propertyName = String.Empty; bool changed = false; this.propertyErrorsLock.Wait(); try { string[] existingErrors; if (!this.propertyErrors.TryGetValue(propertyName, out existingErrors) || !this.ErrorsEqual(errors, existingErrors)) { this.propertyErrors[propertyName] = errors; changed = true; } } finally { this.propertyErrorsLock.Release(); } if (changed) { this.OnValidationStateChanged(new[] { propertyName }); } } /// /// Clear all property errors /// protected virtual void ClearAllPropertyErrors() { List changedProperties; this.propertyErrorsLock.Wait(); try { changedProperties = this.propertyErrors.Keys.ToList(); this.propertyErrors.Clear(); } finally { this.propertyErrorsLock.Release(); } if (changedProperties.Count > 0) { this.OnValidationStateChanged(changedProperties); } } /// /// Validate a single property synchronously, by name /// /// Type of property to validate /// Expression describing the property to validate /// True if the property validated successfully protected virtual bool ValidateProperty(Expression> property) { return this.ValidateProperty(property.NameForProperty()); } /// /// Validate a single property asynchronously, by name /// /// Type ofproperty to validate /// Expression describing the property to validate /// True if the property validated successfully protected virtual Task ValidatePropertyAsync(Expression> property) { return this.ValidatePropertyAsync(property.NameForProperty()); } /// /// Validate a single property synchronously, by name. /// /// Property to validate /// True if the property validated successfully protected bool ValidateProperty([CallerMemberName] string propertyName = null) { try { return this.ValidatePropertyAsync(propertyName).Result; } catch (AggregateException e) { // We're only ever going to get one InnerException here. Let's be nice and unwrap it throw e.InnerException; } } /// /// Validate a single property asynchronously, by name. /// /// Property to validate. Validates the entire model if null or /// True if the property validated successfully /// If you override this, you MUST fire ErrorsChanged and call OnValidationStateChanged() if appropriate protected virtual async Task ValidatePropertyAsync([CallerMemberName] string propertyName = null) { if (this.Validator == null) throw new InvalidOperationException("Can't run validation if a validator hasn't been set"); if (propertyName == null) propertyName = String.Empty; // To allow synchronous calling of this method, we need to resume on the ThreadPool. // Therefore, we might resume on any thread, hence the need for a lock var newErrorsRaw = await this.Validator.ValidatePropertyAsync(propertyName).ConfigureAwait(false); var newErrors = newErrorsRaw == null ? null : newErrorsRaw.ToArray(); bool propertyErrorsChanged = false; await this.propertyErrorsLock.WaitAsync().ConfigureAwait(false); try { if (!this.propertyErrors.ContainsKey(propertyName)) this.propertyErrors.Add(propertyName, null); if (!this.ErrorsEqual(this.propertyErrors[propertyName], newErrors)) { this.propertyErrors[propertyName] = newErrors; propertyErrorsChanged = true; } } finally { this.propertyErrorsLock.Release(); } if (propertyErrorsChanged) this.OnValidationStateChanged(new[] { propertyName }); return newErrors == null || newErrors.Length == 0; } /// /// Raise a PropertyChanged notification for the named property, and validate that property if this.validation is set and this.autoValidate is true /// /// Name of the property which has changed [EditorBrowsable(EditorBrowsableState.Never)] protected override async void OnPropertyChanged(string propertyName) { base.OnPropertyChanged(propertyName); // Save ourselves a little bit of work every time HasErrors is fired as the result of // the validation results changing. if (this.Validator != null && this.AutoValidate && propertyName != "HasErrors") await this.ValidatePropertyAsync(propertyName); } /// /// Called whenever the error state of any properties changes. Calls NotifyOfPropertyChange("HasErrors") by default /// /// List of property names which have changed validation state protected virtual void OnValidationStateChanged(IEnumerable changedProperties) { this.NotifyOfPropertyChange("HasErrors"); foreach (var property in changedProperties) { this.RaiseErrorsChanged(property); } } /// /// Raise the ErrorsChanged event for a given property /// /// Property to raise the ErrorsChanged event for protected virtual void RaiseErrorsChanged(string propertyName) { var handler = this.ErrorsChanged; if (handler != null) this.PropertyChangedDispatcher(() => handler(this, new DataErrorsChangedEventArgs(propertyName))); } /// /// Gets the validation errors for a specified property or for the entire entity. /// /// The name of the property to retrieve validation errors for; or null or System.String.Empty, to retrieve entity-level errors. /// The validation errors for the property or entity. public virtual IEnumerable GetErrors(string propertyName) { string[] errors; if (propertyName == null) propertyName = String.Empty; // We'll just have to wait synchronously for this. Oh well. The lock shouldn't be long. // Everything that awaits uses ConfigureAwait(false), so we shouldn't deadlock if someone calls this on the main thread this.propertyErrorsLock.Wait(); try { this.propertyErrors.TryGetValue(propertyName, out errors); } finally { this.propertyErrorsLock.Release(); } return errors; } /// /// Gets a value indicating whether the entity has validation errors. /// public virtual bool HasErrors { get { return this.propertyErrors.Values.Any(x => x != null && x.Length > 0); } } } }