Skip to content
127 changes: 111 additions & 16 deletions src/Controls/src/SourceGen/KnownMarkups.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}
Expand Down Expand Up @@ -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;
Comment thread
BagavathiPerumal marked this conversation as resolved.
}

if (!isAncestorTypeSource && !HasRelativeSourceBinding(markupNode))
{
xRefSourceType = TryResolveXReferenceSourceType(markupNode, context);
dataTypeSymbol = xRefSourceType;
if (dataTypeSymbol is null)
TryGetXDataType(markupNode, context, out dataTypeSymbol);
}

if (dataTypeSymbol is not null)
{
Expand All @@ -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
Comment thread
BagavathiPerumal marked this conversation as resolved.
&& (!isAncestorTypeSource || ancestorTypeSymbol is not null))
{
context.ReportDiagnostic(propertyNotFoundDiagnostic);
}
Expand Down Expand Up @@ -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)
Comment thread
BagavathiPerumal marked this conversation as resolved.
Comment thread
BagavathiPerumal marked this conversation as resolved.
{
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)
Expand Down
89 changes: 89 additions & 0 deletions src/Controls/tests/SourceGen.UnitTests/BindingDiagnosticsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,95 @@ public class ItemModel
Assert.DoesNotContain(result.Diagnostics, d => d.Id == "MAUIG2045");
}

[Fact]
public void BindingWithRelativeSourceAncestorTypeInvalidPath_ReportsPropertyNotFound()
{
var xaml =
"""
<?xml version="1.0" encoding="UTF-8"?>
<ContentPage
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:test="clr-namespace:Test"
x:Class="Test.TestPage"
x:DataType="test:ViewModel">
<ContentPage.Resources>
<DataTemplate x:Key="MyTemplate">
<Label Text="{Binding Source={RelativeSource AncestorType={x:Type test:TestPage}}, Path=NonExistentProperty}" />
</DataTemplate>
</ContentPage.Resources>
</ContentPage>
""";

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<XamlGenerator>(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 =
"""
<?xml version="1.0" encoding="UTF-8"?>
<ContentPage
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:test="clr-namespace:Test"
x:Class="Test.TestPage"
x:DataType="test:ViewModel">
<ContentPage.Resources>
<DataTemplate x:Key="MyTemplate">
<Label Text="{Binding Source={RelativeSource AncestorType=NonExistentAncestorType}, Path=NonExistentProperty}" />
</DataTemplate>
</ContentPage.Resources>
</ContentPage>
""";

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<XamlGenerator>(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()
{
Expand Down
62 changes: 62 additions & 0 deletions src/Controls/tests/Xaml.UnitTests/Issues/Maui34056.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?xml version="1.0" encoding="UTF-8"?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:Microsoft.Maui.Controls.Xaml.UnitTests"
x:Class="Microsoft.Maui.Controls.Xaml.UnitTests.Maui34056"
x:DataType="local:Maui34056PageViewModel">
<VerticalStackLayout>
<!-- Scenario 1: RelativeSource AncestorType inside DataTemplate with x:DataType (issue fix).
Mirrors the exact user reproduction case (inline x:DataType matches AncestorType). -->
<CollectionView x:Name="TestCollectionView"
ItemsSource="{Binding Items}">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="local:Maui34056ItemViewModel">
<Button x:Name="TestButton"
Text="{Binding ItemName}"
Command="{Binding x:DataType='local:Maui34056PageViewModel', Source={RelativeSource AncestorType={x:Type local:Maui34056PageViewModel}}, Path=TestCommand}" />
Comment thread
BagavathiPerumal marked this conversation as resolved.
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>

<!-- Scenario 2: {RelativeSource Self} inside DataTemplate with x:DataType.
SourceGen must not use x:DataType as the source type; the source is the element itself.
Path=ItemName exists on Maui34056ItemViewModel to ensure the guard, not a failed lookup, prevents compiled binding. -->
<CollectionView x:Name="SelfBindingCollectionView"
ItemsSource="{Binding Items}">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="local:Maui34056ItemViewModel">
<Label x:Name="SelfBindingLabel"
Text="{Binding Path=ItemName, Source={RelativeSource Self}}" />
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
<!-- Scenario 3: RelativeSource AncestorType without inline x:DataType (regression guard).
The ambient DataTemplate x:DataType is Maui34056ItemViewModel, which has no TestCommand.
If SourceGen regressed and used ambient x:DataType instead of AncestorType as the source,
it would produce a different TypedBinding or fall back to runtime Binding, and the
TypedBinding<Maui34056PageViewModel, ICommand> assertion would fail, catching the regression. -->
<CollectionView x:Name="AncestorTypeNoInlineDataTypeCollectionView"
ItemsSource="{Binding Items}">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="local:Maui34056ItemViewModel">
<Button x:Name="AncestorTypeNoInlineDataTypeButton"
Text="{Binding ItemName}"
Command="{Binding Source={RelativeSource AncestorType={x:Type local:Maui34056PageViewModel}}, Path=TestCommand}" />
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
<!-- Scenario 4: RelativeSource AncestorType for an Element subclass (ContentPage).
AncestorType=ContentPage derives from Element, so SourceGen selects FindAncestor mode
(not FindAncestorBindingContext). This verifies the HasImplicitConversion branch in
KnownMarkups.TryGetRelativeSourceAncestorType produces the correct mode. -->
<CollectionView x:Name="FindAncestorCollectionView"
ItemsSource="{Binding Items}">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="local:Maui34056ItemViewModel">
<Label x:Name="FindAncestorLabel"
Text="{Binding Source={RelativeSource AncestorType={x:Type ContentPage}}, Path=Title}" />
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</VerticalStackLayout>
</ContentPage>
Loading
Loading