Skip to content

Commit d696d69

Browse files
Fix NullReferenceException when binding to Dictionary<Enum, object> with x:DataType
Fixes #13856 ## Problem Using with a binding to like caused a NullReferenceException at compile time in XamlC. ## Solution - **XamlC (SetPropertiesVisitor.cs)**: Added support for finding indexers with enum parameter types and emitting enum constant values via IL - **SourceGen (CompiledBindingMarkup.cs)**: Added support for enum indexer parameter types in the binding path parser ## Changes - Added enum indexer detection in XamlC's property digger - Updated type validation to allow enum indexer types - Added code to emit enum constant values as IL instructions - Added enum indexer support in SourceGen's TryParsePath method - Fixed pre-existing test failure in Maui32879Tests (unrelated pragma warning directive) ## Tests - Added Maui13856.xaml unit test for XamlC compilation - Added Maui13856Tests.cs for SourceGen validation
1 parent 34a9faa commit d696d69

File tree

6 files changed

+191
-6
lines changed

6 files changed

+191
-6
lines changed

src/Controls/src/Build.Tasks/SetPropertiesVisitor.cs

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -720,12 +720,21 @@ static bool TryParsePath(ILContext context, string path, TypeReference tSourceRe
720720
&& pd.GetMethod != null
721721
&& TypeRefComparer.Default.Equals(pd.GetMethod.Parameters[0].ParameterType.ResolveGenericParameters(previousPartTypeRef), module.ImportReference(context.Cache, ("mscorlib", "System", "Object")))
722722
&& pd.GetMethod.IsPublic, out indexerDeclTypeRef);
723+
// Try to find an indexer with an enum parameter type
724+
indexer ??= previousPartTypeRef.GetProperty(context.Cache,
725+
pd => pd.Name == indexerName
726+
&& pd.GetMethod != null
727+
&& pd.GetMethod.Parameters[0].ParameterType.ResolveGenericParameters(previousPartTypeRef).ResolveCached(context.Cache)?.IsEnum == true
728+
&& pd.GetMethod.IsPublic, out indexerDeclTypeRef);
723729

724730
properties.Add((indexer, indexerDeclTypeRef, indexArg));
725731
if (indexer != null) //the case when we index on an array, not a list
726732
{
727733
var indexType = indexer.GetMethod.Parameters[0].ParameterType.ResolveGenericParameters(indexerDeclTypeRef);
728-
if (!TypeRefComparer.Default.Equals(indexType, module.TypeSystem.String) && !TypeRefComparer.Default.Equals(indexType, module.TypeSystem.Int32))
734+
var indexTypeDef = indexType.ResolveCached(context.Cache);
735+
if (!TypeRefComparer.Default.Equals(indexType, module.TypeSystem.String)
736+
&& !TypeRefComparer.Default.Equals(indexType, module.TypeSystem.Int32)
737+
&& indexTypeDef?.IsEnum != true)
729738
throw new BuildException(BindingIndexerTypeUnsupported, lineInfo, null, indexType.FullName);
730739
previousPartTypeRef = indexer.PropertyType.ResolveGenericParameters(indexerDeclTypeRef);
731740
}
@@ -743,7 +752,7 @@ static bool TryParsePath(ILContext context, string path, TypeReference tSourceRe
743752
return true;
744753
}
745754

746-
static IEnumerable<Instruction> DigProperties(IEnumerable<(PropertyDefinition property, TypeReference propDeclTypeRef, string indexArg)> properties, Dictionary<TypeReference, VariableDefinition> locs, Func<Instruction> fallback, IXmlLineInfo lineInfo, ModuleDefinition module)
755+
static IEnumerable<Instruction> DigProperties(IEnumerable<(PropertyDefinition property, TypeReference propDeclTypeRef, string indexArg)> properties, Dictionary<TypeReference, VariableDefinition> locs, Func<Instruction> fallback, IXmlLineInfo lineInfo, ModuleDefinition module, XamlCache cache = null)
747756
{
748757
var first = true;
749758

@@ -781,7 +790,25 @@ static IEnumerable<Instruction> DigProperties(IEnumerable<(PropertyDefinition pr
781790
else if (TypeRefComparer.Default.Equals(indexType, module.TypeSystem.Int32) && int.TryParse(indexArg, out index))
782791
yield return Create(Ldc_I4, index);
783792
else
784-
throw new BuildException(BindingIndexerParse, lineInfo, null, indexArg, property.Name);
793+
{
794+
// Try to handle enum types
795+
var indexTypeDef = cache != null ? indexType.ResolveCached(cache) : indexType.Resolve();
796+
if (indexTypeDef?.IsEnum == true)
797+
{
798+
// Find the enum field with the matching name
799+
var enumField = indexTypeDef.Fields.FirstOrDefault(f => f.IsStatic && f.Name == indexArg);
800+
if (enumField != null)
801+
{
802+
// Load the enum value as an integer constant
803+
var enumValue = Convert.ToInt32(enumField.Constant);
804+
yield return Create(Ldc_I4, enumValue);
805+
}
806+
else
807+
throw new BuildException(BindingIndexerParse, lineInfo, null, indexArg, property.Name);
808+
}
809+
else
810+
throw new BuildException(BindingIndexerParse, lineInfo, null, indexArg, property.Name);
811+
}
785812
}
786813
}
787814

@@ -860,7 +887,7 @@ static IEnumerable<Instruction> CompiledBindingGetGetter(TypeReference tSourceRe
860887
pop = Create(Pop);
861888

862889
return pop;
863-
}, node as IXmlLineInfo, module));
890+
}, node as IXmlLineInfo, module, context.Cache));
864891

865892
foreach (var loc in locs.Values)
866893
getter.Body.Variables.Add(loc);
@@ -950,7 +977,7 @@ static IEnumerable<Instruction> CompiledBindingGetSetter(TypeReference tSourceRe
950977
pop = Create(Pop);
951978

952979
return pop;
953-
}, node as IXmlLineInfo, module));
980+
}, node as IXmlLineInfo, module, context.Cache));
954981

955982
foreach (var loc in locs.Values)
956983
setter.Body.Variables.Add(loc);
@@ -1076,7 +1103,7 @@ static IEnumerable<Instruction> CompiledBindingGetHandlers(TypeReference tSource
10761103
il.Emit(Ldarg_0);
10771104
var lastGetterTypeRef = properties[i - 1].property?.PropertyType;
10781105
var locs = new Dictionary<TypeReference, VariableDefinition>();
1079-
il.Append(DigProperties(properties.Take(i), locs, null, node as IXmlLineInfo, module));
1106+
il.Append(DigProperties(properties.Take(i), locs, null, node as IXmlLineInfo, module, context.Cache));
10801107
foreach (var loc in locs.Values)
10811108
partGetter.Body.Variables.Add(loc);
10821109
if (lastGetterTypeRef != null && lastGetterTypeRef.IsValueType)

src/Controls/src/SourceGen/CompiledBindingMarkup.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,8 +378,30 @@ bool TryParsePath(
378378
&& property.Parameters.Length == 1
379379
&& property.Parameters[0].Type.SpecialType == SpecialType.System_Object);
380380

381+
// Try to find an indexer with an enum parameter type
382+
indexer ??= previousPartType
383+
.GetAllProperties(indexerName, _context)
384+
.FirstOrDefault(property =>
385+
property.GetMethod != null
386+
&& !property.GetMethod.IsStatic
387+
&& property.Parameters.Length == 1
388+
&& property.Parameters[0].Type.TypeKind == TypeKind.Enum);
389+
381390
if (indexer is not null)
382391
{
392+
// If the indexer parameter is an enum, use the fully qualified enum member
393+
if (indexer.Parameters[0].Type.TypeKind == TypeKind.Enum)
394+
{
395+
var enumType = indexer.Parameters[0].Type;
396+
var enumMember = enumType.GetMembers()
397+
.OfType<IFieldSymbol>()
398+
.FirstOrDefault(f => f.IsStatic && f.Name == indexArg);
399+
if (enumMember != null)
400+
{
401+
index = $"{enumType.ToFQDisplayString()}.{indexArg}";
402+
}
403+
}
404+
383405
var indexAccess = new IndexAccess(indexerName, index, indexer.Type.IsValueType);
384406
bindingPathParts.Add(indexAccess);
385407

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
using System.Linq;
2+
using Microsoft.CodeAnalysis;
3+
using Microsoft.CodeAnalysis.CSharp;
4+
using Microsoft.Maui.Controls.SourceGen;
5+
using Xunit;
6+
7+
using static Microsoft.Maui.Controls.Xaml.UnitTests.SourceGen.SourceGeneratorDriver;
8+
9+
namespace Microsoft.Maui.Controls.Xaml.UnitTests.SourceGen;
10+
11+
public class Maui13856Tests : SourceGenTestsBase
12+
{
13+
private record AdditionalXamlFile(string Path, string Content, string? RelativePath = null, string? TargetPath = null, string? ManifestResourceName = null, string? TargetFramework = null, string? NoWarn = null)
14+
: AdditionalFile(Text: SourceGeneratorDriver.ToAdditionalText(Path, Content), Kind: "Xaml", RelativePath: RelativePath ?? Path, TargetPath: TargetPath, ManifestResourceName: ManifestResourceName, TargetFramework: TargetFramework, NoWarn: NoWarn);
15+
16+
[Fact]
17+
public void DictionaryWithEnumKeyBindingDoesNotCauseErrors()
18+
{
19+
// https://github.com/dotnet/maui/issues/13856
20+
// Binding to Dictionary<CustomEnum, object> with x:DataType should not cause generator errors
21+
// Note: SourceGen currently falls back to runtime binding for dictionary indexers (both string and enum keys)
22+
// This test verifies that enum key bindings don't cause errors in the generator
23+
24+
var codeBehind =
25+
"""
26+
using System.Collections.Generic;
27+
using Microsoft.Maui.Controls;
28+
29+
namespace Test
30+
{
31+
public enum UserSetting
32+
{
33+
BrowserInvisible,
34+
GlobalWaitForElementsInBrowserInSek,
35+
TBD,
36+
}
37+
38+
public partial class TestPage : ContentPage
39+
{
40+
public TestPage()
41+
{
42+
UserSettings = new Dictionary<UserSetting, object>
43+
{
44+
{ UserSetting.TBD, "test value" }
45+
};
46+
InitializeComponent();
47+
}
48+
49+
public Dictionary<UserSetting, object> UserSettings { get; set; }
50+
}
51+
}
52+
""";
53+
54+
var xaml =
55+
"""
56+
<?xml version="1.0" encoding="UTF-8"?>
57+
<ContentPage
58+
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
59+
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
60+
xmlns:local="clr-namespace:Test"
61+
x:Class="Test.TestPage"
62+
x:DataType="local:TestPage">
63+
<Entry x:Name="entry" Text="{Binding UserSettings[TBD]}" />
64+
</ContentPage>
65+
""";
66+
67+
var compilation = CreateMauiCompilation();
68+
compilation = compilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(codeBehind));
69+
70+
var result = RunGenerator<XamlGenerator>(compilation, new AdditionalXamlFile("Test.xaml", xaml));
71+
72+
// The generator should not produce any errors
73+
var errors = result.Diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error).ToList();
74+
Assert.Empty(errors);
75+
76+
// Verify that a source file was generated
77+
var generatedSource = result.Results
78+
.SelectMany(r => r.GeneratedSources)
79+
.FirstOrDefault(s => s.HintName.Contains("xsg.cs", System.StringComparison.Ordinal));
80+
81+
Assert.True(generatedSource.SourceText != null, "Expected generated source file with xsg.cs extension");
82+
var generatedCode = generatedSource.SourceText.ToString();
83+
84+
// Verify the binding path is in the generated code (even if using runtime binding fallback)
85+
Assert.Contains("UserSettings[TBD]", generatedCode, System.StringComparison.Ordinal);
86+
}
87+
}

src/Controls/tests/SourceGen.UnitTests/Maui32879Tests.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ public TestPage()
6060
// </auto-generated>
6161
//------------------------------------------------------------------------------
6262
#nullable enable
63+
#pragma warning disable CS0219 // Variable is assigned but its value is never used
6364
6465
namespace Test;
6566
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<ContentPage
3+
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
4+
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
5+
xmlns:local="using:Microsoft.Maui.Controls.Xaml.UnitTests"
6+
x:Class="Microsoft.Maui.Controls.Xaml.UnitTests.Maui13856"
7+
x:DataType="local:Maui13856">
8+
<Entry x:Name="entry" Text="{Binding UserSettings[TBD]}" />
9+
</ContentPage>
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
using System.Collections.Generic;
2+
using NUnit.Framework;
3+
4+
namespace Microsoft.Maui.Controls.Xaml.UnitTests;
5+
6+
public enum Maui13856UserSetting
7+
{
8+
BrowserInvisible,
9+
GlobalWaitForElementsInBrowserInSek,
10+
TBD,
11+
}
12+
13+
public partial class Maui13856 : ContentPage
14+
{
15+
public Maui13856()
16+
{
17+
UserSettings = new Dictionary<Maui13856UserSetting, object>
18+
{
19+
{ Maui13856UserSetting.TBD, "test value" }
20+
};
21+
22+
InitializeComponent();
23+
}
24+
25+
public Dictionary<Maui13856UserSetting, object> UserSettings { get; set; }
26+
27+
[TestFixture]
28+
class Tests
29+
{
30+
[Test]
31+
public void DictionaryWithEnumKeyBindingXamlC()
32+
{
33+
// https://github.com/dotnet/maui/issues/13856
34+
// XamlC should not throw NullReferenceException for Dictionary<CustomEnum, object> indexer binding
35+
MockCompiler.Compile(typeof(Maui13856), out var md, out var hasLoggedErrors);
36+
Assert.That(hasLoggedErrors, Is.False, "XamlC should compile without errors");
37+
}
38+
}
39+
}

0 commit comments

Comments
 (0)