Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
4ac5cf9
Fixed the NRE in CarouselViewController on iOS 15.5 & 16.4 (#30838)
Ahamed-Ali Dec 4, 2025
846167d
Fix for TabBar Navigation does not invoke its IQueryAttributable.Appl…
SuthiYuvaraj Dec 4, 2025
c813ca4
Add unit tests for TabBar and FlyoutItem navigation ApplyQueryAttribu…
StephaneDelcroix Dec 4, 2025
7a6b73e
[C] Fix binding to interface-inherited properties like IReadOnlyList<…
StephaneDelcroix Dec 5, 2025
d586748
Fix #31939: CommandParameter TemplateBinding lost during reparenting …
StephaneDelcroix Dec 5, 2025
99b0d94
[XSG][BindingSourceGen] Add support for RelayCommand to compiled bind…
Copilot Dec 5, 2025
bab19cb
Update logic for large title display mode on iOS - shell (#33039)
kubaflo Dec 8, 2025
5fcae9e
Fix for mediapicker (#32952)
HarishwaranVijayakumar Dec 9, 2025
1591977
Initial plan
Copilot Dec 6, 2025
53e3f62
Initial plan for ObservablePropertyAttribute support
Copilot Dec 6, 2025
48eae0b
Add ObservableProperty support to BindingSourceGen and SourceGen
Copilot Dec 6, 2025
7f40475
Apply code formatting
Copilot Dec 6, 2025
6657ded
Remove unrelated file and finalize changes
Copilot Dec 6, 2025
629b003
Add XAML unit tests for ObservableProperty binding support
Copilot Dec 8, 2025
c90fc72
Revert "Add XAML unit tests for ObservableProperty binding support"
simonrozsival Dec 9, 2025
7140213
Remove HWV.js.map
simonrozsival Dec 9, 2025
f804748
Address code review feedback for ObservableProperty support
Copilot Dec 9, 2025
07655f3
Remove unrelated HybridWebView.js.map file
Copilot Dec 9, 2025
405b142
Fix test assertions to check handlers instead of method signature
Copilot Dec 9, 2025
a39fcbc
Remove unrelated HybridWebView.js.map file
Copilot Dec 9, 2025
26c1287
Migrate tests to XUnit
simonrozsival Dec 11, 2025
8a13b09
Merge inflight/current into copilot/add-observable-property-support (…
simonrozsival Dec 17, 2025
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
65 changes: 63 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,54 @@ 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 || string.IsNullOrEmpty(propertyName))
return false;

// ObservableProperty generates a PascalCase property from a camelCase, _camelCase, or m_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
"m_" + char.ToLowerInvariant(propertyName[0]) + propertyName.Substring(1) // m_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 +162,17 @@ private static System.Collections.Generic.IEnumerable<IMethodSymbol> GetAllMetho
baseType = baseType.BaseType;
}
}

private static System.Collections.Generic.IEnumerable<IFieldSymbol> GetAllFields(ITypeSymbol? symbol, string name)
{
while (symbol != null)
{
foreach (var member in symbol.GetMembers(name))
{
if (member is IFieldSymbol field)
yield return field;
}
symbol = symbol.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
Loading