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
14 changes: 14 additions & 0 deletions src/Controls/tests/Xaml.UnitTests/Issues/Maui34713.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?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.Maui34713">
<!-- NOTE: The converter is NOT defined in page resources.
It is expected to be in Application.Resources at runtime.
This tests the scenario from issue #34713. -->

<VerticalStackLayout x:DataType="local:Maui34713ViewModel">
<Label x:Name="label0" Text="{Binding IsActive, Converter={StaticResource BoolToTextConverter}}" />
<Label x:Name="label1" Text="{Binding Name}" />
</VerticalStackLayout>
</ContentPage>
203 changes: 203 additions & 0 deletions src/Controls/tests/Xaml.UnitTests/Issues/Maui34713.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
using System;
using System.Globalization;
using Microsoft.Maui.ApplicationModel;
using Microsoft.Maui.Controls.Core.UnitTests;
using Microsoft.Maui.Dispatching;
Comment on lines +3 to +5
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

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

using Microsoft.Maui.Controls.Internals; appears to be unused in this file (no symbols from that namespace are referenced). With analyzers enabled, unused usings can fail the build; please remove it (or reference the intended type explicitly if it was meant to be used).

Copilot uses AI. Check for mistakes.
using Microsoft.Maui.UnitTests;
using Xunit;

using static Microsoft.Maui.Controls.Xaml.UnitTests.MockSourceGenerator;

namespace Microsoft.Maui.Controls.Xaml.UnitTests;

public partial class Maui34713 : ContentPage
{
public Maui34713()
{
InitializeComponent();
}

[Collection("Issue")]
public class Tests : IDisposable
{
public Tests()
{
DispatcherProvider.SetCurrent(new DispatcherProviderStub());
}

public void Dispose()
{
AppInfo.SetCurrent(null);
Application.SetCurrentApplication(null);
DispatcherProvider.SetCurrent(null);
}

const string SharedCs = @"
using System;
using System.Globalization;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Controls.Xaml;
namespace Microsoft.Maui.Controls.Xaml.UnitTests
{
public class Maui34713ViewModel { public bool IsActive { get; set; } public string Name { get; set; } = """"; }
public class Maui34713BoolToTextConverter : IValueConverter {
public object Convert(object v, Type t, object p, CultureInfo c) => v is true ? ""Active"" : ""Inactive"";
public object ConvertBack(object v, Type t, object p, CultureInfo c) => throw new NotImplementedException();
}
}";

[Fact]
internal void SourceGenResolvesConverterAtCompileTime_ImplicitResources()
{
// When converter IS in page resources (implicit), source gen should
// resolve it at compile time - no runtime ProvideValue needed.
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:local=""clr-namespace:Microsoft.Maui.Controls.Xaml.UnitTests""
x:Class=""Microsoft.Maui.Controls.Xaml.UnitTests.Maui34713Test1"">
<ContentPage.Resources>
<local:Maui34713BoolToTextConverter x:Key=""BoolToTextConverter"" />
</ContentPage.Resources>
<VerticalStackLayout x:DataType=""local:Maui34713ViewModel"">
<Label x:Name=""label0"" Text=""{Binding IsActive, Converter={StaticResource BoolToTextConverter}}"" />
</VerticalStackLayout>
</ContentPage>";

var cs = @"
using Microsoft.Maui.Controls;
using Microsoft.Maui.Controls.Xaml;
namespace Microsoft.Maui.Controls.Xaml.UnitTests
{
[XamlProcessing(XamlInflator.Runtime, true)]
public partial class Maui34713Test1 : ContentPage { public Maui34713Test1() { InitializeComponent(); } }
}" + SharedCs;

var result = CreateMauiCompilation()
.WithAdditionalSource(cs, hintName: "Maui34713Test1.xaml.cs")
.RunMauiSourceGenerator(new AdditionalXamlFile("Issues/Maui34713Test1.xaml", xaml, TargetFramework: "net10.0"));

var generated = result.GeneratedInitializeComponent();

Assert.Contains("TypedBinding", generated, StringComparison.Ordinal);
// Converter should be resolved at compile time - no ProvideValue call
Assert.DoesNotContain(".ProvideValue(", generated, StringComparison.Ordinal);
}

[Fact]
internal void SourceGenResolvesConverterAtCompileTime_ExplicitResourceDictionary()
{
// When converter IS in page resources (explicit RD), source gen should
// also resolve it at compile time.
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:local=""clr-namespace:Microsoft.Maui.Controls.Xaml.UnitTests""
x:Class=""Microsoft.Maui.Controls.Xaml.UnitTests.Maui34713Test2"">
<ContentPage.Resources>
<ResourceDictionary>
<local:Maui34713BoolToTextConverter x:Key=""BoolToTextConverter"" />
</ResourceDictionary>
</ContentPage.Resources>
<VerticalStackLayout x:DataType=""local:Maui34713ViewModel"">
<Label x:Name=""label0"" Text=""{Binding IsActive, Converter={StaticResource BoolToTextConverter}}"" />
</VerticalStackLayout>
</ContentPage>";

var cs = @"
using Microsoft.Maui.Controls;
using Microsoft.Maui.Controls.Xaml;
namespace Microsoft.Maui.Controls.Xaml.UnitTests
{
[XamlProcessing(XamlInflator.Runtime, true)]
public partial class Maui34713Test2 : ContentPage { public Maui34713Test2() { InitializeComponent(); } }
}" + SharedCs;

var result = CreateMauiCompilation()
.WithAdditionalSource(cs, hintName: "Maui34713Test2.xaml.cs")
.RunMauiSourceGenerator(new AdditionalXamlFile("Issues/Maui34713Test2.xaml", xaml, TargetFramework: "net10.0"));

var generated = result.GeneratedInitializeComponent();

Assert.Contains("TypedBinding", generated, StringComparison.Ordinal);
// Converter should be resolved at compile time - no ProvideValue call
Assert.DoesNotContain(".ProvideValue(", generated, StringComparison.Ordinal);
}

[Fact]
internal void SourceGenCompilesBindingWithConverterToTypedBinding()
{
// When the converter is NOT in page resources, the binding should
// still be compiled into a TypedBinding.
var result = CreateMauiCompilation()
.WithAdditionalSource(
@"using System;
using System.Globalization;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Controls.Xaml;

namespace Microsoft.Maui.Controls.Xaml.UnitTests;

[XamlProcessing(XamlInflator.Runtime, true)]
public partial class Maui34713 : ContentPage
{
public Maui34713() => InitializeComponent();
}

public class Maui34713ViewModel
{
public bool IsActive { get; set; }
public string Name { get; set; } = string.Empty;
}

public class Maui34713BoolToTextConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
=> value is true ? ""Active"" : ""Inactive"";

public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
=> throw new NotImplementedException();
}
")
.RunMauiSourceGenerator(typeof(Maui34713));

var generated = result.GeneratedInitializeComponent();

Assert.Contains("TypedBinding", generated, StringComparison.Ordinal);
Assert.Contains("Converter = extension.Converter", generated, StringComparison.Ordinal);
Assert.DoesNotContain("new global::Microsoft.Maui.Controls.Binding(", generated, StringComparison.Ordinal);
}

[Theory]
[XamlInflatorData]
internal void BindingWithConverterFromAppResourcesWorksCorrectly(XamlInflator inflator)
{
var mockApp = new MockApplication();
mockApp.Resources.Add("BoolToTextConverter", new Maui34713BoolToTextConverter());
Application.SetCurrentApplication(mockApp);

var page = new Maui34713(inflator);
page.BindingContext = new Maui34713ViewModel { IsActive = true, Name = "Test" };

Assert.Equal("Active", page.label0.Text);
Assert.Equal("Test", page.label1.Text);
}
}
}

#nullable enable

public class Maui34713ViewModel
{
public bool IsActive { get; set; }
public string Name { get; set; } = string.Empty;
}

public class Maui34713BoolToTextConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
=> value is true ? "Active" : "Inactive";

public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
=> throw new NotImplementedException();
}
Loading