From 4b59d4a13e48e63bc6026af91160a5bd5c1cd55e Mon Sep 17 00:00:00 2001 From: KarthikRajaKalaimani <92777139+KarthikRajaKalaimani@users.noreply.github.com> Date: Tue, 26 May 2026 12:59:08 +0530 Subject: [PATCH 1/8] Test case and fix included --- .../src/Build.Tasks/SetPropertiesVisitor.cs | 26 ++++ src/Controls/src/SourceGen/KnownMarkups.cs | 34 +++-- .../Xaml/MarkupExtensions/BindingExtension.cs | 35 +++-- src/Controls/src/Xaml/XamlServiceProvider.cs | 12 ++ .../Xaml.UnitTests/Issues/Maui35564.xaml | 45 ++++++ .../Xaml.UnitTests/Issues/Maui35564.xaml.cs | 139 ++++++++++++++++++ 6 files changed, 264 insertions(+), 27 deletions(-) create mode 100644 src/Controls/tests/Xaml.UnitTests/Issues/Maui35564.xaml create mode 100644 src/Controls/tests/Xaml.UnitTests/Issues/Maui35564.xaml.cs diff --git a/src/Controls/src/Build.Tasks/SetPropertiesVisitor.cs b/src/Controls/src/Build.Tasks/SetPropertiesVisitor.cs index d92a26c91b3e..eaec6cacb85c 100644 --- a/src/Controls/src/Build.Tasks/SetPropertiesVisitor.cs +++ b/src/Controls/src/Build.Tasks/SetPropertiesVisitor.cs @@ -531,6 +531,17 @@ static bool TryCompileBindingPath(ElementNode node, ILContext context, VariableD return false; } + // When the binding has an explicit RelativeSource or x:Reference source, x:DataType + // inherited from an ancestor (e.g. a DataTemplate) describes the template-item type, + // not the actual binding source. Attempting to compile against that type would produce + // a false XC0045 "property not found" warning and a broken TypedBinding. + // Only compile if x:DataType was written directly on the binding node itself, + // where the developer explicitly annotates the source type. + // Mirrors the same logic in KnownMarkups.ProvideValueForBindingExtension. + bool xDataTypeIsOnBindingNode = n == node; + if (HasRelativeOrReferenceSource(node) && !xDataTypeIsOnBindingNode) + return false; + if (xDataTypeIsInOuterScope) { context.LoggingHelper.LogWarningOrError(BindingWithXDataTypeFromOuterScope, context.XamlFilePath, node.LineNumber, node.LinePosition, 0, 0, null); @@ -640,6 +651,21 @@ 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 explicitly set, x:DataType inherited from an ancestor DataTemplate + // describes template items — not the actual binding source type. + static bool HasRelativeOrReferenceSource(ElementNode bindingNode) + { + if (!bindingNode.Properties.TryGetValue(new XmlName("", "Source"), out INode sourceNode)) + return false; + + return sourceNode is ElementNode sourceElementNode + && sourceElementNode.XmlType.Name is "RelativeSourceExtension" + or "RelativeSource" + or "ReferenceExtension" + or "Reference"; + } + 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..73b1dd8bc7ed 100644 --- a/src/Controls/src/SourceGen/KnownMarkups.cs +++ b/src/Controls/src/SourceGen/KnownMarkups.cs @@ -56,7 +56,7 @@ public static bool ProvideValueForStaticExtension(ElementNode markupNode, Indent value = string.Empty; return false; } - + var field = typeSymbol!.GetAllFields(membername, context).FirstOrDefault(f => f.IsStatic); var property = typeSymbol!.GetAllProperties(membername, context).FirstOrDefault(p => p.IsStatic); @@ -152,7 +152,7 @@ public static bool ProvideValueForRelativeSourceExtension(ElementNode markupNode if (!markupNode.Properties.TryGetValue(new XmlName("", "AncestorType"), out ancestorTypeNode) && !markupNode.Properties.TryGetValue(new XmlName(null, "AncestorType"), out ancestorTypeNode)) markupNode.Properties.TryGetValue(new XmlName(XamlParser.MauiUri, "AncestorType"), out ancestorTypeNode); - + if (ancestorTypeNode is not null) { if (ancestorTypeNode is ElementNode typeExtNode) @@ -260,7 +260,7 @@ public static bool ProvideValueForRelativeSourceExtension(ElementNode markupNode } } - public static bool ProvideValueForDynamicResourceExtension(ElementNode markupNode, IndentedTextWriter writer, SourceGenContext context, NodeSGExtensions.GetNodeValueDelegate? getNodeValue, out ITypeSymbol? returnType, out string value) + public static bool ProvideValueForDynamicResourceExtension(ElementNode markupNode, IndentedTextWriter writer, SourceGenContext context, NodeSGExtensions.GetNodeValueDelegate? getNodeValue, out ITypeSymbol? returnType, out string value) { returnType = context.Compilation.GetTypeByMetadataName("Microsoft.Maui.Controls.Internals.DynamicResource")!; string? key = null; @@ -326,7 +326,7 @@ internal static bool ProvideValueForStyleSheetExtension(ElementNode markupNode, } } - internal static bool ProvideValueForTemplateBindingExtension(ElementNode markupNode, IndentedTextWriter writer, SourceGenContext context, NodeSGExtensions.GetNodeValueDelegate? getNodeValue, out ITypeSymbol? returnType, out string value) + internal static bool ProvideValueForTemplateBindingExtension(ElementNode markupNode, IndentedTextWriter writer, SourceGenContext context, NodeSGExtensions.GetNodeValueDelegate? getNodeValue, out ITypeSymbol? returnType, out string value) { return ProvideValueForBindingExtension(markupNode, writer, context, isTemplateBinding: true, getNodeValue, out returnType, out value); } @@ -336,18 +336,22 @@ internal static bool ProvideValueForBindingExtension(ElementNode markupNode, Ind return ProvideValueForBindingExtension(markupNode, writer, context, isTemplateBinding: false, getNodeValue, out returnType, out value); } - private static bool ProvideValueForBindingExtension(ElementNode markupNode, IndentedTextWriter writer, SourceGenContext context, bool isTemplateBinding, NodeSGExtensions.GetNodeValueDelegate? getNodeValue, out ITypeSymbol? returnType, out string value) + private static bool ProvideValueForBindingExtension(ElementNode markupNode, IndentedTextWriter writer, SourceGenContext context, bool isTemplateBinding, NodeSGExtensions.GetNodeValueDelegate? getNodeValue, out ITypeSymbol? returnType, out string value) { 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 (RelativeSource or x:Reference), x:DataType usually does not + // describe the actual source — skip compilation and fall back to runtime binding. + // Exception: when x:DataType is written directly on the Binding node itself (not inherited + // from an ancestor), the developer is explicitly annotating the source's type. + // In that case we CAN compile a TypedBinding, which avoids reflection and is AOT/trim safe. bool hasExplicitSource = HasExplicitBindingSource(markupNode); - + bool xDataTypeOnBindingNode = hasExplicitSource && markupNode.Properties.ContainsKey(XmlName.xDataType); + context.Variables.TryGetValue(markupNode, out ILocalValue? extVariable); - - if ( !hasExplicitSource + + if ((!hasExplicitSource || xDataTypeOnBindingNode) && extVariable is not null) { TryGetXDataType(markupNode, context, out dataTypeSymbol); @@ -421,7 +425,7 @@ private static bool ProvideValueForBindingExtension(ElementNode markupNode, Inde expression += $", source:global::Microsoft.Maui.Controls.RelativeBindingSource.TemplatedParent"; } else - { + { if (markupNode.Properties.TryGetValue(new XmlName(null, "Source"), out var sourceNode)) expression += $", source: {getNodeValue(sourceNode, context.Compilation.GetTypeByMetadataName("System.String")!).ValueAccessor}"; expression += ") {"; @@ -431,7 +435,7 @@ private static bool ProvideValueForBindingExtension(ElementNode markupNode, Inde expression += $"FallbackValue = {getNodeValue(fallbackValueNode, context.Compilation.GetTypeByMetadataName("System.Object")!).ValueAccessor}, "; if (markupNode.Properties.TryGetValue(new XmlName(null, "TargetNullValue"), out var targetNullValueNode)) expression += $"TargetNullValue = {getNodeValue(targetNullValueNode, context.Compilation.GetTypeByMetadataName("System.Object")!).ValueAccessor}, "; - } + } expression += "}"; value = expression; @@ -892,7 +896,7 @@ internal static bool ProvideValueForStaticResourceExtension(ElementNode node, In var propertyType = typeandconverter?.type ?? propertySymbol?.Type; var converter = typeandconverter?.converter; - + // If no converter from BP, check the property's TypeConverter attribute if (converter == null && propertySymbol != null) { @@ -921,7 +925,7 @@ internal static bool ProvideValueForStaticResourceExtension(ElementNode node, In return true; } } - + // If we get here and getNodeValue is provided, use it as fallback if (getNodeValue != null) { diff --git a/src/Controls/src/Xaml/MarkupExtensions/BindingExtension.cs b/src/Controls/src/Xaml/MarkupExtensions/BindingExtension.cs index 5d86ae0daa12..68e106f0951c 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,11 +84,13 @@ 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); } return new Binding(Path, Mode, Converter, ConverterParameter, StringFormat, Source) @@ -95,17 +98,25 @@ BindingBase CreateBinding() 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 XamlDataTypeProvider { IsDataTypeOnBindingNode: true }) + ? bindingXDataType + : null, }; } } diff --git a/src/Controls/src/Xaml/XamlServiceProvider.cs b/src/Controls/src/Xaml/XamlServiceProvider.cs index c2aa755c0178..757f35f1b06c 100644 --- a/src/Controls/src/Xaml/XamlServiceProvider.cs +++ b/src/Controls/src/Xaml/XamlServiceProvider.cs @@ -369,6 +369,7 @@ 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: @@ -396,9 +397,20 @@ 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; 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..26448d852838 --- /dev/null +++ b/src/Controls/tests/Xaml.UnitTests/Issues/Maui35564.xaml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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..8de694efa86f --- /dev/null +++ b/src/Controls/tests/Xaml.UnitTests/Issues/Maui35564.xaml.cs @@ -0,0 +1,139 @@ +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. +/// +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). + /// + [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); + } + 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); + } + } + } +} + +public class Maui35564Item +{ + public string Name { get; set; } = string.Empty; +} From a18bec43f16ce898c05b187b070b1b1987a5aeca Mon Sep 17 00:00:00 2001 From: KarthikRajaKalaimani <92777139+KarthikRajaKalaimani@users.noreply.github.com> Date: Tue, 26 May 2026 15:14:11 +0530 Subject: [PATCH 2/8] Address code review: W1 interface, W2 null-namespace, W3 regression test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit W1: Add IXamlDataTypeProviderWithBindingNodeInfo internal interface to avoid concrete cast to XamlDataTypeProvider in BindingExtension. Any future IXamlDataTypeProvider implementation can opt in by implementing the extended internal interface. W2: Update HasRelativeSource in SetPropertiesVisitor to also check XmlName(null, "Source") — mirrors the defensive check already in KnownMarkups.HasExplicitBindingSource. Also scope the guard to RelativeSource only (not x:Reference), fixing a regression in Maui23711 where x:Reference with inherited x:DataType should still compile TypedBinding. W3: Add Scenario C test — {RelativeSource Self} inside a DataTemplate with inherited x:DataType must not inherit the item type. Before fix, the item type caused IsAssignableFrom(Label)=false, nulling the Self source. Three new test variants (Runtime/SourceGen/XamlC) cover this regression. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/Build.Tasks/SetPropertiesVisitor.cs | 25 +++++------ .../Xaml/MarkupExtensions/BindingExtension.cs | 2 +- src/Controls/src/Xaml/XamlServiceProvider.cs | 14 ++++++- .../Xaml.UnitTests/Issues/Maui35564.xaml | 16 +++++++ .../Xaml.UnitTests/Issues/Maui35564.xaml.cs | 42 +++++++++++++++++++ 5 files changed, 85 insertions(+), 14 deletions(-) diff --git a/src/Controls/src/Build.Tasks/SetPropertiesVisitor.cs b/src/Controls/src/Build.Tasks/SetPropertiesVisitor.cs index eaec6cacb85c..e130f46a3b49 100644 --- a/src/Controls/src/Build.Tasks/SetPropertiesVisitor.cs +++ b/src/Controls/src/Build.Tasks/SetPropertiesVisitor.cs @@ -531,15 +531,17 @@ static bool TryCompileBindingPath(ElementNode node, ILContext context, VariableD return false; } - // When the binding has an explicit RelativeSource or x:Reference source, x:DataType - // inherited from an ancestor (e.g. a DataTemplate) describes the template-item type, - // not the actual binding source. Attempting to compile against that type would produce - // a false XC0045 "property not found" warning and a broken TypedBinding. + // When the binding has a RelativeSource (e.g. AncestorType), x:DataType inherited + // from an ancestor DataTemplate describes the template-item type, not the actual + // binding source. Attempting to compile against that type would produce a false + // XC0045 "property not found" warning and a broken TypedBinding. // Only compile if x:DataType was written directly on the binding node itself, // where the developer explicitly annotates the source type. + // Note: x:Reference bindings are NOT guarded here — the developer's x:DataType on + // the referenced element's ancestor intentionally describes the source type there. // Mirrors the same logic in KnownMarkups.ProvideValueForBindingExtension. bool xDataTypeIsOnBindingNode = n == node; - if (HasRelativeOrReferenceSource(node) && !xDataTypeIsOnBindingNode) + if (HasRelativeSource(node) && !xDataTypeIsOnBindingNode) return false; if (xDataTypeIsInOuterScope) @@ -652,18 +654,17 @@ static bool IsBindingContextBinding(ElementNode node) } // Returns true when the binding's Source property is a RelativeSource or x:Reference. - // When Source is explicitly set, x:DataType inherited from an ancestor DataTemplate - // describes template items — not the actual binding source type. - static bool HasRelativeOrReferenceSource(ElementNode bindingNode) + // When Source is explicitly set to a RelativeSource, x:DataType inherited from an + // ancestor DataTemplate describes template items — not the actual binding source type. + static bool HasRelativeSource(ElementNode bindingNode) { - if (!bindingNode.Properties.TryGetValue(new XmlName("", "Source"), out INode sourceNode)) + 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"; + or "RelativeSource"; } bool DoesNotInheritDataType(ElementNode node) diff --git a/src/Controls/src/Xaml/MarkupExtensions/BindingExtension.cs b/src/Controls/src/Xaml/MarkupExtensions/BindingExtension.cs index 68e106f0951c..db781cef1a52 100644 --- a/src/Controls/src/Xaml/MarkupExtensions/BindingExtension.cs +++ b/src/Controls/src/Xaml/MarkupExtensions/BindingExtension.cs @@ -114,7 +114,7 @@ BindingBase CreateBinding() // 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 XamlDataTypeProvider { IsDataTypeOnBindingNode: true }) + && dataTypeProvider is IXamlDataTypeProviderWithBindingNodeInfo { IsDataTypeOnBindingNode: true }) ? bindingXDataType : null, }; diff --git a/src/Controls/src/Xaml/XamlServiceProvider.cs b/src/Controls/src/Xaml/XamlServiceProvider.cs index 757f35f1b06c..aa9662807e2f 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; @@ -404,6 +415,7 @@ static bool DoesNotInheritDataType(ElementNode node, HydrationContext context) } string dataType; string IXamlDataTypeProvider.BindingDataType => dataType; + bool IXamlDataTypeProviderWithBindingNodeInfo.IsDataTypeOnBindingNode => IsDataTypeOnBindingNode; internal HydrationContext Context { get; } /// diff --git a/src/Controls/tests/Xaml.UnitTests/Issues/Maui35564.xaml b/src/Controls/tests/Xaml.UnitTests/Issues/Maui35564.xaml index 26448d852838..595ccb7fb827 100644 --- a/src/Controls/tests/Xaml.UnitTests/Issues/Maui35564.xaml +++ b/src/Controls/tests/Xaml.UnitTests/Issues/Maui35564.xaml @@ -41,5 +41,21 @@ + + + + + + + + + diff --git a/src/Controls/tests/Xaml.UnitTests/Issues/Maui35564.xaml.cs b/src/Controls/tests/Xaml.UnitTests/Issues/Maui35564.xaml.cs index 8de694efa86f..c5596122b2f8 100644 --- a/src/Controls/tests/Xaml.UnitTests/Issues/Maui35564.xaml.cs +++ b/src/Controls/tests/Xaml.UnitTests/Issues/Maui35564.xaml.cs @@ -22,6 +22,13 @@ namespace Microsoft.Maui.Controls.Xaml.UnitTests; /// 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 { @@ -130,6 +137,41 @@ internal void RelativeSourceCommandWithExplicitXDataTypeCompilesTypedBinding(Xam 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); + } + } } } From 931e1a882779527d8d8460704e8cdc8bcfd9c1c1 Mon Sep 17 00:00:00 2001 From: KarthikRajaKalaimani <92777139+KarthikRajaKalaimani@users.noreply.github.com> Date: Wed, 27 May 2026 16:39:50 +0530 Subject: [PATCH 3/8] Removed unwanted changes --- src/Controls/src/SourceGen/KnownMarkups.cs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/Controls/src/SourceGen/KnownMarkups.cs b/src/Controls/src/SourceGen/KnownMarkups.cs index 73b1dd8bc7ed..03600f2a5404 100644 --- a/src/Controls/src/SourceGen/KnownMarkups.cs +++ b/src/Controls/src/SourceGen/KnownMarkups.cs @@ -56,7 +56,6 @@ public static bool ProvideValueForStaticExtension(ElementNode markupNode, Indent value = string.Empty; return false; } - var field = typeSymbol!.GetAllFields(membername, context).FirstOrDefault(f => f.IsStatic); var property = typeSymbol!.GetAllProperties(membername, context).FirstOrDefault(p => p.IsStatic); @@ -152,7 +151,6 @@ public static bool ProvideValueForRelativeSourceExtension(ElementNode markupNode if (!markupNode.Properties.TryGetValue(new XmlName("", "AncestorType"), out ancestorTypeNode) && !markupNode.Properties.TryGetValue(new XmlName(null, "AncestorType"), out ancestorTypeNode)) markupNode.Properties.TryGetValue(new XmlName(XamlParser.MauiUri, "AncestorType"), out ancestorTypeNode); - if (ancestorTypeNode is not null) { if (ancestorTypeNode is ElementNode typeExtNode) @@ -260,7 +258,7 @@ public static bool ProvideValueForRelativeSourceExtension(ElementNode markupNode } } - public static bool ProvideValueForDynamicResourceExtension(ElementNode markupNode, IndentedTextWriter writer, SourceGenContext context, NodeSGExtensions.GetNodeValueDelegate? getNodeValue, out ITypeSymbol? returnType, out string value) + public static bool ProvideValueForDynamicResourceExtension(ElementNode markupNode, IndentedTextWriter writer, SourceGenContext context,NodeSGExtensions.GetNodeValueDelegate? getNodeValue, out ITypeSymbol? returnType, out string value) { returnType = context.Compilation.GetTypeByMetadataName("Microsoft.Maui.Controls.Internals.DynamicResource")!; string? key = null; @@ -326,7 +324,7 @@ internal static bool ProvideValueForStyleSheetExtension(ElementNode markupNode, } } - internal static bool ProvideValueForTemplateBindingExtension(ElementNode markupNode, IndentedTextWriter writer, SourceGenContext context, NodeSGExtensions.GetNodeValueDelegate? getNodeValue, out ITypeSymbol? returnType, out string value) + internal static bool ProvideValueForTemplateBindingExtension(ElementNode markupNode, IndentedTextWriter writer, SourceGenContext context,NodeSGExtensions.GetNodeValueDelegate? getNodeValue, out ITypeSymbol? returnType, out string value) { return ProvideValueForBindingExtension(markupNode, writer, context, isTemplateBinding: true, getNodeValue, out returnType, out value); } @@ -336,7 +334,7 @@ internal static bool ProvideValueForBindingExtension(ElementNode markupNode, Ind return ProvideValueForBindingExtension(markupNode, writer, context, isTemplateBinding: false, getNodeValue, out returnType, out value); } - private static bool ProvideValueForBindingExtension(ElementNode markupNode, IndentedTextWriter writer, SourceGenContext context, bool isTemplateBinding, NodeSGExtensions.GetNodeValueDelegate? getNodeValue, out ITypeSymbol? returnType, out string value) + private static bool ProvideValueForBindingExtension(ElementNode markupNode, IndentedTextWriter writer, SourceGenContext context, bool isTemplateBinding,NodeSGExtensions.GetNodeValueDelegate? getNodeValue, out ITypeSymbol? returnType, out string value) { returnType = context.Compilation.GetTypeByMetadataName("Microsoft.Maui.Controls.BindingBase")!; ITypeSymbol? dataTypeSymbol = null; @@ -896,7 +894,6 @@ internal static bool ProvideValueForStaticResourceExtension(ElementNode node, In var propertyType = typeandconverter?.type ?? propertySymbol?.Type; var converter = typeandconverter?.converter; - // If no converter from BP, check the property's TypeConverter attribute if (converter == null && propertySymbol != null) { @@ -925,7 +922,6 @@ internal static bool ProvideValueForStaticResourceExtension(ElementNode node, In return true; } } - // If we get here and getNodeValue is provided, use it as fallback if (getNodeValue != null) { From 1ec07a4ac7e87fa84564fd5b2fb0f5a43c88a5b0 Mon Sep 17 00:00:00 2001 From: KarthikRajaKalaimani <92777139+KarthikRajaKalaimani@users.noreply.github.com> Date: Fri, 29 May 2026 16:42:25 +0530 Subject: [PATCH 4/8] Improved the fix --- .../src/Build.Tasks/SetPropertiesVisitor.cs | 25 +++++++++++-------- src/Controls/src/Xaml/XamlServiceProvider.cs | 3 +++ 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/Controls/src/Build.Tasks/SetPropertiesVisitor.cs b/src/Controls/src/Build.Tasks/SetPropertiesVisitor.cs index e130f46a3b49..5f1636239ad9 100644 --- a/src/Controls/src/Build.Tasks/SetPropertiesVisitor.cs +++ b/src/Controls/src/Build.Tasks/SetPropertiesVisitor.cs @@ -531,17 +531,15 @@ static bool TryCompileBindingPath(ElementNode node, ILContext context, VariableD return false; } - // When the binding has a RelativeSource (e.g. AncestorType), x:DataType inherited - // from an ancestor DataTemplate describes the template-item type, not the actual - // binding source. Attempting to compile against that type would produce a false - // XC0045 "property not found" warning and a broken TypedBinding. + // When the binding has a RelativeSource (e.g. AncestorType) or x:Reference Source, + // x:DataType inherited from an ancestor DataTemplate describes the template-item type, + // not the actual binding source. Attempting to compile against that type would produce + // a false XC0045 "property not found" warning and a broken TypedBinding. // Only compile if x:DataType was written directly on the binding node itself, // where the developer explicitly annotates the source type. - // Note: x:Reference bindings are NOT guarded here — the developer's x:DataType on - // the referenced element's ancestor intentionally describes the source type there. - // Mirrors the same logic in KnownMarkups.ProvideValueForBindingExtension. + // Mirrors the same logic in KnownMarkups.ProvideValueForBindingExtension (SourceGen). bool xDataTypeIsOnBindingNode = n == node; - if (HasRelativeSource(node) && !xDataTypeIsOnBindingNode) + if (HasRelativeOrReferenceSource(node) && !xDataTypeIsOnBindingNode) return false; if (xDataTypeIsInOuterScope) @@ -654,9 +652,12 @@ static bool IsBindingContextBinding(ElementNode node) } // Returns true when the binding's Source property is a RelativeSource or x:Reference. - // When Source is explicitly set to a RelativeSource, x:DataType inherited from an + // When Source is a RelativeSource or x:Reference, x:DataType inherited from an // ancestor DataTemplate describes template items — not the actual binding source type. - static bool HasRelativeSource(ElementNode bindingNode) + // 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)) @@ -664,7 +665,9 @@ static bool HasRelativeSource(ElementNode bindingNode) return sourceNode is ElementNode sourceElementNode && sourceElementNode.XmlType.Name is "RelativeSourceExtension" - or "RelativeSource"; + or "RelativeSource" + or "ReferenceExtension" + or "Reference"; } bool DoesNotInheritDataType(ElementNode node) diff --git a/src/Controls/src/Xaml/XamlServiceProvider.cs b/src/Controls/src/Xaml/XamlServiceProvider.cs index aa9662807e2f..43ca1607971c 100644 --- a/src/Controls/src/Xaml/XamlServiceProvider.cs +++ b/src/Controls/src/Xaml/XamlServiceProvider.cs @@ -387,6 +387,9 @@ static bool DoesNotInheritDataType(ElementNode node, HydrationContext context) // - 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)) { From 7ddd4a96d15a927eeb1ba4339e6d25f360a69c3c Mon Sep 17 00:00:00 2001 From: KarthikRajaKalaimani <92777139+KarthikRajaKalaimani@users.noreply.github.com> Date: Fri, 29 May 2026 17:11:46 +0530 Subject: [PATCH 5/8] Removed unwanted changes --- src/Controls/src/Build.Tasks/SetPropertiesVisitor.cs | 4 ++++ src/Controls/src/SourceGen/KnownMarkups.cs | 8 +++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Controls/src/Build.Tasks/SetPropertiesVisitor.cs b/src/Controls/src/Build.Tasks/SetPropertiesVisitor.cs index 5f1636239ad9..f0306fee20b1 100644 --- a/src/Controls/src/Build.Tasks/SetPropertiesVisitor.cs +++ b/src/Controls/src/Build.Tasks/SetPropertiesVisitor.cs @@ -540,7 +540,9 @@ static bool TryCompileBindingPath(ElementNode node, ILContext context, VariableD // Mirrors the same logic in KnownMarkups.ProvideValueForBindingExtension (SourceGen). bool xDataTypeIsOnBindingNode = n == node; if (HasRelativeOrReferenceSource(node) && !xDataTypeIsOnBindingNode) + { return false; + } if (xDataTypeIsInOuterScope) { @@ -661,7 +663,9 @@ 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" diff --git a/src/Controls/src/SourceGen/KnownMarkups.cs b/src/Controls/src/SourceGen/KnownMarkups.cs index 03600f2a5404..5c8ef2f799cc 100644 --- a/src/Controls/src/SourceGen/KnownMarkups.cs +++ b/src/Controls/src/SourceGen/KnownMarkups.cs @@ -56,6 +56,7 @@ public static bool ProvideValueForStaticExtension(ElementNode markupNode, Indent value = string.Empty; return false; } + var field = typeSymbol!.GetAllFields(membername, context).FirstOrDefault(f => f.IsStatic); var property = typeSymbol!.GetAllProperties(membername, context).FirstOrDefault(p => p.IsStatic); @@ -151,6 +152,7 @@ public static bool ProvideValueForRelativeSourceExtension(ElementNode markupNode if (!markupNode.Properties.TryGetValue(new XmlName("", "AncestorType"), out ancestorTypeNode) && !markupNode.Properties.TryGetValue(new XmlName(null, "AncestorType"), out ancestorTypeNode)) markupNode.Properties.TryGetValue(new XmlName(XamlParser.MauiUri, "AncestorType"), out ancestorTypeNode); + if (ancestorTypeNode is not null) { if (ancestorTypeNode is ElementNode typeExtNode) @@ -258,7 +260,7 @@ public static bool ProvideValueForRelativeSourceExtension(ElementNode markupNode } } - public static bool ProvideValueForDynamicResourceExtension(ElementNode markupNode, IndentedTextWriter writer, SourceGenContext context,NodeSGExtensions.GetNodeValueDelegate? getNodeValue, out ITypeSymbol? returnType, out string value) + public static bool ProvideValueForDynamicResourceExtension(ElementNode markupNode, IndentedTextWriter writer, SourceGenContext context, NodeSGExtensions.GetNodeValueDelegate? getNodeValue, out ITypeSymbol? returnType, out string value) { returnType = context.Compilation.GetTypeByMetadataName("Microsoft.Maui.Controls.Internals.DynamicResource")!; string? key = null; @@ -324,7 +326,7 @@ internal static bool ProvideValueForStyleSheetExtension(ElementNode markupNode, } } - internal static bool ProvideValueForTemplateBindingExtension(ElementNode markupNode, IndentedTextWriter writer, SourceGenContext context,NodeSGExtensions.GetNodeValueDelegate? getNodeValue, out ITypeSymbol? returnType, out string value) + internal static bool ProvideValueForTemplateBindingExtension(ElementNode markupNode, IndentedTextWriter writer, SourceGenContext context, NodeSGExtensions.GetNodeValueDelegate? getNodeValue, out ITypeSymbol? returnType, out string value) { return ProvideValueForBindingExtension(markupNode, writer, context, isTemplateBinding: true, getNodeValue, out returnType, out value); } @@ -334,7 +336,7 @@ internal static bool ProvideValueForBindingExtension(ElementNode markupNode, Ind return ProvideValueForBindingExtension(markupNode, writer, context, isTemplateBinding: false, getNodeValue, out returnType, out value); } - private static bool ProvideValueForBindingExtension(ElementNode markupNode, IndentedTextWriter writer, SourceGenContext context, bool isTemplateBinding,NodeSGExtensions.GetNodeValueDelegate? getNodeValue, out ITypeSymbol? returnType, out string value) + private static bool ProvideValueForBindingExtension(ElementNode markupNode, IndentedTextWriter writer, SourceGenContext context, bool isTemplateBinding, NodeSGExtensions.GetNodeValueDelegate? getNodeValue, out ITypeSymbol? returnType, out string value) { returnType = context.Compilation.GetTypeByMetadataName("Microsoft.Maui.Controls.BindingBase")!; ITypeSymbol? dataTypeSymbol = null; From ec407fbd88a5866ade4ca05dd4f5a0bc6f6014ae Mon Sep 17 00:00:00 2001 From: KarthikRajaKalaimani <92777139+KarthikRajaKalaimani@users.noreply.github.com> Date: Fri, 29 May 2026 17:15:07 +0530 Subject: [PATCH 6/8] removed unwanted changes --- src/Controls/src/SourceGen/KnownMarkups.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Controls/src/SourceGen/KnownMarkups.cs b/src/Controls/src/SourceGen/KnownMarkups.cs index 5c8ef2f799cc..66265603d13b 100644 --- a/src/Controls/src/SourceGen/KnownMarkups.cs +++ b/src/Controls/src/SourceGen/KnownMarkups.cs @@ -425,7 +425,7 @@ private static bool ProvideValueForBindingExtension(ElementNode markupNode, Inde expression += $", source:global::Microsoft.Maui.Controls.RelativeBindingSource.TemplatedParent"; } else - { + { if (markupNode.Properties.TryGetValue(new XmlName(null, "Source"), out var sourceNode)) expression += $", source: {getNodeValue(sourceNode, context.Compilation.GetTypeByMetadataName("System.String")!).ValueAccessor}"; expression += ") {"; @@ -435,7 +435,7 @@ private static bool ProvideValueForBindingExtension(ElementNode markupNode, Inde expression += $"FallbackValue = {getNodeValue(fallbackValueNode, context.Compilation.GetTypeByMetadataName("System.Object")!).ValueAccessor}, "; if (markupNode.Properties.TryGetValue(new XmlName(null, "TargetNullValue"), out var targetNullValueNode)) expression += $"TargetNullValue = {getNodeValue(targetNullValueNode, context.Compilation.GetTypeByMetadataName("System.Object")!).ValueAccessor}, "; - } + } expression += "}"; value = expression; @@ -896,6 +896,7 @@ internal static bool ProvideValueForStaticResourceExtension(ElementNode node, In var propertyType = typeandconverter?.type ?? propertySymbol?.Type; var converter = typeandconverter?.converter; + // If no converter from BP, check the property's TypeConverter attribute if (converter == null && propertySymbol != null) { @@ -924,6 +925,7 @@ internal static bool ProvideValueForStaticResourceExtension(ElementNode node, In return true; } } + // If we get here and getNodeValue is provided, use it as fallback if (getNodeValue != null) { From 47003bfc34459bc2b06d51201fd3c04e06abc982 Mon Sep 17 00:00:00 2001 From: KarthikRajaKalaimani <92777139+KarthikRajaKalaimani@users.noreply.github.com> Date: Wed, 3 Jun 2026 17:55:25 +0530 Subject: [PATCH 7/8] updated --- .../src/Build.Tasks/SetPropertiesVisitor.cs | 185 ++++++++++++++---- src/Controls/src/SourceGen/KnownMarkups.cs | 114 ++++++++++- .../Xaml.UnitTests/Issues/Maui35564.xaml | 14 ++ .../Xaml.UnitTests/Issues/Maui35564.xaml.cs | 44 +++++ 4 files changed, 312 insertions(+), 45 deletions(-) diff --git a/src/Controls/src/Build.Tasks/SetPropertiesVisitor.cs b/src/Controls/src/Build.Tasks/SetPropertiesVisitor.cs index f0306fee20b1..77b9d07be7ac 100644 --- a/src/Controls/src/Build.Tasks/SetPropertiesVisitor.cs +++ b/src/Controls/src/Build.Tasks/SetPropertiesVisitor.cs @@ -524,56 +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; } - // When the binding has a RelativeSource (e.g. AncestorType) or x:Reference Source, - // x:DataType inherited from an ancestor DataTemplate describes the template-item type, - // not the actual binding source. Attempting to compile against that type would produce - // a false XC0045 "property not found" warning and a broken TypedBinding. - // Only compile if x:DataType was written directly on the binding node itself, - // where the developer explicitly annotates the source type. - // Mirrors the same logic in KnownMarkups.ProvideValueForBindingExtension (SourceGen). - bool xDataTypeIsOnBindingNode = n == node; + TypeReference tSourceRef = null; if (HasRelativeOrReferenceSource(node) && !xDataTypeIsOnBindingNode) { - return false; + if (!TryGetRelativeSourceAncestorTypeReference(node, context, module, out tSourceRef)) + { + return false; + } } - if (xDataTypeIsInOuterScope) + if (tSourceRef is null) { - context.LoggingHelper.LogWarningOrError(BindingWithXDataTypeFromOuterScope, context.XamlFilePath, node.LineNumber, node.LinePosition, 0, 0, null); - // continue compilation - } + if (dataTypeNode is null) + { + context.LoggingHelper.LogWarningOrError(BindingWithoutDataType, context.XamlFilePath, node.LineNumber, node.LinePosition, 0, 0, null); - 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; - } + return false; + } - if ((dataTypeNode as ValueNode)?.Value is not string dataType) - throw new BuildException(XDataTypeSyntax, dataTypeNode as IXmlLineInfo, null); + if (xDataTypeIsInOuterScope) + { + context.LoggingHelper.LogWarningOrError(BindingWithXDataTypeFromOuterScope, context.XamlFilePath, node.LineNumber, node.LinePosition, 0, 0, null); + // continue compilation + } - 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 ( 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 @@ -674,6 +683,110 @@ static bool HasRelativeOrReferenceSource(ElementNode bindingNode) 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 66265603d13b..902da5f74489 100644 --- a/src/Controls/src/SourceGen/KnownMarkups.cs +++ b/src/Controls/src/SourceGen/KnownMarkups.cs @@ -341,20 +341,25 @@ 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 usually does not - // describe the actual source — skip compilation and fall back to runtime binding. - // Exception: when x:DataType is written directly on the Binding node itself (not inherited - // from an ancestor), the developer is explicitly annotating the source's type. - // In that case we CAN compile a TypedBinding, which avoids reflection and is AOT/trim safe. + // 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 = hasExplicitSource && markupNode.Properties.ContainsKey(XmlName.xDataType); + bool xDataTypeOnBindingNode = markupNode.Properties.ContainsKey(XmlName.xDataType); + bool isRelativeSourceWithoutAncestorType = IsRelativeSourceWithoutAncestorType(markupNode, context); context.Variables.TryGetValue(markupNode, out ILocalValue? extVariable); - if ((!hasExplicitSource || xDataTypeOnBindingNode) - && 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) { @@ -633,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/tests/Xaml.UnitTests/Issues/Maui35564.xaml b/src/Controls/tests/Xaml.UnitTests/Issues/Maui35564.xaml index 595ccb7fb827..67fffc892e92 100644 --- a/src/Controls/tests/Xaml.UnitTests/Issues/Maui35564.xaml +++ b/src/Controls/tests/Xaml.UnitTests/Issues/Maui35564.xaml @@ -57,5 +57,19 @@ + + + + + + + + + diff --git a/src/Controls/tests/Xaml.UnitTests/Issues/Maui35564.xaml.cs b/src/Controls/tests/Xaml.UnitTests/Issues/Maui35564.xaml.cs index c5596122b2f8..eae7e7e7294a 100644 --- a/src/Controls/tests/Xaml.UnitTests/Issues/Maui35564.xaml.cs +++ b/src/Controls/tests/Xaml.UnitTests/Issues/Maui35564.xaml.cs @@ -59,6 +59,8 @@ public class Tests : IDisposable /// 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] @@ -84,6 +86,12 @@ internal void RelativeSourceCommandBindsToAncestorWithXamlCCompilationEnabled(Xa 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 { @@ -172,6 +180,42 @@ internal void RelativeSourceSelfInsideDataTemplateWithInheritedXDataType(XamlInf 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); + } + } } } From bb1f82e7165e302e11b043e703912aa1bf438d2f Mon Sep 17 00:00:00 2001 From: KarthikRajaKalaimani <92777139+KarthikRajaKalaimani@users.noreply.github.com> Date: Wed, 10 Jun 2026 12:32:23 +0530 Subject: [PATCH 8/8] comments added --- src/Controls/src/Xaml/MarkupExtensions/BindingExtension.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Controls/src/Xaml/MarkupExtensions/BindingExtension.cs b/src/Controls/src/Xaml/MarkupExtensions/BindingExtension.cs index db781cef1a52..010bdf873be7 100644 --- a/src/Controls/src/Xaml/MarkupExtensions/BindingExtension.cs +++ b/src/Controls/src/Xaml/MarkupExtensions/BindingExtension.cs @@ -93,6 +93,9 @@ BindingBase CreateBinding() 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,