Skip to content
Draft
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
24 changes: 17 additions & 7 deletions src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -210,20 +210,30 @@ private static Result<ITypeSymbol> GetLambdaReturnType(LambdaExpressionSyntax la
var lambdaResultType = semanticModel.GetTypeInfo(lambdaBody, t).Type;
if (lambdaResultType == null || lambdaResultType is IErrorTypeSymbol)
{
// Try to infer the type from known patterns (e.g., RelayCommand properties)
// Try to infer the type from known patterns (e.g., RelayCommand, ObservableProperty)
if (lambdaBody is MemberAccessExpressionSyntax memberAccess)
{
var memberName = memberAccess.Name.Identifier.Text;
var expressionType = semanticModel.GetTypeInfo(memberAccess.Expression).Type;

if (expressionType != null &&
expressionType.TryGetRelayCommandPropertyType(memberName, semanticModel.Compilation, out var commandType) &&
commandType != null)

if (expressionType != null)
{
return Result<ITypeSymbol>.Success(commandType);
// Check for RelayCommand-generated properties
if (expressionType.TryGetRelayCommandPropertyType(memberName, semanticModel.Compilation, out var commandType) &&
commandType != null)
{
return Result<ITypeSymbol>.Success(commandType);
}

// Check for ObservableProperty-generated properties
if (expressionType.TryGetObservablePropertyType(memberName, semanticModel.Compilation, out var propertyType) &&
propertyType != null)
{
return Result<ITypeSymbol>.Success(propertyType);
}
}
}

return Result<ITypeSymbol>.Failure(DiagnosticsFactory.LambdaResultCannotBeResolved(lambdaBody.GetLocation()));
}

Expand Down
73 changes: 71 additions & 2 deletions src/Controls/src/BindingSourceGen/ITypeSymbolExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,10 @@ 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)
{
// Check if the method has the RelayCommand attribute
Expand All @@ -93,6 +93,53 @@ public static bool TryGetRelayCommandPropertyType(this ITypeSymbol symbol, strin
return false;
}

/// <summary>
/// Checks if a property name could be generated by CommunityToolkit.Mvvm's [ObservableProperty] attribute,
/// and returns the inferred property type if found.
/// </summary>
/// <param name="symbol">The type to search</param>
/// <param name="propertyName">The name of the property to find</param>
/// <param name="compilation">The compilation (can be null)</param>
/// <param name="propertyType">The inferred property type if an ObservableProperty field is found</param>
/// <returns>True if an ObservableProperty field was found that would generate this property</returns>
public static bool TryGetObservablePropertyType(this ITypeSymbol symbol, string propertyName, Compilation? compilation, out ITypeSymbol? propertyType)
{
propertyType = null;

if (compilation == null)
return false;

// ObservableProperty generates a PascalCase property from a camelCase or _camelCase field
// Try common field naming patterns
var possibleFieldNames = new[]
{
char.ToLowerInvariant(propertyName[0]) + propertyName.Substring(1), // name from Name
"_" + char.ToLowerInvariant(propertyName[0]) + propertyName.Substring(1) // _name from Name
};

// Look for a field with one of the possible names - search in the type and base types
foreach (var fieldName in possibleFieldNames)
{
var fields = GetAllFields(symbol, fieldName);

foreach (var field in fields)
{
// Check if the field has the ObservableProperty attribute
var hasObservableProperty = field.GetAttributes().Any(attr =>
attr.AttributeClass?.Name == "ObservablePropertyAttribute" ||
attr.AttributeClass?.ToDisplayString() == "CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute");

if (hasObservableProperty)
{
propertyType = field.Type;
return true;
}
}
}

return false;
}

private static System.Collections.Generic.IEnumerable<IMethodSymbol> GetAllMethods(ITypeSymbol symbol, string name)
{
// Search in current type
Expand All @@ -114,4 +161,26 @@ private static System.Collections.Generic.IEnumerable<IMethodSymbol> GetAllMetho
baseType = baseType.BaseType;
}
}

private static System.Collections.Generic.IEnumerable<IFieldSymbol> GetAllFields(ITypeSymbol symbol, string name)
{
// Search in current type
foreach (var member in symbol.GetMembers(name))
{
if (member is IFieldSymbol field)
yield return field;
}

// Search in base types
var baseType = symbol.BaseType;
while (baseType != null)
{
foreach (var member in baseType.GetMembers(name))
{
if (member is IFieldSymbol field)
yield return field;
}
baseType = baseType.BaseType;
}
}
}
21 changes: 20 additions & 1 deletion src/Controls/src/BindingSourceGen/PathParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ private bool TryHandleSpecialCases(string memberName, ITypeSymbol expressionType
pathPart = null;

// Check for RelayCommand-generated properties
if (expressionType.TryGetRelayCommandPropertyType(memberName, _context.SemanticModel.Compilation, out var commandType)
if (expressionType.TryGetRelayCommandPropertyType(memberName, _context.SemanticModel.Compilation, out var commandType)
&& commandType != null)
{
var memberType = commandType.CreateTypeDescription(_enabledNullable);
Expand All @@ -105,6 +105,25 @@ private bool TryHandleSpecialCases(string memberName, ITypeSymbol expressionType
return true;
}

// Check for ObservableProperty-generated properties
if (expressionType.TryGetObservablePropertyType(memberName, _context.SemanticModel.Compilation, out var propertyType)
&& propertyType != null)
{
var memberType = propertyType.CreateTypeDescription(_enabledNullable);
var containingType = expressionType.CreateTypeDescription(_enabledNullable);

pathPart = new MemberAccess(
MemberName: memberName,
IsValueType: !propertyType.IsReferenceType,
ContainingType: containingType,
MemberType: memberType,
Kind: AccessorKind.Property,
IsGetterInaccessible: false, // Assume generated property is accessible
IsSetterInaccessible: false); // ObservableProperty properties have setters

return true;
}

return false;
}

Expand Down
20 changes: 13 additions & 7 deletions src/Controls/src/SourceGen/ITypeSymbolExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;

using Microsoft.CodeAnalysis;
using Microsoft.Maui.Controls.Xaml;
using Microsoft.Maui.Controls.BindingSourceGen;
using Microsoft.Maui.Controls.Xaml;

namespace Microsoft.Maui.Controls.SourceGen;

Expand Down Expand Up @@ -85,15 +84,15 @@ public static IEnumerable<IPropertySymbol> GetAllProperties(this ITypeSymbol sym
=> symbol.GetAllMembers(name, context).OfType<IPropertySymbol>();

/// <summary>
/// Tries to get a property by name, and if not found, checks if it could be inferred from a RelayCommand method.
/// Tries to get a property by name, and if not found, checks if it could be inferred from a RelayCommand method or ObservableProperty field.
/// Returns the property type if found or inferred.
/// </summary>
/// <param name="symbol">The type to search</param>
/// <param name="propertyName">The name of the property to find</param>
/// <param name="context">The source generation context</param>
/// <param name="property">The found property symbol (null if inferred from RelayCommand)</param>
/// <param name="propertyType">The property type (either from the property or inferred from RelayCommand)</param>
/// <returns>True if property exists or can be inferred from RelayCommand</returns>
/// <param name="property">The found property symbol (null if inferred from RelayCommand or ObservableProperty)</param>
/// <param name="propertyType">The property type (either from the property or inferred from RelayCommand/ObservableProperty)</param>
/// <returns>True if property exists or can be inferred from RelayCommand or ObservableProperty</returns>
public static bool TryGetProperty(
this ITypeSymbol symbol,
string propertyName,
Expand All @@ -103,7 +102,7 @@ public static bool TryGetProperty(
{
property = symbol.GetAllProperties(propertyName, context)
.FirstOrDefault(p => p.GetMethod != null && !p.GetMethod.IsStatic);

if (property != null)
{
propertyType = property.Type;
Expand All @@ -117,6 +116,13 @@ public static bool TryGetProperty(
return true;
}

// If property not found, check if it could be an ObservableProperty-generated property
// Call the BindingSourceGen extension method directly
if (symbol.TryGetObservablePropertyType(propertyName, context?.Compilation, out propertyType))
{
return true;
}

propertyType = null;
return false;
}
Expand Down
Loading