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
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
}
}