diff --git a/src/Controls/src/BindingSourceGen/ITypeSymbolExtensions.cs b/src/Controls/src/BindingSourceGen/ITypeSymbolExtensions.cs index 43ea23d060b6..5de5a654d496 100644 --- a/src/Controls/src/BindingSourceGen/ITypeSymbolExtensions.cs +++ b/src/Controls/src/BindingSourceGen/ITypeSymbolExtensions.cs @@ -68,24 +68,30 @@ public static bool TryGetRelayCommandPropertyType(this ITypeSymbol symbol, strin // Extract the method name (property name without "Command" suffix) var methodName = propertyName.Substring(0, propertyName.Length - "Command".Length); - // Look for a method with the base name - search in the type and base types - var methods = GetAllMethods(symbol, methodName); - - foreach (var method in methods) + // CommunityToolkit.Mvvm command naming supports these patterns: + // - Save => SaveCommand + // - SaveAsync => SaveCommand + // - OnSave => SaveCommand + // - OnSaveAsync => SaveCommand + foreach (var candidateMethodName in GetRelayCommandMethodNameCandidates(methodName)) { - // Check if the method has the RelayCommand attribute - var hasRelayCommand = method.GetAttributes().Any(attr => - attr.AttributeClass?.Name == "RelayCommandAttribute" || - attr.AttributeClass?.ToDisplayString() == "CommunityToolkit.Mvvm.Input.RelayCommandAttribute"); - - if (hasRelayCommand) + var methods = GetAllMethods(symbol, candidateMethodName); + foreach (var method in methods) { - // Try to find the ICommand interface type - var icommandType = compilation.GetTypeByMetadataName("System.Windows.Input.ICommand"); - if (icommandType != null) + // Check if the method has the RelayCommand attribute + var hasRelayCommand = method.GetAttributes().Any(attr => + attr.AttributeClass?.Name == "RelayCommandAttribute" || + attr.AttributeClass?.ToDisplayString() == "CommunityToolkit.Mvvm.Input.RelayCommandAttribute"); + + if (hasRelayCommand) { - commandType = icommandType; - return true; + // Try to find the ICommand interface type + var icommandType = compilation.GetTypeByMetadataName("System.Windows.Input.ICommand"); + if (icommandType != null) + { + commandType = icommandType; + return true; + } } } } @@ -93,6 +99,28 @@ public static bool TryGetRelayCommandPropertyType(this ITypeSymbol symbol, strin return false; } + private static System.Collections.Generic.IEnumerable GetRelayCommandMethodNameCandidates(string methodName) + { + // CommunityToolkit strips "On" prefix: OnSave() → SaveCommand, not OnSaveCommand. + // So if methodName starts with "On", the base name would only match methods that generate + // a *different* command property (e.g., "OnLoad" method → "LoadCommand", not "OnLoadCommand"). + // We skip these candidates to avoid false-positive diagnostic suppression. + if (!methodName.StartsWith("On", System.StringComparison.Ordinal)) + { + yield return methodName; + yield return methodName + "Async"; + } + + if (methodName.Length > 0 + && char.IsUpper(methodName[0]) + && !methodName.StartsWith("On", System.StringComparison.Ordinal)) + { + var onMethodName = "On" + methodName; + yield return onMethodName; + yield return onMethodName + "Async"; + } + } + /// /// Checks if a property name could be generated by CommunityToolkit.Mvvm's [ObservableProperty] attribute, /// and returns the inferred property type if found. diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/RelayCommandTests.cs b/src/Controls/tests/BindingSourceGen.UnitTests/RelayCommandTests.cs index 27c33ea98aa0..4ed821d03628 100644 --- a/src/Controls/tests/BindingSourceGen.UnitTests/RelayCommandTests.cs +++ b/src/Controls/tests/BindingSourceGen.UnitTests/RelayCommandTests.cs @@ -126,6 +126,58 @@ private void Save() Assert.Equal("System.Windows.Input.ICommand", commandType!.ToDisplayString()); } + [Fact] + public void DetectsRelayCommandMethodWithOnPrefixAndAsyncSuffix() + { + var source = @" + using Microsoft.CodeAnalysis; + using Microsoft.CodeAnalysis.CSharp; + using System.Linq; + using System.Threading.Tasks; + + namespace System.Windows.Input + { + public interface ICommand + { + event System.EventHandler CanExecuteChanged; + bool CanExecute(object parameter); + void Execute(object parameter); + } + } + + namespace CommunityToolkit.Mvvm.Input + { + [System.AttributeUsage(System.AttributeTargets.Method)] + public class RelayCommandAttribute : System.Attribute { } + } + + namespace TestApp + { + public class MyViewModel + { + [CommunityToolkit.Mvvm.Input.RelayCommand] + private Task OnSaveAsync() + { + return Task.CompletedTask; + } + } + } + "; + + var compilation = Microsoft.CodeAnalysis.CSharp.CSharpCompilation.Create("test") + .AddSyntaxTrees(CSharpSyntaxTree.ParseText(source)) + .AddReferences(MetadataReference.CreateFromFile(typeof(object).Assembly.Location)); + + var myViewModelType = compilation.GetTypeByMetadataName("TestApp.MyViewModel"); + Assert.NotNull(myViewModelType); + + var canInfer = myViewModelType.TryGetRelayCommandPropertyType("SaveCommand", compilation, out var commandType); + + Assert.True(canInfer, "Should infer SaveCommand from OnSaveAsync method with [RelayCommand]."); + Assert.NotNull(commandType); + Assert.Equal("System.Windows.Input.ICommand", commandType!.ToDisplayString()); + } + [Fact] public void DoesNotDetectCommandPropertyWithoutAttribute() { diff --git a/src/Controls/tests/SourceGen.UnitTests/BindingDiagnosticsTests.cs b/src/Controls/tests/SourceGen.UnitTests/BindingDiagnosticsTests.cs index 42ef7c303189..e106ffcddcb3 100644 --- a/src/Controls/tests/SourceGen.UnitTests/BindingDiagnosticsTests.cs +++ b/src/Controls/tests/SourceGen.UnitTests/BindingDiagnosticsTests.cs @@ -54,6 +54,48 @@ public class ViewModel Assert.Contains("ViewModel", message, System.StringComparison.Ordinal); } + [Fact] + public void BindingToRelayCommandGeneratedFromOnAsyncMethod_DoesNotReportPropertyNotFound() + { + var xaml = +""" + + +