Skip to content

Commit 485b400

Browse files
[XSG] Fix dotnet#32836: SourceGen handles typed resources in StaticResource correctly (dotnet#32843)
* Fix dotnet#32836: SourceGen handles typed resources in StaticResource correctly When Color (or other non-string typed) resources are used with StaticResource inside markup extensions, they were incorrectly treated as strings causing CS0030 compilation errors. The fix recognizes when a resource variable is already properly typed (not string) and returns it directly without attempting string conversion. Example that now works: <Color x:Key="MyColor">#00FF00</Color> <Label TextColor="{local:MyExtension Source={StaticResource MyColor}}" /> Added comprehensive unit test with full expected code validation. Fixes dotnet#32836 * Add unit tests for issue dotnet#32837 - Issue dotnet#32837: SourceGen doesn't pass values properly to Converters when using StaticResource - Added Xaml.UnitTest that validates all three inflators (Runtime, XamlC, SourceGen) - Added SourceGen.UnitTest for code generation validation - Tests confirm that the fix for dotnet#32836 also resolves dotnet#32837 - Both issues had the same root cause: SourceGen not handling typed resources in StaticResource correctly * Remove unnecessary SourceGen.UnitTest for Maui32837 The Xaml.UnitTest is sufficient to validate the fix across all inflators
1 parent 0fa4f5d commit 485b400

4 files changed

Lines changed: 301 additions & 0 deletions

File tree

src/Controls/src/SourceGen/KnownMarkups.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -732,6 +732,13 @@ internal static bool ProvideValueForStaticResourceExtension(ElementNode node, In
732732
return false;
733733
}
734734

735+
if (variable is ILocalValue localVar && !localVar.Type.Equals(context.Compilation.GetTypeByMetadataName("System.String")!, SymbolEqualityComparer.Default))
736+
{
737+
returnType = localVar.Type;
738+
value = localVar.ValueAccessor;
739+
return true;
740+
}
741+
735742
//if the resource is a string, try to convert it
736743
if (resource.CollectionItems.Count == 1 && resource.CollectionItems[0] is ValueNode vn && vn.Value is string)
737744
{
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
using System;
2+
using System.ComponentModel.DataAnnotations;
3+
using System.IO;
4+
using System.Linq;
5+
using Microsoft.CodeAnalysis;
6+
using Microsoft.CodeAnalysis.CSharp;
7+
using Microsoft.Maui.Controls.SourceGen;
8+
using Xunit;
9+
10+
using static Microsoft.Maui.Controls.Xaml.UnitTests.SourceGen.SourceGeneratorDriver;
11+
12+
namespace Microsoft.Maui.Controls.SourceGen.UnitTests;
13+
14+
public class StaticResourceInMarkup : SourceGenXamlInitializeComponentTestBase
15+
{
16+
17+
[Fact]
18+
public void ColorResourceAsStaticResourceInMarkupExtension_ShouldNotProduceCS0030Error()
19+
{
20+
// Test reproducing issue #32836: Color resource defined in XAML should be treated as Color, not string
21+
// when used with StaticResource inside a markup extension
22+
//
23+
// This test validates that nested StaticResource in markup extensions generates correct code
24+
25+
var xaml =
26+
"""
27+
<?xml version="1.0" encoding="UTF-8"?>
28+
<ContentPage
29+
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
30+
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
31+
xmlns:local="clr-namespace:TestApp"
32+
x:Class="TestApp.TestPage">
33+
<ContentPage.Resources>
34+
<Color x:Key="MyColor">#00FF00</Color>
35+
</ContentPage.Resources>
36+
37+
<Label TextColor="{local:MyExtension Source={StaticResource MyColor}}" />
38+
</ContentPage>
39+
""";
40+
41+
var code =
42+
"""
43+
using System;
44+
using Microsoft.Maui.Controls;
45+
using Microsoft.Maui.Controls.Xaml;
46+
using Microsoft.Maui.Graphics;
47+
48+
namespace TestApp
49+
{
50+
public partial class TestPage : ContentPage
51+
{
52+
public TestPage()
53+
{
54+
InitializeComponent();
55+
}
56+
}
57+
58+
public class MyExtension : IMarkupExtension<Color>
59+
{
60+
public Color Source { get; set; }
61+
62+
public Color ProvideValue(IServiceProvider serviceProvider)
63+
{
64+
return Source;
65+
}
66+
67+
object IMarkupExtension.ProvideValue(IServiceProvider serviceProvider)
68+
{
69+
return (this as IMarkupExtension<Color>).ProvideValue(serviceProvider);
70+
}
71+
}
72+
}
73+
""";
74+
75+
var testXamlFilePath = Path.Combine(Environment.CurrentDirectory, "Test.xaml");
76+
var expected =
77+
$$"""
78+
//------------------------------------------------------------------------------
79+
// <auto-generated>
80+
// This code was generated by a .NET MAUI source generator.
81+
//
82+
// Changes to this file may cause incorrect behavior and will be lost if
83+
// the code is regenerated.
84+
// </auto-generated>
85+
//------------------------------------------------------------------------------
86+
#nullable enable
87+
88+
namespace TestApp;
89+
90+
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Maui.Controls.SourceGen, Version=10.0.0.0, Culture=neutral, PublicKeyToken=null", "10.0.0.0")]
91+
public partial class TestPage
92+
{
93+
private partial void InitializeComponent()
94+
{
95+
// Fallback to Runtime inflation if the page was updated by HotReload
96+
static string? getPathForType(global::System.Type type)
97+
{
98+
var assembly = type.Assembly;
99+
foreach (var xria in global::System.Reflection.CustomAttributeExtensions.GetCustomAttributes<global::Microsoft.Maui.Controls.Xaml.XamlResourceIdAttribute>(assembly))
100+
{
101+
if (xria.Type == type)
102+
return xria.Path;
103+
}
104+
return null;
105+
}
106+
107+
var rlr = global::Microsoft.Maui.Controls.Internals.ResourceLoader.ResourceProvider2?.Invoke(new global::Microsoft.Maui.Controls.Internals.ResourceLoader.ResourceLoadingQuery
108+
{
109+
AssemblyName = typeof(global::TestApp.TestPage).Assembly.GetName(),
110+
ResourcePath = getPathForType(typeof(global::TestApp.TestPage)),
111+
Instance = this,
112+
});
113+
114+
if (rlr?.ResourceContent != null)
115+
{
116+
this.InitializeComponentRuntime();
117+
return;
118+
}
119+
120+
var color = new global::Microsoft.Maui.Graphics.Color(0f, 1f, 0f, 1f) /* #00FF00 */;
121+
global::Microsoft.Maui.VisualDiagnostics.RegisterSourceInfo(color!, new global::System.Uri(@"Test.xaml;assembly=SourceGeneratorDriver.Generated", global::System.UriKind.Relative), 8, 4);
122+
var staticResourceExtension = new global::Microsoft.Maui.Controls.Xaml.StaticResourceExtension();
123+
global::Microsoft.Maui.VisualDiagnostics.RegisterSourceInfo(staticResourceExtension!, new global::System.Uri(@"Test.xaml;assembly=SourceGeneratorDriver.Generated", global::System.UriKind.Relative), 11, 9);
124+
var myExtension = new global::TestApp.MyExtension();
125+
global::Microsoft.Maui.VisualDiagnostics.RegisterSourceInfo(myExtension!, new global::System.Uri(@"Test.xaml;assembly=SourceGeneratorDriver.Generated", global::System.UriKind.Relative), 11, 9);
126+
var label = new global::Microsoft.Maui.Controls.Label();
127+
global::Microsoft.Maui.VisualDiagnostics.RegisterSourceInfo(label!, new global::System.Uri(@"Test.xaml;assembly=SourceGeneratorDriver.Generated", global::System.UriKind.Relative), 11, 3);
128+
var __root = this;
129+
global::Microsoft.Maui.VisualDiagnostics.RegisterSourceInfo(__root!, new global::System.Uri(@"Test.xaml;assembly=SourceGeneratorDriver.Generated", global::System.UriKind.Relative), 2, 2);
130+
#if !_MAUIXAML_SG_NAMESCOPE_DISABLE
131+
global::Microsoft.Maui.Controls.Internals.INameScope iNameScope = global::Microsoft.Maui.Controls.Internals.NameScope.GetNameScope(__root) ?? new global::Microsoft.Maui.Controls.Internals.NameScope();
132+
#endif
133+
#if !_MAUIXAML_SG_NAMESCOPE_DISABLE
134+
global::Microsoft.Maui.Controls.Internals.NameScope.SetNameScope(__root, iNameScope);
135+
#endif
136+
#if !_MAUIXAML_SG_NAMESCOPE_DISABLE
137+
label.transientNamescope = iNameScope;
138+
#endif
139+
__root.Resources["MyColor"] = color;
140+
#line 11 "{{testXamlFilePath}}"
141+
staticResourceExtension.Key = "MyColor";
142+
#line default
143+
var color1 = color;
144+
if (global::Microsoft.Maui.VisualDiagnostics.GetSourceInfo(color1!) == null)
145+
global::Microsoft.Maui.VisualDiagnostics.RegisterSourceInfo(color1!, new global::System.Uri(@"Test.xaml;assembly=SourceGeneratorDriver.Generated", global::System.UriKind.Relative), 11, 9);
146+
#line 11 "{{testXamlFilePath}}"
147+
myExtension.Source = (global::Microsoft.Maui.Graphics.Color)color1;
148+
#line default
149+
var xamlServiceProvider = new global::Microsoft.Maui.Controls.Xaml.Internals.XamlServiceProvider(__root);
150+
var iProvideValueTarget = new global::Microsoft.Maui.Controls.Xaml.Internals.SimpleValueTargetProvider(
151+
new object?[] {label, __root},
152+
global::Microsoft.Maui.Controls.Label.TextColorProperty,
153+
#if !_MAUIXAML_SG_NAMESCOPE_DISABLE
154+
new [] { iNameScope },
155+
#else
156+
null,
157+
#endif
158+
__root);
159+
xamlServiceProvider.Add(typeof(global::Microsoft.Maui.Controls.Xaml.IReferenceProvider), iProvideValueTarget);
160+
xamlServiceProvider.Add(typeof(global::Microsoft.Maui.Controls.Xaml.IProvideValueTarget), iProvideValueTarget);
161+
var xmlNamespaceResolver = new global::Microsoft.Maui.Controls.Xaml.Internals.XmlNamespaceResolver();
162+
xmlNamespaceResolver.Add("__f__", "http://schemas.microsoft.com/dotnet/2021/maui");
163+
xmlNamespaceResolver.Add("__g__", "http://schemas.microsoft.com/dotnet/maui/global");
164+
xmlNamespaceResolver.Add("", "http://schemas.microsoft.com/dotnet/2021/maui");
165+
xmlNamespaceResolver.Add("x", "http://schemas.microsoft.com/winfx/2009/xaml");
166+
xmlNamespaceResolver.Add("local", "clr-namespace:TestApp");
167+
xamlServiceProvider.Add(typeof(global::Microsoft.Maui.Controls.Xaml.IXamlTypeResolver), new global::Microsoft.Maui.Controls.Xaml.Internals.XamlTypeResolver(xmlNamespaceResolver, typeof(global::TestApp.TestPage).Assembly));
168+
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)));
169+
var color2 = (global::Microsoft.Maui.Graphics.Color)((global::Microsoft.Maui.Controls.Xaml.IMarkupExtension<global::Microsoft.Maui.Graphics.Color>)myExtension).ProvideValue(xamlServiceProvider);
170+
if (global::Microsoft.Maui.VisualDiagnostics.GetSourceInfo(color2!) == null)
171+
global::Microsoft.Maui.VisualDiagnostics.RegisterSourceInfo(color2!, new global::System.Uri(@"Test.xaml;assembly=SourceGeneratorDriver.Generated", global::System.UriKind.Relative), 11, 9);
172+
#line 11 "{{testXamlFilePath}}"
173+
label.SetValue(global::Microsoft.Maui.Controls.Label.TextColorProperty, color2);
174+
#line default
175+
#line 11 "{{testXamlFilePath}}"
176+
__root.SetValue(global::Microsoft.Maui.Controls.ContentPage.ContentProperty, label);
177+
#line default
178+
}
179+
}
180+
181+
""";
182+
183+
var (result, generated) = RunGenerator(xaml, code);
184+
Assert.False(result.Diagnostics.Any());
185+
Assert.Equal(expected, generated, ignoreLineEndingDifferences: true);
186+
187+
}
188+
189+
190+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?xml version="1.0" encoding="UTF-8" ?>
2+
<Application xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
3+
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
4+
xmlns:local="clr-namespace:Microsoft.Maui.Controls.Xaml.UnitTests"
5+
x:Class="Microsoft.Maui.Controls.Xaml.UnitTests.Maui32837">
6+
<Application.Resources>
7+
<x:Int32 x:Key="MyRadius">16</x:Int32>
8+
9+
<local:Maui32837IntToCornerRadiusConverter x:Key="IntToCornerRadiusConverter" />
10+
11+
<RoundRectangle x:Key="MyRoundRectangle">
12+
<RoundRectangle.CornerRadius>
13+
<Binding Source="{StaticResource MyRadius}"
14+
Converter="{StaticResource IntToCornerRadiusConverter}"/>
15+
</RoundRectangle.CornerRadius>
16+
</RoundRectangle>
17+
18+
<Style TargetType="Border" ApplyToDerivedTypes="True">
19+
<Setter Property="StrokeShape" Value="{StaticResource MyRoundRectangle}" />
20+
</Style>
21+
</Application.Resources>
22+
</Application>
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
using System;
2+
using System.Globalization;
3+
using Microsoft.Maui.ApplicationModel;
4+
using Microsoft.Maui.Controls.Core.UnitTests;
5+
using Microsoft.Maui.Controls.Shapes;
6+
using Microsoft.Maui.Dispatching;
7+
using Microsoft.Maui.UnitTests;
8+
using NUnit.Framework;
9+
10+
namespace Microsoft.Maui.Controls.Xaml.UnitTests;
11+
12+
public partial class Maui32837 : Application
13+
{
14+
public Maui32837() => InitializeComponent();
15+
16+
class Test
17+
{
18+
[SetUp]
19+
public void Setup()
20+
{
21+
Application.SetCurrentApplication(new MockApplication());
22+
DispatcherProvider.SetCurrent(new DispatcherProviderStub());
23+
}
24+
25+
[TearDown]
26+
public void TearDown()
27+
{
28+
AppInfo.SetCurrent(null);
29+
Application.SetCurrentApplication(null);
30+
}
31+
32+
[Test]
33+
public void ConverterReceivesCorrectValueFromStaticResource([Values] XamlInflator inflator)
34+
{
35+
var app = new Maui32837(inflator);
36+
37+
// Get the converter from resources
38+
var converter = app.Resources["IntToCornerRadiusConverter"] as Maui32837IntToCornerRadiusConverter;
39+
Assert.IsNotNull(converter, "Converter should not be null");
40+
41+
// Get the RoundRectangle from resources
42+
var roundRect = app.Resources["MyRoundRectangle"] as RoundRectangle;
43+
Assert.IsNotNull(roundRect, "RoundRectangle should not be null");
44+
45+
// The binding should have been evaluated and converter should have been called
46+
// Check that the converter was actually invoked by looking at the result
47+
var cornerRadius = roundRect.CornerRadius;
48+
49+
// The converter should have converted the int value 16 to CornerRadius(16)
50+
Assert.That(cornerRadius.TopLeft, Is.EqualTo(16.0),
51+
$"TopLeft corner radius should be 16.0 for {inflator}, but was {cornerRadius.TopLeft}");
52+
Assert.That(cornerRadius.TopRight, Is.EqualTo(16.0),
53+
$"TopRight corner radius should be 16.0 for {inflator}, but was {cornerRadius.TopRight}");
54+
Assert.That(cornerRadius.BottomLeft, Is.EqualTo(16.0),
55+
$"BottomLeft corner radius should be 16.0 for {inflator}, but was {cornerRadius.BottomLeft}");
56+
Assert.That(cornerRadius.BottomRight, Is.EqualTo(16.0),
57+
$"BottomRight corner radius should be 16.0 for {inflator}, but was {cornerRadius.BottomRight}");
58+
}
59+
}
60+
}
61+
62+
#nullable enable
63+
public class Maui32837IntToCornerRadiusConverter : IValueConverter
64+
{
65+
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
66+
{
67+
if (value is int radius)
68+
{
69+
return new CornerRadius(radius);
70+
}
71+
return new CornerRadius(0);
72+
}
73+
74+
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
75+
{
76+
if (value is CornerRadius cornerRadius)
77+
{
78+
return (int)cornerRadius.TopLeft;
79+
}
80+
return 0;
81+
}
82+
}

0 commit comments

Comments
 (0)