diff --git a/src/Controls/src/Build.Tasks/SetPropertiesVisitor.cs b/src/Controls/src/Build.Tasks/SetPropertiesVisitor.cs index 762a0358b393..30dd1131b933 100644 --- a/src/Controls/src/Build.Tasks/SetPropertiesVisitor.cs +++ b/src/Controls/src/Build.Tasks/SetPropertiesVisitor.cs @@ -1174,6 +1174,10 @@ public static IEnumerable SetPropertyValue(VariableDefinition paren if (CanSet(parent, localName, valueNode, context)) return Set(parent, localName, valueNode, iXmlLineInfo, context); + //If it's a C# 14 extension property, set it + if (CanSetExtensionProperty(parent, localName, valueNode, context)) + return SetExtensionProperty(parent, localName, valueNode, iXmlLineInfo, context); + //If it's an already initialized property, add to it if (CanAdd(parent, propertyName, valueNode, iXmlLineInfo, context)) return Add(parent, propertyName, valueNode, iXmlLineInfo, context); @@ -1195,6 +1199,10 @@ public static IEnumerable GetPropertyValue(VariableDefinition paren if (CanGet(parent, localName, context, out _)) return Get(parent, localName, lineInfo, context, out propertyType); + //If it's a C# 14 extension property, get it + if (CanGetExtensionProperty(parent, localName, context, out _)) + return GetExtensionProperty(parent, localName, lineInfo, context, out propertyType); + throw new BuildException(PropertyResolution, lineInfo, null, localName, parent.VariableType.FullName); } @@ -1667,6 +1675,190 @@ static IEnumerable Get(VariableDefinition parent, string localName, ]; } + /// + /// Finds C# 14 extension property getter and setter methods for a given target type and property name. + /// Extension properties are compiled as static get_X/set_X methods in extension container types + /// that have nested types marked with ExtensionAttribute. + /// + static (MethodDefinition Getter, MethodDefinition Setter, TypeReference DeclaringType) FindExtensionPropertyMethods( + TypeReference targetType, string propertyName, ILContext context) + { + var module = context.Body.Method.Module; + var cache = context.Cache; + var getterName = $"get_{propertyName}"; + var setterName = $"set_{propertyName}"; + var extensionAttributeFullName = "System.Runtime.CompilerServices.ExtensionAttribute"; + + // Get all assemblies to search + var assembliesToSearch = new List(); + + // Add the current module's assembly + assembliesToSearch.Add(module.Assembly); + + // Add all referenced assemblies + foreach (var asmRef in module.AssemblyReferences) + { + try + { + var asm = module.AssemblyResolver.Resolve(asmRef); + if (asm != null) + assembliesToSearch.Add(asm); + } + catch + { + // Skip assemblies that can't be resolved + } + } + + foreach (var assembly in assembliesToSearch) + { + foreach (var asmModule in assembly.Modules) + { + foreach (var type in asmModule.Types) + { + // Extension containers must be static classes (abstract and sealed) with ExtensionAttribute + if (!type.IsAbstract || !type.IsSealed) + continue; + + if (!type.CustomAttributes.Any(ca => ca.AttributeType.FullName == extensionAttributeFullName)) + continue; + + // Check if this container has nested types with ExtensionAttribute (C# 14 extension blocks) + var hasExtensionNestedTypes = type.NestedTypes.Any(nt => + nt.CustomAttributes.Any(ca => ca.AttributeType.FullName == extensionAttributeFullName)); + + if (!hasExtensionNestedTypes) + continue; + + // Look for get_PropertyName and set_PropertyName static methods + MethodDefinition getter = null; + MethodDefinition setter = null; + + foreach (var method in type.Methods) + { + if (!method.IsStatic || !method.IsPublic) + continue; + + if (method.Name == getterName && method.Parameters.Count == 1) + { + var paramType = method.Parameters[0].ParameterType; + if (IsAssignableFrom(paramType, targetType, cache)) + { + getter = method; + } + } + else if (method.Name == setterName && method.Parameters.Count == 2) + { + var paramType = method.Parameters[0].ParameterType; + if (IsAssignableFrom(paramType, targetType, cache)) + { + setter = method; + } + } + } + + if (getter != null || setter != null) + { + return (getter, setter, module.ImportReference(type)); + } + } + } + } + + return (null, null, null); + } + + static bool IsAssignableFrom(TypeReference baseType, TypeReference derivedType, XamlCache cache) + { + if (baseType.FullName == derivedType.FullName) + return true; + + return derivedType.InheritsFromOrImplements(cache, baseType); + } + + static bool CanSetExtensionProperty(VariableDefinition parent, string localName, INode node, ILContext context) + { + var (getter, setter, _) = FindExtensionPropertyMethods(parent.VariableType, localName, context); + if (setter == null) + return false; + + // Get the property type from the getter's return type or setter's second parameter + var propertyType = getter?.ReturnType ?? setter.Parameters[1].ParameterType; + var module = context.Body.Method.Module; + + if (node is ValueNode valueNode && valueNode.CanConvertValue(context, propertyType, [propertyType.ResolveCached(context.Cache)])) + return true; + + if (node is not ElementNode elementNode) + return false; + + var vardef = context.Variables[elementNode]; + var implicitOperator = vardef.VariableType.GetImplicitOperatorTo(context.Cache, propertyType, module); + + if (vardef.VariableType.InheritsFromOrImplements(context.Cache, propertyType)) + return true; + if (implicitOperator != null) + return true; + if (propertyType.FullName == "System.Object") + return true; + + return false; + } + + static bool CanGetExtensionProperty(VariableDefinition parent, string localName, ILContext context, out TypeReference propertyType) + { + propertyType = null; + var (getter, _, _) = FindExtensionPropertyMethods(parent.VariableType, localName, context); + if (getter == null) + return false; + + propertyType = context.Body.Method.Module.ImportReference(getter.ReturnType); + return true; + } + + static IEnumerable SetExtensionProperty(VariableDefinition parent, string localName, INode node, IXmlLineInfo iXmlLineInfo, ILContext context) + { + var module = context.Body.Method.Module; + var (getter, setter, declaringType) = FindExtensionPropertyMethods(parent.VariableType, localName, context); + + // Get the property type from the getter's return type or setter's second parameter + var propertyType = module.ImportReference(getter?.ReturnType ?? setter.Parameters[1].ParameterType); + + var setterRef = module.ImportReference(setter); + + // For static extension method: call ExtensionClass.set_PropertyName(target, value) + // Load the target object + yield return Create(Ldloc, parent); + + if (node is ValueNode valueNode) + { + foreach (var instruction in valueNode.PushConvertedValue(context, propertyType, [propertyType.ResolveCached(context.Cache)], (requiredServices) => valueNode.PushServiceProvider(context, requiredServices, propertyRef: null), false, true)) + yield return instruction; + yield return Create(Call, setterRef); + } + else if (node is ElementNode elementNode) + { + foreach (var instruction in context.Variables[elementNode].LoadAs(context.Cache, propertyType, module)) + yield return instruction; + yield return Create(Call, setterRef); + } + } + + static IEnumerable GetExtensionProperty(VariableDefinition parent, string localName, IXmlLineInfo iXmlLineInfo, ILContext context, out TypeReference propertyType) + { + var module = context.Body.Method.Module; + var (getter, _, declaringType) = FindExtensionPropertyMethods(parent.VariableType, localName, context); + + var getterRef = module.ImportReference(getter); + propertyType = module.ImportReference(getter.ReturnType); + + // For static extension method: call ExtensionClass.get_PropertyName(target) + return [ + Create(Ldloc, parent), + Create(Call, getterRef), + ]; + } + static bool CanAdd(VariableDefinition parent, XmlName propertyName, INode valueNode, IXmlLineInfo lineInfo, ILContext context) { var module = context.Body.Method.Module; diff --git a/src/Controls/src/SourceGen/NodeSGExtensions.cs b/src/Controls/src/SourceGen/NodeSGExtensions.cs index 271cb304a2ff..2ec7cc88a2f3 100644 --- a/src/Controls/src/SourceGen/NodeSGExtensions.cs +++ b/src/Controls/src/SourceGen/NodeSGExtensions.cs @@ -160,6 +160,13 @@ .. property.Type.GetAttributes() public static bool CanConvertTo(this ValueNode valueNode, IPropertySymbol property, SourceGenContext context) => CanConvertTo(valueNode, property, context, out _); + public static bool CanConvertTo(this ValueNode valueNode, ITypeSymbol toType, SourceGenContext context) + { + List attributes = [.. toType.GetAttributes()]; + var typeConverter = attributes.FirstOrDefault(ad => ad.AttributeClass?.ToString() == "System.ComponentModel.TypeConverterAttribute")?.ConstructorArguments[0].Value as ITypeSymbol; + return CanConvertTo(valueNode, toType, typeConverter, context); + } + public static bool CanConvertTo(this ValueNode valueNode, ITypeSymbol toType, ITypeSymbol? converter, SourceGenContext context) { var stringValue = (string)valueNode.Value; diff --git a/src/Controls/src/SourceGen/SetPropertyHelpers.cs b/src/Controls/src/SourceGen/SetPropertyHelpers.cs index a2c893990b53..94faca89d944 100644 --- a/src/Controls/src/SourceGen/SetPropertyHelpers.cs +++ b/src/Controls/src/SourceGen/SetPropertyHelpers.cs @@ -65,6 +65,13 @@ public static void SetPropertyValue(IndentedTextWriter writer, ILocalValue paren return; } + //C# 14 extension property + if (!asCollectionItem && CanSetExtensionProperty(parentVar, localName, valueNode, context)) + { + SetExtensionProperty(writer, parentVar, localName, valueNode, context, getNodeValue); + return; + } + if (CanAdd(parentVar, localName, bpFieldSymbol, attached, valueNode, context, getNodeValue)) { Add(writer, parentVar, propertyName, valueNode, context, getNodeValue); @@ -534,4 +541,363 @@ static void Add(IndentedTextWriter writer, ILocalValue parentVar, XmlName proper using (context.ProjectItem.EnableLineInfo ? PrePost.NewLineInfo(writer, (IXmlLineInfo)valueNode, context.ProjectItem) : PrePost.NoBlock()) writer.WriteLine($"{parentObj}.Add(({itemType.ToFQDisplayString()}){cast}{getNodeValue(valueNode, context.Compilation.ObjectType).ValueAccessor});"); } + + /// + /// Finds C# 14 extension property for a given target type and property name. + /// In C# 14, extension properties appear directly on static classes with get_X/set_X accessor methods + /// that take the target type as the first parameter. + /// Results are cached per context to avoid repeated searches. + /// + static IPropertySymbol? FindExtensionProperty( + ITypeSymbol targetType, string propertyName, SourceGenContext context) + { + // Use cache if available + context.extensionPropertyCache ??= new Dictionary<(ITypeSymbol, string), IPropertySymbol?>( + new ExtensionPropertyKeyComparer()); + + var key = (targetType, propertyName); + if (context.extensionPropertyCache.TryGetValue(key, out var cachedResult)) + return cachedResult; + + // Search through all static types + var allStaticTypes = GetAllStaticTypes(context); + + foreach (var type in allStaticTypes) + { + // Look for the property directly on the static class + foreach (var prop in type.GetMembers().OfType()) + { + if (prop.Name != propertyName) + continue; + + // Check if this is an extension property by looking at the accessor parameters + var getter = prop.GetMethod; + var setter = prop.SetMethod; + + ITypeSymbol? extendedType = null; + if (getter != null && getter.Parameters.Length == 1) + extendedType = getter.Parameters[0].Type; + else if (setter != null && setter.Parameters.Length == 2) + extendedType = setter.Parameters[0].Type; + + if (extendedType != null && IsAssignableFrom(extendedType, targetType, context)) + { + context.extensionPropertyCache[key] = prop; + return prop; + } + } + } + + context.extensionPropertyCache[key] = null; + return null; + } + + /// + /// Finds C# 14 extension property getter and setter methods for a given target type and property name. + /// Extension properties are compiled as static get_X/set_X methods in static classes. + /// The getter has 1 parameter (the target) and the setter has 2 parameters (target, value). + /// Results are cached per context to avoid repeated searches. + /// + static (IMethodSymbol? Getter, IMethodSymbol? Setter) FindExtensionPropertyMethods( + ITypeSymbol targetType, string propertyName, SourceGenContext context) + { + // Use cache if available + context.extensionPropertyMethodsCache ??= new Dictionary<(ITypeSymbol, string), (IMethodSymbol?, IMethodSymbol?)>( + new ExtensionPropertyKeyComparer()); + + var key = (targetType, propertyName); + if (context.extensionPropertyMethodsCache.TryGetValue(key, out var cachedResult)) + return cachedResult; + + var getterName = $"get_{propertyName}"; + var setterName = $"set_{propertyName}"; + + // Search through all static types + var allStaticTypes = GetAllStaticTypes(context); + + foreach (var type in allStaticTypes) + { + // Look for get_PropertyName and set_PropertyName static methods + IMethodSymbol? getter = null; + IMethodSymbol? setter = null; + + foreach (var method in type.GetMembers().OfType()) + { + if (!method.IsStatic || method.DeclaredAccessibility != Accessibility.Public) + continue; + + if (method.Name == getterName && method.Parameters.Length == 1) + { + var paramType = method.Parameters[0].Type; + if (IsAssignableFrom(paramType, targetType, context)) + { + getter = method; + } + } + else if (method.Name == setterName && method.Parameters.Length == 2) + { + var paramType = method.Parameters[0].Type; + if (IsAssignableFrom(paramType, targetType, context)) + { + setter = method; + } + } + } + + if (getter != null || setter != null) + { + var result = (getter, setter); + context.extensionPropertyMethodsCache[key] = result; + return result; + } + } + + context.extensionPropertyMethodsCache[key] = (null, null); + return (null, null); + } + + /// + /// Comparer for extension property cache keys that uses symbol equality. + /// + sealed class ExtensionPropertyKeyComparer : IEqualityComparer<(ITypeSymbol, string)> + { + public bool Equals((ITypeSymbol, string) x, (ITypeSymbol, string) y) + => SymbolEqualityComparer.Default.Equals(x.Item1, y.Item1) && x.Item2 == y.Item2; + + public int GetHashCode((ITypeSymbol, string) obj) + => SymbolEqualityComparer.Default.GetHashCode(obj.Item1) ^ obj.Item2.GetHashCode(); + } + + /// + /// Gets all static types from the compilation, with caching to avoid repeated enumeration. + /// + static INamedTypeSymbol[] GetAllStaticTypes(SourceGenContext context) + { + if (context.allStaticTypesCache != null) + return context.allStaticTypesCache; + + var staticTypes = new List(); + foreach (var type in GetAllTypesFromCompilation(context.Compilation)) + { + if (type.IsStatic) + staticTypes.Add(type); + } + + context.allStaticTypesCache = staticTypes.ToArray(); + return context.allStaticTypesCache; + } + + static IEnumerable GetAllTypesFromCompilation(Compilation compilation) + { + // Get types from all referenced assemblies + foreach (var reference in compilation.References) + { + if (compilation.GetAssemblyOrModuleSymbol(reference) is IAssemblySymbol assembly) + { + foreach (var type in GetAllTypesFromNamespace(assembly.GlobalNamespace)) + { + yield return type; + } + } + } + + // Get types from the current compilation + foreach (var type in GetAllTypesFromNamespace(compilation.GlobalNamespace)) + { + yield return type; + } + } + + static IEnumerable GetAllTypesFromNamespace(INamespaceSymbol namespaceSymbol) + { + foreach (var type in namespaceSymbol.GetTypeMembers()) + { + yield return type; + foreach (var nested in GetNestedTypes(type)) + { + yield return nested; + } + } + + foreach (var nestedNs in namespaceSymbol.GetNamespaceMembers()) + { + foreach (var type in GetAllTypesFromNamespace(nestedNs)) + { + yield return type; + } + } + } + + static IEnumerable GetNestedTypes(INamedTypeSymbol type) + { + foreach (var nested in type.GetTypeMembers()) + { + yield return nested; + foreach (var deepNested in GetNestedTypes(nested)) + { + yield return deepNested; + } + } + } + + static bool IsAssignableFrom(ITypeSymbol baseType, ITypeSymbol derivedType, SourceGenContext context) + { + if (SymbolEqualityComparer.Default.Equals(baseType, derivedType)) + return true; + + return derivedType.InheritsFrom(baseType, context) || + (baseType.TypeKind == TypeKind.Interface && derivedType.AllInterfaces.Any(i => SymbolEqualityComparer.Default.Equals(i, baseType))); + } + + static bool CanSetExtensionProperty(ILocalValue parentVar, string localName, INode node, SourceGenContext context) + { + ITypeSymbol? propertyType = null; + + // First try to find extension property as IPropertySymbol (C# 14 semantic model) + var extProp = FindExtensionProperty(parentVar.Type, localName, context); + if (extProp != null && extProp.SetMethod != null) + { + propertyType = extProp.Type; + } + else + { + // Fall back to finding lowered accessor methods + var (getter, setter) = FindExtensionPropertyMethods(parentVar.Type, localName, context); + if (setter == null) + return false; + + // Get the property type from the getter's return type or setter's second parameter + propertyType = getter?.ReturnType ?? setter.Parameters[1].Type; + } + + if (node is ValueNode vn && vn.CanConvertTo(propertyType, context)) + return true; + + if (node is not ElementNode elementNode) + return false; + + if (!context.Variables.TryGetValue(elementNode, out var localVar)) + return false; + + if (localVar.Type.InheritsFrom(propertyType, context)) + return true; + + if (propertyType.TypeKind == TypeKind.Interface && localVar.Type.AllInterfaces.Any(i => SymbolEqualityComparer.Default.Equals(i, propertyType))) + return true; + + if (propertyType.Equals(context.Compilation.ObjectType, SymbolEqualityComparer.Default)) + return true; + + if (context.Compilation.HasImplicitConversion(localVar.Type, propertyType)) + return true; + + return false; + } + + static bool CanGetExtensionProperty(ILocalValue parentVar, string localName, SourceGenContext context, out ITypeSymbol? propertyType) + { + propertyType = null; + + // First try to find extension property as IPropertySymbol (C# 14 semantic model) + var extProp = FindExtensionProperty(parentVar.Type, localName, context); + if (extProp != null && extProp.GetMethod != null) + { + propertyType = extProp.Type; + return true; + } + + // Fall back to finding lowered accessor methods + var (getter, _) = FindExtensionPropertyMethods(parentVar.Type, localName, context); + if (getter == null) + return false; + + propertyType = getter.ReturnType; + return true; + } + + static void SetExtensionProperty(IndentedTextWriter writer, ILocalValue parentVar, string localName, INode node, SourceGenContext context, NodeSGExtensions.GetNodeValueDelegate getNodeValue) + { + // First try to find extension property as IPropertySymbol (C# 14 semantic model) + var extProp = FindExtensionProperty(parentVar.Type, localName, context); + if (extProp != null) + { + // Generate code using property access syntax - the compiler will handle it + var propertyType = extProp.Type; + + if (node is ValueNode valueNode) + { + using (context.ProjectItem.EnableLineInfo ? PrePost.NewLineInfo(writer, (IXmlLineInfo)node, context.ProjectItem) : PrePost.NoBlock()) + { + var valueString = valueNode.ConvertTo(propertyType, writer, context, parentVar); + writer.WriteLine($"{parentVar.ValueAccessor}.{localName} = {valueString};"); + } + } + else if (node is ElementNode elementNode) + { + using (context.ProjectItem.EnableLineInfo ? PrePost.NewLineInfo(writer, (IXmlLineInfo)node, context.ProjectItem) : PrePost.NoBlock()) + { + var localVar = getNodeValue(elementNode, context.Compilation.ObjectType); + string cast = string.Empty; + if (!context.Compilation.HasImplicitConversion(localVar.Type, propertyType)) + { + cast = $"({propertyType.ToFQDisplayString()})"; + } + writer.WriteLine($"{parentVar.ValueAccessor}.{localName} = {cast}{localVar.ValueAccessor};"); + } + } + return; + } + + // Fall back to lowered accessor methods + var (getter, setter) = FindExtensionPropertyMethods(parentVar.Type, localName, context); + + // Get the property type from the getter's return type or setter's second parameter + var propType = getter?.ReturnType ?? setter!.Parameters[1].Type; + + // Generate: ExtensionClass.set_PropertyName(target, value) + var extensionClassName = setter!.ContainingType.ToFQDisplayString(); + var setterName = setter.Name; + + if (node is ValueNode vn) + { + using (context.ProjectItem.EnableLineInfo ? PrePost.NewLineInfo(writer, (IXmlLineInfo)node, context.ProjectItem) : PrePost.NoBlock()) + { + var valueString = vn.ConvertTo(propType, writer, context, parentVar); + writer.WriteLine($"{extensionClassName}.{setterName}({parentVar.ValueAccessor}, {valueString});"); + } + } + else if (node is ElementNode en) + { + using (context.ProjectItem.EnableLineInfo ? PrePost.NewLineInfo(writer, (IXmlLineInfo)node, context.ProjectItem) : PrePost.NoBlock()) + { + var localVar = getNodeValue(en, context.Compilation.ObjectType); + string cast = string.Empty; + if (!context.Compilation.HasImplicitConversion(localVar.Type, propType)) + { + cast = $"({propType.ToFQDisplayString()})"; + } + writer.WriteLine($"{extensionClassName}.{setterName}({parentVar.ValueAccessor}, {cast}{localVar.ValueAccessor});"); + } + } + } + + static string GetExtensionProperty(ILocalValue parentVar, string localName, SourceGenContext context) + { + // First try to find extension property as IPropertySymbol (C# 14 semantic model) + var extProp = FindExtensionProperty(parentVar.Type, localName, context); + if (extProp != null) + { + // Generate code using property access syntax - the compiler will handle it + return $"{parentVar.ValueAccessor}.{localName}"; + } + + // Fall back to lowered accessor methods + var (getter, _) = FindExtensionPropertyMethods(parentVar.Type, localName, context); + + // Generate: ExtensionClass.get_PropertyName(target) + var extensionClassName = getter!.ContainingType.ToFQDisplayString(); + var getterName = getter.Name; + + return $"{extensionClassName}.{getterName}({parentVar.ValueAccessor})"; + } } diff --git a/src/Controls/src/SourceGen/SourceGenContext.cs b/src/Controls/src/SourceGen/SourceGenContext.cs index 89c679f5d10c..33f90b0ee8a8 100644 --- a/src/Controls/src/SourceGen/SourceGenContext.cs +++ b/src/Controls/src/SourceGen/SourceGenContext.cs @@ -45,6 +45,11 @@ class SourceGenContext(IndentedTextWriter writer, Compilation compilation, Sourc public Dictionary lastIdForName = []; + // Cache for C# 14 extension property lookups to avoid repeated searches through all types + internal Dictionary<(ITypeSymbol, string), IPropertySymbol?>? extensionPropertyCache; + internal Dictionary<(ITypeSymbol, string), (IMethodSymbol?, IMethodSymbol?)>? extensionPropertyMethodsCache; + internal INamedTypeSymbol[]? allStaticTypesCache; + public void AddLocalMethod(string code) { if (ParentContext != null) diff --git a/src/Controls/src/Xaml/ApplyPropertiesVisitor.cs b/src/Controls/src/Xaml/ApplyPropertiesVisitor.cs index e513cb13e37c..58f36fc1c985 100644 --- a/src/Controls/src/Xaml/ApplyPropertiesVisitor.cs +++ b/src/Controls/src/Xaml/ApplyPropertiesVisitor.cs @@ -5,6 +5,7 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; +using System.Runtime.CompilerServices; using System.Xml; using Microsoft.Extensions.DependencyInjection; using Microsoft.Maui.Controls.Internals; @@ -667,7 +668,10 @@ static bool TrySetProperty(object element, string localName, object value, IXmlL var propertyInfo = elementType.GetRuntimeProperties().FirstOrDefault(p => p.Name == localName); MethodInfo setter; if (propertyInfo == null || !propertyInfo.CanWrite || (setter = propertyInfo.SetMethod) == null) - return false; + { + // Try to find a C# 14 extension property + return TrySetExtensionProperty(element, elementType, localName, value, lineInfo, serviceProvider, out exception); + } if (!IsVisibleFrom(setter, rootElement)) return false; @@ -691,6 +695,33 @@ static bool TrySetProperty(object element, string localName, object value, IXmlL } } + static bool TrySetExtensionProperty(object element, Type elementType, string propertyName, object value, IXmlLineInfo lineInfo, IServiceProvider serviceProvider, out Exception exception) + { + exception = null; + + var (getter, setter) = FindExtensionPropertyMethods(elementType, propertyName); + if (setter == null) + return false; + + // Get the property type from the getter's return type or setter's second parameter + Type propertyType = getter?.ReturnType ?? setter.GetParameters()[1].ParameterType; + + object convertedValue = value.ConvertTo(propertyType, (Func)null, serviceProvider, out exception); + if (exception != null || (convertedValue != null && !propertyType.IsInstanceOfType(convertedValue))) + return false; + + try + { + setter.Invoke(null, new object[] { element, convertedValue }); + return true; + } + catch (Exception e) + { + exception = e; + return false; + } + } + static bool TryGetProperty(object element, string localName, out object value, IXmlLineInfo lineInfo, object rootElement, out Exception exception, out object targetProperty) { exception = null; @@ -698,23 +729,27 @@ static bool TryGetProperty(object element, string localName, out object value, I var elementType = element.GetType(); PropertyInfo propertyInfo = null; - while (elementType != null && propertyInfo == null) + var searchType = elementType; + while (searchType != null && propertyInfo == null) { try { - propertyInfo = elementType.GetProperty(localName, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly); + propertyInfo = searchType.GetProperty(localName, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly); } catch (AmbiguousMatchException e) { - throw new XamlParseException($"Multiple properties with name '{elementType}.{localName}' found.", lineInfo, innerException: e); + throw new XamlParseException($"Multiple properties with name '{searchType}.{localName}' found.", lineInfo, innerException: e); } - elementType = elementType.BaseType; + searchType = searchType.BaseType; } MethodInfo getter; targetProperty = propertyInfo; if (propertyInfo == null || !propertyInfo.CanRead || (getter = propertyInfo.GetMethod) == null) - return false; + { + // Try to find a C# 14 extension property + return TryGetExtensionProperty(element, elementType, localName, out value, out exception, out targetProperty); + } if (!IsVisibleFrom(getter, rootElement)) return false; @@ -723,6 +758,30 @@ static bool TryGetProperty(object element, string localName, out object value, I return true; } + static bool TryGetExtensionProperty(object element, Type elementType, string propertyName, out object value, out Exception exception, out object targetProperty) + { + exception = null; + value = null; + targetProperty = null; + + var (getter, _) = FindExtensionPropertyMethods(elementType, propertyName); + if (getter == null) + return false; + + targetProperty = getter; + + try + { + value = getter.Invoke(null, new object[] { element }); + return true; + } + catch (Exception e) + { + exception = e; + return false; + } + } + static bool IsVisibleFrom(MethodInfo method, object rootElement) { if (method.IsPublic) @@ -736,6 +795,109 @@ static bool IsVisibleFrom(MethodInfo method, object rootElement) return false; } + /// + /// Finds C# 14 extension property getter and setter methods for a given target type and property name. + /// Extension properties are compiled as static get_X/set_X methods in extension container types + /// that have nested types marked with ExtensionAttribute. + /// + static (MethodInfo Getter, MethodInfo Setter) FindExtensionPropertyMethods(Type targetType, string propertyName) + { + var getterName = $"get_{propertyName}"; + var setterName = $"set_{propertyName}"; + + // Search all loaded assemblies for extension containers + foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) + { + // Skip dynamic and system assemblies for performance + if (assembly.IsDynamic) + continue; + + Type[] types; + try + { + types = assembly.GetTypes(); + } + catch (ReflectionTypeLoadException ex) + { + // Some types may fail to load, use the ones that succeeded + types = ex.Types.Where(t => t != null).ToArray(); + } + catch + { + continue; + } + + foreach (var type in types) + { + // Extension containers must be static classes with ExtensionAttribute + if (!type.IsAbstract || !type.IsSealed) // static classes are abstract and sealed + continue; + + if (!type.IsDefined(typeof(ExtensionAttribute), false)) + continue; + + // Check if this container has nested types with ExtensionAttribute (C# 14 extension blocks) + var hasExtensionNestedTypes = false; + try + { + foreach (var nested in type.GetNestedTypes(BindingFlags.Public | BindingFlags.NonPublic)) + { + if (nested.IsDefined(typeof(ExtensionAttribute), false)) + { + hasExtensionNestedTypes = true; + break; + } + } + } + catch + { + continue; + } + + if (!hasExtensionNestedTypes) + continue; + + // Look for get_PropertyName and set_PropertyName static methods + MethodInfo getter = null; + MethodInfo setter = null; + + try + { + foreach (var method in type.GetMethods(BindingFlags.Public | BindingFlags.Static)) + { + if (method.Name == getterName) + { + var parameters = method.GetParameters(); + if (parameters.Length == 1 && parameters[0].ParameterType.IsAssignableFrom(targetType)) + { + getter = method; + } + } + else if (method.Name == setterName) + { + var parameters = method.GetParameters(); + if (parameters.Length == 2 && parameters[0].ParameterType.IsAssignableFrom(targetType)) + { + setter = method; + } + } + } + } + catch + { + continue; + } + + if (getter != null || setter != null) + { + return (getter, setter); + } + } + } + + return (null, null); + } + static bool TryAddToProperty(object element, XmlName propertyName, object value, string xKey, IXmlLineInfo lineInfo, IServiceProvider serviceProvider, object rootElement, out Exception exception) { exception = null; diff --git a/src/Controls/tests/Xaml.UnitTests/ExtensionProperties.xaml b/src/Controls/tests/Xaml.UnitTests/ExtensionProperties.xaml new file mode 100644 index 000000000000..fbcfb56fce05 --- /dev/null +++ b/src/Controls/tests/Xaml.UnitTests/ExtensionProperties.xaml @@ -0,0 +1,23 @@ + + + + + + diff --git a/src/Controls/tests/Xaml.UnitTests/ExtensionProperties.xaml.cs b/src/Controls/tests/Xaml.UnitTests/ExtensionProperties.xaml.cs new file mode 100644 index 000000000000..93f9c0286080 --- /dev/null +++ b/src/Controls/tests/Xaml.UnitTests/ExtensionProperties.xaml.cs @@ -0,0 +1,235 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#nullable enable +using System.Collections.Generic; +using System.ComponentModel; +using System.Runtime.CompilerServices; +using Microsoft.Maui.Controls.Core.UnitTests; +using Microsoft.Maui.Dispatching; +using Microsoft.Maui.UnitTests; +using NUnit.Framework; + +namespace Microsoft.Maui.Controls.Xaml.UnitTests; + +// C# 14 extension members for Label - adds properties directly to Label +// These should be usable in XAML like regular properties: