Skip to content

Commit fc5287e

Browse files
Merge pull request #4282 from Sergio0694/bugfix/validator-validateall-netfx
Fix MVVM Toolkit LINQ expression issues on .NET Framework
2 parents e471616 + cd5cc40 commit fc5287e

File tree

4 files changed

+128
-13
lines changed

4 files changed

+128
-13
lines changed

Microsoft.Toolkit.Mvvm/ComponentModel/ObservableValidator.cs

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,21 @@ static Action<object> GetValidationAction(Type type)
488488
// Fallback method to create the delegate with a compiled LINQ expression
489489
static Action<object> GetValidationActionFallback(Type type)
490490
{
491+
// Get the collection of all properties to validate
492+
(string Name, MethodInfo GetMethod)[] validatableProperties = (
493+
from property in type.GetProperties(BindingFlags.Instance | BindingFlags.Public)
494+
where property.GetIndexParameters().Length == 0 &&
495+
property.GetCustomAttributes<ValidationAttribute>(true).Any()
496+
let getMethod = property.GetMethod
497+
where getMethod is not null
498+
select (property.Name, getMethod)).ToArray();
499+
500+
// Short path if there are no properties to validate
501+
if (validatableProperties.Length == 0)
502+
{
503+
return static _ => { };
504+
}
505+
491506
// MyViewModel inst0 = (MyViewModel)arg0;
492507
ParameterExpression arg0 = Expression.Parameter(typeof(object));
493508
UnaryExpression inst0 = Expression.Convert(arg0, type);
@@ -513,14 +528,10 @@ static Action<object> GetValidationActionFallback(Type type)
513528
// ObservableValidator externally, but that is fine because IL doesn't really have
514529
// a concept of member visibility, that's purely a C# build-time feature.
515530
BlockExpression body = Expression.Block(
516-
from property in type.GetProperties(BindingFlags.Instance | BindingFlags.Public)
517-
where property.GetIndexParameters().Length == 0 &&
518-
property.GetCustomAttributes<ValidationAttribute>(true).Any()
519-
let getter = property.GetMethod
520-
where getter is not null
531+
from property in validatableProperties
521532
select Expression.Call(inst0, validateMethod, new Expression[]
522533
{
523-
Expression.Convert(Expression.Call(inst0, getter), typeof(object)),
534+
Expression.Convert(Expression.Call(inst0, property.GetMethod), typeof(object)),
524535
Expression.Constant(property.Name)
525536
}));
526537

Microsoft.Toolkit.Mvvm/Messaging/IMessengerExtensions.cs

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,20 @@ static Action<IMessenger, object, TToken> LoadRegistrationMethodsForType(Type re
153153
// The LINQ codegen bloat is not really important for the same reason.
154154
static Action<IMessenger, object, TToken> LoadRegistrationMethodsForTypeFallback(Type recipientType)
155155
{
156+
// Get the collection of validation methods
157+
MethodInfo[] registrationMethods = (
158+
from interfaceType in recipientType.GetInterfaces()
159+
where interfaceType.IsGenericType &&
160+
interfaceType.GetGenericTypeDefinition() == typeof(IRecipient<>)
161+
let messageType = interfaceType.GenericTypeArguments[0]
162+
select MethodInfos.RegisterIRecipient.MakeGenericMethod(messageType, typeof(TToken))).ToArray();
163+
164+
// Short path if there are no message handlers to register
165+
if (registrationMethods.Length == 0)
166+
{
167+
return static (_, _, _) => { };
168+
}
169+
156170
// Input parameters (IMessenger instance, non-generic recipient, token)
157171
ParameterExpression
158172
arg0 = Expression.Parameter(typeof(IMessenger)),
@@ -178,11 +192,7 @@ static Action<IMessenger, object, TToken> LoadRegistrationMethodsForTypeFallback
178192
// We also add an explicit object conversion to cast the input recipient type to
179193
// the actual specific type, so that the exposed message handlers are accessible.
180194
BlockExpression body = Expression.Block(
181-
from interfaceType in recipientType.GetInterfaces()
182-
where interfaceType.IsGenericType &&
183-
interfaceType.GetGenericTypeDefinition() == typeof(IRecipient<>)
184-
let messageType = interfaceType.GenericTypeArguments[0]
185-
let registrationMethod = MethodInfos.RegisterIRecipient.MakeGenericMethod(messageType, typeof(TToken))
195+
from registrationMethod in registrationMethods
186196
select Expression.Call(registrationMethod, new Expression[]
187197
{
188198
arg0,

UnitTests/UnitTests.NetCore/Mvvm/Test_ObservablePropertyAttribute.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
using Microsoft.Toolkit.Mvvm.ComponentModel;
1313
using Microsoft.VisualStudio.TestTools.UnitTesting;
1414

15-
#pragma warning disable SA1124
15+
#pragma warning disable SA1124, SA1307, SA1401
1616

1717
#nullable enable
1818

@@ -371,7 +371,7 @@ public partial class ModelWithValuePropertyWithValidation : ObservableValidator
371371
[MinLength(5)]
372372
private string? value;
373373
}
374-
374+
375375
public partial class ViewModelWithValidatableGeneratedProperties : ObservableValidator
376376
{
377377
[Required]

UnitTests/UnitTests.Shared/Mvvm/Test_ObservableValidator.cs

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.ComponentModel;
88
using System.ComponentModel.DataAnnotations;
99
using System.Linq;
10+
using System.Reflection;
1011
using System.Text.RegularExpressions;
1112
using Microsoft.Toolkit.Mvvm.ComponentModel;
1213
using Microsoft.VisualStudio.TestTools.UnitTesting;
@@ -335,6 +336,54 @@ public void Test_ObservableValidator_ValidateAllProperties()
335336
model.Age = -10;
336337

337338
model.ValidateAllProperties();
339+
340+
Assert.IsTrue(model.HasErrors);
341+
Assert.IsTrue(events.Count == 1);
342+
Assert.IsTrue(events.Any(e => e.PropertyName == nameof(Person.Age)));
343+
}
344+
345+
[TestCategory("Mvvm")]
346+
[TestMethod]
347+
public void Test_ObservableValidator_ValidateAllProperties_WithFallback()
348+
{
349+
var model = new PersonWithDeferredValidation();
350+
var events = new List<DataErrorsChangedEventArgs>();
351+
352+
MethodInfo[] staticMethods = typeof(ObservableValidator).GetMethods(BindingFlags.Static | BindingFlags.NonPublic);
353+
MethodInfo validationMethod = staticMethods.Single(static m => m.Name.Contains("GetValidationActionFallback"));
354+
Func<Type, Action<object>> validationFunc = (Func<Type, Action<object>>)validationMethod.CreateDelegate(typeof(Func<Type, Action<object>>));
355+
Action<object> validationAction = validationFunc(model.GetType());
356+
357+
model.ErrorsChanged += (s, e) => events.Add(e);
358+
359+
validationAction(model);
360+
361+
Assert.IsTrue(model.HasErrors);
362+
Assert.IsTrue(events.Count == 2);
363+
364+
// Note: we can't use an index here because the order used to return properties
365+
// from reflection APIs is an implementation detail and might change at any time.
366+
Assert.IsTrue(events.Any(e => e.PropertyName == nameof(Person.Name)));
367+
Assert.IsTrue(events.Any(e => e.PropertyName == nameof(Person.Age)));
368+
369+
events.Clear();
370+
371+
model.Name = "James";
372+
model.Age = 42;
373+
374+
validationAction(model);
375+
376+
Assert.IsFalse(model.HasErrors);
377+
Assert.IsTrue(events.Count == 2);
378+
Assert.IsTrue(events.Any(e => e.PropertyName == nameof(Person.Name)));
379+
Assert.IsTrue(events.Any(e => e.PropertyName == nameof(Person.Age)));
380+
381+
events.Clear();
382+
383+
model.Age = -10;
384+
385+
validationAction(model);
386+
338387
Assert.IsTrue(model.HasErrors);
339388
Assert.IsTrue(events.Count == 1);
340389
Assert.IsTrue(events.Any(e => e.PropertyName == nameof(Person.Age)));
@@ -414,6 +463,34 @@ public void Test_ObservableValidator_ValidationWithFormattedDisplayName()
414463
Assert.AreEqual(allErrors[1].ErrorMessage, $"SECOND: {nameof(ValidationWithDisplayName.AnotherRequiredField)}.");
415464
}
416465

466+
// See: https://github.com/CommunityToolkit/WindowsCommunityToolkit/issues/4272
467+
[TestCategory("Mvvm")]
468+
[TestMethod]
469+
[DataRow(typeof(MyBase))]
470+
[DataRow(typeof(MyDerived2))]
471+
public void Test_ObservableRecipient_ValidationOnNonValidatableProperties(Type type)
472+
{
473+
MyBase viewmodel = (MyBase)Activator.CreateInstance(type);
474+
475+
viewmodel.ValidateAll();
476+
}
477+
478+
// See: https://github.com/CommunityToolkit/WindowsCommunityToolkit/issues/4272
479+
[TestCategory("Mvvm")]
480+
[TestMethod]
481+
[DataRow(typeof(MyBase))]
482+
[DataRow(typeof(MyDerived2))]
483+
public void Test_ObservableRecipient_ValidationOnNonValidatableProperties_WithFallback(Type type)
484+
{
485+
MyBase viewmodel = (MyBase)Activator.CreateInstance(type);
486+
487+
MethodInfo[] staticMethods = typeof(ObservableValidator).GetMethods(BindingFlags.Static | BindingFlags.NonPublic);
488+
MethodInfo validationMethod = staticMethods.Single(static m => m.Name.Contains("GetValidationActionFallback"));
489+
Func<Type, Action<object>> validationFunc = (Func<Type, Action<object>>)validationMethod.CreateDelegate(typeof(Func<Type, Action<object>>));
490+
491+
validationFunc(viewmodel.GetType())(viewmodel);
492+
}
493+
417494
public class Person : ObservableValidator
418495
{
419496
private string name;
@@ -631,5 +708,22 @@ public string AnotherRequiredField
631708
set => SetProperty(ref this.anotherRequiredField, value, true);
632709
}
633710
}
711+
712+
public class MyBase : ObservableValidator
713+
{
714+
public int? MyDummyInt { get; set; } = 0;
715+
716+
public void ValidateAll()
717+
{
718+
ValidateAllProperties();
719+
}
720+
}
721+
722+
public class MyDerived2 : MyBase
723+
{
724+
public string Name { get; set; }
725+
726+
public int SomeRandomproperty { get; set; }
727+
}
634728
}
635729
}

0 commit comments

Comments
 (0)