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
7 changes: 7 additions & 0 deletions src/Controls/src/SourceGen/KnownMarkups.cs
Original file line number Diff line number Diff line change
Expand Up @@ -772,6 +772,13 @@ internal static bool ProvideValueForStaticResourceExtension(ElementNode node, In
return false;
}

if (variable is ILocalValue localVar && !localVar.Type.Equals(context.Compilation.GetTypeByMetadataName("System.String")!, SymbolEqualityComparer.Default))
{
returnType = localVar.Type;
value = localVar.ValueAccessor;
Comment on lines +775 to +778
Copy link

Copilot AI Nov 24, 2025

Choose a reason for hiding this comment

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

[nitpick] The pattern match variable is ILocalValue localVar is redundant. Since context.Variables is defined as IDictionary<INode, ILocalValue> (line 34 in SourceGenContext.cs), the variable is guaranteed to be of type ILocalValue after the null check on line 768. You could simplify this to directly cast or use the variable:

if (!variable.Type.Equals(context.Compilation.GetTypeByMetadataName("System.String")!, SymbolEqualityComparer.Default))
{
    returnType = variable.Type;
    value = variable.ValueAccessor;
    return true;
}

This would make the code cleaner and avoid the unnecessary pattern matching.

Suggested change
if (variable is ILocalValue localVar && !localVar.Type.Equals(context.Compilation.GetTypeByMetadataName("System.String")!, SymbolEqualityComparer.Default))
{
returnType = localVar.Type;
value = localVar.ValueAccessor;
if (!variable.Type.Equals(context.Compilation.GetTypeByMetadataName("System.String")!, SymbolEqualityComparer.Default))
{
returnType = variable.Type;
value = variable.ValueAccessor;

Copilot uses AI. Check for mistakes.
return true;
}

//if the resource is a string, try to convert it
if (resource.CollectionItems.Count == 1 && resource.CollectionItems[0] is ValueNode vn && vn.Value is string)
{
Expand Down
190 changes: 190 additions & 0 deletions src/Controls/tests/SourceGen.UnitTests/StaticResourceInMarkup.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.Maui.Controls.SourceGen;
using Xunit;

using static Microsoft.Maui.Controls.Xaml.UnitTests.SourceGen.SourceGeneratorDriver;

namespace Microsoft.Maui.Controls.SourceGen.UnitTests;

public class StaticResourceInMarkup : SourceGenXamlInitializeComponentTestBase
{

[Fact]
public void ColorResourceAsStaticResourceInMarkupExtension_ShouldNotProduceCS0030Error()
{
// Test reproducing issue #32836: Color resource defined in XAML should be treated as Color, not string
// when used with StaticResource inside a markup extension
//
// This test validates that nested StaticResource in markup extensions generates correct code

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:TestApp"
x:Class="TestApp.TestPage">
<ContentPage.Resources>
<Color x:Key="MyColor">#00FF00</Color>
</ContentPage.Resources>

<Label TextColor="{local:MyExtension Source={StaticResource MyColor}}" />
</ContentPage>
""";

var code =
"""
using System;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Controls.Xaml;
using Microsoft.Maui.Graphics;

namespace TestApp
{
public partial class TestPage : ContentPage
{
public TestPage()
{
InitializeComponent();
}
}

public class MyExtension : IMarkupExtension<Color>
{
public Color Source { get; set; }

public Color ProvideValue(IServiceProvider serviceProvider)
{
return Source;
}

object IMarkupExtension.ProvideValue(IServiceProvider serviceProvider)
{
return (this as IMarkupExtension<Color>).ProvideValue(serviceProvider);
}
}
}
""";

var testXamlFilePath = Path.Combine(Environment.CurrentDirectory, "Test.xaml");
var expected =
$$"""
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a .NET MAUI source generator.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
#nullable enable

namespace TestApp;

[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Maui.Controls.SourceGen, Version=10.0.0.0, Culture=neutral, PublicKeyToken=null", "10.0.0.0")]
public partial class TestPage
{
private partial void InitializeComponent()
{
// Fallback to Runtime inflation if the page was updated by HotReload
static string? getPathForType(global::System.Type type)
{
var assembly = type.Assembly;
foreach (var xria in global::System.Reflection.CustomAttributeExtensions.GetCustomAttributes<global::Microsoft.Maui.Controls.Xaml.XamlResourceIdAttribute>(assembly))
{
if (xria.Type == type)
return xria.Path;
}
return null;
}

var rlr = global::Microsoft.Maui.Controls.Internals.ResourceLoader.ResourceProvider2?.Invoke(new global::Microsoft.Maui.Controls.Internals.ResourceLoader.ResourceLoadingQuery
{
AssemblyName = typeof(global::TestApp.TestPage).Assembly.GetName(),
ResourcePath = getPathForType(typeof(global::TestApp.TestPage)),
Instance = this,
});

if (rlr?.ResourceContent != null)
{
this.InitializeComponentRuntime();
return;
}

var color = new global::Microsoft.Maui.Graphics.Color(0f, 1f, 0f, 1f) /* #00FF00 */;
global::Microsoft.Maui.VisualDiagnostics.RegisterSourceInfo(color!, new global::System.Uri(@"Test.xaml;assembly=SourceGeneratorDriver.Generated", global::System.UriKind.Relative), 8, 4);
var staticResourceExtension = new global::Microsoft.Maui.Controls.Xaml.StaticResourceExtension();
global::Microsoft.Maui.VisualDiagnostics.RegisterSourceInfo(staticResourceExtension!, new global::System.Uri(@"Test.xaml;assembly=SourceGeneratorDriver.Generated", global::System.UriKind.Relative), 11, 9);
var myExtension = new global::TestApp.MyExtension();
global::Microsoft.Maui.VisualDiagnostics.RegisterSourceInfo(myExtension!, new global::System.Uri(@"Test.xaml;assembly=SourceGeneratorDriver.Generated", global::System.UriKind.Relative), 11, 9);
var label = new global::Microsoft.Maui.Controls.Label();
global::Microsoft.Maui.VisualDiagnostics.RegisterSourceInfo(label!, new global::System.Uri(@"Test.xaml;assembly=SourceGeneratorDriver.Generated", global::System.UriKind.Relative), 11, 3);
var __root = this;
global::Microsoft.Maui.VisualDiagnostics.RegisterSourceInfo(__root!, new global::System.Uri(@"Test.xaml;assembly=SourceGeneratorDriver.Generated", global::System.UriKind.Relative), 2, 2);
#if !_MAUIXAML_SG_NAMESCOPE_DISABLE
global::Microsoft.Maui.Controls.Internals.INameScope iNameScope = global::Microsoft.Maui.Controls.Internals.NameScope.GetNameScope(__root) ?? new global::Microsoft.Maui.Controls.Internals.NameScope();
#endif
#if !_MAUIXAML_SG_NAMESCOPE_DISABLE
global::Microsoft.Maui.Controls.Internals.NameScope.SetNameScope(__root, iNameScope);
#endif
#if !_MAUIXAML_SG_NAMESCOPE_DISABLE
label.transientNamescope = iNameScope;
#endif
__root.Resources["MyColor"] = color;
#line 11 "{{testXamlFilePath}}"
staticResourceExtension.Key = "MyColor";
#line default
var color1 = color;
if (global::Microsoft.Maui.VisualDiagnostics.GetSourceInfo(color1!) == null)
global::Microsoft.Maui.VisualDiagnostics.RegisterSourceInfo(color1!, new global::System.Uri(@"Test.xaml;assembly=SourceGeneratorDriver.Generated", global::System.UriKind.Relative), 11, 9);
#line 11 "{{testXamlFilePath}}"
myExtension.Source = (global::Microsoft.Maui.Graphics.Color)color1;
#line default
var xamlServiceProvider = new global::Microsoft.Maui.Controls.Xaml.Internals.XamlServiceProvider(__root);
var iProvideValueTarget = new global::Microsoft.Maui.Controls.Xaml.Internals.SimpleValueTargetProvider(
new object?[] {label, __root},
global::Microsoft.Maui.Controls.Label.TextColorProperty,
#if !_MAUIXAML_SG_NAMESCOPE_DISABLE
new [] { iNameScope },
#else
null,
#endif
__root);
xamlServiceProvider.Add(typeof(global::Microsoft.Maui.Controls.Xaml.IReferenceProvider), iProvideValueTarget);
xamlServiceProvider.Add(typeof(global::Microsoft.Maui.Controls.Xaml.IProvideValueTarget), iProvideValueTarget);
var xmlNamespaceResolver = new global::Microsoft.Maui.Controls.Xaml.Internals.XmlNamespaceResolver();
xmlNamespaceResolver.Add("__f__", "http://schemas.microsoft.com/dotnet/2021/maui");
xmlNamespaceResolver.Add("__g__", "http://schemas.microsoft.com/dotnet/maui/global");
xmlNamespaceResolver.Add("", "http://schemas.microsoft.com/dotnet/2021/maui");
xmlNamespaceResolver.Add("x", "http://schemas.microsoft.com/winfx/2009/xaml");
xmlNamespaceResolver.Add("local", "clr-namespace:TestApp");
xamlServiceProvider.Add(typeof(global::Microsoft.Maui.Controls.Xaml.IXamlTypeResolver), new global::Microsoft.Maui.Controls.Xaml.Internals.XamlTypeResolver(xmlNamespaceResolver, typeof(global::TestApp.TestPage).Assembly));
xamlServiceProvider.Add(typeof(global::Microsoft.Maui.Controls.Xaml.IXmlLineInfoProvider), new global::Microsoft.Maui.Controls.Xaml.Internals.XmlLineInfoProvider(new global::Microsoft.Maui.Controls.Xaml.XmlLineInfo(11, 9)));
var color2 = (global::Microsoft.Maui.Graphics.Color)((global::Microsoft.Maui.Controls.Xaml.IMarkupExtension<global::Microsoft.Maui.Graphics.Color>)myExtension).ProvideValue(xamlServiceProvider);
if (global::Microsoft.Maui.VisualDiagnostics.GetSourceInfo(color2!) == null)
global::Microsoft.Maui.VisualDiagnostics.RegisterSourceInfo(color2!, new global::System.Uri(@"Test.xaml;assembly=SourceGeneratorDriver.Generated", global::System.UriKind.Relative), 11, 9);
#line 11 "{{testXamlFilePath}}"
label.SetValue(global::Microsoft.Maui.Controls.Label.TextColorProperty, color2);
#line default
#line 11 "{{testXamlFilePath}}"
__root.SetValue(global::Microsoft.Maui.Controls.ContentPage.ContentProperty, label);
#line default
}
}

""";

var (result, generated) = RunGenerator(xaml, code);
Assert.False(result.Diagnostics.Any());
Assert.Equal(expected, generated, ignoreLineEndingDifferences: true);

}


}
22 changes: 22 additions & 0 deletions src/Controls/tests/Xaml.UnitTests/Issues/Maui32837.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8" ?>
<Application 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.Maui32837">
<Application.Resources>
<x:Int32 x:Key="MyRadius">16</x:Int32>

<local:Maui32837IntToCornerRadiusConverter x:Key="IntToCornerRadiusConverter" />

<RoundRectangle x:Key="MyRoundRectangle">
<RoundRectangle.CornerRadius>
<Binding Source="{StaticResource MyRadius}"
Converter="{StaticResource IntToCornerRadiusConverter}"/>
</RoundRectangle.CornerRadius>
</RoundRectangle>

<Style TargetType="Border" ApplyToDerivedTypes="True">
<Setter Property="StrokeShape" Value="{StaticResource MyRoundRectangle}" />
</Style>
</Application.Resources>
</Application>
82 changes: 82 additions & 0 deletions src/Controls/tests/Xaml.UnitTests/Issues/Maui32837.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
using System;
using System.Globalization;
using Microsoft.Maui.ApplicationModel;
using Microsoft.Maui.Controls.Core.UnitTests;
using Microsoft.Maui.Controls.Shapes;
using Microsoft.Maui.Dispatching;
using Microsoft.Maui.UnitTests;
using NUnit.Framework;

namespace Microsoft.Maui.Controls.Xaml.UnitTests;

public partial class Maui32837 : Application
{
public Maui32837() => InitializeComponent();

class Test
{
[SetUp]
public void Setup()
{
Application.SetCurrentApplication(new MockApplication());
DispatcherProvider.SetCurrent(new DispatcherProviderStub());
}

[TearDown]
public void TearDown()
{
AppInfo.SetCurrent(null);
Application.SetCurrentApplication(null);
}

[Test]
public void ConverterReceivesCorrectValueFromStaticResource([Values] XamlInflator inflator)
{
var app = new Maui32837(inflator);

// Get the converter from resources
var converter = app.Resources["IntToCornerRadiusConverter"] as Maui32837IntToCornerRadiusConverter;
Assert.IsNotNull(converter, "Converter should not be null");

// Get the RoundRectangle from resources
var roundRect = app.Resources["MyRoundRectangle"] as RoundRectangle;
Assert.IsNotNull(roundRect, "RoundRectangle should not be null");

// The binding should have been evaluated and converter should have been called
// Check that the converter was actually invoked by looking at the result
var cornerRadius = roundRect.CornerRadius;

// The converter should have converted the int value 16 to CornerRadius(16)
Assert.That(cornerRadius.TopLeft, Is.EqualTo(16.0),
$"TopLeft corner radius should be 16.0 for {inflator}, but was {cornerRadius.TopLeft}");
Assert.That(cornerRadius.TopRight, Is.EqualTo(16.0),
$"TopRight corner radius should be 16.0 for {inflator}, but was {cornerRadius.TopRight}");
Assert.That(cornerRadius.BottomLeft, Is.EqualTo(16.0),
$"BottomLeft corner radius should be 16.0 for {inflator}, but was {cornerRadius.BottomLeft}");
Assert.That(cornerRadius.BottomRight, Is.EqualTo(16.0),
$"BottomRight corner radius should be 16.0 for {inflator}, but was {cornerRadius.BottomRight}");
}
}
}

#nullable enable
public class Maui32837IntToCornerRadiusConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is int radius)
{
return new CornerRadius(radius);
}
return new CornerRadius(0);
}

public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is CornerRadius cornerRadius)
{
return (int)cornerRadius.TopLeft;
}
return 0;
}
}
Loading