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
41 changes: 22 additions & 19 deletions src/Controls/src/SourceGen/KnownMarkups.cs
Original file line number Diff line number Diff line change
Expand Up @@ -341,23 +341,25 @@ private static bool ProvideValueForBindingExtension(ElementNode markupNode, Inde
returnType = context.Compilation.GetTypeByMetadataName("Microsoft.Maui.Controls.BindingBase")!;
ITypeSymbol? dataTypeSymbol = null;

// 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);
// 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.
bool hasExplicitSource = HasExplicitBindingSource(markupNode);

context.Variables.TryGetValue(markupNode, out ILocalValue? extVariable);

if ( !hasRelativeSource
&& extVariable is not null
&& TryGetXDataType(markupNode, context, out dataTypeSymbol)
&& dataTypeSymbol is not null)
if ( !hasExplicitSource
&& extVariable is not null)
{
var compiledBindingMarkup = new CompiledBindingMarkup(markupNode, GetBindingPath(markupNode), extVariable, context);
if (compiledBindingMarkup.TryCompileBinding(dataTypeSymbol, isTemplateBinding, out string? newBindingExpression) && newBindingExpression is not null)
TryGetXDataType(markupNode, context, out dataTypeSymbol);

if (dataTypeSymbol is not null)
{
value = newBindingExpression;
return true;
var compiledBindingMarkup = new CompiledBindingMarkup(markupNode, GetBindingPath(markupNode), extVariable, context);
if (compiledBindingMarkup.TryCompileBinding(dataTypeSymbol, isTemplateBinding, out string? newBindingExpression) && newBindingExpression is not null)
{
value = newBindingExpression;
return true;
}
}
}

Expand Down Expand Up @@ -628,10 +630,10 @@ static bool IsBindingContextBinding(ElementNode node)
&& 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,
// 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.
static bool HasRelativeSourceBinding(ElementNode bindingNode)
static bool HasExplicitBindingSource(ElementNode bindingNode)
{
// Check if Source property exists
if (!bindingNode.Properties.TryGetValue(new XmlName("", "Source"), out INode? sourceNode)
Expand All @@ -640,12 +642,13 @@ static bool HasRelativeSourceBinding(ElementNode bindingNode)
return false;
}

// Check if the Source is a RelativeSourceExtension
// Check if the Source is a RelativeSourceExtension or ReferenceExtension
if (sourceNode is ElementNode sourceElementNode)
{
// Check if the element is a RelativeSourceExtension
return sourceElementNode.XmlType.Name == "RelativeSourceExtension"
|| sourceElementNode.XmlType.Name == "RelativeSource";
return sourceElementNode.XmlType.Name is "RelativeSourceExtension"
or "RelativeSource"
or "ReferenceExtension"
or "Reference";
}

return false;
Expand Down
53 changes: 53 additions & 0 deletions src/Controls/tests/SourceGen.UnitTests/BindingDiagnosticsTests.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.Maui.Controls.SourceGen;
Expand Down Expand Up @@ -250,6 +251,58 @@ public class ViewModel
Assert.Equal(DiagnosticSeverity.Warning, diagnostic.Severity);
}

[Fact]
public void BindingWithXReferenceSourceInDataTemplate_DoesNotReportFalsePositive()
{
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:Name="PageRoot"
x:DataType="test:ViewModel">
<CollectionView ItemsSource="{Binding Items}">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="test:ItemModel">
<Button Text="{Binding Name}"
Command="{Binding Source={x:Reference PageRoot}, Path=BindingContext.SelectItemCommand}"
CommandParameter="{Binding .}" />
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</ContentPage>
""";

var csharp =
"""
namespace Test;

public partial class TestPage : Microsoft.Maui.Controls.ContentPage { }

public class ViewModel
{
public System.Collections.Generic.List<ItemModel> Items { get; set; }
public Microsoft.Maui.Controls.Command SelectItemCommand { get; set; }
}

public class ItemModel
{
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);

// x:Reference bindings skip compilation entirely — no MAUIG2045 should be emitted
// for properties on the DataTemplate's x:DataType (ItemModel).
Assert.DoesNotContain(result.Diagnostics, d => d.Id == "MAUIG2045" && d.GetMessage().Contains("ItemModel", StringComparison.Ordinal));
}

[Fact]
public void BindingIndexerTypeUnsupported_ReportsCorrectDiagnostic()
{
Expand Down
17 changes: 17 additions & 0 deletions src/Controls/tests/Xaml.UnitTests/Issues/Maui34490.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?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.Maui34490"
x:Name="PageRoot"
x:DataType="local:Maui34490ViewModel">
<CollectionView ItemsSource="{Binding Items}">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="local:Maui34490ItemModel">
<Button Text="{Binding Name}"
Command="{Binding Source={x:Reference PageRoot}, Path=BindingContext.SelectItemCommand}"
CommandParameter="{Binding .}" />
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</ContentPage>
49 changes: 49 additions & 0 deletions src/Controls/tests/Xaml.UnitTests/Issues/Maui34490.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using System;
using System.Collections.Generic;
using System.Windows.Input;
using Microsoft.Maui.ApplicationModel;
using Microsoft.Maui.Controls.Core.UnitTests;
using Xunit;

namespace Microsoft.Maui.Controls.Xaml.UnitTests;

public class Maui34490ViewModel
{
public List<Maui34490ItemModel> Items { get; set; }
public ICommand SelectItemCommand { get; set; }
}

public class Maui34490ItemModel
{
public string Name { get; set; }
}

public partial class Maui34490 : ContentPage
{
public Maui34490() => InitializeComponent();

[Collection("Issue")]
public class Tests : IDisposable
{
public Tests() => AppInfo.SetCurrent(new MockAppInfo());
public void Dispose() => AppInfo.SetCurrent(null);

[Theory]
[XamlInflatorData]
internal void XReferenceSourceInDataTemplateShouldNotWarn(XamlInflator inflator)
{
if (inflator == XamlInflator.SourceGen)
{
var result = MockSourceGenerator.CreateMauiCompilation()
.RunMauiSourceGenerator(typeof(Maui34490));
// The path is resolved against ContentPage (x:Reference target), NOT Maui34490ItemModel (x:DataType).
// BindingContext is 'object' on BindableObject, so SelectItemCommand warning on 'object' is expected,
// but a warning mentioning Maui34490ItemModel would mean it's still resolving against the wrong type.
Assert.DoesNotContain(result.Diagnostics, d => d.Id == "MAUIG2045" && d.GetMessage().Contains("Maui34490ItemModel", StringComparison.Ordinal));
}

var page = new Maui34490(inflator);
Assert.NotNull(page);
}
}
}
Loading