| | 1 | | using System.Collections; |
| | 2 | | using System.ComponentModel; |
| | 3 | | using System.ComponentModel.DataAnnotations; |
| | 4 | | using System.Reflection; |
| | 5 | | using System.Runtime.CompilerServices; |
| | 6 | |
|
| | 7 | | namespace Common.Core.Classes; |
| | 8 | |
|
| | 9 | | /// <summary>Base class for models that required the INotifyDataErrorInfo and |
| | 10 | | /// INotifyPropertyChanged interfaces.</summary> |
| | 11 | | /// <remarks>It's base class is <see cref="ModelBase"/> so that functionality is included.<br/> |
| | 12 | | /// It can be used as a base class, or as an instance variable.</remarks> |
| | 13 | | /// <example> |
| | 14 | | /// In a class using it as a base class, the protected constructor will be used: |
| | 15 | | /// <code language="C#"> |
| | 16 | | /// using System.ComponentModel.DataAnnotations; |
| | 17 | | /// using Common.Core.Classes; |
| | 18 | | /// |
| | 19 | | /// public class TestModel : ModelDataError |
| | 20 | | /// { |
| | 21 | | /// public bool IsValid => !HasErrors; |
| | 22 | | /// |
| | 23 | | /// private string? _name; |
| | 24 | | /// [Display( Name = "Full Name" )] |
| | 25 | | /// [Required( ErrorMessage = "Name cannot be empty." )] |
| | 26 | | /// public string Name |
| | 27 | | /// { |
| | 28 | | /// get => ( _name is not null ) ? _name : string.Empty; |
| | 29 | | /// set |
| | 30 | | /// { |
| | 31 | | /// if( value.Equals( _name ) ) return; |
| | 32 | | /// |
| | 33 | | /// name = value; |
| | 34 | | /// ValidateProperty( value ); |
| | 35 | | /// OnPropertyChanged(); |
| | 36 | | /// } |
| | 37 | | /// } |
| | 38 | | /// |
| | 39 | | /// public TestModel() |
| | 40 | | /// { } |
| | 41 | | /// } |
| | 42 | | /// </code> |
| | 43 | | /// In a class using it as an instance variable, the public constructor must be used. |
| | 44 | | /// <br/>The INotifyDataErrorInfo interface must be implemented and the event handler needs to |
| | 45 | | /// subscribe to the core error version so that the ErrorsChanged event is seen as being used: |
| | 46 | | /// <code language="C#"> |
| | 47 | | /// using System.Collections; |
| | 48 | | /// using System.ComponentModel; |
| | 49 | | /// using System.ComponentModel.DataAnnotations; |
| | 50 | | /// using Common.Core.Classes; |
| | 51 | | /// |
| | 52 | | /// public class TestModel : INotifyDataErrorInfo |
| | 53 | | /// { |
| | 54 | | /// public bool IsValid => !_validator.HasErrors; |
| | 55 | | /// |
| | 56 | | /// private string? _name; |
| | 57 | | /// [Display( Name = "Full Name" )] |
| | 58 | | /// [Required( ErrorMessage = "Name cannot be empty." )] |
| | 59 | | /// public string Name |
| | 60 | | /// { |
| | 61 | | /// get => ( _name is not null ) ? _name : string.Empty; |
| | 62 | | /// set |
| | 63 | | /// { |
| | 64 | | /// if( value.Equals( _name ) ) return; |
| | 65 | | /// name = value; |
| | 66 | | /// _validator.ValidateProperty( value ); |
| | 67 | | /// OnPropertyChanged(); |
| | 68 | | /// } |
| | 69 | | /// } |
| | 70 | | /// |
| | 71 | | /// private readonly ModelDataError _validator; |
| | 72 | | /// |
| | 73 | | /// public TestModel() |
| | 74 | | /// { |
| | 75 | | /// // Do this before anything else |
| | 76 | | /// _validator = new ModelDataError( this ); |
| | 77 | | /// _validator.ErrorsChanged += Core_ErrorsChanged; |
| | 78 | | /// } |
| | 79 | | /// |
| | 80 | | /// public bool HasErrors => _validator.HasErrors; |
| | 81 | | /// |
| | 82 | | /// public IEnumerable GetErrors( string? propertyName ) |
| | 83 | | /// { |
| | 84 | | /// return _validator.GetErrors( propertyName ); |
| | 85 | | /// } |
| | 86 | | /// |
| | 87 | | /// public event EventHandler<DataErrorsChangedEventArgs>? ErrorsChanged; |
| | 88 | | /// |
| | 89 | | /// private void Core_ErrorsChanged( object? sender, DataErrorsChangedEventArgs e ) |
| | 90 | | /// { |
| | 91 | | /// ErrorsChanged?.Invoke( this, e ); |
| | 92 | | /// } |
| | 93 | | /// } |
| | 94 | | /// </code> |
| | 95 | | /// </example> |
| | 96 | | public class ModelDataError : ModelBase, INotifyDataErrorInfo |
| | 97 | | { |
| | 98 | | #region INotifyDataErrorInfo Implementation |
| | 99 | |
|
| | 100 | | /// <summary>Gets a value that indicates whether the entity has validation errors.</summary> |
| 5 | 101 | | public bool HasErrors => _errors.Any( propErrors => propErrors.Value.Count > 0 ); |
| | 102 | |
|
| | 103 | | /// <summary>Gets the validation errors for a specified property or for the entire entity.</summary> |
| | 104 | | /// <param name="propertyName">The name of the property to retrieve validation errors for; or <see langword="null"/> |
| | 105 | | /// or Empty, to retrieve entity-level errors.</param> |
| | 106 | | /// <returns>The validation errors for the property or entity.</returns> |
| | 107 | | public IEnumerable GetErrors( string? propertyName ) |
| 3 | 108 | | { |
| | 109 | | // Get entity-level errors when the target property is null or empty |
| 3 | 110 | | if( string.IsNullOrEmpty( propertyName ) ) |
| 1 | 111 | | { |
| | 112 | | // Local function to gather all the entity-level errors |
| | 113 | | [MethodImpl( MethodImplOptions.NoInlining )] |
| | 114 | | IEnumerable<ValidationResult> GetAllErrors() |
| 1 | 115 | | { |
| 1 | 116 | | return _errors.Values.SelectMany( static errors => errors ); |
| 1 | 117 | | } |
| | 118 | |
|
| 1 | 119 | | return GetAllErrors(); |
| | 120 | | } |
| | 121 | |
|
| | 122 | | // Property-level errors, if any |
| 2 | 123 | | if( propertyName is not null && _errors.TryGetValue( propertyName, out List<ValidationResult>? value ) ) |
| 1 | 124 | | { |
| 1 | 125 | | return value; |
| | 126 | | } |
| | 127 | |
|
| | 128 | | // Property not found |
| 1 | 129 | | return Array.Empty<ValidationResult>(); |
| 3 | 130 | | } |
| | 131 | |
|
| | 132 | | /// <summary>Occurs when the validation errors have changed for a property or for the entire entity.</summary> |
| | 133 | | public event EventHandler<DataErrorsChangedEventArgs>? ErrorsChanged; |
| | 134 | |
|
| | 135 | | #endregion |
| | 136 | |
|
| | 137 | | #region Constructors and Variables |
| | 138 | |
|
| 5 | 139 | | private readonly Dictionary<string, List<ValidationResult>> _errors = []; |
| 5 | 140 | | private readonly ConditionalWeakTable<Type, Dictionary<string, string>> _displayNamesMap = []; |
| | 141 | | private readonly PropertyInfo[] _properties; |
| | 142 | | private readonly ValidationContext _context; |
| | 143 | |
|
| | 144 | | /// <summary>Initializes a new instance of the ModelDataError class.</summary> |
| 1 | 145 | | protected ModelDataError() |
| 1 | 146 | | { |
| 1 | 147 | | _properties = GetProperties( GetType() ); |
| 1 | 148 | | _context = new ValidationContext( this ); |
| 1 | 149 | | } |
| | 150 | |
|
| | 151 | | /// <summary>Initializes a new instance of the ModelDataError class.</summary> |
| | 152 | | /// <param name="instance">Object being validated.</param> |
| 4 | 153 | | public ModelDataError( object instance ) |
| 4 | 154 | | { |
| 4 | 155 | | _properties = GetProperties( instance.GetType() ); |
| 4 | 156 | | _context = new ValidationContext( instance ); |
| 4 | 157 | | } |
| | 158 | |
|
| | 159 | | #endregion |
| | 160 | |
|
| | 161 | | #region Public Methods |
| | 162 | |
|
| | 163 | | /// <summary>Validates all the properties in the current instance.</summary> |
| | 164 | | public void ValidateAllProperties() |
| 3 | 165 | | { |
| 21 | 166 | | foreach( var propertyInfo in _properties ) |
| 6 | 167 | | { |
| 6 | 168 | | ValidateProperty( propertyInfo.GetValue( _context.ObjectInstance ), propertyInfo.Name ); |
| 6 | 169 | | } |
| 3 | 170 | | } |
| | 171 | |
|
| | 172 | | /// <summary>Validates a property with a specified name and a given input value.</summary> |
| | 173 | | /// <param name="value">The value to test for the specified property.</param> |
| | 174 | | /// <param name="propertyName">The name of the property to validate.</param> |
| | 175 | | /// <returns><see langword="true"/> if the value is valid, <see langword="false"/> if any errors are found.</returns> |
| | 176 | | public bool ValidateProperty( object? value, [CallerMemberName] string propertyName = "" ) |
| 7 | 177 | | { |
| 7 | 178 | | if( string.IsNullOrEmpty( propertyName ) ) return true; |
| | 179 | |
|
| 7 | 180 | | _context.MemberName = propertyName; |
| 7 | 181 | | _context.DisplayName = GetDisplayNameForProperty( propertyName ); |
| | 182 | |
|
| 7 | 183 | | var results = new List<ValidationResult>(); |
| | 184 | | try |
| 7 | 185 | | { |
| 7 | 186 | | Validator.TryValidateProperty( value, _context, results ); |
| 6 | 187 | | } |
| 1 | 188 | | catch( Exception ex ) |
| 1 | 189 | | { |
| | 190 | | // Handle System.InvalidCastException exception |
| | 191 | | // Unable to cast object of type 'System.DateTime' to type 'System.String'. |
| 1 | 192 | | results.Add( new ValidationResult( ex.Message ) ); |
| 1 | 193 | | } |
| 7 | 194 | | ClearErrors( propertyName ); |
| 7 | 195 | | AddValidationResults( results ); |
| | 196 | |
|
| 7 | 197 | | return results.Count == 0; |
| 7 | 198 | | } |
| | 199 | |
|
| | 200 | | /// <summary>Clears the validation errors for a specified property or for the entire entity.</summary> |
| | 201 | | /// <param name="propertyName">The name of the property to clear validation errors for.<br/> |
| | 202 | | /// If a <see langword="null"/> or empty name is used, all entity-level errors will be cleared. |
| | 203 | | /// </param> |
| | 204 | | public void ClearErrors( string? propertyName = null ) |
| 10 | 205 | | { |
| 10 | 206 | | if( !string.IsNullOrEmpty( propertyName ) ) |
| 9 | 207 | | { |
| 9 | 208 | | if( _errors.Remove( propertyName ) ) |
| 2 | 209 | | { |
| 2 | 210 | | OnErrorsChanged( propertyName ); |
| 2 | 211 | | } |
| 9 | 212 | | } |
| | 213 | | else |
| 1 | 214 | | { |
| 7 | 215 | | foreach( KeyValuePair<string, List<ValidationResult>> property in _errors ) |
| 2 | 216 | | { |
| 2 | 217 | | ClearErrors( property.Key ); |
| 2 | 218 | | } |
| 1 | 219 | | } |
| 10 | 220 | | } |
| | 221 | |
|
| | 222 | | #endregion |
| | 223 | |
|
| | 224 | | #region Private Methods |
| | 225 | |
|
| | 226 | | private void OnErrorsChanged( string propertyName ) |
| 6 | 227 | | { |
| 6 | 228 | | ErrorsChanged?.Invoke( this, new DataErrorsChangedEventArgs( propertyName ) ); |
| 6 | 229 | | } |
| | 230 | |
|
| | 231 | | private void AddValidationResults( List<ValidationResult> results ) |
| 7 | 232 | | { |
| 11 | 233 | | if( results.Count == 0 ) { return; } |
| | 234 | |
|
| | 235 | | // Group validation results by property names |
| 5 | 236 | | var resultsByPropName = from res in results |
| 9 | 237 | | from mname in res.MemberNames |
| 8 | 238 | | group res by mname into g |
| 9 | 239 | | select g; |
| | 240 | |
|
| 23 | 241 | | foreach( var property in resultsByPropName ) |
| 4 | 242 | | { |
| 8 | 243 | | results = property.Select( res => res ).ToList(); |
| 4 | 244 | | if( results.Count > 0 ) |
| 4 | 245 | | { |
| 20 | 246 | | foreach( var result in results ) |
| 4 | 247 | | { |
| 4 | 248 | | AddError( property.Key, result ); |
| 4 | 249 | | } |
| 4 | 250 | | } |
| 4 | 251 | | } |
| 7 | 252 | | } |
| | 253 | |
|
| | 254 | | private void AddError( string propertyName, ValidationResult error ) |
| 4 | 255 | | { |
| 4 | 256 | | if( !_errors.TryGetValue( propertyName, out List<ValidationResult>? value ) ) |
| 4 | 257 | | { |
| 4 | 258 | | value = ( [] ); |
| 4 | 259 | | _errors[propertyName] = value; |
| 4 | 260 | | } |
| | 261 | |
|
| 4 | 262 | | value.Add( error ); |
| 4 | 263 | | OnErrorsChanged( propertyName ); |
| 4 | 264 | | } |
| | 265 | |
|
| | 266 | | private static PropertyInfo[] GetProperties<T>( T type ) where T : Type |
| 5 | 267 | | { |
| | 268 | | // Get all properties with data annotations |
| 5 | 269 | | PropertyInfo[] validationProperties = ( |
| 5 | 270 | | from propInfo in type.GetProperties( BindingFlags.Instance | BindingFlags.Public ) |
| 10 | 271 | | where propInfo.GetIndexParameters().Length == 0 && |
| 10 | 272 | | propInfo.GetCustomAttributes<ValidationAttribute>( true ).Any() |
| 5 | 273 | | select propInfo ).ToArray(); |
| | 274 | |
|
| 5 | 275 | | return validationProperties; |
| 5 | 276 | | } |
| | 277 | |
|
| | 278 | | private string GetDisplayNameForProperty( string propertyName ) |
| 7 | 279 | | { |
| | 280 | | static Dictionary<string, string> GetDisplayNames( Type type ) |
| 4 | 281 | | { |
| 4 | 282 | | Dictionary<string, string> displayNames = []; |
| | 283 | |
|
| 28 | 284 | | foreach( PropertyInfo property in type.GetProperties( BindingFlags.Instance | BindingFlags.Public ) ) |
| 8 | 285 | | { |
| 8 | 286 | | if( property.GetCustomAttribute<DisplayAttribute>() is DisplayAttribute attribute && |
| 8 | 287 | | attribute.GetName() is string displayName ) |
| 3 | 288 | | { |
| 3 | 289 | | displayNames.Add( property.Name, displayName ); |
| 3 | 290 | | } |
| 8 | 291 | | } |
| | 292 | |
|
| 4 | 293 | | return displayNames; |
| 4 | 294 | | } |
| | 295 | |
|
| 7 | 296 | | _ = _displayNamesMap.GetValue( _context.ObjectInstance.GetType(), |
| 11 | 297 | | static t => GetDisplayNames( t ) ).TryGetValue( propertyName, out string? displayName ); |
| | 298 | |
|
| 7 | 299 | | return displayName ?? propertyName; |
| 7 | 300 | | } |
| | 301 | |
|
| | 302 | | #endregion |
| | 303 | | } |