99using System . ComponentModel . DataAnnotations ;
1010using System . Diagnostics . Contracts ;
1111using System . Linq ;
12+ using System . Linq . Expressions ;
1213using System . Reflection ;
1314using System . Runtime . CompilerServices ;
1415
@@ -21,9 +22,9 @@ namespace Microsoft.Toolkit.Mvvm.ComponentModel
2122 public abstract class ObservableValidator : ObservableObject , INotifyDataErrorInfo
2223 {
2324 /// <summary>
24- /// The <see cref="ConditionalWeakTable{TKey,TValue}"/> instance used to track properties to validate for a given viewmodel type .
25+ /// The <see cref="ConditionalWeakTable{TKey,TValue}"/> instance used to track compiled delegates to validate entities .
2526 /// </summary>
26- private static readonly ConditionalWeakTable < Type , PropertyInfo [ ] > ValidatableProperties = new ( ) ;
27+ private static readonly ConditionalWeakTable < Type , Action < object > > EntityValidatorMap = new ( ) ;
2728
2829 /// <summary>
2930 /// The cached <see cref="PropertyChangedEventArgs"/> for <see cref="HasErrors"/>.
@@ -458,26 +459,49 @@ IEnumerable<ValidationResult> GetAllErrors()
458459 /// </remarks>
459460 protected void ValidateAllProperties ( )
460461 {
461- // Helper method to discover all the properties to validate in the current viewmodel type
462- static PropertyInfo [ ] GetValidatableProperties ( Type type )
462+ static Action < object > GetValidationAction ( Type type )
463463 {
464- return (
464+ // MyViewModel inst0 = (MyViewModel)arg0;
465+ ParameterExpression arg0 = Expression . Parameter ( typeof ( object ) ) ;
466+ UnaryExpression inst0 = Expression . Convert ( arg0 , type ) ;
467+
468+ // Get a reference to ValidateProperty(object, string)
469+ MethodInfo validateMethod = typeof ( ObservableValidator ) . GetMethod ( nameof ( ValidateProperty ) , BindingFlags . Instance | BindingFlags . NonPublic ) ! ;
470+
471+ // We want a single compiled LINQ expression that validates all properties in the
472+ // actual type of the executing viewmodel at once. We do this by creating a block
473+ // expression with the unrolled invocations of all properties to validate.
474+ // Essentially, the body will contain the following code:
475+ // ===============================================================================
476+ // {
477+ // inst0.ValidateProperty(inst0.Property0, nameof(MyViewModel.Property0));
478+ // inst0.ValidateProperty(inst0.Property1, nameof(MyViewModel.Property1));
479+ // ...
480+ // }
481+ // ===============================================================================
482+ // We also add an explicit object conversion to represent boxing, if a given property
483+ // is a value type. It will just be a no-op if the value is a reference type already.
484+ // Note that this generated code is technically accessing a protected method from
485+ // ObservableValidator externally, but that is fine because IL doesn't really have
486+ // a concept of member visibility, that's purely a C# build-time feature.
487+ BlockExpression body = Expression . Block (
465488 from property in type . GetProperties ( BindingFlags . Instance | BindingFlags . Public )
466489 where property . GetIndexParameters ( ) . Length == 0 &&
467490 property . GetCustomAttributes < ValidationAttribute > ( true ) . Any ( )
468- select property ) . ToArray ( ) ;
491+ let getter = property . GetMethod
492+ where getter is not null
493+ select Expression . Call ( inst0 , validateMethod , new Expression [ ]
494+ {
495+ Expression . Convert ( Expression . Call ( inst0 , getter ) , typeof ( object ) ) ,
496+ Expression . Constant ( property . Name )
497+ } ) ) ;
498+
499+ return Expression . Lambda < Action < object > > ( body , arg0 ) . Compile ( ) ;
469500 }
470501
471502 // Get or compute the cached list of properties to validate. Here we're using a static lambda to ensure the
472503 // delegate is cached by the C# compiler, see the related issue at https://github.com/dotnet/roslyn/issues/5835.
473- PropertyInfo [ ] propertyInfos = ValidatableProperties . GetValue ( GetType ( ) , static t => GetValidatableProperties ( t ) ) ;
474-
475- foreach ( PropertyInfo propertyInfo in propertyInfos )
476- {
477- object ? propertyValue = propertyInfo . GetValue ( this ) ;
478-
479- ValidateProperty ( propertyValue , propertyInfo . Name ) ;
480- }
504+ EntityValidatorMap . GetValue ( GetType ( ) , static t => GetValidationAction ( t ) ) ( this ) ;
481505 }
482506
483507 /// <summary>
0 commit comments