@@ -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 //
0 commit comments