From 4793e2849eb20d32835ec7dea27542216bf91822 Mon Sep 17 00:00:00 2001 From: Avishai Dernis Date: Sat, 7 Feb 2026 05:21:29 +0200 Subject: [PATCH 1/4] WIP Use implicit field with fieldkeyword when possible for RelayCommand source gen --- ...indableCustomPropertyCompatibleAnalyzer.cs | 2 +- .../Input/Models/CommandInfo.cs | 4 +- .../Input/RelayCommandGenerator.Execute.cs | 125 ++++++++++++------ 3 files changed, 85 insertions(+), 46 deletions(-) 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..7453d39ce 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,30 @@ 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); + // Declare a backing field if needed + FieldDeclarationSyntax? fieldDeclaration = null; + if (commandInfo.FieldName is not null) + { + // Construct the generated field as follows: + // + // /// The backing field for + // [global::System.CodeDom.Compiler.GeneratedCode("...", "...")] + // + // private ? ; + 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); + } // Prepares the argument to pass the underlying method to invoke using ImmutableArrayBuilder commandCreationArguments = ImmutableArrayBuilder.Rent(); @@ -337,7 +342,7 @@ public static ImmutableArray GetSyntax(CommandInfo comm ArrowExpressionClause( AssignmentExpression( SyntaxKind.CoalesceAssignmentExpression, - IdentifierName(commandInfo.FieldName), + commandInfo.FieldName is not null ? IdentifierName(commandInfo.FieldName) : IdentifierName(Token(SyntaxKind.FieldKeyword)), ObjectCreationExpression(IdentifierName(commandClassTypeName)) .AddArgumentListArguments(commandCreationArguments.ToArray())))) .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)); @@ -346,26 +351,31 @@ public static ImmutableArray GetSyntax(CommandInfo comm 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()))); + FieldDeclarationSyntax? cancelCommandFieldDeclaration = null; + 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? ; + 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()))); + + } // Construct the generated property as follows (the explicit delegate cast is needed to avoid overload resolution conflicts): // @@ -393,7 +403,7 @@ public static ImmutableArray GetSyntax(CommandInfo comm ArrowExpressionClause( AssignmentExpression( SyntaxKind.CoalesceAssignmentExpression, - IdentifierName(cancelCommandFieldName), + cancelCommandFieldName is not null ? IdentifierName(cancelCommandFieldName) : IdentifierName(Token(SyntaxKind.FieldKeyword)), InvocationExpression( MemberAccessExpression( SyntaxKind.SimpleMemberAccessExpression, @@ -402,10 +412,20 @@ public static ImmutableArray GetSyntax(CommandInfo comm .AddArgumentListArguments(Argument(IdentifierName(commandInfo.PropertyName)))))) .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)); - return ImmutableArray.Create(fieldDeclaration, propertyDeclaration, cancelCommandFieldDeclaration, cancelCommandPropertyDeclaration); + if (fieldDeclaration is not null && cancelCommandFieldDeclaration is not null) + { + return ImmutableArray.Create(fieldDeclaration, propertyDeclaration, cancelCommandFieldDeclaration, cancelCommandPropertyDeclaration); + } + + return ImmutableArray.Create(propertyDeclaration, cancelCommandPropertyDeclaration); + } + + if (fieldDeclaration is not null) + { + return ImmutableArray.Create(fieldDeclaration, propertyDeclaration); } - return ImmutableArray.Create(fieldDeclaration, propertyDeclaration); + return ImmutableArray.Create(propertyDeclaration); } /// @@ -474,8 +494,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 +519,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); From 42eb6be25f0ab8300321aa7c107bf81d484197ab Mon Sep 17 00:00:00 2001 From: Avishai Dernis Date: Sat, 7 Feb 2026 05:34:02 +0200 Subject: [PATCH 2/4] Changed GetSyntax to use a ImmutableArrayBuilder for RelayCommandGenerator --- .../Input/RelayCommandGenerator.Execute.cs | 28 +++++++++---------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Input/RelayCommandGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Input/RelayCommandGenerator.Execute.cs index 7453d39ce..4f18aca99 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Input/RelayCommandGenerator.Execute.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Input/RelayCommandGenerator.Execute.cs @@ -207,8 +207,9 @@ public static ImmutableArray GetSyntax(CommandInfo comm .Select(static a => AttributeList(SingletonSeparatedList(a.GetSyntax()))) .ToArray(); + ImmutableArrayBuilder declarations = ImmutableArrayBuilder.Rent(); + // Declare a backing field if needed - FieldDeclarationSyntax? fieldDeclaration = null; if (commandInfo.FieldName is not null) { // Construct the generated field as follows: @@ -217,7 +218,7 @@ public static ImmutableArray GetSyntax(CommandInfo comm // [global::System.CodeDom.Compiler.GeneratedCode("...", "...")] // // private ? ; - fieldDeclaration = + FieldDeclarationSyntax fieldDeclaration = FieldDeclaration( VariableDeclaration(NullableType(IdentifierName(commandClassTypeName))) .AddVariables(VariableDeclarator(Identifier(commandInfo.FieldName)))) @@ -230,6 +231,8 @@ public static ImmutableArray GetSyntax(CommandInfo comm 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 @@ -347,6 +350,8 @@ public static ImmutableArray GetSyntax(CommandInfo comm .AddArgumentListArguments(commandCreationArguments.ToArray())))) .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)); + declarations.Add(propertyDeclaration); + // Conditionally declare the additional members for the cancel commands if (commandInfo.IncludeCancelCommand) { @@ -354,7 +359,8 @@ public static ImmutableArray GetSyntax(CommandInfo comm 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"; - FieldDeclarationSyntax? cancelCommandFieldDeclaration = null; + // 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: @@ -362,7 +368,7 @@ public static ImmutableArray GetSyntax(CommandInfo comm // /// The backing field for // [global::System.CodeDom.Compiler.GeneratedCode("...", "...")] // private global::System.Windows.Input.ICommand? ; - cancelCommandFieldDeclaration = + FieldDeclarationSyntax cancelCommandFieldDeclaration = FieldDeclaration( VariableDeclaration(NullableType(IdentifierName("global::System.Windows.Input.ICommand"))) .AddVariables(VariableDeclarator(Identifier(cancelCommandFieldName)))) @@ -375,6 +381,7 @@ public static ImmutableArray GetSyntax(CommandInfo comm 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): @@ -412,20 +419,11 @@ public static ImmutableArray GetSyntax(CommandInfo comm .AddArgumentListArguments(Argument(IdentifierName(commandInfo.PropertyName)))))) .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)); - if (fieldDeclaration is not null && cancelCommandFieldDeclaration is not null) - { - return ImmutableArray.Create(fieldDeclaration, propertyDeclaration, cancelCommandFieldDeclaration, cancelCommandPropertyDeclaration); - } - - return ImmutableArray.Create(propertyDeclaration, cancelCommandPropertyDeclaration); - } + declarations.Add(cancelCommandPropertyDeclaration); - if (fieldDeclaration is not null) - { - return ImmutableArray.Create(fieldDeclaration, propertyDeclaration); } - return ImmutableArray.Create(propertyDeclaration); + return declarations.ToImmutable(); } /// From 6e48ed0089ceecbba221909d47554c6bf7ca542c Mon Sep 17 00:00:00 2001 From: Avishai Dernis Date: Sat, 7 Feb 2026 05:57:26 +0200 Subject: [PATCH 3/4] Fixed field token definitions --- .../Input/RelayCommandGenerator.Execute.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Input/RelayCommandGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Input/RelayCommandGenerator.Execute.cs index 4f18aca99..389f07e3f 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Input/RelayCommandGenerator.Execute.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Input/RelayCommandGenerator.Execute.cs @@ -345,7 +345,7 @@ public static ImmutableArray GetSyntax(CommandInfo comm ArrowExpressionClause( AssignmentExpression( SyntaxKind.CoalesceAssignmentExpression, - commandInfo.FieldName is not null ? IdentifierName(commandInfo.FieldName) : IdentifierName(Token(SyntaxKind.FieldKeyword)), + commandInfo.FieldName is not null ? IdentifierName(commandInfo.FieldName) : IdentifierName("field"), ObjectCreationExpression(IdentifierName(commandClassTypeName)) .AddArgumentListArguments(commandCreationArguments.ToArray())))) .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)); @@ -410,7 +410,7 @@ public static ImmutableArray GetSyntax(CommandInfo comm ArrowExpressionClause( AssignmentExpression( SyntaxKind.CoalesceAssignmentExpression, - cancelCommandFieldName is not null ? IdentifierName(cancelCommandFieldName) : IdentifierName(Token(SyntaxKind.FieldKeyword)), + cancelCommandFieldName is not null ? IdentifierName(cancelCommandFieldName) : IdentifierName("field"), InvocationExpression( MemberAccessExpression( SyntaxKind.SimpleMemberAccessExpression, From 314633dbdc13cae9af9a4b24c01ddfbc512d4b71 Mon Sep 17 00:00:00 2001 From: Avishai Dernis Date: Sat, 7 Feb 2026 17:01:51 +0200 Subject: [PATCH 4/4] Added conditional compilation check for backing fields assertions in RelayCommand tests NOTE: This conditional compilation is based on roslyn version, not C# version or target platform. This could hypothetically lead to reduced test coverage in the future. --- .../Test_ObservablePropertyAttribute.cs | 2 ++ .../Test_RelayCommandAttribute.cs | 12 +++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) 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")!;