Skip to content

Commit 3fc61fe

Browse files
authored
Merge pull request #119 from CommunityToolkit/dev/partial-observable-property-methods
[ObservableProperty] generates partial OnPropertyChanging/Changed methods
2 parents 2689986 + f40eb62 commit 3fc61fe

File tree

3 files changed

+187
-7
lines changed

3 files changed

+187
-7
lines changed

CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,10 +142,18 @@ public static PropertyInfo GetInfo(IFieldSymbol fieldSymbol, out ImmutableArray<
142142
/// </summary>
143143
/// <param name="propertyInfo">The input <see cref="PropertyInfo"/> instance to process.</param>
144144
/// <returns>The generated <see cref="MemberDeclarationSyntax"/> instance for <paramref name="propertyInfo"/>.</returns>
145-
public static MemberDeclarationSyntax GetSyntax(PropertyInfo propertyInfo)
145+
public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInfo)
146146
{
147147
ImmutableArray<StatementSyntax>.Builder setterStatements = ImmutableArray.CreateBuilder<StatementSyntax>();
148148

149+
// Add the OnPropertyChanging() call first:
150+
//
151+
// On<PROPERTY_NAME>Changing(value);
152+
setterStatements.Add(
153+
ExpressionStatement(
154+
InvocationExpression(IdentifierName($"On{propertyInfo.PropertyName}Changing"))
155+
.AddArgumentListArguments(Argument(IdentifierName("value")))));
156+
149157
// Gather the statements to notify dependent properties
150158
foreach (string propertyName in propertyInfo.PropertyChangingNames)
151159
{
@@ -192,6 +200,14 @@ public static MemberDeclarationSyntax GetSyntax(PropertyInfo propertyInfo)
192200
Argument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(propertyInfo.PropertyName))))));
193201
}
194202

203+
// Add the OnPropertyChanged() call:
204+
//
205+
// On<PROPERTY_NAME>Changed(value);
206+
setterStatements.Add(
207+
ExpressionStatement(
208+
InvocationExpression(IdentifierName($"On{propertyInfo.PropertyName}Changed"))
209+
.AddArgumentListArguments(Argument(IdentifierName("value")))));
210+
195211
// Gather the statements to notify dependent properties
196212
foreach (string propertyName in propertyInfo.PropertyChangedNames)
197213
{
@@ -292,6 +308,57 @@ public static MemberDeclarationSyntax GetSyntax(PropertyInfo propertyInfo)
292308
.WithBody(Block(setterIfStatement)));
293309
}
294310

311+
/// <summary>
312+
/// Gets the <see cref="MemberDeclarationSyntax"/> instances for the <c>OnPropertyChanging</c> and <c>OnPropertyChanged</c> methods for the input field.
313+
/// </summary>
314+
/// <param name="propertyInfo">The input <see cref="PropertyInfo"/> instance to process.</param>
315+
/// <returns>The generated <see cref="MemberDeclarationSyntax"/> instances for the <c>OnPropertyChanging</c> and <c>OnPropertyChanged</c> methods.</returns>
316+
public static ImmutableArray<MemberDeclarationSyntax> GetOnPropertyChangeMethodsSyntax(PropertyInfo propertyInfo)
317+
{
318+
// Get the parameter type syntax (adding the nullability annotation, if needed)
319+
TypeSyntax parameterType = propertyInfo.IsNullableReferenceType
320+
? NullableType(IdentifierName(propertyInfo.TypeName))
321+
: IdentifierName(propertyInfo.TypeName);
322+
323+
// Construct the generated method as follows:
324+
//
325+
// /// <summary>Executes the logic for when <see cref="<PROPERTY_NAME>"/> is changing.</summary>
326+
// [global::System.CodeDom.Compiler.GeneratedCode("...", "...")]
327+
// partial void On<PROPERTY_NAME>Changing(<PROPERTY_TYPE> value);
328+
MemberDeclarationSyntax onPropertyChangingDeclaration =
329+
MethodDeclaration(PredefinedType(Token(SyntaxKind.VoidKeyword)), Identifier($"On{propertyInfo.PropertyName}Changing"))
330+
.AddModifiers(Token(SyntaxKind.PartialKeyword))
331+
.AddParameterListParameters(Parameter(Identifier("value")).WithType(parameterType))
332+
.AddAttributeLists(
333+
AttributeList(SingletonSeparatedList(
334+
Attribute(IdentifierName("global::System.CodeDom.Compiler.GeneratedCode"))
335+
.AddArgumentListArguments(
336+
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservablePropertyGenerator).FullName))),
337+
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservablePropertyGenerator).Assembly.GetName().Version.ToString()))))))
338+
.WithOpenBracketToken(Token(TriviaList(Comment($"/// <summary>Executes the logic for when <see cref=\"{propertyInfo.PropertyName}\"/> is changing.</summary>")), SyntaxKind.OpenBracketToken, TriviaList())))
339+
.WithSemicolonToken(Token(SyntaxKind.SemicolonToken));
340+
341+
// Construct the generated method as follows:
342+
//
343+
// /// <summary>Executes the logic for when <see cref="<PROPERTY_NAME>"/> ust changed.</summary>
344+
// [global::System.CodeDom.Compiler.GeneratedCode("...", "...")]
345+
// partial void On<PROPERTY_NAME>Changed(<PROPERTY_TYPE> value);
346+
MemberDeclarationSyntax onPropertyChangedDeclaration =
347+
MethodDeclaration(PredefinedType(Token(SyntaxKind.VoidKeyword)), Identifier($"On{propertyInfo.PropertyName}Changed"))
348+
.AddModifiers(Token(SyntaxKind.PartialKeyword))
349+
.AddParameterListParameters(Parameter(Identifier("value")).WithType(parameterType))
350+
.AddAttributeLists(
351+
AttributeList(SingletonSeparatedList(
352+
Attribute(IdentifierName("global::System.CodeDom.Compiler.GeneratedCode"))
353+
.AddArgumentListArguments(
354+
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservablePropertyGenerator).FullName))),
355+
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservablePropertyGenerator).Assembly.GetName().Version.ToString()))))))
356+
.WithOpenBracketToken(Token(TriviaList(Comment($"/// <summary>Executes the logic for when <see cref=\"{propertyInfo.PropertyName}\"/> just changed.</summary>")), SyntaxKind.OpenBracketToken, TriviaList())))
357+
.WithSemicolonToken(Token(SyntaxKind.SemicolonToken));
358+
359+
return ImmutableArray.Create(onPropertyChangingDeclaration, onPropertyChangedDeclaration);
360+
}
361+
295362
/// <summary>
296363
/// Gets a <see cref="CompilationUnitSyntax"/> instance with the cached args of a specified type.
297364
/// </summary>

CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -69,17 +69,18 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
6969
.GroupBy(HierarchyInfo.Comparer.Default)
7070
.WithComparers(HierarchyInfo.Comparer.Default, PropertyInfo.Comparer.Default.ForImmutableArray());
7171

72-
// Generate the requested properties
72+
// Generate the requested properties and methods
7373
context.RegisterSourceOutput(groupedPropertyInfo, static (context, item) =>
7474
{
75-
// Generate all properties for the current type
76-
ImmutableArray<MemberDeclarationSyntax> propertyDeclarations =
75+
// Generate all member declarations for the current type
76+
ImmutableArray<MemberDeclarationSyntax> memberDeclarations =
7777
item.Properties
78-
.Select(Execute.GetSyntax)
78+
.Select(Execute.GetPropertySyntax)
79+
.Concat(item.Properties.Select(Execute.GetOnPropertyChangeMethodsSyntax).SelectMany(static l => l))
7980
.ToImmutableArray();
8081

81-
// Insert all properties into the same partial type declaration
82-
CompilationUnitSyntax compilationUnit = item.Hierarchy.GetCompilationUnit(propertyDeclarations);
82+
// Insert all members into the same partial type declaration
83+
CompilationUnitSyntax compilationUnit = item.Hierarchy.GetCompilationUnit(memberDeclarations);
8384

8485
context.AddSource(
8586
hintName: $"{item.Hierarchy.FilenameHint}.cs",

tests/CommunityToolkit.Mvvm.UnitTests/Test_ObservablePropertyAttribute.cs

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,38 @@ public void Test_AlsoNotifyChangeFor()
273273
CollectionAssert.AreEqual(new[] { nameof(model.Surname), nameof(model.FullName), nameof(model.Alias) }, propertyNames);
274274
}
275275

276+
[TestMethod]
277+
public void Test_OnPropertyChangingAndChangedPartialMethods()
278+
{
279+
ViewModelWithImplementedUpdateMethods model = new();
280+
281+
model.Name = nameof(Test_OnPropertyChangingAndChangedPartialMethods);
282+
283+
Assert.AreEqual(nameof(Test_OnPropertyChangingAndChangedPartialMethods), model.NameChangingValue);
284+
Assert.AreEqual(nameof(Test_OnPropertyChangingAndChangedPartialMethods), model.NameChangedValue);
285+
286+
model.Number = 99;
287+
288+
Assert.AreEqual(99, model.NumberChangedValue);
289+
}
290+
291+
[TestMethod]
292+
public void Test_OnPropertyChangingAndChangedPartialMethodWithAdditionalValidation()
293+
{
294+
ViewModelWithImplementedUpdateMethodAndAdditionalValidation model = new();
295+
296+
// The actual validation is performed inside the model itself.
297+
// This test validates that the order with which methods/events are generated is:
298+
// - On<PROPERTY_NAME>Changing(value);
299+
// - OnProperyChanging();
300+
// - field = value;
301+
// - On<PROPERTY_NAME>Changed(value);
302+
// - OnProperyChanged();
303+
model.Name = "B";
304+
305+
Assert.AreEqual("B", model.Name);
306+
}
307+
276308
public partial class SampleModel : ObservableObject
277309
{
278310
/// <summary>
@@ -398,4 +430,84 @@ public partial class ViewModelWithValidatableGeneratedProperties : ObservableVal
398430

399431
public void RunValidation() => ValidateAllProperties();
400432
}
433+
434+
public partial class ViewModelWithImplementedUpdateMethods : ObservableObject
435+
{
436+
[ObservableProperty]
437+
public string? name = "Bob";
438+
439+
[ObservableProperty]
440+
public int number = 42;
441+
442+
public string? NameChangingValue { get; private set; }
443+
444+
public string? NameChangedValue { get; private set; }
445+
446+
public int NumberChangedValue { get; private set; }
447+
448+
partial void OnNameChanging(string? value)
449+
{
450+
NameChangingValue = value;
451+
}
452+
453+
partial void OnNameChanged(string? value)
454+
{
455+
NameChangedValue = value;
456+
}
457+
458+
partial void OnNumberChanged(int value)
459+
{
460+
NumberChangedValue = value;
461+
}
462+
}
463+
464+
public partial class ViewModelWithImplementedUpdateMethodAndAdditionalValidation : ObservableObject
465+
{
466+
private int step;
467+
468+
[ObservableProperty]
469+
public string? name = "A";
470+
471+
partial void OnNameChanging(string? value)
472+
{
473+
Assert.AreEqual(0, this.step);
474+
475+
this.step = 1;
476+
477+
Assert.AreEqual("A", this.name);
478+
Assert.AreEqual("B", value);
479+
}
480+
481+
partial void OnNameChanged(string? value)
482+
{
483+
Assert.AreEqual(2, this.step);
484+
485+
this.step = 3;
486+
487+
Assert.AreEqual("B", this.name);
488+
Assert.AreEqual("B", value);
489+
}
490+
491+
protected override void OnPropertyChanging(PropertyChangingEventArgs e)
492+
{
493+
base.OnPropertyChanging(e);
494+
495+
Assert.AreEqual(1, this.step);
496+
497+
this.step = 2;
498+
499+
Assert.AreEqual("A", this.name);
500+
Assert.AreEqual(nameof(Name), e.PropertyName);
501+
}
502+
503+
protected override void OnPropertyChanged(PropertyChangedEventArgs e)
504+
{
505+
base.OnPropertyChanged(e);
506+
507+
Assert.AreEqual(3, this.step);
508+
509+
Assert.AreEqual("B", this.name);
510+
Assert.AreEqual(nameof(Name), e.PropertyName);
511+
}
512+
}
401513
}

0 commit comments

Comments
 (0)