diff --git a/src/Controls/src/SourceGen/KnownMarkups.cs b/src/Controls/src/SourceGen/KnownMarkups.cs
index 195b6554023e..40c255621179 100644
--- a/src/Controls/src/SourceGen/KnownMarkups.cs
+++ b/src/Controls/src/SourceGen/KnownMarkups.cs
@@ -189,13 +189,17 @@ public static bool ProvideValueForRelativeSourceExtension(ElementNode markupNode
}
else if (ancestorTypeNode is ValueNode vnType)
{
- // Try to parse as a type name directly (without x:Type)
+ // Try to parse as a type name directly (without x:Type).
+ // Cache the resolved symbol in context.Types keyed by the ValueNode so that
+ // TryGetRelativeSourceAncestorType can look it up without re-resolving.
var typeName = vnType.Value as string;
if (!IsNullOrEmpty(typeName))
{
XmlType xmlType = TypeArgumentsParser.ParseSingle(typeName!, markupNode.NamespaceResolver, markupNode as IXmlLineInfo);
xmlType.TryResolveTypeSymbol(null, context.Compilation, context.XmlnsCache, context.TypeCache, out var resolvedType);
ancestorTypeSymbol = resolvedType;
+ if (resolvedType is not null)
+ context.Types[vnType] = resolvedType;
}
}
}
@@ -343,20 +347,41 @@ private static bool ProvideValueForBindingExtension(ElementNode markupNode, Inde
returnType = context.Compilation.GetTypeByMetadataName("Microsoft.Maui.Controls.BindingBase")!;
ITypeSymbol? dataTypeSymbol = null;
- // When Source is RelativeSource, the type is determined at runtime — skip compilation.
- // When Source is x:Reference, resolve the referenced element's type and compile against it.
- // Otherwise, use x:DataType from the current scope.
- bool hasRelativeSource = HasRelativeSourceBinding(markupNode);
-
context.Variables.TryGetValue(markupNode, out ILocalValue? extVariable);
- if ( !hasRelativeSource
- && extVariable is not null)
+ if (extVariable is not null)
{
- ITypeSymbol? xRefSourceType = TryResolveXReferenceSourceType(markupNode, context);
- dataTypeSymbol = xRefSourceType;
- if (dataTypeSymbol is null)
- TryGetXDataType(markupNode, context, out dataTypeSymbol);
+ // Determine the source type for compiled binding based on the binding's Source configuration:
+ //
+ // 1. RelativeSource with a resolvable AncestorType: use the AncestorType as the source
+ // type. The symbol is already registered in context.Types by
+ // ProvideValueForRelativeSourceExtension, enabling trim-safe TypedBinding generation.
+ //
+ // 2. RelativeSource without AncestorType (Self, TemplatedParent, or FindAncestor without
+ // a type): the binding source is resolved at runtime. Using x:DataType as the source
+ // type here would produce a compiled binding with an incorrect source type, leading to
+ // runtime failures. Fall through to the string-based Binding path instead.
+ //
+ // 3. x:Reference: resolve the referenced element's type and compile against it.
+ //
+ // 4. No explicit source: use x:DataType if available to produce a compiled TypedBinding.
+ // isAncestorTypeSource is true whenever AncestorType was present, regardless of whether
+ // the type resolved successfully. This prevents a BindingPropertyNotFound diagnostic from
+ // firing on a path that was never compiled before — even when resolution fails.
+ TryGetRelativeSourceAncestorType(markupNode, context, out var ancestorTypeSymbol, out bool isAncestorTypeSource);
+ ITypeSymbol? xRefSourceType = null;
+ if (ancestorTypeSymbol is not null)
+ {
+ dataTypeSymbol = ancestorTypeSymbol;
+ }
+
+ if (!isAncestorTypeSource && !HasRelativeSourceBinding(markupNode))
+ {
+ xRefSourceType = TryResolveXReferenceSourceType(markupNode, context);
+ dataTypeSymbol = xRefSourceType;
+ if (dataTypeSymbol is null)
+ TryGetXDataType(markupNode, context, out dataTypeSymbol);
+ }
if (dataTypeSymbol is not null)
{
@@ -367,10 +392,17 @@ private static bool ProvideValueForBindingExtension(ElementNode markupNode, Inde
return true;
}
- // Emit property-not-found diagnostic only for x:DataType-sourced bindings.
- // For x:Reference bindings, silently fall back to runtime — these bindings
- // were never compiled before, so emitting a new warning would be a regression.
- if (propertyNotFoundDiagnostic is not null && xRefSourceType is null)
+ // Emit property-not-found diagnostic when the source type was known at compile time
+ // but the binding path doesn't exist on that type. Specifically:
+ // - x:DataType bindings: always emit (existing behavior).
+ // - AncestorType bindings with a resolved type: emit, because the type is known and the
+ // path is provably wrong — consistent with x:DataType behavior. Suppress only when the
+ // AncestorType itself failed to resolve (ancestorTypeSymbol == null), since no type
+ // inference was possible.
+ // - x:Reference bindings: always suppress — they were never compiled before.
+ if (propertyNotFoundDiagnostic is not null
+ && xRefSourceType is null
+ && (!isAncestorTypeSource || ancestorTypeSymbol is not null))
{
context.ReportDiagnostic(propertyNotFoundDiagnostic);
}
@@ -708,6 +740,69 @@ static bool HasRelativeSourceBinding(ElementNode bindingNode)
return null;
}
+
+ // Checks if the binding has a Source property that is a RelativeSource extension
+ // with a resolvable AncestorType. If so, returns the already-resolved AncestorType
+ // symbol from context.Types (populated earlier by ProvideValueForRelativeSourceExtension).
+ // This allows AncestorType bindings to use the compiled (trim-safe) TypedBinding path.
+ //
+ // Ordering guarantee: RelativeSourceExtension is registered in GetKnownEarlyMarkupExtensions
+ // and BindingExtension in GetKnownLateMarkupExtensions (see NodeSGExtensions.cs). Early markup
+ // extensions are always resolved before late ones, so context.Types is guaranteed to already
+ // contain the AncestorType symbol (if resolvable) by the time this method runs — no re-resolution
+ // or ordering fallback is needed here.
+ static bool TryGetRelativeSourceAncestorType(ElementNode bindingNode, SourceGenContext context, out ITypeSymbol? ancestorType, out bool hasAncestorType)
+ {
+ ancestorType = null;
+ hasAncestorType = false;
+
+ // Check if Source property exists
+ if (!bindingNode.Properties.TryGetValue(new XmlName("", "Source"), out INode? sourceNode)
+ && !bindingNode.Properties.TryGetValue(new XmlName(null, "Source"), out sourceNode))
+ {
+ return false;
+ }
+
+ // Check if the Source is a RelativeSourceExtension
+ if (sourceNode is not ElementNode relativeSourceNode
+ || (relativeSourceNode.XmlType.Name != "RelativeSourceExtension"
+ && relativeSourceNode.XmlType.Name != "RelativeSource"))
+ {
+ return false;
+ }
+
+ // Find the AncestorType property on the RelativeSource node
+ 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);
+
+ if (ancestorTypeNode is null)
+ {
+ return false;
+ }
+
+ // AncestorType node is present — mark the attempt regardless of resolution outcome.
+ hasAncestorType = true;
+
+ // The AncestorType is typically an x:Type extension (ElementNode).
+ // ProvideValueForRelativeSourceExtension already resolved this type
+ // and registered it in context.Types — just look it up.
+ if (ancestorTypeNode is ElementNode typeExtNode)
+ {
+ return context.Types.TryGetValue(typeExtNode, out ancestorType) && ancestorType is not null;
+ }
+
+ // AncestorType may also be a bare string (ValueNode), e.g. AncestorType="local:MyViewModel".
+ // ProvideValueForRelativeSourceExtension resolves this form and caches the result in
+ // context.Types, so reuse that cached value here to avoid duplicating resolution logic.
+ if (ancestorTypeNode is ValueNode vnType)
+ {
+ context.Types.TryGetValue(vnType, out ancestorType);
+ return ancestorType is not null;
+ }
+
+ return false;
+ }
}
internal static bool ProvideValueForDataTemplateExtension(ElementNode markupNode, IndentedTextWriter writer, SourceGenContext context, NodeSGExtensions.GetNodeValueDelegate? getNodeValue, out ITypeSymbol? returnType, out string value)
diff --git a/src/Controls/tests/SourceGen.UnitTests/BindingDiagnosticsTests.cs b/src/Controls/tests/SourceGen.UnitTests/BindingDiagnosticsTests.cs
index cd520fe8e676..cc6038f66480 100644
--- a/src/Controls/tests/SourceGen.UnitTests/BindingDiagnosticsTests.cs
+++ b/src/Controls/tests/SourceGen.UnitTests/BindingDiagnosticsTests.cs
@@ -354,6 +354,95 @@ public class ItemModel
Assert.DoesNotContain(result.Diagnostics, d => d.Id == "MAUIG2045");
}
+ [Fact]
+ public void BindingWithRelativeSourceAncestorTypeInvalidPath_ReportsPropertyNotFound()
+ {
+ var xaml =
+"""
+
+
+
+
+
+
+
+
+""";
+
+ var csharp =
+"""
+namespace Test;
+
+public partial class TestPage : Microsoft.Maui.Controls.ContentPage { }
+
+public class ViewModel
+{
+ public string Name { get; set; }
+}
+""";
+
+ var compilation = CreateMauiCompilation()
+ .AddSyntaxTrees(Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree.ParseText(csharp));
+ var result = RunGenerator(compilation, new AdditionalXamlFile("Test.xaml", xaml), assertNoCompilationErrors: false);
+
+ // AncestorType=TestPage is resolvable at compile time, so the path is provably wrong on
+ // that type — MAUIG2045 must fire, consistent with x:DataType binding behavior.
+ var diagnostic = result.Diagnostics.FirstOrDefault(d => d.Id == "MAUIG2045");
+ Assert.NotNull(diagnostic);
+ Assert.Equal(DiagnosticSeverity.Warning, diagnostic.Severity);
+
+ var message = diagnostic.GetMessage();
+ Assert.Contains("NonExistentProperty", message, System.StringComparison.Ordinal);
+ Assert.Contains("TestPage", message, System.StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void BindingWithRelativeSourceUnresolvedAncestorTypeInvalidPath_SuppressesPropertyNotFound()
+ {
+ var xaml =
+"""
+
+
+
+
+
+
+
+
+""";
+
+ var csharp =
+"""
+namespace Test;
+
+public partial class TestPage : Microsoft.Maui.Controls.ContentPage { }
+
+public class ViewModel
+{
+ public string Name { get; set; }
+}
+""";
+
+ var compilation = CreateMauiCompilation()
+ .AddSyntaxTrees(Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree.ParseText(csharp));
+ var result = RunGenerator(compilation, new AdditionalXamlFile("Test.xaml", xaml), assertNoCompilationErrors: false);
+
+ // AncestorType="NonExistentAncestorType" cannot be resolved to a type, so no type inference
+ // was possible for this binding — MAUIG2045 must stay suppressed even though Path is invalid,
+ // since this binding was never compiled before (it always fell back to runtime Binding).
+ Assert.DoesNotContain(result.Diagnostics, d => d.Id == "MAUIG2045");
+ }
+
[Fact]
public void BindingIndexerTypeUnsupported_ReportsCorrectDiagnostic()
{
diff --git a/src/Controls/tests/Xaml.UnitTests/Issues/Maui34056.xaml b/src/Controls/tests/Xaml.UnitTests/Issues/Maui34056.xaml
new file mode 100644
index 000000000000..b6d6aab958f6
--- /dev/null
+++ b/src/Controls/tests/Xaml.UnitTests/Issues/Maui34056.xaml
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Controls/tests/Xaml.UnitTests/Issues/Maui34056.xaml.cs b/src/Controls/tests/Xaml.UnitTests/Issues/Maui34056.xaml.cs
new file mode 100644
index 000000000000..e4713dddbe08
--- /dev/null
+++ b/src/Controls/tests/Xaml.UnitTests/Issues/Maui34056.xaml.cs
@@ -0,0 +1,173 @@
+using System.Collections.ObjectModel;
+using System.Windows.Input;
+using Microsoft.Maui.Controls.Internals;
+using Xunit;
+
+namespace Microsoft.Maui.Controls.Xaml.UnitTests;
+
+// Page-level ViewModel — this is what the RelativeSource AncestorType points to
+public class Maui34056PageViewModel
+{
+ public ObservableCollection Items { get; } =
+ [new Maui34056ItemViewModel { ItemName = "Item 1" }];
+
+ public ICommand TestCommand { get; } = new Command(() => { });
+}
+
+// Item ViewModel — this is what the DataTemplate's x:DataType is set to
+public class Maui34056ItemViewModel
+{
+ public string ItemName { get; set; } = "";
+}
+
+public partial class Maui34056 : ContentPage
+{
+ public Maui34056()
+ {
+ InitializeComponent();
+ BindingContext = new Maui34056PageViewModel();
+ }
+
+ [Collection("Issue")]
+ public class Maui34056Tests
+ {
+ [Theory]
+ [XamlInflatorData]
+ internal void RelativeSourceAncestorTypeInDataTemplateGeneratesCompiledBinding(XamlInflator inflator)
+ {
+ var page = new Maui34056(inflator);
+
+ var template = ((CollectionView)page.TestCollectionView).ItemTemplate;
+ var content = template.CreateContent() as Button;
+ Assert.NotNull(content);
+
+ var bindingContext = content.GetContext(Button.CommandProperty);
+ Assert.NotNull(bindingContext);
+ var binding = bindingContext.Bindings.GetValue();
+
+ if (inflator is XamlInflator.Runtime)
+ {
+ // Runtime inflator uses the string-based Binding — no compile-time type info available.
+ Assert.IsType(binding);
+ }
+ else
+ {
+ // SourceGen: produces a trim-safe TypedBinding using AncestorType as the source type (the PR fix).
+ // XamlC: produces a TypedBinding here via the inline x:DataType='local:Maui34056PageViewModel'
+ // attribute on the Binding markup — not via AncestorType resolution. XamlC's AncestorType-only
+ // behavior (Scenario 3, no inline x:DataType) still falls back to runtime Binding.
+ var typedBinding = Assert.IsType>(binding);
+
+ // Verify the RelativeSource is correctly configured (mode + ancestor type).
+ var relativeSource = Assert.IsType(typedBinding.Source);
+ Assert.Equal(RelativeBindingSourceMode.FindAncestorBindingContext, relativeSource.Mode);
+ Assert.Equal(typeof(Maui34056PageViewModel), relativeSource.AncestorType);
+ }
+ }
+
+ [Theory]
+ [XamlInflatorData]
+ internal void RelativeSourceAncestorTypeWithoutInlineXDataTypeGeneratesCompiledBinding(XamlInflator inflator)
+ {
+ // Regression guard for SourceGen: the ambient DataTemplate x:DataType is
+ // Maui34056ItemViewModel (no TestCommand). If SourceGen regressed and used ambient
+ // x:DataType instead of AncestorType as the source, the TypedBinding assertion would
+ // fail, catching the regression.
+ // XamlC pre-existing behavior: does not compile AncestorType bindings without an
+ // explicit inline x:DataType — falls back to runtime Binding. This is separate from
+ // the SourceGen fix and not addressed by this PR.
+ var page = new Maui34056(inflator);
+
+ var template = ((CollectionView)page.AncestorTypeNoInlineDataTypeCollectionView).ItemTemplate;
+ var content = template.CreateContent() as Button;
+ Assert.NotNull(content);
+
+ var bindingContext = content.GetContext(Button.CommandProperty);
+ Assert.NotNull(bindingContext);
+ var binding = bindingContext.Bindings.GetValue();
+
+ if (inflator is XamlInflator.Runtime or XamlInflator.XamlC)
+ {
+ // Runtime: no compile-time type info.
+ // XamlC: pre-existing behavior — does not compile AncestorType without inline x:DataType.
+ Assert.IsType(binding);
+ }
+ else
+ {
+ // SourceGen: compiles to TypedBinding using AncestorType as the source.
+ var typedBinding = Assert.IsType>(binding);
+
+ var relativeSource = Assert.IsType(typedBinding.Source);
+ Assert.Equal(RelativeBindingSourceMode.FindAncestorBindingContext, relativeSource.Mode);
+ Assert.Equal(typeof(Maui34056PageViewModel), relativeSource.AncestorType);
+ }
+ }
+
+ [Theory]
+ [XamlInflatorData]
+ internal void RelativeSourceSelfInDataTemplateWithXDataTypeUsesStringBinding(XamlInflator inflator)
+ {
+ // Verifies SourceGen does not use the DataTemplate's x:DataType as the source type for
+ // {RelativeSource Self} bindings. The source is the element itself, resolved at runtime.
+ // Path=ItemName exists on Maui34056ItemViewModel to ensure the guard is what prevents
+ // compiled binding, not a failed type lookup. XamlC behavior is pre-existing and separate.
+ var page = new Maui34056(inflator);
+
+ var template = ((CollectionView)page.SelfBindingCollectionView).ItemTemplate;
+ var content = template.CreateContent() as Label;
+ Assert.NotNull(content);
+
+ var bindingContext = content.GetContext(Label.TextProperty);
+ Assert.NotNull(bindingContext);
+ var binding = bindingContext.Bindings.GetValue();
+
+ if (inflator is XamlInflator.XamlC)
+ {
+ // XamlC pre-existing behavior: compiles RelativeSource Self using DataTemplate x:DataType.
+ // This is a separate issue, not addressed by this fix.
+ Assert.IsType>(binding);
+ }
+ else
+ {
+ // Runtime: no compile-time type info, always string-based Binding.
+ // SourceGen (the fix): HasRelativeSourceBinding blocks x:DataType path for Self bindings.
+ Assert.IsType(binding);
+ }
+ }
+
+ [Theory]
+ [XamlInflatorData]
+ internal void RelativeSourceElementAncestorTypeUsesFindAncestorMode(XamlInflator inflator)
+ {
+ // Verifies that when AncestorType is an Element subclass (ContentPage), SourceGen selects
+ // RelativeBindingSourceMode.FindAncestor (not FindAncestorBindingContext) and produces a
+ // TypedBinding. This exercises the HasImplicitConversion/FindAncestor
+ // branch in KnownMarkups.cs that the other scenarios do not cover.
+ var page = new Maui34056(inflator);
+ page.Title = "TestTitle";
+
+ var template = ((CollectionView)page.FindAncestorCollectionView).ItemTemplate;
+ var content = template.CreateContent() as Label;
+ Assert.NotNull(content);
+
+ var bindingContext = content.GetContext(Label.TextProperty);
+ Assert.NotNull(bindingContext);
+ var binding = bindingContext.Bindings.GetValue();
+
+ if (inflator is XamlInflator.Runtime or XamlInflator.XamlC)
+ {
+ // Runtime: no compile-time type info.
+ // XamlC: pre-existing behavior — does not compile AncestorType without inline x:DataType.
+ Assert.IsType(binding);
+ }
+ else
+ {
+ // SourceGen: AncestorType=ContentPage (an Element subclass) → FindAncestor mode.
+ var typedBinding = Assert.IsType>(binding);
+ var relativeSource = Assert.IsType(typedBinding.Source);
+ Assert.Equal(RelativeBindingSourceMode.FindAncestor, relativeSource.Mode);
+ Assert.Equal(typeof(ContentPage), relativeSource.AncestorType);
+ }
+ }
+ }
+}
diff --git a/src/Controls/tests/Xaml.UnitTests/Issues/Maui34056StringAncestorType.sgen.xaml b/src/Controls/tests/Xaml.UnitTests/Issues/Maui34056StringAncestorType.sgen.xaml
new file mode 100644
index 000000000000..3129a8595145
--- /dev/null
+++ b/src/Controls/tests/Xaml.UnitTests/Issues/Maui34056StringAncestorType.sgen.xaml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Controls/tests/Xaml.UnitTests/Issues/Maui34056StringAncestorType.xaml.cs b/src/Controls/tests/Xaml.UnitTests/Issues/Maui34056StringAncestorType.xaml.cs
new file mode 100644
index 000000000000..65045f44ec4e
--- /dev/null
+++ b/src/Controls/tests/Xaml.UnitTests/Issues/Maui34056StringAncestorType.xaml.cs
@@ -0,0 +1,43 @@
+using System.Windows.Input;
+using Microsoft.Maui.Controls.Internals;
+using Xunit;
+
+namespace Microsoft.Maui.Controls.Xaml.UnitTests;
+
+public partial class Maui34056StringAncestorType : ContentPage
+{
+ public Maui34056StringAncestorType()
+ {
+ InitializeComponent();
+ BindingContext = new Maui34056PageViewModel();
+ }
+
+ [Collection("Issue")]
+ public class Tests
+ {
+ [Fact]
+ internal void RelativeSourceAncestorTypeAsStringGeneratesCompiledBinding()
+ {
+ // Covers AncestorType specified as a bare string (ValueNode form), e.g. AncestorType="local:MyViewModel".
+ // Previously this silently fell back to a runtime string Binding; after the fix it should compile to
+ // a trim-safe TypedBinding.
+ var page = new Maui34056StringAncestorType(XamlInflator.SourceGen);
+
+ var template = page.StringAncestorTypeCollectionView.ItemTemplate;
+ var content = template.CreateContent() as Button;
+ Assert.NotNull(content);
+
+ var bindingContext = content.GetContext(Button.CommandProperty);
+ Assert.NotNull(bindingContext);
+ var binding = bindingContext.Bindings.GetValue();
+
+ // SourceGen should produce a TypedBinding for the string AncestorType form.
+ var typedBinding = Assert.IsType>(binding);
+
+ // Also verify the RelativeSource is correctly configured.
+ var relativeSource = Assert.IsType(typedBinding.Source);
+ Assert.Equal(RelativeBindingSourceMode.FindAncestorBindingContext, relativeSource.Mode);
+ Assert.Equal(typeof(Maui34056PageViewModel), relativeSource.AncestorType);
+ }
+ }
+}