Skip to content

Commit 35c1b33

Browse files
committed
Fix the ValidatingModelBase, in the face of WPF asking for model-wide errors
Fixes #11
1 parent b6b9811 commit 35c1b33

File tree

3 files changed

+38
-21
lines changed

3 files changed

+38
-21
lines changed

Stylet/IValidationAdapter.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System.Collections.Generic;
1+
using System;
2+
using System.Collections.Generic;
23
using System.Threading.Tasks;
34

45
namespace Stylet
@@ -36,14 +37,16 @@ public interface IModelValidator
3637
/// <summary>
3738
/// Validate a single property by name, and return an array of validation errors for that property (or null if validation was successful)
3839
/// </summary>
39-
/// <param name="propertyName">Property to validate</param>
40-
/// <returns>Array of validation errors, or null if validation was successful</returns>
40+
/// <param name="propertyName">Property to validate, or <see cref="String.Empty"/> to validate the entire model</param>
41+
/// <returns>Array of validation errors, or null / empty if validation was successful</returns>
4142
Task<IEnumerable<string>> ValidatePropertyAsync(string propertyName);
4243

4344
/// <summary>
4445
/// Validate all properties, and return the results for all properties
4546
/// </summary>
4647
/// <remarks>
48+
/// Use a key of <see cref="String.Empty"/> to indicate validation errors for the entire model.
49+
///
4750
/// If a property validates successfully, you MUST return a null entry for it in the returned dictionary!
4851
/// </remarks>
4952
/// <returns>A dictionary of property name => array of validation errors (or null if that property validated successfully)</returns>

Stylet/ValidatingModelBase.cs

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -100,13 +100,14 @@ protected virtual async Task<bool> ValidateAsync()
100100
if (this.Validator == null)
101101
throw new InvalidOperationException("Can't run validation if a validator hasn't been set");
102102

103-
bool anyChanged = false;
104-
105103
// We need the ConfigureAwait(false), as we might be called synchronously
106104
// However this means that the stuff after the await can be run in parallel on multiple threads
107105
// Therefore, we need the lock
108106
// However, we can't raise PropertyChanged events from within the lock, otherwise deadlock
109107
var results = await this.Validator.ValidateAllPropertiesAsync().ConfigureAwait(false);
108+
if (results == null)
109+
results = new Dictionary<string, IEnumerable<string>>();
110+
110111
var changedProperties = new List<string>();
111112
await this.propertyErrorsLock.WaitAsync().ConfigureAwait(false);
112113
{
@@ -119,21 +120,19 @@ protected virtual async Task<bool> ValidateAsync()
119120
continue;
120121
else
121122
this.propertyErrors[kvp.Key] = newErrors;
122-
anyChanged = true;
123123
changedProperties.Add(kvp.Key);
124124
}
125125

126126
// If they haven't included a key in their validation results, that counts as no validation error
127127
foreach (var removedKey in this.propertyErrors.Keys.Except(results.Keys).ToArray())
128128
{
129129
this.propertyErrors[removedKey] = null;
130-
anyChanged = true;
131130
changedProperties.Add(removedKey);
132131
}
133132
}
134133
this.propertyErrorsLock.Release();
135134

136-
if (anyChanged)
135+
if (changedProperties.Count > 0)
137136
this.OnValidationStateChanged(changedProperties);
138137

139138
return !this.HasErrors;
@@ -182,16 +181,16 @@ protected bool ValidateProperty([CallerMemberName] string propertyName = null)
182181
/// <summary>
183182
/// Validate a single property asynchronously, by name.
184183
/// </summary>
185-
/// <param name="propertyName">Property to validate. Validates all properties if null or String.Empty</param>
184+
/// <param name="propertyName">Property to validate. Validates the entire model if null or <see cref="String.Empty"/></param>
186185
/// <returns>True if the property validated successfully</returns>
187186
/// <remarks>If you override this, you MUST fire ErrorsChanged and call OnValidationStateChanged() if appropriate</remarks>
188187
protected virtual async Task<bool> ValidatePropertyAsync([CallerMemberName] string propertyName = null)
189188
{
190189
if (this.Validator == null)
191190
throw new InvalidOperationException("Can't run validation if a validator hasn't been set");
192191

193-
if (String.IsNullOrEmpty(propertyName))
194-
return await this.ValidateAsync().ConfigureAwait(false);
192+
if (propertyName == null)
193+
propertyName = String.Empty;
195194

196195
// To allow synchronous calling of this method, we need to resume on the ThreadPool.
197196
// Therefore, we might resume on any thread, hence the need for a lock
@@ -234,12 +233,12 @@ protected override async void OnPropertyChanged(string propertyName)
234233
}
235234

236235
/// <summary>
237-
/// Called whenever the error state of any properties changes. Calls NotifyOfPropertyChange(() => this.HasErrors) by default
236+
/// Called whenever the error state of any properties changes. Calls NotifyOfPropertyChange("HasErrors") by default
238237
/// </summary>
239238
/// <param name="changedProperties">List of property names which have changed validation state</param>
240239
protected virtual void OnValidationStateChanged(IEnumerable<string> changedProperties)
241240
{
242-
this.NotifyOfPropertyChange(() => this.HasErrors);
241+
this.NotifyOfPropertyChange("HasErrors");
243242
foreach (var property in changedProperties)
244243
{
245244
this.RaiseErrorsChanged(property);
@@ -264,14 +263,16 @@ protected virtual void RaiseErrorsChanged(string propertyName)
264263
/// <returns>The validation errors for the property or entity.</returns>
265264
public virtual IEnumerable GetErrors(string propertyName)
266265
{
267-
string[] errors = null;
266+
string[] errors;
267+
268+
if (propertyName == null)
269+
propertyName = String.Empty;
268270

269271
// We'll just have to wait synchronously for this. Oh well. The lock shouldn't be long.
270272
// Everything that awaits uses ConfigureAwait(false), so we shouldn't deadlock if someone calls this on the main thread
271273
this.propertyErrorsLock.Wait();
272274
{
273-
if (this.propertyErrors.ContainsKey(propertyName))
274-
errors = this.propertyErrors[propertyName];
275+
this.propertyErrors.TryGetValue(propertyName, out errors);
275276
}
276277
this.propertyErrorsLock.Release();
277278

StyletUnitTests/ValidatingModelBaseTests.cs

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -137,19 +137,19 @@ public void ValidatePropertyAsyncByNameCallsAdapterValidate()
137137
}
138138

139139
[Test]
140-
public void ValidatePropertyAsyncWithNullCallsAdapterValidate()
140+
public void ValidatePropertyAsyncWitNullCallsAdapterValidatePropertyWithEmptyString()
141141
{
142-
this.validator.Setup(x => x.ValidateAllPropertiesAsync()).ReturnsAsync(new Dictionary<string, IEnumerable<string>>()).Verifiable();
143-
this.model.ValidatePropertyAsync(null).Wait();
142+
this.validator.Setup(x => x.ValidatePropertyAsync(String.Empty)).ReturnsAsync(Enumerable.Empty<string>()).Verifiable();
143+
this.model.ValidatePropertyAsync(String.Empty).Wait();
144144

145145
this.validator.Verify();
146146
}
147147

148148

149149
[Test]
150-
public void ValidatePropertyAsyncWithEmptyStringCallsAdapterValidate()
150+
public void ValidatePropertyAsyncWithEmptyStringCallsAdapterValidatePropertyWithEmptyString()
151151
{
152-
this.validator.Setup(x => x.ValidateAllPropertiesAsync()).ReturnsAsync(new Dictionary<string, IEnumerable<string>>()).Verifiable();
152+
this.validator.Setup(x => x.ValidatePropertyAsync(String.Empty)).ReturnsAsync(Enumerable.Empty<string>()).Verifiable();
153153
this.model.ValidatePropertyAsync(String.Empty).Wait();
154154

155155
this.validator.Verify();
@@ -342,6 +342,19 @@ public void GetErrorsReturnsErrorsForProperty()
342342
Assert.That(errors, Is.EquivalentTo(new[] { "error1", "error2" }));
343343
}
344344

345+
[Test]
346+
public void GetErrorsWithNullReturnsModelErrors()
347+
{
348+
this.validator.Setup(x => x.ValidateAllPropertiesAsync()).ReturnsAsync(new Dictionary<string, IEnumerable<string>>()
349+
{
350+
{ "", new[] { "error1", "error2" } }
351+
});
352+
353+
this.model.Validate();
354+
var errors = this.model.GetErrors(null);
355+
Assert.That(errors, Is.EquivalentTo(new[] { "error1", "error2" }));
356+
}
357+
345358
[Test]
346359
public void SettingPropertyValidatesIfAutoValidateIsTrue()
347360
{

0 commit comments

Comments
 (0)