Skip to content

Commit 2e10189

Browse files
committed
Switched to LINQ expressions for ValidateAllProperties
1 parent ae01304 commit 2e10189

File tree

1 file changed

+38
-14
lines changed

1 file changed

+38
-14
lines changed

Microsoft.Toolkit.Mvvm/ComponentModel/ObservableValidator.cs

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using System.ComponentModel.DataAnnotations;
1010
using System.Diagnostics.Contracts;
1111
using System.Linq;
12+
using System.Linq.Expressions;
1213
using System.Reflection;
1314
using 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

Comments
 (0)