Skip to content

Commit 223a38b

Browse files
committed
Fixed ValidateAllProperties generation for generated properties
1 parent 81504ce commit 223a38b

File tree

3 files changed

+82
-12
lines changed

3 files changed

+82
-12
lines changed

Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -442,7 +442,7 @@ private static PropertyDeclarationSyntax CreatePropertyDeclaration(
442442
/// <param name="fieldSymbol">The input <see cref="IFieldSymbol"/> instance to process.</param>
443443
/// <returns>The generated property name for <paramref name="fieldSymbol"/>.</returns>
444444
[Pure]
445-
private static string GetGeneratedPropertyName(IFieldSymbol fieldSymbol)
445+
public static string GetGeneratedPropertyName(IFieldSymbol fieldSymbol)
446446
{
447447
string propertyName = fieldSymbol.Name;
448448

Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidateAllPropertiesGenerator.cs

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
using Microsoft.Toolkit.Mvvm.SourceGenerators.Extensions;
1616
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
1717

18+
#pragma warning disable SA1008
19+
1820
namespace Microsoft.Toolkit.Mvvm.SourceGenerators
1921
{
2022
/// <summary>
@@ -39,8 +41,10 @@ public void Execute(GeneratorExecutionContext context)
3941
return;
4042
}
4143

42-
// Get the symbol for the ValidationAttribute type
43-
INamedTypeSymbol validationSymbol = context.Compilation.GetTypeByMetadataName("System.ComponentModel.DataAnnotations.ValidationAttribute")!;
44+
// Get the symbol for the required attributes
45+
INamedTypeSymbol
46+
validationSymbol = context.Compilation.GetTypeByMetadataName("System.ComponentModel.DataAnnotations.ValidationAttribute")!,
47+
observablePropertySymbol = context.Compilation.GetTypeByMetadataName("Microsoft.Toolkit.Mvvm.ComponentModel.ObservablePropertyAttribute")!;
4448

4549
// Prepare the attributes to add to the first class declaration
4650
AttributeListSyntax[] classAttributes = new[]
@@ -138,14 +142,14 @@ public void Execute(GeneratorExecutionContext context)
138142
Parameter(Identifier("obj")).WithType(PredefinedType(Token(SyntaxKind.ObjectKeyword))))
139143
.WithBody(Block(
140144
LocalDeclarationStatement(
141-
VariableDeclaration(IdentifierName("var")) // Cannot Token(SyntaxKind.VarKeyword) here (throws an ArgumentException)
145+
VariableDeclaration(IdentifierName("var")) // Cannot use Token(SyntaxKind.VarKeyword) here (throws an ArgumentException)
142146
.AddVariables(
143147
VariableDeclarator(Identifier("instance"))
144148
.WithInitializer(EqualsValueClause(
145149
CastExpression(
146150
IdentifierName(classSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)),
147151
IdentifierName("obj")))))))
148-
.AddStatements(EnumerateValidationStatements(classSymbol, validationSymbol).ToArray())),
152+
.AddStatements(EnumerateValidationStatements(classSymbol, validationSymbol, observablePropertySymbol).ToArray())),
149153
ReturnStatement(IdentifierName("ValidateAllProperties")))))))
150154
.NormalizeWhitespace()
151155
.ToFullString();
@@ -159,28 +163,47 @@ public void Execute(GeneratorExecutionContext context)
159163
}
160164

161165
/// <summary>
162-
/// Gets a sequence of statements to validate declared properties.
166+
/// Gets a sequence of statements to validate declared properties (including generated ones).
163167
/// </summary>
164168
/// <param name="classSymbol">The input <see cref="INamedTypeSymbol"/> instance to process.</param>
165169
/// <param name="validationSymbol">The type symbol for the <c>ValidationAttribute</c> type.</param>
170+
/// <param name="observablePropertySymbol">The type symbol for the <c>ObservablePropertyAttribute</c> type.</param>
166171
/// <returns>The sequence of <see cref="StatementSyntax"/> instances to validate declared properties.</returns>
167172
[Pure]
168-
private static IEnumerable<StatementSyntax> EnumerateValidationStatements(INamedTypeSymbol classSymbol, INamedTypeSymbol validationSymbol)
173+
private static IEnumerable<StatementSyntax> EnumerateValidationStatements(INamedTypeSymbol classSymbol, INamedTypeSymbol validationSymbol, INamedTypeSymbol observablePropertySymbol)
169174
{
170-
foreach (var propertySymbol in classSymbol.GetMembers().OfType<IPropertySymbol>())
175+
foreach (var memberSymbol in classSymbol.GetMembers())
171176
{
172-
if (propertySymbol.IsIndexer)
177+
if (memberSymbol is not (IPropertySymbol { IsIndexer: false } or IFieldSymbol))
173178
{
174179
continue;
175180
}
176181

177-
ImmutableArray<AttributeData> attributes = propertySymbol.GetAttributes();
182+
ImmutableArray<AttributeData> attributes = memberSymbol.GetAttributes();
178183

184+
// Also include fields that are annotated with [ObservableProperty]. This is necessary because
185+
// all generators run in an undefined order and looking at the same original compilation, so the
186+
// current one wouldn't be able to see generated properties from other generators directly.
187+
if (memberSymbol is IFieldSymbol &&
188+
!attributes.Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, observablePropertySymbol)))
189+
{
190+
continue;
191+
}
192+
193+
// Skip the current member if there are no validation attributes applied to it
179194
if (!attributes.Any(a => a.AttributeClass?.InheritsFrom(validationSymbol) == true))
180195
{
181196
continue;
182197
}
183198

199+
// Get the target property name either directly or matching the generated one
200+
string propertyName = memberSymbol switch
201+
{
202+
IPropertySymbol propertySymbol => propertySymbol.Name,
203+
IFieldSymbol fieldSymbol => ObservablePropertyGenerator.GetGeneratedPropertyName(fieldSymbol),
204+
_ => throw new InvalidOperationException("Invalid symbol type")
205+
};
206+
184207
// This enumerator produces a sequence of statements as follows:
185208
//
186209
// __ObservableValidatorHelper.ValidateProperty(instance, instance.<PROPERTY_0>, nameof(instance.<PROPERTY_0>));
@@ -200,14 +223,14 @@ private static IEnumerable<StatementSyntax> EnumerateValidationStatements(INamed
200223
MemberAccessExpression(
201224
SyntaxKind.SimpleMemberAccessExpression,
202225
IdentifierName("instance"),
203-
IdentifierName(propertySymbol.Name))),
226+
IdentifierName(propertyName))),
204227
Argument(
205228
InvocationExpression(IdentifierName("nameof"))
206229
.AddArgumentListArguments(Argument(
207230
MemberAccessExpression(
208231
SyntaxKind.SimpleMemberAccessExpression,
209232
IdentifierName("instance"),
210-
IdentifierName(propertySymbol.Name)))))));
233+
IdentifierName(propertyName)))))));
211234
}
212235
}
213236
}

UnitTests/UnitTests.NetCore/Mvvm/Test_ObservablePropertyAttribute.cs

Lines changed: 47 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.Diagnostics.CodeAnalysis;
10+
using System.Linq;
1011
using System.Reflection;
1112
using Microsoft.Toolkit.Mvvm.ComponentModel;
1213
using Microsoft.VisualStudio.TestTools.UnitTesting;
@@ -119,6 +120,33 @@ public void Test_ValidationAttributes()
119120
Assert.AreEqual(testAttribute.Animal, Animal.Llama);
120121
}
121122

123+
// See https://github.com/CommunityToolkit/WindowsCommunityToolkit/issues/4184
124+
[TestCategory("Mvvm")]
125+
[TestMethod]
126+
public void Test_GeneratedPropertiesWithValidationAttributesOverFields()
127+
{
128+
var model = new ViewModelWithValidatableGeneratedProperties();
129+
130+
List<string?> propertyNames = new();
131+
132+
model.PropertyChanged += (s, e) => propertyNames.Add(e.PropertyName);
133+
134+
// Assign these fields directly to bypass the validation that is executed in the generated setters.
135+
// We only need those generated properties to be there to check whether they are correctly detected.
136+
model.first = "A";
137+
model.last = "This is a very long name that exceeds the maximum length of 60 for this property";
138+
139+
Assert.IsFalse(model.HasErrors);
140+
141+
model.RunValidation();
142+
143+
Assert.IsTrue(model.HasErrors);
144+
145+
ValidationResult[] validationErrors = model.GetErrors().ToArray();
146+
147+
Assert.AreEqual(validationErrors.Length, 2);
148+
}
149+
122150
public partial class SampleModel : ObservableObject
123151
{
124152
/// <summary>
@@ -195,5 +223,24 @@ public enum Animal
195223
Dog,
196224
Llama
197225
}
226+
227+
public partial class ViewModelWithValidatableGeneratedProperties : ObservableValidator
228+
{
229+
[Required]
230+
[MinLength(2)]
231+
[MaxLength(60)]
232+
[Display(Name = "FirstName")]
233+
[ObservableProperty]
234+
public string first = "Bob";
235+
236+
[Display(Name = "LastName")]
237+
[Required]
238+
[MinLength(2)]
239+
[MaxLength(60)]
240+
[ObservableProperty]
241+
public string last = "Jones";
242+
243+
public void RunValidation() => ValidateAllProperties();
244+
}
198245
}
199246
}

0 commit comments

Comments
 (0)