From 4823acd0e72a5ed2b05b07265f6e06d28a3e25ea Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 5 Nov 2025 14:16:25 +0100 Subject: [PATCH 1/4] Add test --- .../InitializeComponent/CompiledBindings.cs | 175 ++++++++++++++++++ 1 file changed, 175 insertions(+) diff --git a/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/CompiledBindings.cs b/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/CompiledBindings.cs index 2fff5969530d..2829f4726005 100644 --- a/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/CompiledBindings.cs +++ b/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/CompiledBindings.cs @@ -181,6 +181,181 @@ static bool ShouldUseSetter(global::Microsoft.Maui.Controls.BindingMode mode) } } +"""; + + var (result, generated) = RunGenerator(xaml, code); + Assert.False(result.Diagnostics.Any()); + TestAssertions.AssertEqualIgnoringLineEndings(expected, generated); + } + + [Fact] + public void CorrectlyDetectsNullableTypes() + { + var xaml = +""" + + +"""; + + var code = +""" +#nullable enable +using System; +using Microsoft.Maui.Controls; +using Microsoft.Maui.Controls.Xaml; + +namespace Test; + +[XamlProcessing(XamlInflator.SourceGen)] +public partial class TestPage : ContentPage +{ + public Product? Product { get; set; } = null; + + public TestPage() + { + InitializeComponent(); + } +} + +public class Product +{ + public int Size { get; set; } +} +"""; + var testXamlFilePath = Path.Combine(Environment.CurrentDirectory, "Test.xaml"); + var expected = $$""" +//------------------------------------------------------------------------------ +// +// 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. +// +//------------------------------------------------------------------------------ +#nullable enable + +namespace Test; + +[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(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::Test.TestPage).Assembly.GetName(), + ResourcePath = getPathForType(typeof(global::Test.TestPage)), + Instance = this, + }); + + if (rlr?.ResourceContent != null) + { + this.InitializeComponentRuntime(); + return; + } + + var bindingExtension = new global::Microsoft.Maui.Controls.Xaml.BindingExtension(); + global::Microsoft.Maui.VisualDiagnostics.RegisterSourceInfo(bindingExtension!, new global::System.Uri(@"Test.xaml;assembly=SourceGeneratorDriver.Generated", global::System.UriKind.Relative), 8, 5); + 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 +#line 8 "{{testXamlFilePath}}" + bindingExtension.Path = "Product.Size"; +#line default + var bindingBase = CreateTypedBindingFrom_bindingExtension(bindingExtension); + if (global::Microsoft.Maui.VisualDiagnostics.GetSourceInfo(bindingBase!) == null) + global::Microsoft.Maui.VisualDiagnostics.RegisterSourceInfo(bindingBase!, new global::System.Uri(@"Test.xaml;assembly=SourceGeneratorDriver.Generated", global::System.UriKind.Relative), 8, 5); + __root.SetBinding(global::Microsoft.Maui.Controls.Page.TitleProperty, bindingBase); + static global::Microsoft.Maui.Controls.BindingBase CreateTypedBindingFrom_bindingExtension(global::Microsoft.Maui.Controls.Xaml.BindingExtension extension) +{ + return Create( + getter: static source => source.Product?.Size ?? extension.TargetNullValue as int? ?? default, + extension.Mode, + extension.Converter, + extension.ConverterParameter, + extension.StringFormat, + extension.Source, + extension.FallbackValue, + extension.TargetNullValue); + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Maui.Controls.BindingSourceGen, Version=10.0.0.0, Culture=neutral, PublicKeyToken=null", "10.0.0.0")] + static global::Microsoft.Maui.Controls.BindingBase Create( + global::System.Func getter, + global::Microsoft.Maui.Controls.BindingMode mode = global::Microsoft.Maui.Controls.BindingMode.Default, + global::Microsoft.Maui.Controls.IValueConverter? converter = null, + object? converterParameter = null, + string? stringFormat = null, + object? source = null, + object? fallbackValue = null, + object? targetNullValue = null) + { + global::System.Action? setter = null; + if (ShouldUseSetter(mode)) + { + setter = static (source, value) => + { + if (source.Product is {} p0) + { + p0.Size = value; + } + }; + } + + var binding = new global::Microsoft.Maui.Controls.Internals.TypedBinding( + getter: source => (getter(source), true), + setter, + handlers: new global::System.Tuple, string>[] + { + new(static source => source, "Product"), + new(static source => source.Product, "Size"), + }) + { + Mode = mode, + Converter = converter, + ConverterParameter = converterParameter, + StringFormat = stringFormat, + Source = source, + FallbackValue = fallbackValue, + TargetNullValue = targetNullValue + }; + + return binding; + } + + + [global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + static bool ShouldUseSetter(global::Microsoft.Maui.Controls.BindingMode mode) + => mode == global::Microsoft.Maui.Controls.BindingMode.OneWayToSource + || mode == global::Microsoft.Maui.Controls.BindingMode.TwoWay + || mode == global::Microsoft.Maui.Controls.BindingMode.Default; +} + + } +} + """; var (result, generated) = RunGenerator(xaml, code); From 3c99ed223dd08c90da5f755f84e535e912a627ec Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 5 Nov 2025 14:17:05 +0100 Subject: [PATCH 2/4] Generate unwrapping to return non-nullable value types --- .../src/SourceGen/CompiledBindingMarkup.cs | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/Controls/src/SourceGen/CompiledBindingMarkup.cs b/src/Controls/src/SourceGen/CompiledBindingMarkup.cs index bc7f6c505f79..c8fc106a21f6 100644 --- a/src/Controls/src/SourceGen/CompiledBindingMarkup.cs +++ b/src/Controls/src/SourceGen/CompiledBindingMarkup.cs @@ -84,7 +84,7 @@ public bool TryCompileBinding(ITypeSymbol sourceType, bool isTemplateBinding, ou static global::Microsoft.Maui.Controls.BindingBase {{methodName}}({{extensionTypeName}} extension) { return Create( - getter: {{GenerateGetterLambda(binding.Path)}}, + getter: {{GenerateGetterLambda(binding, targetNullValueExpression: isTemplateBinding ? null : "extension.TargetNullValue")}}, extension.Mode, extension.Converter, extension.ConverterParameter, @@ -278,17 +278,32 @@ bool TryParsePath( return true; } - string GenerateGetterLambda(EquatableArray bindingPath) + string GenerateGetterLambda(BindingInvocationDescription binding, string? targetNullValueExpression) { string expression = "source"; bool forceConditionalAccessToNextPart = false; + bool hasConditionalAccess = false; - foreach (var part in bindingPath) + foreach (var part in binding.Path) { // Note: AccessExpressionBuilder will happily call unsafe accessors and it expects them to be available. // By calling BindingCodeWriter.GenerateBindingMethod(...), we are ensuring that the unsafe accessors are available. expression = AccessExpressionBuilder.ExtendExpression(expression, MaybeWrapInConditionalAccess(part, forceConditionalAccessToNextPart)); forceConditionalAccessToNextPart = part is Cast; + hasConditionalAccess |= forceConditionalAccessToNextPart || part is ConditionalAccess; + } + + if (hasConditionalAccess && binding.PropertyType is { IsValueType: true, IsNullable: false }) + { + // for non-nullable value types with conditional access in the path, we need to unwrap the getter result + // with fallback to either the target null value or default + if (targetNullValueExpression is not null) + { + var nullablePropertyType = binding.PropertyType with { IsNullable = true }; + expression = $"{expression} ?? {targetNullValueExpression} as {nullablePropertyType}"; + } + + expression = $"{expression} ?? default"; } return $"static source => {expression}"; From c9421dd2f8f1efad96c046951c90740c1261736b Mon Sep 17 00:00:00 2001 From: Stephane Delcroix Date: Wed, 5 Nov 2025 20:51:20 +0100 Subject: [PATCH 3/4] Apply suggestion from @StephaneDelcroix --- .../SourceGen.UnitTests/InitializeComponent/CompiledBindings.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/CompiledBindings.cs b/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/CompiledBindings.cs index 2829f4726005..e6a2319ec984 100644 --- a/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/CompiledBindings.cs +++ b/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/CompiledBindings.cs @@ -185,7 +185,7 @@ static bool ShouldUseSetter(global::Microsoft.Maui.Controls.BindingMode mode) var (result, generated) = RunGenerator(xaml, code); Assert.False(result.Diagnostics.Any()); - TestAssertions.AssertEqualIgnoringLineEndings(expected, generated); + Assert.Equal(expected, generated, ignoreLineEndingDifferences: true); } [Fact] From d3019809201115334e0e98b82c80b97d85725d81 Mon Sep 17 00:00:00 2001 From: Stephane Delcroix Date: Wed, 5 Nov 2025 20:51:34 +0100 Subject: [PATCH 4/4] Apply suggestion from @StephaneDelcroix --- .../SourceGen.UnitTests/InitializeComponent/CompiledBindings.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/CompiledBindings.cs b/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/CompiledBindings.cs index e6a2319ec984..920d29e087ca 100644 --- a/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/CompiledBindings.cs +++ b/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/CompiledBindings.cs @@ -360,6 +360,6 @@ static bool ShouldUseSetter(global::Microsoft.Maui.Controls.BindingMode mode) var (result, generated) = RunGenerator(xaml, code); Assert.False(result.Diagnostics.Any()); - TestAssertions.AssertEqualIgnoringLineEndings(expected, generated); + Assert.Equal(expected, generated, ignoreLineEndingDifferences: true); } }