Skip to content

Commit 7464bd0

Browse files
committed
Enabled notification for ObservableValidator.HasErrors
1 parent db2742a commit 7464bd0

File tree

2 files changed

+46
-18
lines changed

2 files changed

+46
-18
lines changed

Microsoft.Toolkit.Mvvm/ComponentModel/ObservableValidator.cs

Lines changed: 41 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,33 +19,28 @@ namespace Microsoft.Toolkit.Mvvm.ComponentModel
1919
/// </summary>
2020
public abstract class ObservableValidator : ObservableObject, INotifyDataErrorInfo
2121
{
22+
/// <summary>
23+
/// The cached <see cref="PropertyChangedEventArgs"/> for <see cref="HasErrors"/>.
24+
/// </summary>
25+
private static readonly PropertyChangedEventArgs HasErrorsChangedEventArgs = new PropertyChangedEventArgs(nameof(HasErrors));
26+
2227
/// <summary>
2328
/// The <see cref="Dictionary{TKey,TValue}"/> instance used to store previous validation results.
2429
/// </summary>
2530
private readonly Dictionary<string, List<ValidationResult>> errors = new Dictionary<string, List<ValidationResult>>();
2631

32+
/// <summary>
33+
/// Indicates the total number of properties with errors (not total errors).
34+
/// This is used to allow <see cref="HasErrors"/> to operate in O(1) time, as it can just
35+
/// check whether this value is not 0 instead of having to traverse <see cref="errors"/>.
36+
/// </summary>
37+
private int totalErrors;
38+
2739
/// <inheritdoc/>
2840
public event EventHandler<DataErrorsChangedEventArgs>? ErrorsChanged;
2941

3042
/// <inheritdoc/>
31-
public bool HasErrors
32-
{
33-
get
34-
{
35-
// This uses the value enumerator for Dictionary<TKey, TValue>.ValueCollection, so it doesn't
36-
// allocate. Accessing this property is O(n), but we can stop as soon as we find at least one
37-
// error in the whole entity, and doing this saves 8 bytes in the object size (no fields needed).
38-
foreach (var value in this.errors.Values)
39-
{
40-
if (value.Count > 0)
41-
{
42-
return true;
43-
}
44-
}
45-
46-
return false;
47-
}
48-
}
43+
public bool HasErrors => this.totalErrors > 0;
4944

5045
/// <summary>
5146
/// Compares the current and new values for a given property. If the value has changed,
@@ -285,6 +280,34 @@ private void ValidateProperty(object? value, string? propertyName)
285280
new ValidationContext(this, null, null) { MemberName = propertyName },
286281
propertyErrors);
287282

283+
// Update the shared counter for the number of errors, and raise the
284+
// property changed event if necessary. We decrement the number of total
285+
// errors if the current property is valid but it wasn't so before this
286+
// validation, and we increment it if the validation failed after being
287+
// correct before. The property changed event is raised whenever the
288+
// number of total errors is either decremented to 0, or incremented to 1.
289+
if (isValid)
290+
{
291+
if (errorsChanged)
292+
{
293+
this.totalErrors--;
294+
295+
if (this.totalErrors == 0)
296+
{
297+
OnPropertyChanged(HasErrorsChangedEventArgs);
298+
}
299+
}
300+
}
301+
else if (!errorsChanged)
302+
{
303+
this.totalErrors++;
304+
305+
if (this.totalErrors == 1)
306+
{
307+
OnPropertyChanged(HasErrorsChangedEventArgs);
308+
}
309+
}
310+
288311
// Only raise the event once if needed. This happens either when the target property
289312
// had existing errors and is now valid, or if the validation has failed and there are
290313
// new errors to broadcast, regardless of the previous validation state for the property.

UnitTests/UnitTests.Shared/Mvvm/Test_ObservableValidator.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,21 @@ public class Test_ObservableValidator
1919
public void Test_ObservableValidator_HasErrors()
2020
{
2121
var model = new Person();
22+
var args = new List<PropertyChangedEventArgs>();
2223

2324
Assert.IsFalse(model.HasErrors);
2425

2526
model.Name = "No";
2627

2728
Assert.IsTrue(model.HasErrors);
29+
Assert.AreEqual(args.Count, 1);
30+
Assert.AreEqual(args[0].PropertyName, nameof(INotifyDataErrorInfo.HasErrors));
2831

2932
model.Name = "Valid";
3033

3134
Assert.IsFalse(model.HasErrors);
35+
Assert.AreEqual(args.Count, 2);
36+
Assert.AreEqual(args[2].PropertyName, nameof(INotifyDataErrorInfo.HasErrors));
3237
}
3338

3439
[TestCategory("Mvvm")]

0 commit comments

Comments
 (0)