Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion src/Controls/src/SourceGen/KnownMarkups.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
"""
<?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"
x:Class="Test.TestPage">
<Grid>
<Grid.GestureRecognizers>
<TapGestureRecognizer Command="{Binding Navigate}"
CommandParameter="{Binding Path=., Source={RelativeSource AncestorType={x:Type ContentPage}}}" />
</Grid.GestureRecognizers>
</Grid>
</ContentPage>
""";

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 =
"""
<?xml version="1.0" encoding="UTF-8"?>
<ContentView
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.ChildView"
x:DataType="test:ChildView">
<Grid>
<Grid.GestureRecognizers>
<TapGestureRecognizer Command="{Binding Navigate}"
CommandParameter="{Binding Path=., Source={RelativeSource AncestorType={x:Type test:MainPage}}}" />
</Grid.GestureRecognizers>
</Grid>
</ContentView>
""";

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);
}
}
16 changes: 16 additions & 0 deletions src/Controls/tests/Xaml.UnitTests/Issues/Maui33247.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
Copy link

Copilot AI Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test file is named "Maui33500.xaml" but the PR description states this fixes issue #33247. According to XAML unit testing guidelines, test files for GitHub issues should be named "MauiXXXXX.xaml" where XXXXX is the GitHub issue number. This file should be renamed to "Maui33247.xaml" to match the issue being fixed.

Copilot uses AI. Check for mistakes.
<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.Maui33247"
x:DataType="local:Maui33247">
<!-- Test: RelativeSource AncestorType with Path=. should return the ContentPage itself -->
<Grid x:Name="MainGrid" BackgroundColor="Blue">
<Grid.GestureRecognizers>
<TapGestureRecognizer x:Name="DirectTapGesture"
Command="{Binding Navigate}"
CommandParameter="{Binding Path=., Source={RelativeSource AncestorType={x:Type local:Maui33247}}}" />
</Grid.GestureRecognizers>
<Label Text="Direct binding test" />
</Grid>
</ContentPage>
46 changes: 46 additions & 0 deletions src/Controls/tests/Xaml.UnitTests/Issues/Maui33247.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using System.Windows.Input;
Copy link

Copilot AI Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test file is named "Maui33500" but the PR description states this fixes issue #33247. According to XAML unit testing guidelines, test files for GitHub issues should be named "MauiXXXXX" where XXXXX is the GitHub issue number. This file should be renamed to "Maui33247.xaml" and "Maui33247.xaml.cs" to match the issue being fixed.

Copilot uses AI. Check for mistakes.
using Xunit;

namespace Microsoft.Maui.Controls.Xaml.UnitTests;

/// <summary>
/// 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.
/// </summary>
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<Maui33247>(page.DirectTapGesture.CommandParameter);
Assert.Same(page, page.DirectTapGesture.CommandParameter);
}
}
}
Loading