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
21 changes: 18 additions & 3 deletions src/Controls/src/SourceGen/CompiledBindingMarkup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -278,17 +278,32 @@ bool TryParsePath(
return true;
}

string GenerateGetterLambda(EquatableArray<IPathPart> 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}";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,181 @@ static bool ShouldUseSetter(global::Microsoft.Maui.Controls.BindingMode mode)
}
}

""";

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

[Fact]
public void CorrectlyDetectsNullableTypes()
{
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:test="clr-namespace:Test"
x:Class="Test.TestPage"
x:DataType="test:TestPage"
Title="{Binding Product.Size}"/>
""";

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 = $$"""
//------------------------------------------------------------------------------
// <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 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<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::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<global::Test.TestPage, int> 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<global::Test.TestPage, int>? 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<global::Test.TestPage, int>(
getter: source => (getter(source), true),
setter,
handlers: new global::System.Tuple<global::System.Func<global::Test.TestPage, object?>, 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);
Expand Down
Loading