Skip to content

Commit c9d5028

Browse files
[XSG] Generate unwrapping for compiled bindings with conditional access to non-nullable value types (#32402)
* Add test * Generate unwrapping to return non-nullable value types * Apply suggestion from @StephaneDelcroix * Apply suggestion from @StephaneDelcroix --------- Co-authored-by: Stephane Delcroix <[email protected]>
1 parent 28b0aba commit c9d5028

File tree

2 files changed

+193
-3
lines changed

2 files changed

+193
-3
lines changed

src/Controls/src/SourceGen/CompiledBindingMarkup.cs

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ public bool TryCompileBinding(ITypeSymbol sourceType, bool isTemplateBinding, ou
8484
static global::Microsoft.Maui.Controls.BindingBase {{methodName}}({{extensionTypeName}} extension)
8585
{
8686
return Create(
87-
getter: {{GenerateGetterLambda(binding.Path)}},
87+
getter: {{GenerateGetterLambda(binding, targetNullValueExpression: isTemplateBinding ? null : "extension.TargetNullValue")}},
8888
extension.Mode,
8989
extension.Converter,
9090
extension.ConverterParameter,
@@ -278,17 +278,32 @@ bool TryParsePath(
278278
return true;
279279
}
280280

281-
string GenerateGetterLambda(EquatableArray<IPathPart> bindingPath)
281+
string GenerateGetterLambda(BindingInvocationDescription binding, string? targetNullValueExpression)
282282
{
283283
string expression = "source";
284284
bool forceConditionalAccessToNextPart = false;
285+
bool hasConditionalAccess = false;
285286

286-
foreach (var part in bindingPath)
287+
foreach (var part in binding.Path)
287288
{
288289
// Note: AccessExpressionBuilder will happily call unsafe accessors and it expects them to be available.
289290
// By calling BindingCodeWriter.GenerateBindingMethod(...), we are ensuring that the unsafe accessors are available.
290291
expression = AccessExpressionBuilder.ExtendExpression(expression, MaybeWrapInConditionalAccess(part, forceConditionalAccessToNextPart));
291292
forceConditionalAccessToNextPart = part is Cast;
293+
hasConditionalAccess |= forceConditionalAccessToNextPart || part is ConditionalAccess;
294+
}
295+
296+
if (hasConditionalAccess && binding.PropertyType is { IsValueType: true, IsNullable: false })
297+
{
298+
// for non-nullable value types with conditional access in the path, we need to unwrap the getter result
299+
// with fallback to either the target null value or default
300+
if (targetNullValueExpression is not null)
301+
{
302+
var nullablePropertyType = binding.PropertyType with { IsNullable = true };
303+
expression = $"{expression} ?? {targetNullValueExpression} as {nullablePropertyType}";
304+
}
305+
306+
expression = $"{expression} ?? default";
292307
}
293308

294309
return $"static source => {expression}";

src/Controls/tests/SourceGen.UnitTests/InitializeComponent/CompiledBindings.cs

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,181 @@ static bool ShouldUseSetter(global::Microsoft.Maui.Controls.BindingMode mode)
181181
}
182182
}
183183
184+
""";
185+
186+
var (result, generated) = RunGenerator(xaml, code);
187+
Assert.False(result.Diagnostics.Any());
188+
Assert.Equal(expected, generated, ignoreLineEndingDifferences: true);
189+
}
190+
191+
[Fact]
192+
public void CorrectlyDetectsNullableTypes()
193+
{
194+
var xaml =
195+
"""
196+
<?xml version="1.0" encoding="UTF-8"?>
197+
<ContentPage
198+
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
199+
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
200+
xmlns:test="clr-namespace:Test"
201+
x:Class="Test.TestPage"
202+
x:DataType="test:TestPage"
203+
Title="{Binding Product.Size}"/>
204+
""";
205+
206+
var code =
207+
"""
208+
#nullable enable
209+
using System;
210+
using Microsoft.Maui.Controls;
211+
using Microsoft.Maui.Controls.Xaml;
212+
213+
namespace Test;
214+
215+
[XamlProcessing(XamlInflator.SourceGen)]
216+
public partial class TestPage : ContentPage
217+
{
218+
public Product? Product { get; set; } = null;
219+
220+
public TestPage()
221+
{
222+
InitializeComponent();
223+
}
224+
}
225+
226+
public class Product
227+
{
228+
public int Size { get; set; }
229+
}
230+
""";
231+
var testXamlFilePath = Path.Combine(Environment.CurrentDirectory, "Test.xaml");
232+
var expected = $$"""
233+
//------------------------------------------------------------------------------
234+
// <auto-generated>
235+
// This code was generated by a .NET MAUI source generator.
236+
//
237+
// Changes to this file may cause incorrect behavior and will be lost if
238+
// the code is regenerated.
239+
// </auto-generated>
240+
//------------------------------------------------------------------------------
241+
#nullable enable
242+
243+
namespace Test;
244+
245+
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Maui.Controls.SourceGen, Version=10.0.0.0, Culture=neutral, PublicKeyToken=null", "10.0.0.0")]
246+
public partial class TestPage
247+
{
248+
private partial void InitializeComponent()
249+
{
250+
// Fallback to Runtime inflation if the page was updated by HotReload
251+
static string? getPathForType(global::System.Type type)
252+
{
253+
var assembly = type.Assembly;
254+
foreach (var xria in global::System.Reflection.CustomAttributeExtensions.GetCustomAttributes<global::Microsoft.Maui.Controls.Xaml.XamlResourceIdAttribute>(assembly))
255+
{
256+
if (xria.Type == type)
257+
return xria.Path;
258+
}
259+
return null;
260+
}
261+
262+
var rlr = global::Microsoft.Maui.Controls.Internals.ResourceLoader.ResourceProvider2?.Invoke(new global::Microsoft.Maui.Controls.Internals.ResourceLoader.ResourceLoadingQuery
263+
{
264+
AssemblyName = typeof(global::Test.TestPage).Assembly.GetName(),
265+
ResourcePath = getPathForType(typeof(global::Test.TestPage)),
266+
Instance = this,
267+
});
268+
269+
if (rlr?.ResourceContent != null)
270+
{
271+
this.InitializeComponentRuntime();
272+
return;
273+
}
274+
275+
var bindingExtension = new global::Microsoft.Maui.Controls.Xaml.BindingExtension();
276+
global::Microsoft.Maui.VisualDiagnostics.RegisterSourceInfo(bindingExtension!, new global::System.Uri(@"Test.xaml;assembly=SourceGeneratorDriver.Generated", global::System.UriKind.Relative), 8, 5);
277+
var __root = this;
278+
global::Microsoft.Maui.VisualDiagnostics.RegisterSourceInfo(__root!, new global::System.Uri(@"Test.xaml;assembly=SourceGeneratorDriver.Generated", global::System.UriKind.Relative), 2, 2);
279+
#if !_MAUIXAML_SG_NAMESCOPE_DISABLE
280+
global::Microsoft.Maui.Controls.Internals.INameScope iNameScope = global::Microsoft.Maui.Controls.Internals.NameScope.GetNameScope(__root) ?? new global::Microsoft.Maui.Controls.Internals.NameScope();
281+
#endif
282+
#if !_MAUIXAML_SG_NAMESCOPE_DISABLE
283+
global::Microsoft.Maui.Controls.Internals.NameScope.SetNameScope(__root, iNameScope);
284+
#endif
285+
#line 8 "{{testXamlFilePath}}"
286+
bindingExtension.Path = "Product.Size";
287+
#line default
288+
var bindingBase = CreateTypedBindingFrom_bindingExtension(bindingExtension);
289+
if (global::Microsoft.Maui.VisualDiagnostics.GetSourceInfo(bindingBase!) == null)
290+
global::Microsoft.Maui.VisualDiagnostics.RegisterSourceInfo(bindingBase!, new global::System.Uri(@"Test.xaml;assembly=SourceGeneratorDriver.Generated", global::System.UriKind.Relative), 8, 5);
291+
__root.SetBinding(global::Microsoft.Maui.Controls.Page.TitleProperty, bindingBase);
292+
static global::Microsoft.Maui.Controls.BindingBase CreateTypedBindingFrom_bindingExtension(global::Microsoft.Maui.Controls.Xaml.BindingExtension extension)
293+
{
294+
return Create(
295+
getter: static source => source.Product?.Size ?? extension.TargetNullValue as int? ?? default,
296+
extension.Mode,
297+
extension.Converter,
298+
extension.ConverterParameter,
299+
extension.StringFormat,
300+
extension.Source,
301+
extension.FallbackValue,
302+
extension.TargetNullValue);
303+
304+
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Maui.Controls.BindingSourceGen, Version=10.0.0.0, Culture=neutral, PublicKeyToken=null", "10.0.0.0")]
305+
static global::Microsoft.Maui.Controls.BindingBase Create(
306+
global::System.Func<global::Test.TestPage, int> getter,
307+
global::Microsoft.Maui.Controls.BindingMode mode = global::Microsoft.Maui.Controls.BindingMode.Default,
308+
global::Microsoft.Maui.Controls.IValueConverter? converter = null,
309+
object? converterParameter = null,
310+
string? stringFormat = null,
311+
object? source = null,
312+
object? fallbackValue = null,
313+
object? targetNullValue = null)
314+
{
315+
global::System.Action<global::Test.TestPage, int>? setter = null;
316+
if (ShouldUseSetter(mode))
317+
{
318+
setter = static (source, value) =>
319+
{
320+
if (source.Product is {} p0)
321+
{
322+
p0.Size = value;
323+
}
324+
};
325+
}
326+
327+
var binding = new global::Microsoft.Maui.Controls.Internals.TypedBinding<global::Test.TestPage, int>(
328+
getter: source => (getter(source), true),
329+
setter,
330+
handlers: new global::System.Tuple<global::System.Func<global::Test.TestPage, object?>, string>[]
331+
{
332+
new(static source => source, "Product"),
333+
new(static source => source.Product, "Size"),
334+
})
335+
{
336+
Mode = mode,
337+
Converter = converter,
338+
ConverterParameter = converterParameter,
339+
StringFormat = stringFormat,
340+
Source = source,
341+
FallbackValue = fallbackValue,
342+
TargetNullValue = targetNullValue
343+
};
344+
345+
return binding;
346+
}
347+
348+
349+
[global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
350+
static bool ShouldUseSetter(global::Microsoft.Maui.Controls.BindingMode mode)
351+
=> mode == global::Microsoft.Maui.Controls.BindingMode.OneWayToSource
352+
|| mode == global::Microsoft.Maui.Controls.BindingMode.TwoWay
353+
|| mode == global::Microsoft.Maui.Controls.BindingMode.Default;
354+
}
355+
356+
}
357+
}
358+
184359
""";
185360

186361
var (result, generated) = RunGenerator(xaml, code);

0 commit comments

Comments
 (0)