Skip to content
Merged
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
58 changes: 43 additions & 15 deletions src/Controls/src/BindingSourceGen/ITypeSymbolExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,31 +68,59 @@ 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");
Comment on lines +81 to +84
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels odd that we have to hardcode names from another library. Would another library appear that has a similar pattern?

Is the real fix, there needs to be some attribute (something?) that RelayCommandAttribute opts into for this behavior? Something more general-purpose?


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;
}
}
}
}

return false;
}

private static System.Collections.Generic.IEnumerable<string> 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";
}
}

/// <summary>
/// Checks if a property name could be generated by CommunityToolkit.Mvvm's [ObservableProperty] attribute,
/// and returns the inferred property type if found.
Expand Down
52 changes: 52 additions & 0 deletions src/Controls/tests/BindingSourceGen.UnitTests/RelayCommandTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
42 changes: 42 additions & 0 deletions src/Controls/tests/SourceGen.UnitTests/BindingDiagnosticsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,48 @@ public class ViewModel
Assert.Contains("ViewModel", message, System.StringComparison.Ordinal);
}

[Fact]
public void BindingToRelayCommandGeneratedFromOnAsyncMethod_DoesNotReportPropertyNotFound()
{
var xaml =
"""
<?xml version="1.0" encoding="UTF-8"?>
<ContentPage
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:test="clr-namespace:Test"
x:Class="Test.TestPage"
x:DataType="test:ViewModel">
<Button Command="{Binding SaveCommand}" />
</ContentPage>
""";

var csharp = @"
namespace CommunityToolkit.Mvvm.Input
{
[System.AttributeUsage(System.AttributeTargets.Method)]
public class RelayCommandAttribute : System.Attribute { }
}

namespace Test
{
public partial class TestPage : Microsoft.Maui.Controls.ContentPage { }

public class ViewModel
{
[CommunityToolkit.Mvvm.Input.RelayCommand]
private System.Threading.Tasks.Task OnSaveAsync() => System.Threading.Tasks.Task.CompletedTask;
}
}
";

var compilation = CreateMauiCompilation()
.AddSyntaxTrees(Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree.ParseText(csharp));
var result = RunGenerator<XamlGenerator>(compilation, new AdditionalXamlFile("Test.xaml", xaml), assertNoCompilationErrors: false);

Assert.DoesNotContain(result.Diagnostics, d => d.Id == "MAUIG2045");
}

[Fact]
public void BindingIndexerNotClosed_ReportsCorrectDiagnostic()
{
Expand Down
Loading