Skip to content

Commit 27a83b7

Browse files
Enable Binding inteceptors source generator by default (#22856)
* 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]>
1 parent 5b6db39 commit 27a83b7

File tree

7 files changed

+100
-15
lines changed

7 files changed

+100
-15
lines changed

docs/design/FeatureSwitches.md

+29
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ The following switches are toggled for applications running on Mono for `TrimMod
1010
| 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. |
1111
| MauiQueryPropertyAttributeSupport | Microsoft.Maui.RuntimeFeature.IsQueryPropertyAttributeSupported | When disabled, the `[QueryProperty(...)]` attributes won't be used to set values to properties when navigating. |
1212
| 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. |
13+
| _MauiBindingInterceptorsSupport | Microsoft.Maui.RuntimeFeature.AreBindingInterceptorsSupported | When disabled, MAUI won't intercept any calls to `SetBinding` methods and try to compile them. Enabled by default. |
1314

1415
## MauiEnableIVisualAssemblyScanning
1516

@@ -32,3 +33,31 @@ When disabled, MAUI won't look for implicit cast operators when converting value
3233
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.
3334

3435
_Note: Prefer using the `TypeConverterAttribute` as it can help the trimmer achieve better binary size in certain scenarios._
36+
37+
## _MauiBindingInterceptorsSupport
38+
39+
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.
40+
41+
This feature is a counterpart of [XAML Compiled bindings](https://learn.microsoft.com/dotnet/maui/fundamentals/data-binding/compiled-bindings).
42+
43+
It is necessary to use this feature instead of the string-based bindings in NativeAOT apps and in apps with full trimming enabled.
44+
45+
### Example use-case
46+
47+
String-based binding in code:
48+
```c#
49+
label.BindingContext = new PageViewModel { Customer = new CustomerViewModel { Name = "John" } };
50+
label.SetBinding(Label.TextProperty, "Customer.Name");
51+
```
52+
53+
Compiled binding in code:
54+
```csharp
55+
label.SetBinding<PageViewModel, string>(Label.TextProperty, static vm => vm.Customer.Name);
56+
// or with type inference:
57+
label.SetBinding(Label.TextProperty, static (PageViewModel vm) => vm.Customer.Name);
58+
```
59+
60+
Compiled binding in XAML:
61+
```xml
62+
<Label Text="{Binding Customer.Name}" x:DataType="local:PageViewModel" />
63+
```

src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs

+10-15
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ private static Result<SetBindingInvocationDescription> GetBindingForGeneration(G
6868
return Result<SetBindingInvocationDescription>.Failure(DiagnosticsFactory.UnableToResolvePath(invocation.GetLocation()));
6969
}
7070

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

124-
private static EquatableArray<DiagnosticInfo> VerifyCorrectOverload(InvocationExpressionSyntax invocation, GeneratorSyntaxContext context, CancellationToken t)
124+
private static DiagnosticInfo[] VerifyCorrectOverload(InvocationExpressionSyntax invocation, GeneratorSyntaxContext context, CancellationToken t)
125125
{
126126
var argumentList = invocation.ArgumentList.Arguments;
127-
128127
if (argumentList.Count < 2)
129128
{
130129
throw new ArgumentOutOfRangeException(nameof(invocation));
131130
}
132131

133132
var secondArgument = argumentList[1].Expression;
134-
135-
if (secondArgument is IdentifierNameSyntax)
133+
if (secondArgument is LambdaExpressionSyntax)
136134
{
137-
var type = context.SemanticModel.GetTypeInfo(secondArgument, cancellationToken: t).Type;
138-
if (type != null && type.Name == "Func")
139-
{
140-
return new EquatableArray<DiagnosticInfo>([DiagnosticsFactory.GetterIsNotLambda(secondArgument.GetLocation())]);
141-
}
142-
else // String and Binding
143-
{
144-
return new EquatableArray<DiagnosticInfo>([DiagnosticsFactory.SuboptimalSetBindingOverload(secondArgument.GetLocation())]);
145-
}
135+
return [];
146136
}
147137

148-
return [];
138+
var secondArgumentType = context.SemanticModel.GetTypeInfo(secondArgument, cancellationToken: t).Type;
139+
return secondArgumentType switch
140+
{
141+
{ Name: "Func", ContainingNamespace.Name: "System" } => [DiagnosticsFactory.GetterIsNotLambda(secondArgument.GetLocation())],
142+
_ => [DiagnosticsFactory.SuboptimalSetBindingOverload(secondArgument.GetLocation())],
143+
};
149144
}
150145

151146
private static Result<LambdaExpressionSyntax> ExtractLambda(InvocationExpressionSyntax invocation)

src/Controls/src/Build.Tasks/Controls.Build.Tasks.csproj

+3
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
<ProjectReference Include="..\Core\Controls.Core.csproj" />
3636
<ProjectReference Include="..\Xaml\Controls.Xaml.csproj" />
3737
<ProjectReference Include="..\SourceGen\Controls.SourceGen.csproj" ReferenceOutputAssembly="false" />
38+
<ProjectReference Include="..\BindingSourceGen\Controls.BindingSourceGen.csproj" ReferenceOutputAssembly="false" />
3839
</ItemGroup>
3940

4041
<ItemGroup>
@@ -49,6 +50,8 @@
4950
<None Include="$(PkgSystem_CodeDom)\lib\netstandard2.0\System.CodeDom.dll" Visible="false" Pack="true" PackagePath="buildTransitive\netstandard2.0" />
5051
<None Include="$(ArtifactsBinDir)Controls.SourceGen\$(Configuration)\netstandard2.0\Microsoft.Maui.Controls.SourceGen.dll" Visible="false" Pack="true" PackagePath="buildTransitive\netstandard2.0" />
5152
<None Include="$(ArtifactsBinDir)Controls.SourceGen\$(Configuration)\netstandard2.0\Microsoft.Maui.Controls.SourceGen.pdb" Visible="false" Pack="true" PackagePath="buildTransitive\netstandard2.0" />
53+
<None Include="$(ArtifactsBinDir)Controls.BindingSourceGen\$(Configuration)\netstandard2.0\Microsoft.Maui.Controls.BindingSourceGen.dll" Visible="false" Pack="true" PackagePath="buildTransitive\netstandard2.0" />
54+
<None Include="$(ArtifactsBinDir)Controls.BindingSourceGen\$(Configuration)\netstandard2.0\Microsoft.Maui.Controls.BindingSourceGen.pdb" Visible="false" Pack="true" PackagePath="buildTransitive\netstandard2.0" />
5255
<None Remove="$(OutputPath)*.xml" />
5356
<None Include="nuget\**" PackagePath="" Pack="true" Exclude="nuget\**\*.aotprofile.txt" />
5457
</ItemGroup>

src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/Microsoft.Maui.Controls.targets

+13
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,15 @@
2121
<Analyzer Include="$(MSBuildThisFileDirectory)Microsoft.Maui.Controls.SourceGen.dll" IsImplicitlyDefined="true" />
2222
</ItemGroup>
2323

24+
<!-- Enable BindingSourceGen -->
25+
<PropertyGroup>
26+
<_MauiBindingInterceptorsSupport Condition=" '$(_MauiBindingInterceptorsSupport)' == '' and '$(DisableMauiAnalyzers)' != 'true' ">true</_MauiBindingInterceptorsSupport>
27+
<InterceptorsPreviewNamespaces Condition=" '$(_MauiBindingInterceptorsSupport)' == 'true' ">$(InterceptorsPreviewNamespaces);Microsoft.Maui.Controls.Generated</InterceptorsPreviewNamespaces>
28+
</PropertyGroup>
29+
<ItemGroup Condition=" '$(_MauiBindingInterceptorsSupport)' == 'true' ">
30+
<Analyzer Include="$(MSBuildThisFileDirectory)Microsoft.Maui.Controls.BindingSourceGen.dll" IsImplicitlyDefined="true" />
31+
</ItemGroup>
32+
2433
<ItemGroup Condition="'$(AndroidEnableProfiledAot)' == 'true' and '$(MauiUseDefaultAotProfile)' != 'false'">
2534
<AndroidAotProfile Include="$(MSBuildThisFileDirectory)maui.aotprofile" />
2635
<AndroidAotProfile Include="$(MSBuildThisFileDirectory)maui-blazor.aotprofile" />
@@ -242,6 +251,10 @@
242251
Condition="'$(MauiImplicitCastOperatorsUsageViaReflectionSupport)' != ''"
243252
Value="$(MauiImplicitCastOperatorsUsageViaReflectionSupport)"
244253
Trim="true" />
254+
<RuntimeHostConfigurationOption Include="Microsoft.Maui.RuntimeFeature.Microsoft.Maui.RuntimeFeature.AreBindingInterceptorsSupported"
255+
Condition="'$(_MauiBindingInterceptorsSupport)' != ''"
256+
Value="$(_MauiBindingInterceptorsSupport)"
257+
Trim="true" />
245258
</ItemGroup>
246259
</Target>
247260

src/Controls/src/Core/BindableObjectExtensions.cs

+5
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,11 @@ public static void SetBinding<TSource, TProperty>(
139139
object? fallbackValue = null,
140140
object? targetNullValue = null)
141141
{
142+
if (!RuntimeFeature.AreBindingInterceptorsSupported)
143+
{
144+
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.");
145+
}
146+
142147
throw new InvalidOperationException($"Call to SetBinding<{typeof(TSource)}, {typeof(TProperty)}> was not intercepted.");
143148
}
144149
#nullable disable

src/Controls/tests/BindingSourceGen.UnitTests/DiagnosticsTests.cs

+34
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,40 @@ public void DoesNotReportWarningWhenUsingOverloadWithStringVariablePath()
9797
Assert.Empty(result.SourceGeneratorDiagnostics);
9898
}
9999

100+
[Fact]
101+
public void DoesNotReportWarningWhenUsingOverloadWithNameofInPath()
102+
{
103+
var source = """
104+
using Microsoft.Maui.Controls;
105+
var label = new Label();
106+
var slider = new Slider();
107+
108+
label.BindingContext = slider;
109+
label.SetBinding(Label.ScaleProperty, nameof(Slider.Value));
110+
""";
111+
112+
var result = SourceGenHelpers.Run(source);
113+
Assert.Empty(result.SourceGeneratorDiagnostics);
114+
}
115+
116+
[Fact]
117+
public void DoesNotReportWarningWhenUsingOverloadWithMethodCallThatReturnsString()
118+
{
119+
var source = """
120+
using Microsoft.Maui.Controls;
121+
var label = new Label();
122+
var slider = new Slider();
123+
124+
label.BindingContext = slider;
125+
label.SetBinding(Label.ScaleProperty, GetPath());
126+
127+
static string GetPath() => "Value";
128+
""";
129+
130+
var result = SourceGenHelpers.Run(source);
131+
Assert.Empty(result.SourceGeneratorDiagnostics);
132+
}
133+
100134
[Fact]
101135
public void ReportsUnableToResolvePathWhenUsingMethodCall()
102136
{

src/Core/src/RuntimeFeature.cs

+6
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ internal static class RuntimeFeature
1818
private const bool IsShellSearchResultsRendererDisplayMemberNameSupportedByDefault = true;
1919
private const bool IsQueryPropertyAttributeSupportedByDefault = true;
2020
private const bool IsImplicitCastOperatorsUsageViaReflectionSupportedByDefault = true;
21+
private const bool AreBindingInterceptorsSupportedByDefault = true;
2122

2223
#pragma warning disable IL4000 // Return value does not match FeatureGuardAttribute 'System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute'.
2324
#if !NETSTANDARD
@@ -55,6 +56,11 @@ internal static bool IsShellSearchResultsRendererDisplayMemberNameSupported
5556
AppContext.TryGetSwitch("Microsoft.Maui.RuntimeFeature.IsImplicitCastOperatorsUsageViaReflectionSupported", out bool isSupported)
5657
? isSupported
5758
: IsImplicitCastOperatorsUsageViaReflectionSupportedByDefault;
59+
60+
internal static bool AreBindingInterceptorsSupported =>
61+
AppContext.TryGetSwitch("Microsoft.Maui.RuntimeFeature.AreBindingInterceptorsSupported", out bool areSupported)
62+
? areSupported
63+
: AreBindingInterceptorsSupportedByDefault;
5864
#pragma warning restore IL4000
5965
}
6066
}

0 commit comments

Comments
 (0)