Skip to content

Commit 53ac5d8

Browse files
committed
Implement code generation for [AlsoBroadcastChange]
1 parent 32a17cf commit 53ac5d8

File tree

4 files changed

+108
-13
lines changed

4 files changed

+108
-13
lines changed

CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Unshipped.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,4 @@ MVVMTK0018 | CommunityToolkit.Mvvm.SourceGenerators.ObservableObjectGenerator |
2626
MVVMTK0019 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/error
2727
MVVMTK0020 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/error
2828
MVVMTK0021 | CommunityToolkit.Mvvm.SourceGenerators.ObservableRecipientGenerator | Error | See https://aka.ms/mvvmtoolkit/error
29+
MVVMTK0022 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/error

CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models;
2121
/// <param name="PropertyChangingNames">The sequence of property changing properties to notify.</param>
2222
/// <param name="PropertyChangedNames">The sequence of property changed properties to notify.</param>
2323
/// <param name="NotifiedCommandNames">The sequence of commands to notify.</param>
24+
/// <param name="AlsoBroadcastChange">Whether or not the generated property also broadcasts changes.</param>
2425
/// <param name="ValidationAttributes">The sequence of validation attributes for the generated property.</param>
2526
internal sealed record PropertyInfo(
2627
string TypeName,
@@ -30,6 +31,7 @@ internal sealed record PropertyInfo(
3031
ImmutableArray<string> PropertyChangingNames,
3132
ImmutableArray<string> PropertyChangedNames,
3233
ImmutableArray<string> NotifiedCommandNames,
34+
bool AlsoBroadcastChange,
3335
ImmutableArray<AttributeInfo> ValidationAttributes)
3436
{
3537
/// <summary>
@@ -47,6 +49,7 @@ protected override void AddToHashCode(ref HashCode hashCode, PropertyInfo obj)
4749
hashCode.AddRange(obj.PropertyChangingNames);
4850
hashCode.AddRange(obj.PropertyChangedNames);
4951
hashCode.AddRange(obj.NotifiedCommandNames);
52+
hashCode.Add(obj.AlsoBroadcastChange);
5053
hashCode.AddRange(obj.ValidationAttributes, AttributeInfo.Comparer.Default);
5154
}
5255

@@ -61,6 +64,7 @@ protected override bool AreEqual(PropertyInfo x, PropertyInfo y)
6164
x.PropertyChangingNames.SequenceEqual(y.PropertyChangingNames) &&
6265
x.PropertyChangedNames.SequenceEqual(y.PropertyChangedNames) &&
6366
x.NotifiedCommandNames.SequenceEqual(y.NotifiedCommandNames) &&
67+
x.AlsoBroadcastChange == y.AlsoBroadcastChange &&
6468
x.ValidationAttributes.SequenceEqual(y.ValidationAttributes, AttributeInfo.Comparer.Default);
6569
}
6670
}

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

Lines changed: 87 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ internal static class Execute
7575
ImmutableArray<string>.Builder propertyChangingNames = ImmutableArray.CreateBuilder<string>();
7676
ImmutableArray<string>.Builder notifiedCommandNames = ImmutableArray.CreateBuilder<string>();
7777
ImmutableArray<AttributeInfo>.Builder validationAttributes = ImmutableArray.CreateBuilder<AttributeInfo>();
78+
bool alsoBroadcastChange = false;
7879

7980
// Track the property changing event for the property, if the type supports it
8081
if (shouldInvokeOnPropertyChanging)
@@ -90,7 +91,8 @@ internal static class Execute
9091
{
9192
// Gather dependent property and command names
9293
if (TryGatherDependentPropertyChangedNames(fieldSymbol, attributeData, propertyChangedNames, builder) ||
93-
TryGatherDependentCommandNames(fieldSymbol, attributeData, notifiedCommandNames, builder))
94+
TryGatherDependentCommandNames(fieldSymbol, attributeData, notifiedCommandNames, builder) ||
95+
TryGetIsBroadcastingChanges(fieldSymbol, attributeData, builder, out alsoBroadcastChange))
9496
{
9597
continue;
9698
}
@@ -129,6 +131,7 @@ internal static class Execute
129131
propertyChangingNames.ToImmutable(),
130132
propertyChangedNames.ToImmutable(),
131133
notifiedCommandNames.ToImmutable(),
134+
alsoBroadcastChange,
132135
validationAttributes.ToImmutable());
133136
}
134137

@@ -326,6 +329,48 @@ bool IsCommandNameValidWithGeneratedMembers(string commandName)
326329
return false;
327330
}
328331

332+
/// <summary>
333+
/// Checks whether a given generated property should also broadcast changes.
334+
/// </summary>
335+
/// <param name="fieldSymbol">The input <see cref="IFieldSymbol"/> instance to process.</param>
336+
/// <param name="attributeData">The <see cref="AttributeData"/> instance for <paramref name="fieldSymbol"/>.</param>
337+
/// <param name="diagnostics">The current collection of gathered diagnostics.</param>
338+
/// <param name="alsoBroadcastChange">Whether or not the resulting property should also broadcast changes.</param>
339+
/// <returns>Whether or not the generated property for <paramref name="fieldSymbol"/> used <c>[AlsoBroadcastChange]</c>.</returns>
340+
private static bool TryGetIsBroadcastingChanges(
341+
IFieldSymbol fieldSymbol,
342+
AttributeData attributeData,
343+
ImmutableArray<Diagnostic>.Builder diagnostics,
344+
out bool alsoBroadcastChange)
345+
{
346+
if (attributeData.AttributeClass?.HasFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.AlsoBroadcastChangeAttribute") == true)
347+
{
348+
// If the containing type is valid, track it
349+
if (fieldSymbol.ContainingType.InheritsFromFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.ObservableRecipient") ||
350+
fieldSymbol.ContainingType.HasOrInheritsAttributeWithFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.ObservableRecipientAttribute"))
351+
{
352+
alsoBroadcastChange = true;
353+
354+
return true;
355+
}
356+
357+
// Otherwise just emit the diagnostic and then ignore the attribute
358+
diagnostics.Add(
359+
InvalidContainingTypeForAlsoBroadcastChangeFieldError,
360+
fieldSymbol,
361+
fieldSymbol.ContainingType,
362+
fieldSymbol.Name);
363+
364+
alsoBroadcastChange = false;
365+
366+
return true;
367+
}
368+
369+
alsoBroadcastChange = false;
370+
371+
return false;
372+
}
373+
329374
/// <summary>
330375
/// Gets a <see cref="CompilationUnitSyntax"/> instance with the cached args for property changing notifications.
331376
/// </summary>
@@ -361,6 +406,33 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf
361406
{
362407
ImmutableArray<StatementSyntax>.Builder setterStatements = ImmutableArray.CreateBuilder<StatementSyntax>();
363408

409+
// Get the property type syntax (adding the nullability annotation, if needed)
410+
TypeSyntax propertyType = propertyInfo.IsNullableReferenceType
411+
? NullableType(IdentifierName(propertyInfo.TypeName))
412+
: IdentifierName(propertyInfo.TypeName);
413+
414+
// In case the backing field is exactly named "value", we need to add the "this." prefix to ensure that comparisons and assignments
415+
// with it in the generated setter body are executed correctly and without conflicts with the implicit value parameter.
416+
ExpressionSyntax fieldExpression = propertyInfo.FieldName switch
417+
{
418+
"value" => MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, ThisExpression(), IdentifierName("value")),
419+
string name => IdentifierName(name)
420+
};
421+
422+
if (propertyInfo.AlsoBroadcastChange)
423+
{
424+
// If broadcasting changes are required, also store the old value.
425+
// This code generates a statement as follows:
426+
//
427+
// <PROPERTY_TYPE> __oldValue = <FIELD_EXPRESSIONS>;
428+
setterStatements.Add(
429+
LocalDeclarationStatement(
430+
VariableDeclaration(propertyType)
431+
.AddVariables(
432+
VariableDeclarator(Identifier("__oldValue"))
433+
.WithInitializer(EqualsValueClause(fieldExpression)))));
434+
}
435+
364436
// Add the OnPropertyChanging() call first:
365437
//
366438
// On<PROPERTY_NAME>Changing(value);
@@ -384,14 +456,6 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf
384456
IdentifierName(propertyName))))));
385457
}
386458

387-
// In case the backing field is exactly named "value", we need to add the "this." prefix to ensure that comparisons and assignments
388-
// with it in the generated setter body are executed correctly and without conflicts with the implicit value parameter.
389-
ExpressionSyntax fieldExpression = propertyInfo.FieldName switch
390-
{
391-
"value" => MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, ThisExpression(), IdentifierName("value")),
392-
string name => IdentifierName(name)
393-
};
394-
395459
// Add the assignment statement:
396460
//
397461
// <FIELD_EXPRESSION> = value;
@@ -452,10 +516,20 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf
452516
IdentifierName("NotifyCanExecuteChanged")))));
453517
}
454518

455-
// Get the property type syntax (adding the nullability annotation, if needed)
456-
TypeSyntax propertyType = propertyInfo.IsNullableReferenceType
457-
? NullableType(IdentifierName(propertyInfo.TypeName))
458-
: IdentifierName(propertyInfo.TypeName);
519+
// Also broadcast the change, if requested
520+
if (propertyInfo.AlsoBroadcastChange)
521+
{
522+
// This code generates a statement as follows:
523+
//
524+
// Broadcast(__oldValue, value, "<PROPERTY_NAME>");
525+
setterStatements.Add(
526+
ExpressionStatement(
527+
InvocationExpression(IdentifierName("Broadcast"))
528+
.AddArgumentListArguments(
529+
Argument(IdentifierName("__oldValue")),
530+
Argument(IdentifierName("value")),
531+
Argument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(propertyInfo.PropertyName))))));
532+
}
459533

460534
// Generate the inner setter block as follows:
461535
//

CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,4 +347,20 @@ internal static class DiagnosticDescriptors
347347
isEnabledByDefault: true,
348348
description: "Cannot apply [ObservableRecipient] to a type that already inherits this attribute from a base type.",
349349
helpLinkUri: "https://aka.ms/mvvmtoolkit");
350+
351+
/// <summary>
352+
/// Gets a <see cref="DiagnosticDescriptor"/> indicating when <c>[AlsoBroadcastChange]</c> is applied to a field in an invalid type.
353+
/// <para>
354+
/// Format: <c>"The field {0}.{1} cannot be annotated with [AlsoBroadcastChange], as its containing type doesn't inherit from ObservableRecipient, nor does it use [ObservableRecipient]"</c>.
355+
/// </para>
356+
/// </summary>
357+
public static readonly DiagnosticDescriptor InvalidContainingTypeForAlsoBroadcastChangeFieldError = new DiagnosticDescriptor(
358+
id: "MVVMTK0022",
359+
title: "Invalid containing type for [ObservableProperty] field",
360+
messageFormat: "The field {0}.{1} cannot be annotated with [AlsoBroadcastChange], as its containing type doesn't inherit from ObservableRecipient, nor does it use [ObservableRecipient]",
361+
category: typeof(ObservablePropertyGenerator).FullName,
362+
defaultSeverity: DiagnosticSeverity.Error,
363+
isEnabledByDefault: true,
364+
description: "Fields annotated with [AlsoBroadcastChange] must be contained in a type that inherits from ObservableRecipient or that is annotated with [ObservableRecipient] (including base types).",
365+
helpLinkUri: "https://aka.ms/mvvmtoolkit");
350366
}

0 commit comments

Comments
 (0)