Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.Input.Models;
/// A model with gathered info on a given command method.
/// </summary>
/// <param name="MethodName">The name of the target method.</param>
/// <param name="FieldName">The resulting field name for the generated command.</param>
/// <param name="FieldName">The resulting field name for the generated command, or null if the <see langword="field"/> is available.</param>
/// <param name="PropertyName">The resulting property name for the generated command.</param>
/// <param name="CommandInterfaceType">The command interface type name.</param>
/// <param name="CommandClassType">The command class type name.</param>
Expand All @@ -26,7 +26,7 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.Input.Models;
/// <param name="ForwardedAttributes">The sequence of forwarded attributes for the generated members.</param>
internal sealed record CommandInfo(
string MethodName,
string FieldName,
string? FieldName,
string PropertyName,
string CommandInterfaceType,
string CommandClassType,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,17 @@ public static bool TryGetInfo(

token.ThrowIfCancellationRequested();

// Get the option to force a backing field, if any
if (!TryGetUseBackingField(
attributeData,
semanticModel,
out bool useBackingField))
{
goto Failure;
}

token.ThrowIfCancellationRequested();

// Get all forwarded attributes (don't stop in case of errors, just ignore faulting attributes)
GatherForwardedAttributes(
methodSymbol,
Expand All @@ -147,7 +158,7 @@ public static bool TryGetInfo(

commandInfo = new CommandInfo(
methodSymbol.Name,
fieldName,
useBackingField ? fieldName : null,
propertyName,
commandInterfaceType,
commandClassType,
Expand Down Expand Up @@ -207,25 +218,33 @@ public static ImmutableArray<MemberDeclarationSyntax> GetSyntax(CommandInfo comm
.Select(static a => AttributeList(SingletonSeparatedList(a.GetSyntax())))
.ToArray();

// Construct the generated field as follows:
//
// /// <summary>The backing field for <see cref="<COMMAND_PROPERTY_NAME>"/></summary>
// [global::System.CodeDom.Compiler.GeneratedCode("...", "...")]
// <FORWARDED_ATTRIBUTES>
// private <COMMAND_TYPE>? <COMMAND_FIELD_NAME>;
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($"/// <summary>The backing field for <see cref=\"{commandInfo.PropertyName}\"/>.</summary>")), SyntaxKind.OpenBracketToken, TriviaList())))
.AddAttributeLists(forwardedFieldAttributes);
using ImmutableArrayBuilder<MemberDeclarationSyntax> declarations = ImmutableArrayBuilder<MemberDeclarationSyntax>.Rent();

// Declare a backing field if needed
if (commandInfo.FieldName is not null)
{
// Construct the generated field as follows:
//
// /// <summary>The backing field for <see cref="<COMMAND_PROPERTY_NAME>"/></summary>
// [global::System.CodeDom.Compiler.GeneratedCode("...", "...")]
// <FORWARDED_ATTRIBUTES>
// private <COMMAND_TYPE>? <COMMAND_FIELD_NAME>;
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($"/// <summary>The backing field for <see cref=\"{commandInfo.PropertyName}\"/>.</summary>")), SyntaxKind.OpenBracketToken, TriviaList())))
.AddAttributeLists(forwardedFieldAttributes);

declarations.Add(fieldDeclaration);
}

// Prepares the argument to pass the underlying method to invoke
using ImmutableArrayBuilder<ArgumentSyntax> commandCreationArguments = ImmutableArrayBuilder<ArgumentSyntax>.Rent();
Expand Down Expand Up @@ -337,35 +356,44 @@ public static ImmutableArray<MemberDeclarationSyntax> 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 cancelCommandPropertyName = $"{commandInfo.PropertyName.Substring(0, commandInfo.PropertyName.Length - "Command".Length)}CancelCommand";
string? cancelCommandFieldName = commandInfo.FieldName is not null ? $"{commandInfo.FieldName[..^"Command".Length]}CancelCommand" : null;
string cancelCommandPropertyName = $"{commandInfo.PropertyName[..^"Command".Length]}CancelCommand";

// Construct the generated field for the cancel command as follows:
//
// /// <summary>The backing field for <see cref="<COMMAND_PROPERTY_NAME>"/></summary>
// [global::System.CodeDom.Compiler.GeneratedCode("...", "...")]
// private global::System.Windows.Input.ICommand? <CANCEL_COMMAND_FIELD_NAME>;
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($"/// <summary>The backing field for <see cref=\"{cancelCommandPropertyName}\"/>.</summary>")), 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:
//
// /// <summary>The backing field for <see cref="<COMMAND_PROPERTY_NAME>"/></summary>
// [global::System.CodeDom.Compiler.GeneratedCode("...", "...")]
// private global::System.Windows.Input.ICommand? <CANCEL_COMMAND_FIELD_NAME>;
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($"/// <summary>The backing field for <see cref=\"{cancelCommandPropertyName}\"/>.</summary>")), SyntaxKind.OpenBracketToken, TriviaList())));

declarations.Add(cancelCommandFieldDeclaration);
}

// Construct the generated property as follows (the explicit delegate cast is needed to avoid overload resolution conflicts):
//
Expand Down Expand Up @@ -393,7 +421,7 @@ public static ImmutableArray<MemberDeclarationSyntax> GetSyntax(CommandInfo comm
ArrowExpressionClause(
AssignmentExpression(
SyntaxKind.CoalesceAssignmentExpression,
IdentifierName(cancelCommandFieldName),
cancelCommandFieldName is not null ? IdentifierName(cancelCommandFieldName) : IdentifierName("field"),
InvocationExpression(
MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
Expand All @@ -402,10 +430,11 @@ public static ImmutableArray<MemberDeclarationSyntax> GetSyntax(CommandInfo comm
.AddArgumentListArguments(Argument(IdentifierName(commandInfo.PropertyName))))))
.WithSemicolonToken(Token(SyntaxKind.SemicolonToken));

return ImmutableArray.Create<MemberDeclarationSyntax>(fieldDeclaration, propertyDeclaration, cancelCommandFieldDeclaration, cancelCommandPropertyDeclaration);
declarations.Add(cancelCommandPropertyDeclaration);

}

return ImmutableArray.Create<MemberDeclarationSyntax>(fieldDeclaration, propertyDeclaration);
return declarations.ToImmutable();
}

/// <summary>
Expand Down Expand Up @@ -760,6 +789,43 @@ private static bool TryGetIncludeCancelCommandSwitch(
}
}

/// <summary>
/// Checks whether or not the user has requested to force using a backing field.
/// </summary>
/// <param name="attributeData">The <see cref="AttributeData"/> instance the method was annotated with.</param>
/// <param name="semanticModel">The <see cref="SemanticModel"/> instance for the current run.</param>
/// <param name="useBackingField">Whether or not to use a backing field.</param>
/// <returns>Whether or not a value for <paramref name="useBackingField"/> could be retrieved successfully.</returns>
private static bool TryGetUseBackingField(
AttributeData attributeData,
SemanticModel semanticModel,
out bool useBackingField)
{
// Try to get the custom switch for cancel command generation (the default is false)
if (!attributeData.TryGetNamedArgument("ForceBackingField", out useBackingField))
{
useBackingField = false;
}

// We can only 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.
// Otherwise, we must use a backing field.
#if ROSLYN_5_0_0_OR_GREATER
if (!semanticModel.Compilation.HasLanguageVersionAtLeastEqualTo(LanguageVersion.CSharp14))
{
useBackingField = true;
}
#elif ROSLYN_4_12_0_OR_GREATER
if (!semanticModel.Compilation.IsLanguageVersionPreview())
{
useBackingField = true;
}
#else
useBackingField = true;
#endif

return true;
}

/// <summary>
/// Tries to get the expression type for the "CanExecute" property, if available.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,4 +116,9 @@ public sealed class RelayCommandAttribute : Attribute
/// </summary>
/// <remarks>Using this property is not valid if the target command doesn't map to a cancellable asynchronous command.</remarks>
public bool IncludeCancelCommand { get; init; }

/// <summary>
/// Gets or sets a value indicating whether or not a backing field should generated regardless of the availablity of the <see langword="field"/> keyword.
/// </summary>
public bool ForceBackingField { get; init; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<RequiredAttribute>());
Expand All @@ -579,6 +580,7 @@ public void Test_RelayCommandAttribute_WithExplicitAttributesForFieldAndProperty
Assert.IsNotNull(fooField.GetCustomAttribute<MaxLengthAttribute>());
Assert.AreEqual(100, fooField.GetCustomAttribute<MaxLengthAttribute>()!.Length);

#endif
PropertyInfo fooProperty = typeof(MyViewModelWithExplicitFieldAndPropertyAttributes).GetProperty("FooCommand")!;

Assert.IsNotNull(fooProperty.GetCustomAttribute<RequiredAttribute>());
Expand Down Expand Up @@ -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<TestValidationAttribute>()!);
#endif

PropertyInfo fooBarProperty = typeof(MyViewModelWithExplicitFieldAndPropertyAttributes).GetProperty("FooBarCommand")!;

ValidateTestAttribute(fooBarProperty.GetCustomAttribute<TestValidationAttribute>()!);

#if !ROSLYN_4_12_0_OR_GREATER
FieldInfo barBazField = typeof(MyViewModelWithExplicitFieldAndPropertyAttributes).GetField("barBazCommand", BindingFlags.Instance | BindingFlags.NonPublic)!;

Assert.IsNotNull(barBazField.GetCustomAttribute<Test_ObservablePropertyAttribute.TestAttribute>());
#endif

PropertyInfo barBazCommand = typeof(MyViewModelWithExplicitFieldAndPropertyAttributes).GetProperty("BarBazCommand")!;

Expand Down Expand Up @@ -670,23 +676,27 @@ public void Test_RelayCommandAttribute_WithPartialCommandMethodDefinitions()
_ = Assert.IsInstanceOfType<RelayCommand>(model.BazCommand);
_ = Assert.IsInstanceOfType<AsyncRelayCommand>(model.FooBarCommand);

#if !ROSLYN_4_12_0_OR_GREATER
FieldInfo bazField = typeof(ModelWithPartialCommandMethods).GetField("bazCommand", BindingFlags.Instance | BindingFlags.NonPublic)!;

Assert.IsNotNull(bazField.GetCustomAttribute<RequiredAttribute>());
Assert.IsNotNull(bazField.GetCustomAttribute<MinLengthAttribute>());
Assert.AreEqual(1, bazField.GetCustomAttribute<MinLengthAttribute>()!.Length);
#endif

PropertyInfo bazProperty = typeof(ModelWithPartialCommandMethods).GetProperty("BazCommand")!;

Assert.IsNotNull(bazProperty.GetCustomAttribute<MinLengthAttribute>());
Assert.AreEqual(2, bazProperty.GetCustomAttribute<MinLengthAttribute>()!.Length);
Assert.IsNotNull(bazProperty.GetCustomAttribute<XmlIgnoreAttribute>());

#if !ROSLYN_4_12_0_OR_GREATER
FieldInfo fooBarField = typeof(ModelWithPartialCommandMethods).GetField("fooBarCommand", BindingFlags.Instance | BindingFlags.NonPublic)!;

Assert.IsNotNull(fooBarField.GetCustomAttribute<RequiredAttribute>());
Assert.IsNotNull(fooBarField.GetCustomAttribute<MinLengthAttribute>());
Assert.AreEqual(1, fooBarField.GetCustomAttribute<MinLengthAttribute>()!.Length);
Assert.AreEqual(1, fooBarField.GetCustomAttribute<MinLengthAttribute>()!.Length);
#endif

PropertyInfo fooBarProperty = typeof(ModelWithPartialCommandMethods).GetProperty("FooBarCommand")!;

Expand Down
Loading