From 13348aff283e7f8705b08c288136f5a976e49725 Mon Sep 17 00:00:00 2001 From: Antony Male Date: Sun, 4 May 2014 18:53:31 +0100 Subject: [PATCH] Add synchronous versions of the validate methods on ValidatingModelBase --- Stylet/ValidatingModelBase.cs | 119 +++++++++++++++++++++++++++------- 1 file changed, 94 insertions(+), 25 deletions(-) diff --git a/Stylet/ValidatingModelBase.cs b/Stylet/ValidatingModelBase.cs index 99acc36..e871a64 100644 --- a/Stylet/ValidatingModelBase.cs +++ b/Stylet/ValidatingModelBase.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Linq.Expressions; using System.Runtime.CompilerServices; using System.Text; +using System.Threading; using System.Threading.Tasks; namespace Stylet @@ -20,6 +21,7 @@ namespace Stylet /// public event EventHandler ErrorsChanged; + private readonly SemaphoreSlim propertyErrorsLock = new SemaphoreSlim(1, 1); private readonly Dictionary propertyErrors = new Dictionary(); private IValidatorAdapter _validator; @@ -41,11 +43,18 @@ namespace Stylet /// protected bool autoValidate { get; set; } + /// + /// Instantiate, without using an IValidatorAdapter + /// public ValidatingModelBase() { this.autoValidate = true; } + /// + /// Instantiate, using the specified IValidatorAdapter + /// + /// Validator adapter to use to perform validations public ValidatingModelBase(IValidatorAdapter validator) : this() { this.validator = validator; @@ -61,8 +70,19 @@ namespace Stylet } /// - /// Validate all properties. If you override this, you MUST fire ErrorsChanged as appropriate, and call ValidationStateChanged + /// Validate all properties, synchronously /// + /// True if all properties validated successfully + protected virtual bool Validate() + { + return this.ValidateAsync().Result; + } + + /// + /// 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) @@ -71,19 +91,27 @@ namespace Stylet var handler = this.ErrorsChanged; bool anyChanged = false; - foreach (var kvp in await this.validator.ValidateAllPropertiesAsync()) + // 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 + var results = await this.validator.ValidateAllPropertiesAsync().ConfigureAwait(false); + await this.propertyErrorsLock.WaitAsync().ConfigureAwait(false); { - if (!this.propertyErrors.ContainsKey(kvp.Key)) - this.propertyErrors.Add(kvp.Key, null); + foreach (var kvp in results) + { + if (!this.propertyErrors.ContainsKey(kvp.Key)) + this.propertyErrors[kvp.Key] = kvp.Value; - if (this.ErrorsEqual(this.propertyErrors[kvp.Key], kvp.Value)) - continue; + if (this.ErrorsEqual(this.propertyErrors[kvp.Key], kvp.Value)) + continue; - this.propertyErrors[kvp.Key] = kvp.Value; - anyChanged = true; - if (handler != null) - handler(this, new DataErrorsChangedEventArgs(kvp.Key)); + this.propertyErrors[kvp.Key] = kvp.Value; + anyChanged = true; + if (handler != null) + this.PropertyChangedDispatcher(() => handler(this, new DataErrorsChangedEventArgs(kvp.Key))); + } } + this.propertyErrorsLock.Release(); if (anyChanged) this.OnValidationStateChanged(); @@ -92,38 +120,70 @@ namespace Stylet } /// - /// Call ValidateProperty, deriving the name of the property in a type-safe manner + /// Validate a single property synchronously, by name /// /// Expression describing the property to validate - /// True if the property validates successfully + /// True if the property validated successfully + protected virtual bool ValidateProperty(Expression> property) + { + return this.ValidateProperty(property.NameForProperty()); + } + + /// + /// Validate a single property asynchronously, by name + /// + /// 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, by name. If you override this, you MUST fire ErrorsChange and call OnValidationStateChanged() if appropriate + /// Validate a single property synchronously, by name. /// /// Property to validate /// True if the property validated successfully + protected virtual bool ValidateProperty([CallerMemberName] string propertyName = null) + { + return this.ValidatePropertyAsync(propertyName).Result; + } + + /// + /// Validate a single property asynchronously, by name. + /// + /// Property to validate + /// True if the property validated successfully + /// If you override this, you MUST fire ErrorsChange 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 (!this.propertyErrors.ContainsKey(propertyName)) - this.propertyErrors.Add(propertyName, null); + // 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 newErrors = await this.validator.ValidatePropertyAsync(propertyName).ConfigureAwait(false); + bool propertyErrorsChanged = false; - var newErrors = await this.validator.ValidatePropertyAsync(propertyName); - if (!this.ErrorsEqual(this.propertyErrors[propertyName], newErrors)) + await this.propertyErrorsLock.WaitAsync().ConfigureAwait(false); { - this.propertyErrors[propertyName] = newErrors; + if (!this.propertyErrors.ContainsKey(propertyName)) + this.propertyErrors.Add(propertyName, null); - var handler = this.ErrorsChanged; - if (handler != null) - handler(this, new DataErrorsChangedEventArgs(propertyName)); - this.OnValidationStateChanged(); + if (!this.ErrorsEqual(this.propertyErrors[propertyName], newErrors)) + { + this.propertyErrors[propertyName] = newErrors; + propertyErrorsChanged = true; + var handler = this.ErrorsChanged; + if (handler != null) + this.PropertyChangedDispatcher(() => handler(this, new DataErrorsChangedEventArgs(propertyName))); + } } + this.propertyErrorsLock.Release(); + + if (propertyErrorsChanged) + this.OnValidationStateChanged(); + return newErrors == null || newErrors.Length == 0; } @@ -152,9 +212,18 @@ namespace Stylet /// The validation errors for the property or entity. public IEnumerable GetErrors(string propertyName) { - if (this.propertyErrors.ContainsKey(propertyName)) - return this.propertyErrors[propertyName]; - return null; + string[] errors = null; + + // 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(); + { + if (this.propertyErrors.ContainsKey(propertyName)) + errors = this.propertyErrors[propertyName]; + } + this.propertyErrorsLock.Release(); + + return errors; } ///