Skip to content

Commit

Permalink
Enable Binding inteceptors source generator by default (#22856)
Browse files Browse the repository at this point in the history
* Enable the BindingSourceGen analyzer by default

* Improve filtering of SetBinding overloads

* Add feature switch documentation

* Revisit feature switch name

* Update docs/design/FeatureSwitches.md

Co-authored-by: Jonathan Peppers <[email protected]>

---------

Co-authored-by: Jonathan Peppers <[email protected]>
  • Loading branch information
simonrozsival and jonathanpeppers authored Jun 24, 2024
1 parent 5b6db39 commit 27a83b7
Show file tree
Hide file tree
Showing 7 changed files with 100 additions and 15 deletions.
29 changes: 29 additions & 0 deletions docs/design/FeatureSwitches.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ The following switches are toggled for applications running on Mono for `TrimMod
| MauiShellSearchResultsRendererDisplayMemberNameSupported | Microsoft.Maui.RuntimeFeature.IsShellSearchResultsRendererDisplayMemberNameSupported | When disabled, it is necessary to always set `ItemTemplate` of any `SearchHandler`. Displaying search results through `DisplayMemberName` will not work. |
| MauiQueryPropertyAttributeSupport | Microsoft.Maui.RuntimeFeature.IsQueryPropertyAttributeSupported | When disabled, the `[QueryProperty(...)]` attributes won't be used to set values to properties when navigating. |
| MauiImplicitCastOperatorsUsageViaReflectionSupport | Microsoft.Maui.RuntimeFeature.IsImplicitCastOperatorsUsageViaReflectionSupported | When disabled, MAUI won't look for implicit cast operators when converting values from one type to another. This feature is not trim-compatible. |
| _MauiBindingInterceptorsSupport | Microsoft.Maui.RuntimeFeature.AreBindingInterceptorsSupported | When disabled, MAUI won't intercept any calls to `SetBinding` methods and try to compile them. Enabled by default. |

## MauiEnableIVisualAssemblyScanning

Expand All @@ -32,3 +33,31 @@ When disabled, MAUI won't look for implicit cast operators when converting value
If your library or your app defines an implicit operator on a type that can be used in one of the previous scenarios, you should define a custom `TypeConverter` for your type and attach it to the type using the `[TypeConverter(typeof(MyTypeConverter))]` attribute.

_Note: Prefer using the `TypeConverterAttribute` as it can help the trimmer achieve better binary size in certain scenarios._

## _MauiBindingInterceptorsSupport

When enabled, MAUI will enable a source generator which will identify calls to the `SetBinding<TSource, TProperty>(this BindableObject target, BindableProperty property, Func<TSource, TProperty> getter, ...)` methods and generate optimized bindings based on the lambda expression passed as the `getter` parameter.

This feature is a counterpart of [XAML Compiled bindings](https://learn.microsoft.com/dotnet/maui/fundamentals/data-binding/compiled-bindings).

It is necessary to use this feature instead of the string-based bindings in NativeAOT apps and in apps with full trimming enabled.

### Example use-case

String-based binding in code:
```c#
label.BindingContext = new PageViewModel { Customer = new CustomerViewModel { Name = "John" } };
label.SetBinding(Label.TextProperty, "Customer.Name");
```

Compiled binding in code:
```csharp
label.SetBinding<PageViewModel, string>(Label.TextProperty, static vm => vm.Customer.Name);
// or with type inference:
label.SetBinding(Label.TextProperty, static (PageViewModel vm) => vm.Customer.Name);
```

Compiled binding in XAML:
```xml
<Label Text="{Binding Customer.Name}" x:DataType="local:PageViewModel" />
```
25 changes: 10 additions & 15 deletions src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ private static Result<SetBindingInvocationDescription> GetBindingForGeneration(G
return Result<SetBindingInvocationDescription>.Failure(DiagnosticsFactory.UnableToResolvePath(invocation.GetLocation()));
}

var overloadDiagnostics = VerifyCorrectOverload(invocation, context, t);
var overloadDiagnostics = new EquatableArray<DiagnosticInfo>(VerifyCorrectOverload(invocation, context, t));
if (overloadDiagnostics.Length > 0)
{
return Result<SetBindingInvocationDescription>.Failure(overloadDiagnostics);
Expand Down Expand Up @@ -121,31 +121,26 @@ private static bool IsNullableContextEnabled(GeneratorSyntaxContext context)
return (nullableContext & NullableContext.Enabled) == NullableContext.Enabled;
}

private static EquatableArray<DiagnosticInfo> VerifyCorrectOverload(InvocationExpressionSyntax invocation, GeneratorSyntaxContext context, CancellationToken t)
private static DiagnosticInfo[] VerifyCorrectOverload(InvocationExpressionSyntax invocation, GeneratorSyntaxContext context, CancellationToken t)
{
var argumentList = invocation.ArgumentList.Arguments;

if (argumentList.Count < 2)
{
throw new ArgumentOutOfRangeException(nameof(invocation));
}

var secondArgument = argumentList[1].Expression;

if (secondArgument is IdentifierNameSyntax)
if (secondArgument is LambdaExpressionSyntax)
{
var type = context.SemanticModel.GetTypeInfo(secondArgument, cancellationToken: t).Type;
if (type != null && type.Name == "Func")
{
return new EquatableArray<DiagnosticInfo>([DiagnosticsFactory.GetterIsNotLambda(secondArgument.GetLocation())]);
}
else // String and Binding
{
return new EquatableArray<DiagnosticInfo>([DiagnosticsFactory.SuboptimalSetBindingOverload(secondArgument.GetLocation())]);
}
return [];
}

return [];
var secondArgumentType = context.SemanticModel.GetTypeInfo(secondArgument, cancellationToken: t).Type;
return secondArgumentType switch
{
{ Name: "Func", ContainingNamespace.Name: "System" } => [DiagnosticsFactory.GetterIsNotLambda(secondArgument.GetLocation())],
_ => [DiagnosticsFactory.SuboptimalSetBindingOverload(secondArgument.GetLocation())],
};
}

private static Result<LambdaExpressionSyntax> ExtractLambda(InvocationExpressionSyntax invocation)
Expand Down
3 changes: 3 additions & 0 deletions src/Controls/src/Build.Tasks/Controls.Build.Tasks.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
<ProjectReference Include="..\Core\Controls.Core.csproj" />
<ProjectReference Include="..\Xaml\Controls.Xaml.csproj" />
<ProjectReference Include="..\SourceGen\Controls.SourceGen.csproj" ReferenceOutputAssembly="false" />
<ProjectReference Include="..\BindingSourceGen\Controls.BindingSourceGen.csproj" ReferenceOutputAssembly="false" />
</ItemGroup>

<ItemGroup>
Expand All @@ -49,6 +50,8 @@
<None Include="$(PkgSystem_CodeDom)\lib\netstandard2.0\System.CodeDom.dll" Visible="false" Pack="true" PackagePath="buildTransitive\netstandard2.0" />
<None Include="$(ArtifactsBinDir)Controls.SourceGen\$(Configuration)\netstandard2.0\Microsoft.Maui.Controls.SourceGen.dll" Visible="false" Pack="true" PackagePath="buildTransitive\netstandard2.0" />
<None Include="$(ArtifactsBinDir)Controls.SourceGen\$(Configuration)\netstandard2.0\Microsoft.Maui.Controls.SourceGen.pdb" Visible="false" Pack="true" PackagePath="buildTransitive\netstandard2.0" />
<None Include="$(ArtifactsBinDir)Controls.BindingSourceGen\$(Configuration)\netstandard2.0\Microsoft.Maui.Controls.BindingSourceGen.dll" Visible="false" Pack="true" PackagePath="buildTransitive\netstandard2.0" />
<None Include="$(ArtifactsBinDir)Controls.BindingSourceGen\$(Configuration)\netstandard2.0\Microsoft.Maui.Controls.BindingSourceGen.pdb" Visible="false" Pack="true" PackagePath="buildTransitive\netstandard2.0" />
<None Remove="$(OutputPath)*.xml" />
<None Include="nuget\**" PackagePath="" Pack="true" Exclude="nuget\**\*.aotprofile.txt" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,15 @@
<Analyzer Include="$(MSBuildThisFileDirectory)Microsoft.Maui.Controls.SourceGen.dll" IsImplicitlyDefined="true" />
</ItemGroup>

<!-- Enable BindingSourceGen -->
<PropertyGroup>
<_MauiBindingInterceptorsSupport Condition=" '$(_MauiBindingInterceptorsSupport)' == '' and '$(DisableMauiAnalyzers)' != 'true' ">true</_MauiBindingInterceptorsSupport>
<InterceptorsPreviewNamespaces Condition=" '$(_MauiBindingInterceptorsSupport)' == 'true' ">$(InterceptorsPreviewNamespaces);Microsoft.Maui.Controls.Generated</InterceptorsPreviewNamespaces>
</PropertyGroup>
<ItemGroup Condition=" '$(_MauiBindingInterceptorsSupport)' == 'true' ">
<Analyzer Include="$(MSBuildThisFileDirectory)Microsoft.Maui.Controls.BindingSourceGen.dll" IsImplicitlyDefined="true" />
</ItemGroup>

<ItemGroup Condition="'$(AndroidEnableProfiledAot)' == 'true' and '$(MauiUseDefaultAotProfile)' != 'false'">
<AndroidAotProfile Include="$(MSBuildThisFileDirectory)maui.aotprofile" />
<AndroidAotProfile Include="$(MSBuildThisFileDirectory)maui-blazor.aotprofile" />
Expand Down Expand Up @@ -242,6 +251,10 @@
Condition="'$(MauiImplicitCastOperatorsUsageViaReflectionSupport)' != ''"
Value="$(MauiImplicitCastOperatorsUsageViaReflectionSupport)"
Trim="true" />
<RuntimeHostConfigurationOption Include="Microsoft.Maui.RuntimeFeature.Microsoft.Maui.RuntimeFeature.AreBindingInterceptorsSupported"
Condition="'$(_MauiBindingInterceptorsSupport)' != ''"
Value="$(_MauiBindingInterceptorsSupport)"
Trim="true" />
</ItemGroup>
</Target>

Expand Down
5 changes: 5 additions & 0 deletions src/Controls/src/Core/BindableObjectExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,11 @@ public static void SetBinding<TSource, TProperty>(
object? fallbackValue = null,
object? targetNullValue = null)
{
if (!RuntimeFeature.AreBindingInterceptorsSupported)
{
throw new InvalidOperationException($"Call to SetBinding<{typeof(TSource)}, {typeof(TProperty)}> could not be intercepted because the feature has been disabled. Consider removing the DisableMauiAnalyzers property from your project file or set the _MauiBindingInterceptorsSupport property to true instead.");
}

throw new InvalidOperationException($"Call to SetBinding<{typeof(TSource)}, {typeof(TProperty)}> was not intercepted.");
}
#nullable disable
Expand Down
34 changes: 34 additions & 0 deletions src/Controls/tests/BindingSourceGen.UnitTests/DiagnosticsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,40 @@ public void DoesNotReportWarningWhenUsingOverloadWithStringVariablePath()
Assert.Empty(result.SourceGeneratorDiagnostics);
}

[Fact]
public void DoesNotReportWarningWhenUsingOverloadWithNameofInPath()
{
var source = """
using Microsoft.Maui.Controls;
var label = new Label();
var slider = new Slider();

label.BindingContext = slider;
label.SetBinding(Label.ScaleProperty, nameof(Slider.Value));
""";

var result = SourceGenHelpers.Run(source);
Assert.Empty(result.SourceGeneratorDiagnostics);
}

[Fact]
public void DoesNotReportWarningWhenUsingOverloadWithMethodCallThatReturnsString()
{
var source = """
using Microsoft.Maui.Controls;
var label = new Label();
var slider = new Slider();

label.BindingContext = slider;
label.SetBinding(Label.ScaleProperty, GetPath());

static string GetPath() => "Value";
""";

var result = SourceGenHelpers.Run(source);
Assert.Empty(result.SourceGeneratorDiagnostics);
}

[Fact]
public void ReportsUnableToResolvePathWhenUsingMethodCall()
{
Expand Down
6 changes: 6 additions & 0 deletions src/Core/src/RuntimeFeature.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ internal static class RuntimeFeature
private const bool IsShellSearchResultsRendererDisplayMemberNameSupportedByDefault = true;
private const bool IsQueryPropertyAttributeSupportedByDefault = true;
private const bool IsImplicitCastOperatorsUsageViaReflectionSupportedByDefault = true;
private const bool AreBindingInterceptorsSupportedByDefault = true;

#pragma warning disable IL4000 // Return value does not match FeatureGuardAttribute 'System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute'.
#if !NETSTANDARD
Expand Down Expand Up @@ -55,6 +56,11 @@ internal static bool IsShellSearchResultsRendererDisplayMemberNameSupported
AppContext.TryGetSwitch("Microsoft.Maui.RuntimeFeature.IsImplicitCastOperatorsUsageViaReflectionSupported", out bool isSupported)
? isSupported
: IsImplicitCastOperatorsUsageViaReflectionSupportedByDefault;

internal static bool AreBindingInterceptorsSupported =>
AppContext.TryGetSwitch("Microsoft.Maui.RuntimeFeature.AreBindingInterceptorsSupported", out bool areSupported)
? areSupported
: AreBindingInterceptorsSupportedByDefault;
#pragma warning restore IL4000
}
}

0 comments on commit 27a83b7

Please sign in to comment.