Skip to content

Commit e4f5648

Browse files
committed
Added AlsoNotifyForAttribute type
1 parent 62f883d commit e4f5648

File tree

5 files changed

+138
-3
lines changed

5 files changed

+138
-3
lines changed

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using Microsoft.CodeAnalysis;
88
using Microsoft.CodeAnalysis.CSharp;
99
using Microsoft.CodeAnalysis.CSharp.Syntax;
10+
using Microsoft.Toolkit.Mvvm.ComponentModel;
1011

1112
namespace Microsoft.Toolkit.Mvvm.SourceGenerators
1213
{
@@ -32,7 +33,7 @@ private sealed class SyntaxReceiver : ISyntaxContextReceiver
3233
public void OnVisitSyntaxNode(GeneratorSyntaxContext context)
3334
{
3435
if (context.Node is FieldDeclarationSyntax { AttributeLists: { Count: > 0 } } fieldDeclaration &&
35-
context.SemanticModel.Compilation.GetTypeByMetadataName("Microsoft.Toolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") is INamedTypeSymbol attributeSymbol)
36+
context.SemanticModel.Compilation.GetTypeByMetadataName(typeof(ObservablePropertyAttribute).FullName) is INamedTypeSymbol attributeSymbol)
3637
{
3738
SyntaxTriviaList leadingTrivia = fieldDeclaration.GetLeadingTrivia();
3839

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

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ private void OnExecute(
8383
var classDeclarationSyntax =
8484
ClassDeclaration(classDeclarationSymbol.Name)
8585
.WithModifiers(classDeclaration.Modifiers)
86-
.AddMembers(items.Select(item => CreatePropertyDeclaration(item.LeadingTrivia, item.FieldSymbol, isNotifyPropertyChanging)).ToArray());
86+
.AddMembers(items.Select(item => CreatePropertyDeclaration(context, item.LeadingTrivia, item.FieldSymbol, isNotifyPropertyChanging)).ToArray());
8787

8888
TypeDeclarationSyntax typeDeclarationSyntax = classDeclarationSyntax;
8989

@@ -121,12 +121,13 @@ private void OnExecute(
121121
/// <summary>
122122
/// Creates a <see cref="PropertyDeclarationSyntax"/> instance for a specified field.
123123
/// </summary>
124+
/// <param name="context">The input <see cref="GeneratorExecutionContext"/> instance to use.</param>
124125
/// <param name="leadingTrivia">The leading trivia for the field to process.</param>
125126
/// <param name="fieldSymbol">The input <see cref="IFieldSymbol"/> instance to process.</param>
126127
/// <param name="isNotifyPropertyChanging">Indicates whether or not <see cref="INotifyPropertyChanging"/> is also implemented.</param>
127128
/// <returns>A generated <see cref="PropertyDeclarationSyntax"/> instance for the input field.</returns>
128129
[Pure]
129-
private PropertyDeclarationSyntax CreatePropertyDeclaration(SyntaxTriviaList leadingTrivia, IFieldSymbol fieldSymbol, bool isNotifyPropertyChanging)
130+
private PropertyDeclarationSyntax CreatePropertyDeclaration(GeneratorExecutionContext context, SyntaxTriviaList leadingTrivia, IFieldSymbol fieldSymbol, bool isNotifyPropertyChanging)
130131
{
131132
// Get the field type and the target property name
132133
string
@@ -164,6 +165,24 @@ private PropertyDeclarationSyntax CreatePropertyDeclaration(SyntaxTriviaList lea
164165
IdentifierName("value"))),
165166
ExpressionStatement(InvocationExpression(IdentifierName("OnPropertyChanged"))));
166167

168+
INamedTypeSymbol attributeSymbol = context.Compilation.GetTypeByMetadataName(typeof(AlsoNotifyForAttribute).FullName)!;
169+
170+
// Add dependent property notifications, if needed
171+
if (fieldSymbol.GetAttributes().FirstOrDefault(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, attributeSymbol)) is AttributeData attributeData &&
172+
attributeData.ConstructorArguments.Length == 1)
173+
{
174+
foreach (TypedConstant attributeArgument in attributeData.ConstructorArguments[0].Values)
175+
{
176+
if (attributeArgument.Value is string dependentPropertyName)
177+
{
178+
// OnPropertyChanged("OtherPropertyName");
179+
setter = setter.AddStatements(ExpressionStatement(
180+
InvocationExpression(IdentifierName("OnPropertyChanged"))
181+
.AddArgumentListArguments(Argument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(dependentPropertyName))))));
182+
}
183+
}
184+
}
185+
167186
// Construct the generated property as follows:
168187
//
169188
// <FIELD_TRIVIA>

Microsoft.Toolkit.Mvvm.SourceGenerators/Microsoft.Toolkit.Mvvm.SourceGenerators.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
</ItemGroup>
1515

1616
<ItemGroup>
17+
<Compile Include="..\Microsoft.Toolkit.Mvvm\ComponentModel\Attributes\AlsoNotifyForAttribute.cs" Link="Microsoft.Toolkit.Mvvm\ComponentModel\Attributes\AlsoNotifyForAttribute.cs" />
1718
<Compile Include="..\Microsoft.Toolkit.Mvvm\ComponentModel\Attributes\INotifyPropertyChangedAttribute.cs" Link="Microsoft.Toolkit.Mvvm\ComponentModel\Attributes\INotifyPropertyChangedAttribute.cs" />
1819
<Compile Include="..\Microsoft.Toolkit.Mvvm\ComponentModel\Attributes\ObservableObjectAttribute.cs" Link="Microsoft.Toolkit.Mvvm\ComponentModel\Attributes\ObservableObjectAttribute.cs" />
1920
<Compile Include="..\Microsoft.Toolkit.Mvvm\ComponentModel\Attributes\ObservablePropertyAttribute.cs" Link="Microsoft.Toolkit.Mvvm\ComponentModel\Attributes\ObservablePropertyAttribute.cs" />
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
#pragma warning disable CS1574
6+
7+
using System;
8+
using System.ComponentModel;
9+
10+
namespace Microsoft.Toolkit.Mvvm.ComponentModel
11+
{
12+
/// <summary>
13+
/// An attribute that can be used to support <see cref="ObservablePropertyAttribute"/> in generated properties. When this attribute is
14+
/// used, the generated property setter will also call <see cref="ObservableObject.OnPropertyChanged(string?)"/> (or the equivalent
15+
/// method in the target class) for the properties specified in the attribute data. This can be useful to keep the code compact when
16+
/// there are one or more dependent properties that should also be reported as updated when the value of the annotated observable
17+
/// property is changed. If this attribute is used in a field without <see cref="ObservablePropertyAttribute"/>, it is ignored.
18+
/// <para>
19+
/// In order to use this attribute, the containing type has to implement the <see cref="INotifyPropertyChanged"/> interface
20+
/// and expose a method with the same signature as <see cref="ObservableObject.OnPropertyChanged(string?)"/>. If the containing
21+
/// type also implements the <see cref="INotifyPropertyChanging"/> interface and exposes a method with the same signature as
22+
/// <see cref="ObservableObject.OnPropertyChanging(string?)"/>, then this method will be invoked as well by the property setter.
23+
/// </para>
24+
/// <para>
25+
/// This attribute can be used as follows:
26+
/// <code>
27+
/// partial class MyViewModel : ObservableObject
28+
/// {
29+
/// [ObservableProperty]
30+
/// [AlsoNotifyFor(nameof(FullName))]
31+
/// private string name;
32+
///
33+
/// [ObservableProperty]
34+
/// [AlsoNotifyFor(nameof(FullName))]
35+
/// private string surname;
36+
///
37+
/// public string FullName => $"{Name} {Surname}";
38+
/// }
39+
/// </code>
40+
/// </para>
41+
/// And with this, code analogous to this will be generated:
42+
/// <code>
43+
/// partial class MyViewModel
44+
/// {
45+
/// public string Name
46+
/// {
47+
/// get => name;
48+
/// set => SetProperty(ref name, value);
49+
/// }
50+
///
51+
/// public string Surname
52+
/// {
53+
/// get => surname;
54+
/// set
55+
/// {
56+
/// if (SetProperty(ref name, value))
57+
/// {
58+
/// OnPropertyChanged(nameof(FullName));
59+
/// }
60+
/// }
61+
/// }
62+
/// }
63+
/// </code>
64+
/// </summary>
65+
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = false)]
66+
public sealed class AlsoNotifyForAttribute : Attribute
67+
{
68+
/// <summary>
69+
/// Initializes a new instance of the <see cref="AlsoNotifyForAttribute"/> class.
70+
/// </summary>
71+
/// <param name="propertyNames">The property names to also notify when the annotated property changes.</param>
72+
public AlsoNotifyForAttribute(params string[] propertyNames)
73+
{
74+
PropertyNames = propertyNames;
75+
}
76+
77+
/// <summary>
78+
/// Gets the property names to also notify when the annotated property changes.
79+
/// </summary>
80+
public string[] PropertyNames { get; }
81+
}
82+
}

UnitTests/UnitTests.NetCore/Mvvm/Test_ObservablePropertyAttribute.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33
// See the LICENSE file in the project root for more information.
44

5+
using System.Collections.Generic;
56
using System.ComponentModel;
67
using Microsoft.Toolkit.Mvvm.ComponentModel;
78
using Microsoft.VisualStudio.TestTools.UnitTesting;
@@ -50,6 +51,23 @@ public void Test_ObservablePropertyAttribute_Events()
5051
Assert.AreEqual(changed.Item2, 42);
5152
}
5253

54+
[TestCategory("Mvvm")]
55+
[TestMethod]
56+
public void Test_AlsoNotifyForAttribute_Events()
57+
{
58+
var model = new DependentPropertyModel();
59+
60+
(PropertyChangedEventArgs, int) changed = default;
61+
List<string> propertyNames = new();
62+
63+
model.PropertyChanged += (s, e) => propertyNames.Add(e.PropertyName);
64+
65+
model.Name = "Bob";
66+
model.Surname = "Ross";
67+
68+
CollectionAssert.AreEqual(new[] { nameof(model.Name), nameof(model.FullName), nameof(model.Surname), nameof(model.FullName) }, propertyNames);
69+
}
70+
5371
public partial class SampleModel : ObservableObject
5472
{
5573
/// <summary>
@@ -58,5 +76,19 @@ public partial class SampleModel : ObservableObject
5876
[ObservableProperty]
5977
private int data;
6078
}
79+
80+
[INotifyPropertyChanged]
81+
public sealed partial class DependentPropertyModel
82+
{
83+
[ObservableProperty]
84+
[AlsoNotifyFor(nameof(FullName))]
85+
private string name;
86+
87+
[ObservableProperty]
88+
[AlsoNotifyFor(nameof(FullName))]
89+
private string surname;
90+
91+
public string FullName => $"{Name} {Surname}";
92+
}
6193
}
6294
}

0 commit comments

Comments
 (0)