diff --git a/src/Controls/src/SourceGen/KnownMarkups.cs b/src/Controls/src/SourceGen/KnownMarkups.cs index d6f2c250eff6..fc4dee30ed1b 100644 --- a/src/Controls/src/SourceGen/KnownMarkups.cs +++ b/src/Controls/src/SourceGen/KnownMarkups.cs @@ -308,7 +308,16 @@ private static bool ProvideValueForBindingExtension(ElementNode markupNode, Inde { returnType = context.Compilation.GetTypeByMetadataName("Microsoft.Maui.Controls.BindingBase")!; ITypeSymbol? dataTypeSymbol = null; - if ( context.Variables.TryGetValue(markupNode, out ILocalValue extVariable) + + // Check if the binding has a Source property with a RelativeSource. + // In this case, we should NOT compile the binding using x:DataType because + // the source type will be determined at runtime by the RelativeSource, not x:DataType. + bool hasRelativeSource = HasRelativeSourceBinding(markupNode); + + context.Variables.TryGetValue(markupNode, out ILocalValue? extVariable); + + if ( !hasRelativeSource + && extVariable is not null && TryGetXDataType(markupNode, context, out dataTypeSymbol) && dataTypeSymbol is not null) { @@ -588,6 +597,29 @@ static bool IsBindingContextBinding(ElementNode node) && propertyName.NamespaceURI == "" && propertyName.LocalName == "BindingContext"; } + + // Checks if the binding has a Source property that is a RelativeSource extension. + // When a binding uses RelativeSource, the source type is determined at runtime, + // so we should NOT compile the binding using x:DataType. + static bool HasRelativeSourceBinding(ElementNode bindingNode) + { + // 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 ElementNode sourceElementNode) + { + // Check if the element is a RelativeSourceExtension + return sourceElementNode.XmlType.Name == "RelativeSourceExtension" + || sourceElementNode.XmlType.Name == "RelativeSource"; + } + + 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/InitializeComponent/RelativeSourceBindings.cs b/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/RelativeSourceBindings.cs index bedde10d5c1f..09e0593d99e8 100644 --- a/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/RelativeSourceBindings.cs +++ b/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/RelativeSourceBindings.cs @@ -281,4 +281,125 @@ public class TestViewModel Assert.Contains("RelativeBindingSource", generated, StringComparison.Ordinal); Assert.Contains("FindAncestorBindingContext", generated, StringComparison.Ordinal); } + + [Fact] + public void SelfPathWithFindAncestorGeneratesValidCode() + { + // Test for regression: RelativeSource AncestorType with Path=. should return the ancestor itself + var xaml = +""" + + + + + + + + +"""; + + var code = +""" +#nullable enable +using System.Windows.Input; +using Microsoft.Maui.Controls; +using Microsoft.Maui.Controls.Xaml; + +namespace Test; + +[XamlProcessing(XamlInflator.SourceGen)] +public partial class TestPage : ContentPage +{ + public TestPage() + { + InitializeComponent(); + BindingContext = this; + } + + public ICommand Navigate => new Command((param) => { }); +} +"""; + + var (result, generated) = RunGenerator(xaml, code); + + // Verify no diagnostics + Assert.False(result.Diagnostics.Any(d => d.Severity == Microsoft.CodeAnalysis.DiagnosticSeverity.Error), + $"Generator produced errors: {string.Join(", ", result.Diagnostics.Where(d => d.Severity == Microsoft.CodeAnalysis.DiagnosticSeverity.Error).Select(d => d.GetMessage()))}"); + + // Verify generated code exists + Assert.NotNull(generated); + + // Verify RelativeBindingSource is generated correctly with FindAncestor mode (ContentPage is an Element) + Assert.Contains("RelativeBindingSource", generated, StringComparison.Ordinal); + Assert.Contains("FindAncestor", generated, StringComparison.Ordinal); + Assert.Contains("ContentPage", generated, StringComparison.Ordinal); + } + + [Fact] + public void SelfPathWithFindAncestorCustomTypeGeneratesValidCode() + { + // Test for regression: RelativeSource AncestorType with Path=. and custom page type + // This replicates the user's scenario where MainPage is a custom type + var xaml = +""" + + + + + + + + +"""; + + var code = +""" +#nullable enable +using System.Windows.Input; +using Microsoft.Maui.Controls; +using Microsoft.Maui.Controls.Xaml; + +namespace Test; + +[XamlProcessing(XamlInflator.SourceGen)] +public partial class ChildView : ContentView +{ + public ChildView() + { + InitializeComponent(); + BindingContext = this; + } + + public ICommand Navigate => new Command((param) => { }); +} + +public class MainPage : ContentPage +{ +} +"""; + + var (result, generated) = RunGenerator(xaml, code); + + // Verify no diagnostics + Assert.False(result.Diagnostics.Any(d => d.Severity == Microsoft.CodeAnalysis.DiagnosticSeverity.Error), + $"Generator produced errors: {string.Join(", ", result.Diagnostics.Where(d => d.Severity == Microsoft.CodeAnalysis.DiagnosticSeverity.Error).Select(d => d.GetMessage()))}"); + + // Verify generated code exists + Assert.NotNull(generated); + + // Verify RelativeBindingSource is generated correctly with FindAncestor mode and custom type + Assert.Contains("RelativeBindingSource", generated, StringComparison.Ordinal); + Assert.Contains("FindAncestor", generated, StringComparison.Ordinal); + Assert.Contains("Test.MainPage", generated, StringComparison.Ordinal); + } } diff --git a/src/Controls/tests/Xaml.UnitTests/Issues/Maui33247.xaml b/src/Controls/tests/Xaml.UnitTests/Issues/Maui33247.xaml new file mode 100644 index 000000000000..0dc7503b345f --- /dev/null +++ b/src/Controls/tests/Xaml.UnitTests/Issues/Maui33247.xaml @@ -0,0 +1,16 @@ + + + + + + + + + diff --git a/src/Controls/tests/Xaml.UnitTests/Issues/Maui33247.xaml.cs b/src/Controls/tests/Xaml.UnitTests/Issues/Maui33247.xaml.cs new file mode 100644 index 000000000000..60a2ac9c0f52 --- /dev/null +++ b/src/Controls/tests/Xaml.UnitTests/Issues/Maui33247.xaml.cs @@ -0,0 +1,46 @@ +using System.Windows.Input; +using Xunit; + +namespace Microsoft.Maui.Controls.Xaml.UnitTests; + +/// +/// Test for regression: RelativeSource AncestorType with Path=. returns null in v10.0.20 +/// +/// Scenario: A TapGestureRecognizer tries to get a reference to the parent ContentPage via +/// CommandParameter="{Binding Path=., Source={RelativeSource AncestorType={x:Type local:Maui33247}}}" +/// +/// Expected: The CommandParameter should receive the ContentPage reference. +/// Bug: In v10.0.20, the CommandParameter was null when using SourceGen because the source generator +/// was incorrectly compiling the binding to use x:DataType as the source type instead of allowing +/// the RelativeSource to resolve the source at runtime. +/// +/// Fix: When a binding has a Source property with a RelativeSource, skip the compiled binding path +/// and use the fallback string-based binding instead. +/// +public partial class Maui33247 : ContentPage +{ + public Maui33247() + { + InitializeComponent(); + BindingContext = this; + } + + public ICommand Navigate => new Command((param) => { }); + + [Collection("Issue")] + public class Tests + { + [Theory] + [XamlInflatorData] + internal void DirectRelativeSourceAncestorTypeWithSelfPathReturnsAncestor(XamlInflator inflator) + { + // Test that RelativeSource AncestorType with Path=. returns the ancestor object for direct children + var page = new Maui33247(inflator); + + // The CommandParameter with RelativeSource AncestorType and Path=. should be the ContentPage itself + Assert.NotNull(page.DirectTapGesture.CommandParameter); + Assert.IsType(page.DirectTapGesture.CommandParameter); + Assert.Same(page, page.DirectTapGesture.CommandParameter); + } + } +}