Skip to content

Commit b77ba0d

Browse files
authored
Merge pull request #970 from CommunityToolkit/dev/partial-property-fixes
Bug fixes to generated partial properties
2 parents 812f545 + 73e7a88 commit b77ba0d

File tree

5 files changed

+135
-13
lines changed

5 files changed

+135
-13
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models;
2323
/// <param name="NotifiedCommandNames">The sequence of commands to notify.</param>
2424
/// <param name="NotifyPropertyChangedRecipients">Whether or not the generated property also broadcasts changes.</param>
2525
/// <param name="NotifyDataErrorInfo">Whether or not the generated property also validates its value.</param>
26+
/// <param name="IsRequired">Whether or not the generated property should be marked as required.</param>
2627
/// <param name="IsOldPropertyValueDirectlyReferenced">Whether the old property value is being directly referenced.</param>
2728
/// <param name="IsReferenceTypeOrUnconstrainedTypeParameter">Indicates whether the property is of a reference type or an unconstrained type parameter.</param>
2829
/// <param name="IncludeMemberNotNullOnSetAccessor">Indicates whether to include nullability annotations on the setter.</param>
@@ -41,6 +42,7 @@ internal sealed record PropertyInfo(
4142
EquatableArray<string> NotifiedCommandNames,
4243
bool NotifyPropertyChangedRecipients,
4344
bool NotifyDataErrorInfo,
45+
bool IsRequired,
4446
bool IsOldPropertyValueDirectlyReferenced,
4547
bool IsReferenceTypeOrUnconstrainedTypeParameter,
4648
bool IncludeMemberNotNullOnSetAccessor,

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

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,11 @@ public static bool TryGetInfo(
348348

349349
token.ThrowIfCancellationRequested();
350350

351+
// Check whether the property should be required
352+
bool isRequired = GetIsRequiredProperty(memberSymbol);
353+
354+
token.ThrowIfCancellationRequested();
355+
351356
propertyInfo = new PropertyInfo(
352357
memberSyntax.Kind(),
353358
typeNameWithNullabilityAnnotations,
@@ -361,6 +366,7 @@ public static bool TryGetInfo(
361366
notifiedCommandNames.ToImmutable(),
362367
notifyRecipients,
363368
notifyDataErrorInfo,
369+
isRequired,
364370
isOldPropertyValueDirectlyReferenced,
365371
isReferenceTypeOrUnconstrainedTypeParameter,
366372
includeMemberNotNullOnSetAccessor,
@@ -1028,6 +1034,20 @@ private static bool TryGetAccessibilityModifiers(
10281034
return true;
10291035
}
10301036

1037+
/// <summary>
1038+
/// Checks whether an input member is a required property.
1039+
/// </summary>
1040+
/// <param name="memberSymbol">The input <see cref="ISymbol"/> instance to process.</param>
1041+
/// <returns>Whether <paramref name="memberSymbol"/> is a required property.</returns>
1042+
private static bool GetIsRequiredProperty(ISymbol memberSymbol)
1043+
{
1044+
#if ROSLYN_4_3_1_OR_GREATER
1045+
return memberSymbol is IPropertySymbol { IsRequired: true };
1046+
#else
1047+
return false;
1048+
#endif
1049+
}
1050+
10311051
/// <summary>
10321052
/// Gets a <see cref="CompilationUnitSyntax"/> instance with the cached args for property changing notifications.
10331053
/// </summary>
@@ -1324,11 +1344,6 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf
13241344
// Also add any forwarded attributes
13251345
setAccessor = setAccessor.AddAttributeLists(forwardedSetAccessorAttributes);
13261346

1327-
// Prepare the modifiers for the property
1328-
SyntaxTokenList propertyModifiers = propertyInfo.AnnotatedMemberKind is SyntaxKind.PropertyDeclaration
1329-
? propertyInfo.PropertyAccessibility.ToSyntaxTokenList().Add(Token(SyntaxKind.PartialKeyword))
1330-
: propertyInfo.PropertyAccessibility.ToSyntaxTokenList();
1331-
13321347
// Construct the generated property as follows:
13331348
//
13341349
// <XML_SUMMARY>
@@ -1352,7 +1367,7 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf
13521367
.WithOpenBracketToken(Token(TriviaList(Comment(xmlSummary)), SyntaxKind.OpenBracketToken, TriviaList())),
13531368
AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage")))))
13541369
.AddAttributeLists(forwardedPropertyAttributes)
1355-
.WithModifiers(propertyModifiers)
1370+
.WithModifiers(GetPropertyModifiers(propertyInfo))
13561371
.AddAccessorListAccessors(
13571372
AccessorDeclaration(SyntaxKind.GetAccessorDeclaration)
13581373
.WithModifiers(propertyInfo.GetterAccessibility.ToSyntaxTokenList())
@@ -1362,6 +1377,32 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf
13621377
setAccessor);
13631378
}
13641379

1380+
/// <summary>
1381+
/// Gets all modifiers that need to be added to a generated property.
1382+
/// </summary>
1383+
/// <param name="propertyInfo">The input <see cref="PropertyInfo"/> instance to process.</param>
1384+
/// <returns>The list of necessary modifiers for <paramref name="propertyInfo"/>.</returns>
1385+
private static SyntaxTokenList GetPropertyModifiers(PropertyInfo propertyInfo)
1386+
{
1387+
SyntaxTokenList propertyModifiers = propertyInfo.PropertyAccessibility.ToSyntaxTokenList();
1388+
1389+
#if ROSLYN_4_3_1_OR_GREATER
1390+
// Add the 'required' modifier if the original member also had it
1391+
if (propertyInfo.IsRequired)
1392+
{
1393+
propertyModifiers = propertyModifiers.Add(Token(SyntaxKind.RequiredKeyword));
1394+
}
1395+
#endif
1396+
1397+
// Add the 'partial' modifier if the original member is a partial property
1398+
if (propertyInfo.AnnotatedMemberKind is SyntaxKind.PropertyDeclaration)
1399+
{
1400+
propertyModifiers = propertyModifiers.Add(Token(SyntaxKind.PartialKeyword));
1401+
}
1402+
1403+
return propertyModifiers;
1404+
}
1405+
13651406
/// <summary>
13661407
/// Gets the <see cref="MemberDeclarationSyntax"/> instances for the <c>OnPropertyChanging</c> and <c>OnPropertyChanged</c> methods for the input field.
13671408
/// </summary>

src/CommunityToolkit.Mvvm/ComponentModel/Attributes/NotifyPropertyChangedRecipientsAttribute.cs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
namespace CommunityToolkit.Mvvm.ComponentModel;
88

99
/// <summary>
10-
/// An attribute that can be used to support <see cref="ObservablePropertyAttribute"/> in generated properties, when applied to fields
10+
/// An attribute that can be used to support <see cref="ObservablePropertyAttribute"/> in generated properties, when applied to fields and properties
1111
/// contained in a type that is either inheriting from <see cref="ObservableRecipient"/>, or annotated with <see cref="ObservableRecipientAttribute"/>.
1212
/// When this attribute is used, the generated property setter will also call <see cref="ObservableRecipient.Broadcast{T}(T, T, string?)"/>.
1313
/// This allows generated properties to opt-in into broadcasting behavior without having to fallback into a full explicit observable property.
@@ -18,7 +18,7 @@ namespace CommunityToolkit.Mvvm.ComponentModel;
1818
/// {
1919
/// [ObservableProperty]
2020
/// [NotifyPropertyChangedRecipients]
21-
/// private string username;
21+
/// public partial string Username;
2222
/// }
2323
/// </code>
2424
/// </para>
@@ -27,10 +27,10 @@ namespace CommunityToolkit.Mvvm.ComponentModel;
2727
/// <code>
2828
/// partial class MyViewModel
2929
/// {
30-
/// public string Username
30+
/// public partial string Username
3131
/// {
32-
/// get => username;
33-
/// set => SetProperty(ref username, value, broadcast: true);
32+
/// get => field;
33+
/// set => SetProperty(ref field, value, broadcast: true);
3434
/// }
3535
/// }
3636
/// </code>
@@ -39,7 +39,10 @@ namespace CommunityToolkit.Mvvm.ComponentModel;
3939
/// This attribute can also be added to a class, and if so it will affect all generated properties in that type and inherited types.
4040
/// </para>
4141
/// </summary>
42-
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
42+
/// <remarks>
43+
/// Just like <see cref="ObservablePropertyAttribute"/>, this attribute can also be used on fields as well.
44+
/// </remarks>
45+
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
4346
public sealed class NotifyPropertyChangedRecipientsAttribute : Attribute
4447
{
4548
}

src/CommunityToolkit.Mvvm/ComponentModel/Attributes/ObservablePropertyAttribute.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ namespace CommunityToolkit.Mvvm.ComponentModel;
2020
/// partial class MyViewModel : ObservableObject
2121
/// {
2222
/// [ObservableProperty]
23-
/// public partial string name { get; set; }
23+
/// public partial string Name { get; set; }
2424
///
2525
/// [ObservableProperty]
2626
/// public partial bool IsEnabled { get; set; }

tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsCodegen.cs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,82 @@ partial int Number
9292
VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, LanguageVersion.Preview, ("MyApp.MyViewModel.g.cs", result));
9393
}
9494

95+
// See https://github.com/CommunityToolkit/dotnet/issues/969
96+
[TestMethod]
97+
public void ObservablePropertyWithValueType_OnPartialProperty_RequiredProperty_WorksCorrectly()
98+
{
99+
string source = """
100+
using System;
101+
using CommunityToolkit.Mvvm.ComponentModel;
102+
103+
namespace MyApp;
104+
105+
partial class MyViewModel : ObservableObject
106+
{
107+
[ObservableProperty]
108+
public required partial int Number { get; private set; }
109+
}
110+
""";
111+
112+
string result = """
113+
// <auto-generated/>
114+
#pragma warning disable
115+
#nullable enable
116+
namespace MyApp
117+
{
118+
/// <inheritdoc/>
119+
partial class MyViewModel
120+
{
121+
/// <inheritdoc/>
122+
[global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", <ASSEMBLY_VERSION>)]
123+
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
124+
public required partial int Number
125+
{
126+
get => field;
127+
private set
128+
{
129+
if (!global::System.Collections.Generic.EqualityComparer<int>.Default.Equals(field, value))
130+
{
131+
OnNumberChanging(value);
132+
OnNumberChanging(default, value);
133+
OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Number);
134+
field = value;
135+
OnNumberChanged(value);
136+
OnNumberChanged(default, value);
137+
OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Number);
138+
}
139+
}
140+
}
141+
142+
/// <summary>Executes the logic for when <see cref="Number"/> is changing.</summary>
143+
/// <param name="value">The new property value being set.</param>
144+
/// <remarks>This method is invoked right before the value of <see cref="Number"/> is changed.</remarks>
145+
[global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", <ASSEMBLY_VERSION>)]
146+
partial void OnNumberChanging(int value);
147+
/// <summary>Executes the logic for when <see cref="Number"/> is changing.</summary>
148+
/// <param name="oldValue">The previous property value that is being replaced.</param>
149+
/// <param name="newValue">The new property value being set.</param>
150+
/// <remarks>This method is invoked right before the value of <see cref="Number"/> is changed.</remarks>
151+
[global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", <ASSEMBLY_VERSION>)]
152+
partial void OnNumberChanging(int oldValue, int newValue);
153+
/// <summary>Executes the logic for when <see cref="Number"/> just changed.</summary>
154+
/// <param name="value">The new property value that was set.</param>
155+
/// <remarks>This method is invoked right after the value of <see cref="Number"/> is changed.</remarks>
156+
[global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", <ASSEMBLY_VERSION>)]
157+
partial void OnNumberChanged(int value);
158+
/// <summary>Executes the logic for when <see cref="Number"/> just changed.</summary>
159+
/// <param name="oldValue">The previous property value that was replaced.</param>
160+
/// <param name="newValue">The new property value that was set.</param>
161+
/// <remarks>This method is invoked right after the value of <see cref="Number"/> is changed.</remarks>
162+
[global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", <ASSEMBLY_VERSION>)]
163+
partial void OnNumberChanged(int oldValue, int newValue);
164+
}
165+
}
166+
""";
167+
168+
VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, LanguageVersion.Preview, ("MyApp.MyViewModel.g.cs", result));
169+
}
170+
95171
[TestMethod]
96172
public void ObservablePropertyWithValueType_OnPartialProperty_WithExplicitModifiers_WorksCorrectly1()
97173
{

0 commit comments

Comments
 (0)