diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTRelayCommandIsNotGeneratedBindableCustomPropertyCompatibleAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTRelayCommandIsNotGeneratedBindableCustomPropertyCompatibleAnalyzer.cs
index 10029b84b..3feec713b 100644
--- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTRelayCommandIsNotGeneratedBindableCustomPropertyCompatibleAnalyzer.cs
+++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTRelayCommandIsNotGeneratedBindableCustomPropertyCompatibleAnalyzer.cs
@@ -60,7 +60,7 @@ public override void Initialize(AnalysisContext context)
return;
}
- (_, string propertyName) = RelayCommandGenerator.Execute.GetGeneratedFieldAndPropertyNames(methodSymbol);
+ (_, string propertyName) = RelayCommandGenerator.Execute.GetGeneratedFieldAndPropertyNames(methodSymbol, context.Compilation);
if (DoesGeneratedBindableCustomPropertyAttributeIncludePropertyName(generatedBindableCustomPropertyAttribute, propertyName))
{
diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Input/Models/CommandInfo.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Input/Models/CommandInfo.cs
index 70372a03f..e1ce546ac 100644
--- a/src/CommunityToolkit.Mvvm.SourceGenerators/Input/Models/CommandInfo.cs
+++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Input/Models/CommandInfo.cs
@@ -11,7 +11,7 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.Input.Models;
/// A model with gathered info on a given command method.
///
/// The name of the target method.
-/// The resulting field name for the generated command.
+/// The resulting field name for the generated command, or null if the is available.
/// The resulting property name for the generated command.
/// The command interface type name.
/// The command class type name.
@@ -26,7 +26,7 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.Input.Models;
/// The sequence of forwarded attributes for the generated members.
internal sealed record CommandInfo(
string MethodName,
- string FieldName,
+ string? FieldName,
string PropertyName,
string CommandInterfaceType,
string CommandClassType,
diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Input/RelayCommandGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Input/RelayCommandGenerator.Execute.cs
index 57eb1ebcf..389f07e3f 100644
--- a/src/CommunityToolkit.Mvvm.SourceGenerators/Input/RelayCommandGenerator.Execute.cs
+++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Input/RelayCommandGenerator.Execute.cs
@@ -59,7 +59,7 @@ public static bool TryGetInfo(
token.ThrowIfCancellationRequested();
// Get the command field and property names
- (string fieldName, string propertyName) = GetGeneratedFieldAndPropertyNames(methodSymbol);
+ (string? fieldName, string propertyName) = GetGeneratedFieldAndPropertyNames(methodSymbol, semanticModel.Compilation);
token.ThrowIfCancellationRequested();
@@ -207,25 +207,33 @@ public static ImmutableArray GetSyntax(CommandInfo comm
.Select(static a => AttributeList(SingletonSeparatedList(a.GetSyntax())))
.ToArray();
- // Construct the generated field as follows:
- //
- // /// The backing field for
- // [global::System.CodeDom.Compiler.GeneratedCode("...", "...")]
- //
- // private ? ;
- FieldDeclarationSyntax fieldDeclaration =
- FieldDeclaration(
- VariableDeclaration(NullableType(IdentifierName(commandClassTypeName)))
- .AddVariables(VariableDeclarator(Identifier(commandInfo.FieldName))))
- .AddModifiers(Token(SyntaxKind.PrivateKeyword))
- .AddAttributeLists(
- AttributeList(SingletonSeparatedList(
- Attribute(IdentifierName("global::System.CodeDom.Compiler.GeneratedCode"))
- .AddArgumentListArguments(
- AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(RelayCommandGenerator).FullName))),
- AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(RelayCommandGenerator).Assembly.GetName().Version.ToString()))))))
- .WithOpenBracketToken(Token(TriviaList(Comment($"/// The backing field for .")), SyntaxKind.OpenBracketToken, TriviaList())))
- .AddAttributeLists(forwardedFieldAttributes);
+ ImmutableArrayBuilder declarations = ImmutableArrayBuilder.Rent();
+
+ // Declare a backing field if needed
+ if (commandInfo.FieldName is not null)
+ {
+ // Construct the generated field as follows:
+ //
+ // /// The backing field for
+ // [global::System.CodeDom.Compiler.GeneratedCode("...", "...")]
+ //
+ // private ? ;
+ FieldDeclarationSyntax fieldDeclaration =
+ FieldDeclaration(
+ VariableDeclaration(NullableType(IdentifierName(commandClassTypeName)))
+ .AddVariables(VariableDeclarator(Identifier(commandInfo.FieldName))))
+ .AddModifiers(Token(SyntaxKind.PrivateKeyword))
+ .AddAttributeLists(
+ AttributeList(SingletonSeparatedList(
+ Attribute(IdentifierName("global::System.CodeDom.Compiler.GeneratedCode"))
+ .AddArgumentListArguments(
+ AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(RelayCommandGenerator).FullName))),
+ AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(RelayCommandGenerator).Assembly.GetName().Version.ToString()))))))
+ .WithOpenBracketToken(Token(TriviaList(Comment($"/// The backing field for .")), SyntaxKind.OpenBracketToken, TriviaList())))
+ .AddAttributeLists(forwardedFieldAttributes);
+
+ declarations.Add(fieldDeclaration);
+ }
// Prepares the argument to pass the underlying method to invoke
using ImmutableArrayBuilder commandCreationArguments = ImmutableArrayBuilder.Rent();
@@ -337,35 +345,44 @@ public static ImmutableArray GetSyntax(CommandInfo comm
ArrowExpressionClause(
AssignmentExpression(
SyntaxKind.CoalesceAssignmentExpression,
- IdentifierName(commandInfo.FieldName),
+ commandInfo.FieldName is not null ? IdentifierName(commandInfo.FieldName) : IdentifierName("field"),
ObjectCreationExpression(IdentifierName(commandClassTypeName))
.AddArgumentListArguments(commandCreationArguments.ToArray()))))
.WithSemicolonToken(Token(SyntaxKind.SemicolonToken));
+ declarations.Add(propertyDeclaration);
+
// Conditionally declare the additional members for the cancel commands
if (commandInfo.IncludeCancelCommand)
{
// Prepare all necessary member and type names
- string cancelCommandFieldName = $"{commandInfo.FieldName.Substring(0, commandInfo.FieldName.Length - "Command".Length)}CancelCommand";
+ string? cancelCommandFieldName = commandInfo.FieldName is not null ? $"{commandInfo.FieldName.Substring(0, commandInfo.FieldName.Length - "Command".Length)}CancelCommand" : null;
string cancelCommandPropertyName = $"{commandInfo.PropertyName.Substring(0, commandInfo.PropertyName.Length - "Command".Length)}CancelCommand";
- // Construct the generated field for the cancel command as follows:
- //
- // /// The backing field for
- // [global::System.CodeDom.Compiler.GeneratedCode("...", "...")]
- // private global::System.Windows.Input.ICommand? ;
- FieldDeclarationSyntax cancelCommandFieldDeclaration =
- FieldDeclaration(
- VariableDeclaration(NullableType(IdentifierName("global::System.Windows.Input.ICommand")))
- .AddVariables(VariableDeclarator(Identifier(cancelCommandFieldName))))
- .AddModifiers(Token(SyntaxKind.PrivateKeyword))
- .AddAttributeLists(
- AttributeList(SingletonSeparatedList(
- Attribute(IdentifierName("global::System.CodeDom.Compiler.GeneratedCode"))
- .AddArgumentListArguments(
- AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(RelayCommandGenerator).FullName))),
- AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(RelayCommandGenerator).Assembly.GetName().Version.ToString()))))))
- .WithOpenBracketToken(Token(TriviaList(Comment($"/// The backing field for .")), SyntaxKind.OpenBracketToken, TriviaList())));
+ // Declare a backing field for the cancel command if needed.
+ // This is only needed if we can't use the field keyword for the main command, as otherwise the cancel command can just use its own field keyword.
+ if (cancelCommandFieldName is not null)
+ {
+ // Construct the generated field for the cancel command as follows:
+ //
+ // /// The backing field for
+ // [global::System.CodeDom.Compiler.GeneratedCode("...", "...")]
+ // private global::System.Windows.Input.ICommand? ;
+ FieldDeclarationSyntax cancelCommandFieldDeclaration =
+ FieldDeclaration(
+ VariableDeclaration(NullableType(IdentifierName("global::System.Windows.Input.ICommand")))
+ .AddVariables(VariableDeclarator(Identifier(cancelCommandFieldName))))
+ .AddModifiers(Token(SyntaxKind.PrivateKeyword))
+ .AddAttributeLists(
+ AttributeList(SingletonSeparatedList(
+ Attribute(IdentifierName("global::System.CodeDom.Compiler.GeneratedCode"))
+ .AddArgumentListArguments(
+ AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(RelayCommandGenerator).FullName))),
+ AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(RelayCommandGenerator).Assembly.GetName().Version.ToString()))))))
+ .WithOpenBracketToken(Token(TriviaList(Comment($"/// The backing field for .")), SyntaxKind.OpenBracketToken, TriviaList())));
+
+ declarations.Add(cancelCommandFieldDeclaration);
+ }
// Construct the generated property as follows (the explicit delegate cast is needed to avoid overload resolution conflicts):
//
@@ -393,7 +410,7 @@ public static ImmutableArray GetSyntax(CommandInfo comm
ArrowExpressionClause(
AssignmentExpression(
SyntaxKind.CoalesceAssignmentExpression,
- IdentifierName(cancelCommandFieldName),
+ cancelCommandFieldName is not null ? IdentifierName(cancelCommandFieldName) : IdentifierName("field"),
InvocationExpression(
MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
@@ -402,10 +419,11 @@ public static ImmutableArray GetSyntax(CommandInfo comm
.AddArgumentListArguments(Argument(IdentifierName(commandInfo.PropertyName))))))
.WithSemicolonToken(Token(SyntaxKind.SemicolonToken));
- return ImmutableArray.Create(fieldDeclaration, propertyDeclaration, cancelCommandFieldDeclaration, cancelCommandPropertyDeclaration);
+ declarations.Add(cancelCommandPropertyDeclaration);
+
}
- return ImmutableArray.Create(fieldDeclaration, propertyDeclaration);
+ return declarations.ToImmutable();
}
///
@@ -474,8 +492,9 @@ private static bool IsCommandDefinitionUnique(IMethodSymbol methodSymbol, in Imm
/// Get the generated field and property names for the input method.
///
/// The input instance to process.
+ /// The compilation info, used to determine language version.
/// The generated field and property names for .
- public static (string FieldName, string PropertyName) GetGeneratedFieldAndPropertyNames(IMethodSymbol methodSymbol)
+ public static (string? FieldName, string PropertyName) GetGeneratedFieldAndPropertyNames(IMethodSymbol methodSymbol, Compilation? compilation = null)
{
string propertyName = methodSymbol.Name;
@@ -498,6 +517,24 @@ public static (string FieldName, string PropertyName) GetGeneratedFieldAndProper
propertyName += "Command";
+ if (compilation is not null)
+ {
+ // We can use the field keyword as the generated field name if the language version is C# 14 or greater, or if it's C# 13 and the preview features are enabled.
+ // In this case, there is no need to generate a backing field, as the property itself will be auto-generated with an underlying field.
+ bool useFieldKeyword = false;
+
+#if ROSLYN_5_0_0_OR_GREATER
+ useFieldKeyword = compilation.HasLanguageVersionAtLeastEqualTo(LanguageVersion.CSharp14);
+#elif ROSLYN_4_12_0_OR_GREATER
+ useFieldKeyword = compilation.IsLanguageVersionPreview();
+#endif
+
+ if (useFieldKeyword)
+ {
+ return (null, propertyName);
+ }
+ }
+
char firstCharacter = propertyName[0];
char loweredFirstCharacter = char.ToLower(firstCharacter, CultureInfo.InvariantCulture);
diff --git a/tests/CommunityToolkit.Mvvm.UnitTests/Test_ObservablePropertyAttribute.cs b/tests/CommunityToolkit.Mvvm.UnitTests/Test_ObservablePropertyAttribute.cs
index 646058a7a..3a12701b3 100644
--- a/tests/CommunityToolkit.Mvvm.UnitTests/Test_ObservablePropertyAttribute.cs
+++ b/tests/CommunityToolkit.Mvvm.UnitTests/Test_ObservablePropertyAttribute.cs
@@ -1003,6 +1003,7 @@ public void Test_ObservableProperty_WithExplicitAttributeForProperty()
Assert.AreEqual((Animal)67, testAttribute2.Animal);
}
+#if !ROSLYN_4_12_0_OR_GREATER
// See https://github.com/CommunityToolkit/dotnet/issues/446
[TestMethod]
public void Test_ObservableProperty_CommandNamesThatCantBeLowered()
@@ -1021,6 +1022,7 @@ public void Test_ObservableProperty_CommandNamesThatCantBeLowered()
Assert.AreSame(model.c中文Command, fieldInfo?.GetValue(model));
}
+#endif
// See https://github.com/CommunityToolkit/dotnet/issues/375
[TestMethod]
diff --git a/tests/CommunityToolkit.Mvvm.UnitTests/Test_RelayCommandAttribute.cs b/tests/CommunityToolkit.Mvvm.UnitTests/Test_RelayCommandAttribute.cs
index 24293805a..8fa2e679c 100644
--- a/tests/CommunityToolkit.Mvvm.UnitTests/Test_RelayCommandAttribute.cs
+++ b/tests/CommunityToolkit.Mvvm.UnitTests/Test_RelayCommandAttribute.cs
@@ -571,6 +571,7 @@ public void Test_RelayCommandAttribute_CanExecuteWithNullabilityAnnotations()
[TestMethod]
public void Test_RelayCommandAttribute_WithExplicitAttributesForFieldAndProperty()
{
+#if !ROSLYN_4_12_0_OR_GREATER
FieldInfo fooField = typeof(MyViewModelWithExplicitFieldAndPropertyAttributes).GetField("fooCommand", BindingFlags.Instance | BindingFlags.NonPublic)!;
Assert.IsNotNull(fooField.GetCustomAttribute());
@@ -579,6 +580,7 @@ public void Test_RelayCommandAttribute_WithExplicitAttributesForFieldAndProperty
Assert.IsNotNull(fooField.GetCustomAttribute());
Assert.AreEqual(100, fooField.GetCustomAttribute()!.Length);
+#endif
PropertyInfo fooProperty = typeof(MyViewModelWithExplicitFieldAndPropertyAttributes).GetProperty("FooCommand")!;
Assert.IsNotNull(fooProperty.GetCustomAttribute());
@@ -618,17 +620,21 @@ static void ValidateTestAttribute(TestValidationAttribute testAttribute)
Assert.AreEqual(Test_ObservablePropertyAttribute.Animal.Llama, testAttribute.Animal);
}
+#if !ROSLYN_4_12_0_OR_GREATER
FieldInfo fooBarField = typeof(MyViewModelWithExplicitFieldAndPropertyAttributes).GetField("fooBarCommand", BindingFlags.Instance | BindingFlags.NonPublic)!;
ValidateTestAttribute(fooBarField.GetCustomAttribute()!);
+#endif
PropertyInfo fooBarProperty = typeof(MyViewModelWithExplicitFieldAndPropertyAttributes).GetProperty("FooBarCommand")!;
ValidateTestAttribute(fooBarProperty.GetCustomAttribute()!);
+#if !ROSLYN_4_12_0_OR_GREATER
FieldInfo barBazField = typeof(MyViewModelWithExplicitFieldAndPropertyAttributes).GetField("barBazCommand", BindingFlags.Instance | BindingFlags.NonPublic)!;
Assert.IsNotNull(barBazField.GetCustomAttribute());
+#endif
PropertyInfo barBazCommand = typeof(MyViewModelWithExplicitFieldAndPropertyAttributes).GetProperty("BarBazCommand")!;
@@ -670,11 +676,13 @@ public void Test_RelayCommandAttribute_WithPartialCommandMethodDefinitions()
_ = Assert.IsInstanceOfType(model.BazCommand);
_ = Assert.IsInstanceOfType(model.FooBarCommand);
+#if !ROSLYN_4_12_0_OR_GREATER
FieldInfo bazField = typeof(ModelWithPartialCommandMethods).GetField("bazCommand", BindingFlags.Instance | BindingFlags.NonPublic)!;
Assert.IsNotNull(bazField.GetCustomAttribute());
Assert.IsNotNull(bazField.GetCustomAttribute());
Assert.AreEqual(1, bazField.GetCustomAttribute()!.Length);
+#endif
PropertyInfo bazProperty = typeof(ModelWithPartialCommandMethods).GetProperty("BazCommand")!;
@@ -682,11 +690,13 @@ public void Test_RelayCommandAttribute_WithPartialCommandMethodDefinitions()
Assert.AreEqual(2, bazProperty.GetCustomAttribute()!.Length);
Assert.IsNotNull(bazProperty.GetCustomAttribute());
+#if !ROSLYN_4_12_0_OR_GREATER
FieldInfo fooBarField = typeof(ModelWithPartialCommandMethods).GetField("fooBarCommand", BindingFlags.Instance | BindingFlags.NonPublic)!;
Assert.IsNotNull(fooBarField.GetCustomAttribute());
Assert.IsNotNull(fooBarField.GetCustomAttribute());
- Assert.AreEqual(1, fooBarField.GetCustomAttribute()!.Length);
+ Assert.AreEqual(1, fooBarField.GetCustomAttribute()!.Length);
+#endif
PropertyInfo fooBarProperty = typeof(ModelWithPartialCommandMethods).GetProperty("FooBarCommand")!;