@@ -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.
0 commit comments