diff --git a/src/Controls/src/Build.Tasks/SetPropertiesVisitor.cs b/src/Controls/src/Build.Tasks/SetPropertiesVisitor.cs index d92a26c91b3e..77b9d07be7ac 100644 --- a/src/Controls/src/Build.Tasks/SetPropertiesVisitor.cs +++ b/src/Controls/src/Build.Tasks/SetPropertiesVisitor.cs @@ -524,43 +524,65 @@ static bool TryCompileBindingPath(ElementNode node, ILContext context, VariableD n = GetParent(n); } - if (dataTypeNode is null) - { - context.LoggingHelper.LogWarningOrError(BindingWithoutDataType, context.XamlFilePath, node.LineNumber, node.LinePosition, 0, 0, null); + bool xDataTypeIsOnBindingNode = n == node; + if (IsRelativeSourceWithoutAncestorType(node, context, module)) + { return false; } - if (xDataTypeIsInOuterScope) + TypeReference tSourceRef = null; + if (HasRelativeOrReferenceSource(node) && !xDataTypeIsOnBindingNode) { - context.LoggingHelper.LogWarningOrError(BindingWithXDataTypeFromOuterScope, context.XamlFilePath, node.LineNumber, node.LinePosition, 0, 0, null); - // continue compilation + if (!TryGetRelativeSourceAncestorTypeReference(node, context, module, out tSourceRef)) + { + return false; + } } - if ( dataTypeNode is ElementNode enode - && enode.XmlType.NamespaceUri == XamlParser.X2009Uri - && enode.XmlType.Name == nameof(Xaml.NullExtension)) + if (tSourceRef is null) { - context.LoggingHelper.LogWarningOrError(BindingWithNullDataType, context.XamlFilePath, node.LineNumber, node.LinePosition, 0, 0, null); - return false; - } + if (dataTypeNode is null) + { + context.LoggingHelper.LogWarningOrError(BindingWithoutDataType, context.XamlFilePath, node.LineNumber, node.LinePosition, 0, 0, null); - if ((dataTypeNode as ValueNode)?.Value is not string dataType) - throw new BuildException(XDataTypeSyntax, dataTypeNode as IXmlLineInfo, null); + return false; + } - XmlType dtXType = null; - try - { - dtXType = TypeArgumentsParser.ParseSingle(dataType, node.NamespaceResolver, dataTypeNode as IXmlLineInfo) - ?? throw new BuildException(XDataTypeSyntax, dataTypeNode as IXmlLineInfo, null); - } - catch (XamlParseException) - { - var prefix = dataType.Contains(":") ? dataType.Substring(0, dataType.IndexOf(":", StringComparison.Ordinal)) : ""; - throw new BuildException(XmlnsUndeclared, dataTypeNode as IXmlLineInfo, null, prefix); + if (xDataTypeIsInOuterScope) + { + context.LoggingHelper.LogWarningOrError(BindingWithXDataTypeFromOuterScope, context.XamlFilePath, node.LineNumber, node.LinePosition, 0, 0, null); + // continue compilation + } + + if ( dataTypeNode is ElementNode enode + && enode.XmlType.NamespaceUri == XamlParser.X2009Uri + && enode.XmlType.Name == nameof(Xaml.NullExtension)) + { + context.LoggingHelper.LogWarningOrError(BindingWithNullDataType, context.XamlFilePath, node.LineNumber, node.LinePosition, 0, 0, null); + return false; + } + + if ((dataTypeNode as ValueNode)?.Value is not string dataType) + throw new BuildException(XDataTypeSyntax, dataTypeNode as IXmlLineInfo, null); + + XmlType dtXType = null; + try + { + dtXType = TypeArgumentsParser.ParseSingle(dataType, node.NamespaceResolver, dataTypeNode as IXmlLineInfo) + ?? throw new BuildException(XDataTypeSyntax, dataTypeNode as IXmlLineInfo, null); + } + catch (XamlParseException) + { + var prefix = dataType.Contains(":") ? dataType.Substring(0, dataType.IndexOf(":", StringComparison.Ordinal)) : ""; + throw new BuildException(XmlnsUndeclared, dataTypeNode as IXmlLineInfo, null, prefix); + } + + tSourceRef = dtXType.GetTypeReference(context.Cache, module, (IXmlLineInfo)node); + if (tSourceRef == null) + return false; } - var tSourceRef = dtXType.GetTypeReference(context.Cache, module, (IXmlLineInfo)node); if (tSourceRef == null) return false; //throw @@ -640,6 +662,131 @@ static bool IsBindingContextBinding(ElementNode node) && propertyName.LocalName == nameof(BindableObject.BindingContext); } + // Returns true when the binding's Source property is a RelativeSource or x:Reference. + // When Source is a RelativeSource or x:Reference, x:DataType inherited from an + // ancestor DataTemplate describes template items — not the actual binding source type. + // This matches the behaviour in KnownMarkups.ProvideValueForBindingExtension (SourceGen) + // and the runtime BindingExtension.ProvideValue (which already sets DataType=null for + // all non-RelativeBindingSource sources, including x:Reference resolved objects). + static bool HasRelativeOrReferenceSource(ElementNode bindingNode) + { + if (!bindingNode.Properties.TryGetValue(new XmlName("", "Source"), out INode sourceNode) + && !bindingNode.Properties.TryGetValue(new XmlName(null, "Source"), out sourceNode)) + { + return false; + } + + return sourceNode is ElementNode sourceElementNode + && sourceElementNode.XmlType.Name is "RelativeSourceExtension" + or "RelativeSource" + or "ReferenceExtension" + or "Reference"; + } + + static bool IsRelativeSourceWithoutAncestorType(ElementNode bindingNode, ILContext context, ModuleDefinition module) + { + if (!TryGetRelativeSourceNode(bindingNode, out var relativeSourceNode)) + { + return false; + } + + return !TryGetRelativeSourceAncestorTypeReference(bindingNode, context, module, out _); + } + + static bool TryGetRelativeSourceAncestorTypeReference(ElementNode bindingNode, ILContext context, ModuleDefinition module, out TypeReference ancestorType) + { + ancestorType = null; + + if (!TryGetRelativeSourceNode(bindingNode, out var relativeSourceNode)) + { + return false; + } + + if (!relativeSourceNode.Properties.TryGetValue(new XmlName("", "AncestorType"), out INode ancestorTypeNode) + && !relativeSourceNode.Properties.TryGetValue(new XmlName(null, "AncestorType"), out ancestorTypeNode) + && !relativeSourceNode.Properties.TryGetValue(new XmlName(XamlParser.MauiUri, "AncestorType"), out ancestorTypeNode)) + { + return false; + } + + if (ancestorTypeNode is ElementNode ancestorTypeElement) + { + if (context.TypeExtensions.TryGetValue(ancestorTypeElement, out var mappedType)) + { + ancestorType = module.ImportReference(mappedType); + return true; + } + + if (!TryGetTypeNameFromTypeExtensionNode(ancestorTypeElement, out var typeName)) + { + return false; + } + + return TryResolveTypeFromName(typeName, ancestorTypeElement, context, module, out ancestorType); + } + + if (ancestorTypeNode is ValueNode { Value: string directTypeName }) + { + return TryResolveTypeFromName(directTypeName, bindingNode, context, module, out ancestorType); + } + + return false; + } + + static bool TryGetRelativeSourceNode(ElementNode bindingNode, out ElementNode relativeSourceNode) + { + relativeSourceNode = null; + + if ((!bindingNode.Properties.TryGetValue(new XmlName("", "Source"), out INode sourceNode) + && !bindingNode.Properties.TryGetValue(new XmlName(null, "Source"), out sourceNode)) + || sourceNode is not ElementNode sourceElementNode) + { + return false; + } + + if (sourceElementNode.XmlType.Name is not "RelativeSourceExtension" and not "RelativeSource") + { + return false; + } + + relativeSourceNode = sourceElementNode; + return true; + } + + static bool TryGetTypeNameFromTypeExtensionNode(ElementNode typeNode, out string typeName) + { + typeName = null; + + if (!typeNode.Properties.TryGetValue(new XmlName("", "TypeName"), out INode typeNameNode) + && !typeNode.Properties.TryGetValue(new XmlName(null, "TypeName"), out typeNameNode) + && !typeNode.Properties.TryGetValue(new XmlName(XamlParser.MauiUri, "TypeName"), out typeNameNode) + && typeNode.CollectionItems.Count == 1) + { + typeNameNode = typeNode.CollectionItems[0]; + } + + typeName = (typeNameNode as ValueNode)?.Value as string; + return !string.IsNullOrEmpty(typeName); + } + + static bool TryResolveTypeFromName(string typeName, ElementNode namespaceNode, ILContext context, ModuleDefinition module, out TypeReference typeReference) + { + typeReference = null; + + XmlType xmlType; + try + { + xmlType = TypeArgumentsParser.ParseSingle(typeName, namespaceNode.NamespaceResolver, namespaceNode as IXmlLineInfo); + } + catch (XamlParseException) + { + return false; + } + + typeReference = xmlType?.GetTypeReference(context.Cache, module, namespaceNode as IXmlLineInfo); + return typeReference is not null; + } + bool DoesNotInheritDataType(ElementNode node) { return GetParent(node) is ElementNode parentNode diff --git a/src/Controls/src/SourceGen/KnownMarkups.cs b/src/Controls/src/SourceGen/KnownMarkups.cs index e14831785316..902da5f74489 100644 --- a/src/Controls/src/SourceGen/KnownMarkups.cs +++ b/src/Controls/src/SourceGen/KnownMarkups.cs @@ -340,17 +340,26 @@ private static bool ProvideValueForBindingExtension(ElementNode markupNode, Inde { returnType = context.Compilation.GetTypeByMetadataName("Microsoft.Maui.Controls.BindingBase")!; ITypeSymbol? dataTypeSymbol = null; - - // When Source is explicitly set (RelativeSource or x:Reference), x:DataType does not describe - // the actual source — skip compilation and fall back to runtime binding. + + // When Source is explicitly set, inherited x:DataType usually does not describe the actual source. + // RelativeSource with AncestorType is the exception: AncestorType defines the source type. + // RelativeSource Self should never compile to TypedBinding because the source is the view itself. bool hasExplicitSource = HasExplicitBindingSource(markupNode); - + bool xDataTypeOnBindingNode = markupNode.Properties.ContainsKey(XmlName.xDataType); + bool isRelativeSourceWithoutAncestorType = IsRelativeSourceWithoutAncestorType(markupNode, context); + context.Variables.TryGetValue(markupNode, out ILocalValue? extVariable); - - if ( !hasExplicitSource - && extVariable is not null) + + if (!isRelativeSourceWithoutAncestorType && extVariable is not null) { - TryGetXDataType(markupNode, context, out dataTypeSymbol); + if (!hasExplicitSource || xDataTypeOnBindingNode) + { + TryGetXDataType(markupNode, context, out dataTypeSymbol); + } + else if (TryGetRelativeSourceAncestorType(markupNode, context, out var ancestorType)) + { + dataTypeSymbol = ancestorType; + } if (dataTypeSymbol is not null) { @@ -629,6 +638,97 @@ static bool IsBindingContextBinding(ElementNode node) && propertyName.LocalName == "BindingContext"; } + static bool IsRelativeSourceWithoutAncestorType(ElementNode bindingNode, SourceGenContext context) + { + if (!TryGetRelativeSourceNode(bindingNode, out var relativeSourceNode)) + { + return false; + } + + return !TryGetRelativeSourceAncestorType(bindingNode, context, out _); + } + + static bool TryGetRelativeSourceAncestorType(ElementNode bindingNode, SourceGenContext context, out ITypeSymbol? ancestorType) + { + ancestorType = null; + + if (!TryGetRelativeSourceNode(bindingNode, out var relativeSourceNode)) + { + return false; + } + + if (!relativeSourceNode.Properties.TryGetValue(new XmlName("", "AncestorType"), out INode? ancestorTypeNode) + && !relativeSourceNode.Properties.TryGetValue(new XmlName(null, "AncestorType"), out ancestorTypeNode) + && !relativeSourceNode.Properties.TryGetValue(new XmlName(XamlParser.MauiUri, "AncestorType"), out ancestorTypeNode)) + { + return false; + } + + if (ancestorTypeNode is ElementNode typeExtNode) + { + if (context.Types.TryGetValue(typeExtNode, out var resolvedType)) + { + ancestorType = resolvedType; + return true; + } + + if (!typeExtNode.Properties.TryGetValue(new XmlName("", "TypeName"), out INode? typeNameNode) + && !typeExtNode.Properties.TryGetValue(new XmlName(null, "TypeName"), out typeNameNode) + && !typeExtNode.Properties.TryGetValue(new XmlName(XamlParser.MauiUri, "TypeName"), out typeNameNode) + && typeExtNode.CollectionItems.Count == 1) + { + typeNameNode = typeExtNode.CollectionItems[0]; + } + + if (typeNameNode is ValueNode { Value: string typeName } && !IsNullOrEmpty(typeName)) + { + XmlType xmlType = TypeArgumentsParser.ParseSingle(typeName, typeExtNode.NamespaceResolver, typeExtNode as IXmlLineInfo); + if (xmlType.TryResolveTypeSymbol(null, context.Compilation, context.XmlnsCache, context.TypeCache, out var resolvedAncestorType) + && resolvedAncestorType is not null) + { + ancestorType = resolvedAncestorType; + context.Types[typeExtNode] = resolvedAncestorType; + return true; + } + } + + return false; + } + + if (ancestorTypeNode is ValueNode { Value: string directTypeName } && !IsNullOrEmpty(directTypeName)) + { + XmlType xmlType = TypeArgumentsParser.ParseSingle(directTypeName, bindingNode.NamespaceResolver, bindingNode as IXmlLineInfo); + if (xmlType.TryResolveTypeSymbol(null, context.Compilation, context.XmlnsCache, context.TypeCache, out var resolvedAncestorType) + && resolvedAncestorType is not null) + { + ancestorType = resolvedAncestorType; + return true; + } + } + + return false; + } + + static bool TryGetRelativeSourceNode(ElementNode bindingNode, out ElementNode relativeSourceNode) + { + relativeSourceNode = null!; + + if ((!bindingNode.Properties.TryGetValue(new XmlName("", "Source"), out INode? sourceNode) + && !bindingNode.Properties.TryGetValue(new XmlName(null, "Source"), out sourceNode)) + || sourceNode is not ElementNode sourceElementNode) + { + return false; + } + + if (sourceElementNode.XmlType.Name is not "RelativeSourceExtension" and not "RelativeSource") + { + return false; + } + + relativeSourceNode = sourceElementNode; + return true; + } + // Checks if the binding has a Source property set to RelativeSource or x:Reference. // When Source is explicitly set, x:DataType does not describe the actual binding source, // so we should NOT compile the binding using x:DataType. diff --git a/src/Controls/src/Xaml/MarkupExtensions/BindingExtension.cs b/src/Controls/src/Xaml/MarkupExtensions/BindingExtension.cs index 5d86ae0daa12..010bdf873be7 100644 --- a/src/Controls/src/Xaml/MarkupExtensions/BindingExtension.cs +++ b/src/Controls/src/Xaml/MarkupExtensions/BindingExtension.cs @@ -2,6 +2,7 @@ using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using Microsoft.Maui.Controls.Internals; +using Microsoft.Maui.Controls.Xaml.Internals; namespace Microsoft.Maui.Controls.Xaml { @@ -83,29 +84,42 @@ BindingBase IMarkupExtension.ProvideValue(IServiceProvider serviceP BindingBase CreateBinding() { Type bindingXDataType = null; + IXamlDataTypeProvider dataTypeProvider = null; if (serviceProvider is not null && (serviceProvider.GetService(typeof(IXamlTypeResolver)) is IXamlTypeResolver typeResolver) - && (serviceProvider.GetService(typeof(IXamlDataTypeProvider)) is IXamlDataTypeProvider dataTypeProvider) - && dataTypeProvider.BindingDataType != null) + && (serviceProvider.GetService(typeof(IXamlDataTypeProvider)) is IXamlDataTypeProvider dtProvider) + && dtProvider.BindingDataType != null) { + dataTypeProvider = dtProvider; typeResolver.TryResolve(dataTypeProvider.BindingDataType, out bindingXDataType); } + // Runtime inflation still creates a string-path Binding. This path is intentionally + // not trim-safe for AOT (it is reflection-based and carries trim warnings). + // SourceGen/XamlC TypedBinding generation is the trim-safe path. return new Binding(Path, Mode, Converter, ConverterParameter, StringFormat, Source) { UpdateSourceEventName = UpdateSourceEventName, FallbackValue = FallbackValue, TargetNullValue = TargetNullValue, - // When Source is set to a concrete element reference (e.g. x:Reference), the - // DataType from IXamlDataTypeProvider reflects the DataTemplate item type, not - // the explicit source type. Assigning that mismatched DataType causes - // BindingExpression.Apply to null-out the binding source when - // IsXamlCBindingWithSourceCompilationEnabled is true (.NET 10 default for - // AOT/trimmed builds). See https://github.com/dotnet/maui/issues/33291. + // When Source is set to any explicit source, the DataType from + // IXamlDataTypeProvider may reflect the DataTemplate item type (inherited from an + // ancestor DataTemplate) rather than the explicit source type. Assigning that + // mismatched DataType causes BindingExpression.Apply to null-out the binding + // source when IsXamlCBindingWithSourceCompilationEnabled is true (.NET 10 default + // for AOT/trimmed builds). // - // RelativeBindingSource is excluded: the developer likely set x:DataType on - // the binding to describe the expected type of the resolved ancestor, and - // that validation should be preserved. - DataType = (Source is null || Source is RelativeBindingSource) ? bindingXDataType : null, + // - Source=null → DataType applies (BindingContext IS described by x:DataType) + // - Source={x:Reference} → DataType must be null (see https://github.com/dotnet/maui/issues/33291) + // - Source={RelativeSource AncestorType=...} + // • x:DataType was set *directly* on the Binding itself: the developer explicitly + // typed the binding source, so preserve DataType for the type-mismatch check. + // • x:DataType was *inherited* from a DataTemplate ancestor: the type describes + // the template item, not the ancestor — set DataType to null to avoid false + // mismatch failures (see https://github.com/dotnet/maui/issues/35564). + DataType = Source is null || (Source is RelativeBindingSource + && dataTypeProvider is IXamlDataTypeProviderWithBindingNodeInfo { IsDataTypeOnBindingNode: true }) + ? bindingXDataType + : null, }; } } diff --git a/src/Controls/src/Xaml/XamlServiceProvider.cs b/src/Controls/src/Xaml/XamlServiceProvider.cs index c2aa755c0178..43ca1607971c 100644 --- a/src/Controls/src/Xaml/XamlServiceProvider.cs +++ b/src/Controls/src/Xaml/XamlServiceProvider.cs @@ -320,7 +320,18 @@ public string LookupNamespace(string prefix) public void Add(string prefix, string ns) => namespaces.Add(prefix, ns); } - public class XamlDataTypeProvider : IXamlDataTypeProvider + /// + /// Extended internal interface for implementations + /// that can report whether the x:DataType was declared directly on the binding node + /// (as opposed to being inherited from an ancestor such as a DataTemplate). + /// This avoids a concrete cast to in consumers. + /// + internal interface IXamlDataTypeProviderWithBindingNodeInfo : IXamlDataTypeProvider + { + bool IsDataTypeOnBindingNode { get; } + } + + public class XamlDataTypeProvider : IXamlDataTypeProviderWithBindingNodeInfo { public XamlDataTypeProvider(string dataType) => this.dataType = dataType; @@ -369,12 +380,16 @@ static bool DoesNotInheritDataType(ElementNode node, HydrationContext context) INode dataTypeNode = null; ElementNode n = node as ElementNode; + var firstNode = n; // Special handling for BindingContext={Binding ...} // The order of checks is: // - x:DataType on the binding itself // - SKIP looking for x:DataType on the parent // - continue looking for x:DataType on the parent's parent... + // Note: skipNode = GetParent(node), so skipNode CANNOT equal firstNode (= node). + // The first loop iteration always checks the binding node itself for x:DataType, + // regardless of whether it is a BindingContext binding. ElementNode skipNode = null; if (IsBindingContextBinding(node)) { @@ -396,9 +411,21 @@ static bool DoesNotInheritDataType(ElementNode node, HydrationContext context) } if (dataTypeNode is ValueNode valueNode) this.dataType = valueNode.Value as string; + // Track whether x:DataType was found directly on the binding node, not inherited from + // an ancestor (e.g. a DataTemplate). This lets BindingExtension correctly skip the + // DataType for RelativeSource bindings whose DataType is only the DataTemplate item type. + IsDataTypeOnBindingNode = dataTypeNode != null && n == firstNode; } string dataType; string IXamlDataTypeProvider.BindingDataType => dataType; + bool IXamlDataTypeProviderWithBindingNodeInfo.IsDataTypeOnBindingNode => IsDataTypeOnBindingNode; internal HydrationContext Context { get; } + + /// + /// Gets whether the x:DataType was found directly on the binding node itself + /// (as opposed to being inherited from an ancestor element such as a DataTemplate). + /// + [EditorBrowsable(EditorBrowsableState.Never)] + internal bool IsDataTypeOnBindingNode { get; } } } diff --git a/src/Controls/tests/Xaml.UnitTests/Issues/Maui35564.xaml b/src/Controls/tests/Xaml.UnitTests/Issues/Maui35564.xaml new file mode 100644 index 000000000000..67fffc892e92 --- /dev/null +++ b/src/Controls/tests/Xaml.UnitTests/Issues/Maui35564.xaml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Controls/tests/Xaml.UnitTests/Issues/Maui35564.xaml.cs b/src/Controls/tests/Xaml.UnitTests/Issues/Maui35564.xaml.cs new file mode 100644 index 000000000000..eae7e7e7294a --- /dev/null +++ b/src/Controls/tests/Xaml.UnitTests/Issues/Maui35564.xaml.cs @@ -0,0 +1,225 @@ +using System; +using System.Collections.ObjectModel; +using System.Windows.Input; +using Microsoft.Maui.Controls.Internals; +using Microsoft.Maui.Dispatching; +using Microsoft.Maui.UnitTests; +using Xunit; + +namespace Microsoft.Maui.Controls.Xaml.UnitTests; + +/// +/// Regression test for https://github.com/dotnet/maui/issues/35564 +/// +/// Scenario A (Runtime inflator): +/// A TapGestureRecognizer inside a CollectionView ItemTemplate binds its Command +/// to the *page* using Source={RelativeSource AncestorType=...}, while the +/// DataTemplate has x:DataType="local:Maui35564Item" (the item model type). +/// When IsXamlCBindingWithSourceCompilationEnabled is true (AOT), BindingExtension +/// must NOT propagate the DataTemplate's x:DataType to a RelativeSource binding. +/// +/// Scenario B (SourceGen inflator): +/// The binding has x:DataType=local:Maui35564 directly on it, alongside +/// Source={RelativeSource AncestorType=...}. SourceGen must compile this to a +/// TypedBinding (no reflection) so the binding survives AOT/linker trimming. +/// +/// Scenario C (Regression guard — RelativeSource Self with inherited x:DataType): +/// A {RelativeSource Self} binding inside a DataTemplate that has x:DataType= +/// "local:Maui35564Item". The inherited item type must NOT be applied to Self +/// bindings — before the fix, Maui35564Item.IsAssignableFrom(Label) = false +/// would null out the source; after the fix DataType = null for inherited-type +/// RelativeSource bindings, so Self resolves to the Label correctly. +/// +public partial class Maui35564 : ContentPage +{ + public ObservableCollection Items { get; } = new() + { + new Maui35564Item { Name = "Item A" }, + new Maui35564Item { Name = "Item B" }, + }; + + public ICommand ItemTappedCommand { get; } = new Command(_ => { }); + + public Maui35564() + { + InitializeComponent(); + BindingContext = this; + } + + [Collection("Issue")] + public class Tests : IDisposable + { + const string FeatureSwitch = + "Microsoft.Maui.RuntimeFeature.IsXamlCBindingWithSourceCompilationEnabled"; + + public Tests() => DispatcherProvider.SetCurrent(new DispatcherProviderStub()); + public void Dispose() => DispatcherProvider.SetCurrent(null); + + /// + /// Scenario A: RelativeSource binding without x:DataType directly on the binding node. + /// The DataTemplate's inherited x:DataType (Maui35564Item) must NOT be used to validate + /// the RelativeSource binding's resolved ancestor (the Maui35564 page). + /// For SourceGen, AncestorType should be resolved as the canonical source type and + /// compiled to TypedBinding even without x:DataType on the binding node. + /// + [Theory] + [XamlInflatorData] + internal void RelativeSourceCommandBindsToAncestorWithXamlCCompilationEnabled(XamlInflator inflator) + { + AppContext.SetSwitch(FeatureSwitch, true); + try + { + var page = new Maui35564(inflator); + page.BindingContext = page; + + var itemLayout = page.TheCollectionView.ItemTemplate.CreateContent() as VerticalStackLayout; + Assert.NotNull(itemLayout); + + var container = new VerticalStackLayout(); + container.Add(itemLayout); + page.Content = container; + + itemLayout.BindingContext = new Maui35564Item { Name = "Test" }; + + var tapGesture = itemLayout.GestureRecognizers[0] as TapGestureRecognizer; + Assert.NotNull(tapGesture); + + Assert.NotNull(tapGesture.Command); + Assert.Same(page.ItemTappedCommand, tapGesture.Command); + + if (inflator == XamlInflator.SourceGen) + { + var binding = tapGesture.GetContext(TapGestureRecognizer.CommandProperty).Bindings.GetValue(); + Assert.IsAssignableFrom(binding); + } + } + finally + { + AppContext.SetSwitch(FeatureSwitch, false); + } + } + + /// + /// Scenario B: RelativeSource binding WITH x:DataType directly on the binding node. + /// This is the real-world pattern users write in AOT apps. SourceGen must compile it + /// to a TypedBinding (no reflection) so the binding survives linker trimming. + /// For all inflators, the Command must resolve correctly. + /// For the SourceGen inflator specifically, the binding must be a TypedBinding. + /// + [Theory] + [XamlInflatorData] + internal void RelativeSourceCommandWithExplicitXDataTypeCompilesTypedBinding(XamlInflator inflator) + { + AppContext.SetSwitch(FeatureSwitch, true); + try + { + var page = new Maui35564(inflator); + page.BindingContext = page; + + var itemLayout = page.TheCollectionView2.ItemTemplate.CreateContent() as VerticalStackLayout; + Assert.NotNull(itemLayout); + + var container = new VerticalStackLayout(); + container.Add(itemLayout); + page.Content = container; + + itemLayout.BindingContext = new Maui35564Item { Name = "Test" }; + + var tapGesture = itemLayout.GestureRecognizers[0] as TapGestureRecognizer; + Assert.NotNull(tapGesture); + + // Command must resolve to the page's command for ALL inflators. + Assert.NotNull(tapGesture.Command); + Assert.Same(page.ItemTappedCommand, tapGesture.Command); + + // For the SourceGen inflator, the binding must be a TypedBinding — not a reflective + // Binding — so it survives AOT linker trimming. + if (inflator == XamlInflator.SourceGen) + { + var binding = tapGesture.GetContext(TapGestureRecognizer.CommandProperty).Bindings.GetValue(); + Assert.IsAssignableFrom(binding); + } + } + finally + { + AppContext.SetSwitch(FeatureSwitch, false); + } + } + /// + /// Scenario C: {RelativeSource Self} inside a DataTemplate that has x:DataType="Maui35564Item". + /// The inherited item type (Maui35564Item) must NOT be applied to the Self binding — + /// Self resolves to the Label itself, not to the DataTemplate item. + /// Before the fix: inherited DataType caused IsAssignableFrom(Label)=false → source=null. + /// After the fix: DataType=null for inherited-type RelativeSource bindings → Self resolves. + /// + [Theory] + [XamlInflatorData] + internal void RelativeSourceSelfInsideDataTemplateWithInheritedXDataType(XamlInflator inflator) + { + AppContext.SetSwitch(FeatureSwitch, true); + try + { + var page = new Maui35564(inflator); + page.BindingContext = page; + + var itemLayout = page.TheCollectionView3.ItemTemplate.CreateContent() as VerticalStackLayout; + Assert.NotNull(itemLayout); + + itemLayout.BindingContext = new Maui35564Item { Name = "Test" }; + + var label = itemLayout.Children[0] as Label; + Assert.NotNull(label); + + // Label.Text must equal the Label's own AutomationId ("scenario-c"), bound via {RelativeSource Self}. + // If the inherited x:DataType were applied, the Self source would be nulled out + // and Text would be null or empty. + Assert.Equal("scenario-c", label.Text); + } + finally + { + AppContext.SetSwitch(FeatureSwitch, false); + } + } + + /// + /// Scenario D: {RelativeSource Self} with x:DataType directly on the binding node. + /// Self bindings should not be compiled to TypedBinding, even with explicit x:DataType, + /// because the source is the view element itself. + /// + [Theory] + [XamlInflatorData] + internal void RelativeSourceSelfWithExplicitXDataTypeStaysUncompiled(XamlInflator inflator) + { + AppContext.SetSwitch(FeatureSwitch, true); + try + { + var page = new Maui35564(inflator); + page.BindingContext = page; + + var itemLayout = page.TheCollectionView4.ItemTemplate.CreateContent() as VerticalStackLayout; + Assert.NotNull(itemLayout); + + itemLayout.BindingContext = new Maui35564Item { Name = "Test" }; + + var label = itemLayout.Children[0] as Label; + Assert.NotNull(label); + Assert.Equal("scenario-d", label.Text); + + if (inflator == XamlInflator.SourceGen) + { + var binding = label.GetContext(Label.TextProperty).Bindings.GetValue(); + Assert.IsNotAssignableFrom(binding); + } + } + finally + { + AppContext.SetSwitch(FeatureSwitch, false); + } + } + } +} + +public class Maui35564Item +{ + public string Name { get; set; } = string.Empty; +}