9
9
using System . ComponentModel . DataAnnotations ;
10
10
using System . Diagnostics . Contracts ;
11
11
using System . Linq ;
12
+ using System . Linq . Expressions ;
12
13
using System . Reflection ;
13
14
using System . Runtime . CompilerServices ;
14
15
@@ -21,9 +22,9 @@ namespace Microsoft.Toolkit.Mvvm.ComponentModel
21
22
public abstract class ObservableValidator : ObservableObject , INotifyDataErrorInfo
22
23
{
23
24
/// <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 .
25
26
/// </summary>
26
- private static readonly ConditionalWeakTable < Type , PropertyInfo [ ] > ValidatableProperties = new ( ) ;
27
+ private static readonly ConditionalWeakTable < Type , Action < object > > EntityValidatorMap = new ( ) ;
27
28
28
29
/// <summary>
29
30
/// The cached <see cref="PropertyChangedEventArgs"/> for <see cref="HasErrors"/>.
@@ -458,26 +459,49 @@ IEnumerable<ValidationResult> GetAllErrors()
458
459
/// </remarks>
459
460
protected void ValidateAllProperties ( )
460
461
{
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 )
463
463
{
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 (
465
488
from property in type . GetProperties ( BindingFlags . Instance | BindingFlags . Public )
466
489
where property . GetIndexParameters ( ) . Length == 0 &&
467
490
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 ( ) ;
469
500
}
470
501
471
502
// Get or compute the cached list of properties to validate. Here we're using a static lambda to ensure the
472
503
// 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 ) ;
481
505
}
482
506
483
507
/// <summary>
0 commit comments