Skip to content

Commit a3e4d89

Browse files
committed
Added RelayCommandAttribute named optional parameter to force backing field usage
1 parent de845d7 commit a3e4d89

3 files changed

Lines changed: 59 additions & 25 deletions

File tree

src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTRelayCommandIsNotGeneratedBindableCustomPropertyCompatibleAnalyzer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ public override void Initialize(AnalysisContext context)
6060
return;
6161
}
6262

63-
(_, string propertyName) = RelayCommandGenerator.Execute.GetGeneratedFieldAndPropertyNames(methodSymbol, context.Compilation);
63+
(_, string propertyName) = RelayCommandGenerator.Execute.GetGeneratedFieldAndPropertyNames(methodSymbol);
6464

6565
if (DoesGeneratedBindableCustomPropertyAttributeIncludePropertyName(generatedBindableCustomPropertyAttribute, propertyName))
6666
{

src/CommunityToolkit.Mvvm.SourceGenerators/Input/RelayCommandGenerator.Execute.cs

Lines changed: 53 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ public static bool TryGetInfo(
5959
token.ThrowIfCancellationRequested();
6060

6161
// Get the command field and property names
62-
(string? fieldName, string propertyName) = GetGeneratedFieldAndPropertyNames(methodSymbol, semanticModel.Compilation);
62+
(string fieldName, string propertyName) = GetGeneratedFieldAndPropertyNames(methodSymbol);
6363

6464
token.ThrowIfCancellationRequested();
6565

@@ -135,6 +135,17 @@ public static bool TryGetInfo(
135135

136136
token.ThrowIfCancellationRequested();
137137

138+
// Get the option to force a backing field, if any
139+
if (!TryGetUseBackingField(
140+
attributeData,
141+
semanticModel,
142+
out bool useBackingField))
143+
{
144+
goto Failure;
145+
}
146+
147+
token.ThrowIfCancellationRequested();
148+
138149
// Get all forwarded attributes (don't stop in case of errors, just ignore faulting attributes)
139150
GatherForwardedAttributes(
140151
methodSymbol,
@@ -147,7 +158,7 @@ public static bool TryGetInfo(
147158

148159
commandInfo = new CommandInfo(
149160
methodSymbol.Name,
150-
fieldName,
161+
useBackingField ? fieldName : null,
151162
propertyName,
152163
commandInterfaceType,
153164
commandClassType,
@@ -356,8 +367,8 @@ public static ImmutableArray<MemberDeclarationSyntax> GetSyntax(CommandInfo comm
356367
if (commandInfo.IncludeCancelCommand)
357368
{
358369
// Prepare all necessary member and type names
359-
string? cancelCommandFieldName = commandInfo.FieldName is not null ? $"{commandInfo.FieldName.Substring(0, commandInfo.FieldName.Length - "Command".Length)}CancelCommand" : null;
360-
string cancelCommandPropertyName = $"{commandInfo.PropertyName.Substring(0, commandInfo.PropertyName.Length - "Command".Length)}CancelCommand";
370+
string? cancelCommandFieldName = commandInfo.FieldName is not null ? $"{commandInfo.FieldName[..^"Command".Length]}CancelCommand" : null;
371+
string cancelCommandPropertyName = $"{commandInfo.PropertyName[..^"Command".Length]}CancelCommand";
361372

362373
// Declare a backing field for the cancel command if needed.
363374
// 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.
@@ -492,9 +503,8 @@ private static bool IsCommandDefinitionUnique(IMethodSymbol methodSymbol, in Imm
492503
/// Get the generated field and property names for the input method.
493504
/// </summary>
494505
/// <param name="methodSymbol">The input <see cref="IMethodSymbol"/> instance to process.</param>
495-
/// <param name="compilation">The compilation info, used to determine language version.</param>
496506
/// <returns>The generated field and property names for <paramref name="methodSymbol"/>.</returns>
497-
public static (string? FieldName, string PropertyName) GetGeneratedFieldAndPropertyNames(IMethodSymbol methodSymbol, Compilation? compilation = null)
507+
public static (string FieldName, string PropertyName) GetGeneratedFieldAndPropertyNames(IMethodSymbol methodSymbol)
498508
{
499509
string propertyName = methodSymbol.Name;
500510

@@ -517,24 +527,6 @@ public static (string? FieldName, string PropertyName) GetGeneratedFieldAndPrope
517527

518528
propertyName += "Command";
519529

520-
if (compilation is not null)
521-
{
522-
// 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.
523-
// In this case, there is no need to generate a backing field, as the property itself will be auto-generated with an underlying field.
524-
bool useFieldKeyword = false;
525-
526-
#if ROSLYN_5_0_0_OR_GREATER
527-
useFieldKeyword = compilation.HasLanguageVersionAtLeastEqualTo(LanguageVersion.CSharp14);
528-
#elif ROSLYN_4_12_0_OR_GREATER
529-
useFieldKeyword = compilation.IsLanguageVersionPreview();
530-
#endif
531-
532-
if (useFieldKeyword)
533-
{
534-
return (null, propertyName);
535-
}
536-
}
537-
538530
char firstCharacter = propertyName[0];
539531
char loweredFirstCharacter = char.ToLower(firstCharacter, CultureInfo.InvariantCulture);
540532

@@ -797,6 +789,43 @@ private static bool TryGetIncludeCancelCommandSwitch(
797789
}
798790
}
799791

792+
/// <summary>
793+
/// Checks whether or not the user has requested to force using a backing field.
794+
/// </summary>
795+
/// <param name="attributeData">The <see cref="AttributeData"/> instance the method was annotated with.</param>
796+
/// <param name="semanticModel">The <see cref="SemanticModel"/> instance for the current run.</param>
797+
/// <param name="useBackingField">Whether or not to use a backing field.</param>
798+
/// <returns>Whether or not a value for <paramref name="useBackingField"/> could be retrieved successfully.</returns>
799+
private static bool TryGetUseBackingField(
800+
AttributeData attributeData,
801+
SemanticModel semanticModel,
802+
out bool useBackingField)
803+
{
804+
// Try to get the custom switch for cancel command generation (the default is false)
805+
if (!attributeData.TryGetNamedArgument("ForceBackingField", out useBackingField))
806+
{
807+
useBackingField = false;
808+
}
809+
810+
// 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.
811+
// Otherwise, we must use a backing field.
812+
#if ROSLYN_5_0_0_OR_GREATER
813+
if (!semanticModel.Compilation.HasLanguageVersionAtLeastEqualTo(LanguageVersion.CSharp14))
814+
{
815+
useBackingField = true;
816+
}
817+
#elif ROSLYN_4_12_0_OR_GREATER
818+
if (!semanticModel.Compilation.IsLanguageVersionPreview())
819+
{
820+
useBackingField = true;
821+
}
822+
#else
823+
useBackingField = true;
824+
#endif
825+
826+
return true;
827+
}
828+
800829
/// <summary>
801830
/// Tries to get the expression type for the "CanExecute" property, if available.
802831
/// </summary>

src/CommunityToolkit.Mvvm/Input/Attributes/RelayCommandAttribute.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,4 +116,9 @@ public sealed class RelayCommandAttribute : Attribute
116116
/// </summary>
117117
/// <remarks>Using this property is not valid if the target command doesn't map to a cancellable asynchronous command.</remarks>
118118
public bool IncludeCancelCommand { get; init; }
119+
120+
/// <summary>
121+
/// Gets or sets a value indicating whether or not a backing field should generated regardless of the availablity of the <see langword="field"/> keyword.
122+
/// </summary>
123+
public bool ForceBackingField { get; init; }
119124
}

0 commit comments

Comments
 (0)