Skip to content

Commit 96f6e97

Browse files
committed
Enabled validation support for generated properties
1 parent 9c33591 commit 96f6e97

File tree

4 files changed

+118
-65
lines changed

4 files changed

+118
-65
lines changed

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

Lines changed: 115 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ public void Execute(GeneratorExecutionContext context)
5757
/// <param name="classDeclaration">The <see cref="ClassDeclarationSyntax"/> node to process.</param>
5858
/// <param name="classDeclarationSymbol">The <see cref="INamedTypeSymbol"/> for <paramref name="classDeclaration"/>.</param>
5959
/// <param name="items">The sequence of fields to process.</param>
60-
private void OnExecute(
60+
private static void OnExecute(
6161
GeneratorExecutionContext context,
6262
ClassDeclarationSyntax classDeclaration,
6363
INamedTypeSymbol classDeclarationSymbol,
@@ -127,47 +127,18 @@ private void OnExecute(
127127
/// <param name="isNotifyPropertyChanging">Indicates whether or not <see cref="INotifyPropertyChanging"/> is also implemented.</param>
128128
/// <returns>A generated <see cref="PropertyDeclarationSyntax"/> instance for the input field.</returns>
129129
[Pure]
130-
private PropertyDeclarationSyntax CreatePropertyDeclaration(GeneratorExecutionContext context, SyntaxTriviaList leadingTrivia, IFieldSymbol fieldSymbol, bool isNotifyPropertyChanging)
130+
private static PropertyDeclarationSyntax CreatePropertyDeclaration(GeneratorExecutionContext context, SyntaxTriviaList leadingTrivia, IFieldSymbol fieldSymbol, bool isNotifyPropertyChanging)
131131
{
132132
// Get the field type and the target property name
133133
string
134134
typeName = fieldSymbol.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
135-
propertyName = fieldSymbol.Name;
136-
137-
if (propertyName.StartsWith("m_"))
138-
{
139-
propertyName = propertyName.Substring(2);
140-
}
141-
else if (propertyName.StartsWith("_"))
142-
{
143-
propertyName = propertyName.TrimStart('_');
144-
}
145-
146-
propertyName = $"{char.ToUpper(propertyName[0])}{propertyName.Substring(1)}";
147-
148-
BlockSyntax setter = Block();
149-
150-
// Add the OnPropertyChanging() call if necessary
151-
if (isNotifyPropertyChanging)
152-
{
153-
setter = setter.AddStatements(ExpressionStatement(InvocationExpression(IdentifierName("OnPropertyChanging"))));
154-
}
155-
156-
// Add the following statements:
157-
//
158-
// <FIELD_NAME> = value;
159-
// OnPropertyChanged();
160-
setter = setter.AddStatements(
161-
ExpressionStatement(
162-
AssignmentExpression(
163-
SyntaxKind.SimpleAssignmentExpression,
164-
IdentifierName(fieldSymbol.Name),
165-
IdentifierName("value"))),
166-
ExpressionStatement(InvocationExpression(IdentifierName("OnPropertyChanged"))));
135+
propertyName = GetGeneratedPropertyName(fieldSymbol);
167136

137+
INamedTypeSymbol observableValidatorSymbol = context.Compilation.GetTypeByMetadataName("Microsoft.Toolkit.Mvvm.ComponentModel.ObservableValidator")!;
168138
INamedTypeSymbol alsoNotifyForAttributeSymbol = context.Compilation.GetTypeByMetadataName(typeof(AlsoNotifyForAttribute).FullName)!;
169139
INamedTypeSymbol? validationAttributeSymbol = context.Compilation.GetTypeByMetadataName("System.ComponentModel.DataAnnotations.ValidationAttribute");
170140

141+
List<StatementSyntax> dependentPropertyNotificationStatements = new();
171142
List<AttributeSyntax> validationAttributes = new();
172143

173144
foreach (AttributeData attributeData in fieldSymbol.GetAttributes())
@@ -180,7 +151,7 @@ private PropertyDeclarationSyntax CreatePropertyDeclaration(GeneratorExecutionCo
180151
if (attributeArgument.Value is string dependentPropertyName)
181152
{
182153
// OnPropertyChanged("OtherPropertyName");
183-
setter = setter.AddStatements(ExpressionStatement(
154+
dependentPropertyNotificationStatements.Add(ExpressionStatement(
184155
InvocationExpression(IdentifierName("OnPropertyChanged"))
185156
.AddArgumentListArguments(Argument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(dependentPropertyName))))));
186157
}
@@ -189,28 +160,105 @@ private PropertyDeclarationSyntax CreatePropertyDeclaration(GeneratorExecutionCo
189160
else if (validationAttributeSymbol is not null &&
190161
attributeData.AttributeClass?.InheritsFrom(validationAttributeSymbol) == true)
191162
{
163+
// Track the current validation attribute
192164
validationAttributes.Add(attributeData.AsAttributeSyntax());
193165
}
194166
}
195167

168+
BlockSyntax setterBlock;
169+
170+
if (validationAttributes.Count > 0)
171+
{
172+
// Generate the inner setter block as follows:
173+
//
174+
// if (SetProperty(ref <FIELD_NAME>, value, true))
175+
// {
176+
// OnPropertyChanged("Property1"); // Optional
177+
// OnPropertyChanged("Property2");
178+
// ...
179+
// OnPropertyChanged("PropertyN");
180+
// }
181+
setterBlock = Block(
182+
IfStatement(
183+
InvocationExpression(IdentifierName("SetProperty"))
184+
.AddArgumentListArguments(
185+
Argument(IdentifierName(fieldSymbol.Name)).WithRefOrOutKeyword(Token(SyntaxKind.RefKeyword)),
186+
Argument(IdentifierName("value")),
187+
Argument(LiteralExpression(SyntaxKind.TrueLiteralExpression))),
188+
Block(dependentPropertyNotificationStatements)));
189+
}
190+
else
191+
{
192+
BlockSyntax updateAndNotificationBlock = Block();
193+
194+
// Add the OnPropertyChanging() call if necessary
195+
if (isNotifyPropertyChanging)
196+
{
197+
updateAndNotificationBlock = updateAndNotificationBlock.AddStatements(ExpressionStatement(InvocationExpression(IdentifierName("OnPropertyChanging"))));
198+
}
199+
200+
// Add the following statements:
201+
//
202+
// <FIELD_NAME> = value;
203+
// OnPropertyChanged();
204+
updateAndNotificationBlock = updateAndNotificationBlock.AddStatements(
205+
ExpressionStatement(
206+
AssignmentExpression(
207+
SyntaxKind.SimpleAssignmentExpression,
208+
IdentifierName(fieldSymbol.Name),
209+
IdentifierName("value"))),
210+
ExpressionStatement(InvocationExpression(IdentifierName("OnPropertyChanged"))));
211+
212+
// Add the dependent property notifications at the end
213+
updateAndNotificationBlock = updateAndNotificationBlock.AddStatements(dependentPropertyNotificationStatements.ToArray());
214+
215+
// Generate the inner setter block as follows:
216+
//
217+
// if (!global::System.Collections.Generic.EqualityComparer<<FIELD_TYPE>>.Default.Equals(<FIELD_NAME>, value))
218+
// {
219+
// OnPropertyChanging(); // Optional
220+
// <FIELD_NAME> = value;
221+
// OnPropertyChanged();
222+
// OnPropertyChanged("Property1"); // Optional
223+
// OnPropertyChanged("Property2");
224+
// ...
225+
// OnPropertyChanged("PropertyN");
226+
// }
227+
setterBlock = Block(
228+
IfStatement(
229+
PrefixUnaryExpression(
230+
SyntaxKind.LogicalNotExpression,
231+
InvocationExpression(
232+
MemberAccessExpression(
233+
SyntaxKind.SimpleMemberAccessExpression,
234+
MemberAccessExpression(
235+
SyntaxKind.SimpleMemberAccessExpression,
236+
GenericName(Identifier("global::System.Collections.Generic.EqualityComparer"))
237+
.AddTypeArgumentListArguments(IdentifierName(typeName)),
238+
IdentifierName("Default")),
239+
IdentifierName("Equals")))
240+
.AddArgumentListArguments(
241+
Argument(IdentifierName(fieldSymbol.Name)),
242+
Argument(IdentifierName("value")))),
243+
updateAndNotificationBlock));
244+
}
245+
196246
// Construct the generated property as follows:
197247
//
198248
// <FIELD_TRIVIA>
199249
// [global::System.CodeDom.Compiler.GeneratedCode("...", "...")]
200250
// [global::System.Diagnostics.DebuggerNonUserCode]
201251
// [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
202-
// <VALIDATION_ATTRIBUTES> // Optional
252+
// <VALIDATION_ATTRIBUTE1> // Optional
253+
// <VALIDATION_ATTRIBUTE2>
254+
// ...
255+
// <VALIDATION_ATTRIBUTEN>
203256
// public <FIELD_TYPE> <PROPERTY_NAME>
204257
// {
205258
// get => <FIELD_NAME>;
206259
// set
207260
// {
208-
// if (!global::System.Collections.Generic.EqualityComparer<<FIELD_TYPE>>.Default.Equals(<FIELD_NAME>, value))
209-
// {
210-
// OnPropertyChanging(); // Optional
211-
// <FIELD_NAME> = value;
212-
// OnPropertyChanged();
213-
// }
261+
// <BODY>
214262
// }
215263
// }
216264
return
@@ -219,8 +267,8 @@ private PropertyDeclarationSyntax CreatePropertyDeclaration(GeneratorExecutionCo
219267
AttributeList(SingletonSeparatedList(
220268
Attribute(IdentifierName($"global::System.CodeDom.Compiler.GeneratedCode"))
221269
.AddArgumentListArguments(
222-
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(GetType().FullName))),
223-
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(GetType().Assembly.GetName().Version.ToString())))))),
270+
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservablePropertyGenerator).FullName))),
271+
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservablePropertyGenerator).Assembly.GetName().Version.ToString())))))),
224272
AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.DebuggerNonUserCode")))),
225273
AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage")))))
226274
.AddAttributeLists(validationAttributes.Select(static a => AttributeList(SingletonSeparatedList(a))).ToArray())
@@ -231,23 +279,29 @@ private PropertyDeclarationSyntax CreatePropertyDeclaration(GeneratorExecutionCo
231279
.WithExpressionBody(ArrowExpressionClause(IdentifierName(fieldSymbol.Name)))
232280
.WithSemicolonToken(Token(SyntaxKind.SemicolonToken)),
233281
AccessorDeclaration(SyntaxKind.SetAccessorDeclaration)
234-
.AddBodyStatements(
235-
IfStatement(
236-
PrefixUnaryExpression(
237-
SyntaxKind.LogicalNotExpression,
238-
InvocationExpression(
239-
MemberAccessExpression(
240-
SyntaxKind.SimpleMemberAccessExpression,
241-
MemberAccessExpression(
242-
SyntaxKind.SimpleMemberAccessExpression,
243-
GenericName(Identifier("global::System.Collections.Generic.EqualityComparer"))
244-
.AddTypeArgumentListArguments(IdentifierName(typeName)),
245-
IdentifierName("Default")),
246-
IdentifierName("Equals")))
247-
.AddArgumentListArguments(
248-
Argument(IdentifierName(fieldSymbol.Name)),
249-
Argument(IdentifierName("value")))),
250-
setter)));
282+
.WithBody(setterBlock));
283+
}
284+
285+
/// <summary>
286+
/// Get the generated property name for an input field.
287+
/// </summary>
288+
/// <param name="fieldSymbol">The input <see cref="IFieldSymbol"/> instance to process.</param>
289+
/// <returns>The generated property name for <paramref name="fieldSymbol"/>.</returns>
290+
[Pure]
291+
private static string GetGeneratedPropertyName(IFieldSymbol fieldSymbol)
292+
{
293+
string propertyName = fieldSymbol.Name;
294+
295+
if (propertyName.StartsWith("m_"))
296+
{
297+
propertyName = propertyName.Substring(2);
298+
}
299+
else if (propertyName.StartsWith("_"))
300+
{
301+
propertyName = propertyName.TrimStart('_');
302+
}
303+
304+
return $"{char.ToUpper(propertyName[0])}{propertyName.Substring(1)}";
251305
}
252306
}
253307
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ private static IEnumerable<StatementSyntax> EnumerateValidationStatements(INamed
154154
//
155155
// __ObservableValidatorHelper.ValidateProperty(instance, instance.<PROPERTY_0>, nameof(instance.<PROPERTY_0>));
156156
// __ObservableValidatorHelper.ValidateProperty(instance, instance.<PROPERTY_0>, nameof(instance.<PROPERTY_0>));
157-
// // ...
157+
// ...
158158
// __ObservableValidatorHelper.ValidateProperty(instance, instance.<PROPERTY_1>, nameof(instance.<PROPERTY_1>));
159159
yield return
160160
ExpressionStatement(

Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ private static IEnumerable<StatementSyntax> EnumerateRegistrationStatements(INam
179179
//
180180
// messenger.Register<<TYPE_0>>(recipient);
181181
// messenger.Register<<TYPE_1>>(recipient);
182-
// // ...
182+
// ...
183183
// messenger.Register<<TYPE_N>>(recipient);
184184
yield return
185185
ExpressionStatement(
@@ -213,7 +213,7 @@ private static IEnumerable<StatementSyntax> EnumerateRegistrationStatementsWithT
213213
//
214214
// messenger.Register<<TYPE_0>, TToken>(recipient, token);
215215
// messenger.Register<<TYPE_1>, TToken>(recipient, token);
216-
// // ...
216+
// ...
217217
// messenger.Register<<TYPE_N>, TToken>(recipient, token);
218218
yield return
219219
ExpressionStatement(

UnitTests/UnitTests.NetCore/Mvvm/Test_ObservablePropertyAttribute.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
using System.Collections.Generic;
77
using System.ComponentModel;
88
using System.ComponentModel.DataAnnotations;
9-
using System.Configuration;
109
using System.Reflection;
1110
using Microsoft.Toolkit.Mvvm.ComponentModel;
1211
using Microsoft.VisualStudio.TestTools.UnitTesting;

0 commit comments

Comments
 (0)