Skip to content

Commit

Permalink
[XC] Add feature switch to enable compiling bindings with source (#24924
Browse files Browse the repository at this point in the history
)

* Add feature switch for compilation of bindings with source

* Add tests

* Improve binding context type mismatch error messages

* Enable compilation of bindings with source in tests

* Enable the feature switch in tests

* Fix propagating CompileBindingsWithSource via ILContext
  • Loading branch information
simonrozsival authored Sep 27, 2024
1 parent 75dbba7 commit 0d0f25a
Show file tree
Hide file tree
Showing 24 changed files with 173 additions and 14 deletions.
10 changes: 10 additions & 0 deletions docs/design/FeatureSwitches.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ The following switches are toggled for applications running on Mono for `TrimMod
| 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. |
| MauiEnableXamlCBindingWithSourceCompilation | Microsoft.Maui.RuntimeFeature.XamlCBindingWithSourceCompilationEnabled | When enabled, MAUI will compile all bindings, including those where the `Source` property is used. |

## MauiEnableIVisualAssemblyScanning

Expand Down Expand Up @@ -61,3 +62,12 @@ Compiled binding in XAML:
```xml
<Label Text="{Binding Customer.Name}" x:DataType="local:PageViewModel" />
```

## MauiEnableXamlCBindingWithSourceCompilation

XamlC skipped compilation of bindings with the `Source` property set to any value in previous releases. Some bindings might start producing build errors or start failing at runtime after this feature is enabled. After enabling this feature, make sure all bindings have the right `x:DataType` so they are compiled correctly. For bindings which should not be compiled, clear the data type like this:
```
{Binding MyProperty, Source={x:Reference MyTarget}, x:DataType={x:Null}}
```

This feature is disabled by default, unless `TrimMode=true` or `PublishAot=true`. For fully trimmed and NativeAOT apps, the feature is enabled.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
</PropertyGroup>
<PropertyGroup>
<XFDisableTargetsValidation>True</XFDisableTargetsValidation>
<MauiEnableXamlCBindingWithSourceCompilation>true</MauiEnableXamlCBindingWithSourceCompilation>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DefineConstants>TRACE;DEBUG;PERF;APP</DefineConstants>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
<ApplicationVersion>1</ApplicationVersion>
<_FastDeploymentDiagnosticLogging>True</_FastDeploymentDiagnosticLogging>
<MauiEnableXamlCBindingWithSourceCompilation>true</MauiEnableXamlCBindingWithSourceCompilation>
</PropertyGroup>

<PropertyGroup>
Expand Down
3 changes: 2 additions & 1 deletion src/Controls/samples/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<UseMaui Condition=" '$(UseWorkload)' == 'true' ">true</UseMaui>
<DefaultXamlRuntime Condition=" '$(UseWorkload)' != 'true' ">Maui</DefaultXamlRuntime>
<WarningsNotAsErrors>$(WarningsNotAsErrors);XC0022;XC0023</WarningsNotAsErrors>
<MauiEnableXamlCBindingWithSourceCompilation>true</MauiEnableXamlCBindingWithSourceCompilation>
</PropertyGroup>
<Import Project="../../../Directory.Build.props" />
</Project>
</Project>
1 change: 1 addition & 0 deletions src/Controls/src/Build.Tasks/BuildException.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ class BuildExceptionCode
public static BuildExceptionCode BindingWithoutDataType = new BuildExceptionCode("XC", 0022, nameof(BindingWithoutDataType), "https://learn.microsoft.com/dotnet/maui/fundamentals/data-binding/compiled-bindings"); //warning
public static BuildExceptionCode BindingWithNullDataType = new BuildExceptionCode("XC", 0023, nameof(BindingWithNullDataType), "https://learn.microsoft.com/dotnet/maui/fundamentals/data-binding/compiled-bindings"); //warning
public static BuildExceptionCode BindingWithXDataTypeFromOuterScope = new BuildExceptionCode("XC", 0024, nameof(BindingWithXDataTypeFromOuterScope), "https://learn.microsoft.com/dotnet/maui/fundamentals/data-binding/compiled-bindings"); //warning
public static BuildExceptionCode BindingWithSourceCompilationSkipped = new BuildExceptionCode("XC", 0025, nameof(BindingWithSourceCompilationSkipped), "https://learn.microsoft.com/dotnet/maui/fundamentals/data-binding/compiled-bindings"); //warning

//Bindings, conversions
public static BuildExceptionCode Conversion = new BuildExceptionCode("XC", 0040, nameof(Conversion), "");
Expand Down
3 changes: 3 additions & 0 deletions src/Controls/src/Build.Tasks/ErrorMessages.resx
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,9 @@
<data name="BindingWithXDataTypeFromOuterScope" xml:space="preserve">
<value>Binding might be compiled incorrectly since the x:DataType annotation comes from an outer scope. Make sure you annotate all DataTemplate XAML elements with the correct x:DataType. See https://learn.microsoft.com/dotnet/maui/fundamentals/data-binding/compiled-bindings for more information.</value>
</data>
<data name="BindingWithSourceCompilationSkipped" xml:space="preserve">
<value>Binding was not compiled because it has an explicitly set Source property and compilation of bindings with Source is not enabled. Consider enabling this optimization by setting the &lt;MauiEnableXamlCBindingWithSourceCompilation&gt;true&lt;/MauiEnableXamlCBindingWithSourceCompilation&gt; in your project file and make sure the correct x:DataType is specified for this binding. See https://learn.microsoft.com/dotnet/maui/fundamentals/data-binding/compiled-bindings for more information.</value>
</data>
<data name="BindingPropertyNotFound" xml:space="preserve">
<value>Binding: Property "{0}" not found on "{1}".</value>
<comment>0 is property name, 1 is type name</comment>
Expand Down
3 changes: 3 additions & 0 deletions src/Controls/src/Build.Tasks/ILContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public ILContext(ILProcessor il, MethodBody body, ModuleDefinition module, XamlC
ParentContextValues = parentContextValues;
Module = module;
Cache = cache;
CompileBindingsWithSource = false;
}

public XamlCache Cache { get; private set; }
Expand All @@ -46,5 +47,7 @@ public ILContext(ILProcessor il, MethodBody body, ModuleDefinition module, XamlC
public TaskLoggingHelper LoggingHelper { get; internal set; }

public bool ValidateOnly { get; set; }

public bool CompileBindingsWithSource { get; set; }
}
}
18 changes: 15 additions & 3 deletions src/Controls/src/Build.Tasks/SetPropertiesVisitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -300,10 +300,21 @@ public static IEnumerable<Instruction> ProvideValue(VariableDefinitionReference

if (bindingExtensionType.HasValue)
{
if (TryCompileBindingPath(node, context, vardefref.VariableDefinition, bindingExtensionType.Value, isStandaloneBinding: bpRef is null, out var instructions))
// for backwards compatibility, it is possible to disable compilation of bindings with the `Source` property via a feature switch
// this feature switch is enabled by default only for NativeAOT and full trimming mode
bool hasSource = node.Properties.ContainsKey(new XmlName("", "Source"));
bool skipBindingCompilation = hasSource && !context.CompileBindingsWithSource;
if (!skipBindingCompilation)
{
foreach (var instruction in instructions)
yield return instruction;
if (TryCompileBindingPath(node, context, vardefref.VariableDefinition, bindingExtensionType.Value, isStandaloneBinding: bpRef is null, out var instructions))
{
foreach (var instruction in instructions)
yield return instruction;
}
}
else
{
context.LoggingHelper.LogWarningOrError(BuildExceptionCode.BindingWithSourceCompilationSkipped, context.XamlFilePath, node.LineNumber, node.LinePosition, 0, 0, null);
}
}

Expand Down Expand Up @@ -1759,6 +1770,7 @@ static void SetDataTemplate(IElementNode parentNode, ElementNode node, ILContext
XamlFilePath = parentContext.XamlFilePath,
LoggingHelper = parentContext.LoggingHelper,
ValidateOnly = parentContext.ValidateOnly,
CompileBindingsWithSource = parentContext.CompileBindingsWithSource,
};

//Instanciate nested class
Expand Down
2 changes: 2 additions & 0 deletions src/Controls/src/Build.Tasks/XamlCTask.cs
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ public class XamlCTask : XamlTask
public bool OptimizeIL { get; set; } = true;
public bool DefaultCompile { get; set; }
public bool ForceCompile { get; set; }
public bool CompileBindingsWithSource { get; set; }
public string TargetFramework { get; set; }

public int WarningLevel { get; set; } = 4; //unused so far
Expand Down Expand Up @@ -415,6 +416,7 @@ bool TryCoreCompile(MethodDefinition initComp, ILRootNode rootnode, string xamlF
XamlFilePath = xamlFilePath,
LoggingHelper = loggingHelper,
ValidateOnly = ValidateOnly,
CompileBindingsWithSource = CompileBindingsWithSource,
};


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@
DebugSymbols = "$(DebugSymbols)"
DebugType = "$(DebugType)"
DefaultCompile = "true"
CompileBindingsWithSource = "$(MauiEnableXamlCBindingWithSourceCompilation)"
ValidateOnly = "$(_MauiXamlCValidateOnly)"
TargetFramework = "$(TargetFramework)"
KeepXamlResources = "$(MauiKeepXamlResources)"
Expand Down Expand Up @@ -225,14 +226,15 @@
Text="The %24(TargetFrameworkVersion) for $(ProjectName) ($(TargetFrameworkVersion)) is less than the minimum required %24(TargetFrameworkVersion) for Microsoft.Maui ($(MinTargetFrameworkVersionForMaui)). You need to increase the %24(TargetFrameworkVersion) for $(ProjectName)." />
</Target>

<Target Name="_MauiPrepareForILLink" BeforeTargets="PrepareForILLink;_GenerateRuntimeConfigurationFilesInputCache">
<Target Name="_MauiPrepareForILLink" BeforeTargets="PrepareForILLink;_GenerateRuntimeConfigurationFilesInputCache;XamlC">
<PropertyGroup>
<MauiEnableIVisualAssemblyScanning Condition="'$(MauiEnableIVisualAssemblyScanning)' == ''">false</MauiEnableIVisualAssemblyScanning>
</PropertyGroup>
<PropertyGroup Condition="'$(PublishAot)' == 'true' or '$(TrimMode)' == 'full'">
<MauiShellSearchResultsRendererDisplayMemberNameSupported Condition="'$(MauiShellSearchResultsRendererDisplayMemberNameSupported)' == ''">false</MauiShellSearchResultsRendererDisplayMemberNameSupported>
<MauiQueryPropertyAttributeSupport Condition="'$(MauiQueryPropertyAttributeSupport)' == ''">false</MauiQueryPropertyAttributeSupport>
<MauiImplicitCastOperatorsUsageViaReflectionSupport Condition="'$(MauiImplicitCastOperatorsUsageViaReflectionSupport)' == ''">false</MauiImplicitCastOperatorsUsageViaReflectionSupport>
<MauiEnableXamlCBindingWithSourceCompilation Condition="'$(MauiEnableXamlCBindingWithSourceCompilation)' == ''">true</MauiEnableXamlCBindingWithSourceCompilation>
</PropertyGroup>
<ItemGroup>
<RuntimeHostConfigurationOption Include="Microsoft.Maui.RuntimeFeature.IsIVisualAssemblyScanningEnabled"
Expand All @@ -255,6 +257,10 @@
Condition="'$(_MauiBindingInterceptorsSupport)' != ''"
Value="$(_MauiBindingInterceptorsSupport)"
Trim="true" />
<RuntimeHostConfigurationOption Include="Microsoft.Maui.RuntimeFeature.Microsoft.Maui.RuntimeFeature.IsXamlCBindingWithSourceCompilationEnabled"
Condition="'$(MauiEnableXamlCBindingWithSourceCompilation)' != ''"
Value="$(MauiEnableXamlCBindingWithSourceCompilation)"
Trim="false" />
</ItemGroup>
</Target>

Expand Down
12 changes: 9 additions & 3 deletions src/Controls/src/Core/Binding.cs
Original file line number Diff line number Diff line change
Expand Up @@ -124,10 +124,16 @@ internal override void Apply(object context, BindableObject bindObj, BindablePro
var isApplied = IsApplied;

var bindingContext = src ?? Context ?? context;
if (DataType != null && bindingContext != null && !DataType.IsAssignableFrom(bindingContext.GetType()))

// Do not check type mismatch if this is a binding with Source and compilation of bindings with Source is disabled
bool skipTypeMismatchCheck = Source is not null && !RuntimeFeature.IsXamlCBindingWithSourceCompilationEnabled;
if (!skipTypeMismatchCheck)
{
BindingDiagnostics.SendBindingFailure(this, "Binding", "Mismatch between the specified x:DataType and the current binding context");
bindingContext = null;
if (DataType != null && bindingContext != null && !DataType.IsAssignableFrom(bindingContext.GetType()))
{
BindingDiagnostics.SendBindingFailure(this, "Binding", $"Mismatch between the specified x:DataType ({DataType}) and the current binding context ({bindingContext.GetType()}).");
bindingContext = null;
}
}

base.Apply(bindingContext, bindObj, targetProperty, fromBindingContextChanged, specificity);
Expand Down
5 changes: 5 additions & 0 deletions src/Controls/src/Core/TypedBinding.cs
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,11 @@ internal override void Unapply(bool fromBindingContextChanged = false)
internal void ApplyCore(object sourceObject, BindableObject target, BindableProperty property, bool fromTarget, SetterSpecificity specificity)
{
var isTSource = sourceObject is TSource;
if (!isTSource && sourceObject is not null)
{
BindingDiagnostics.SendBindingFailure(this, "Binding", $"Mismatch between the specified x:DataType ({typeof(TSource)}) and the current binding context ({sourceObject.GetType()})");
}

var mode = this.GetRealizedMode(property);
if ((mode == BindingMode.OneWay || mode == BindingMode.OneTime) && fromTarget)
return;
Expand Down
4 changes: 2 additions & 2 deletions src/Controls/src/Xaml/MarkupExtensions/BindingExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ BindingBase IMarkupExtension<BindingBase>.ProvideValue(IServiceProvider serviceP
return TypedBinding;

[UnconditionalSuppressMessage("TrimAnalysis", "IL2026",
Justification = "This code is only reachable in XamlC compiled code when there is a missing x:DataType and the binding could not be compiled. " +
"In that case, we produce a warning that the binding could not be compiled.")]
Justification = "If this method is invoked, we have already produced warnings in XamlC " +
"when the compilation of this binding failed or was skipped.")]
BindingBase CreateBinding()
{
Type bindingXDataType = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
<IsPackable>false</IsPackable>
<Nullable>enable</Nullable>
<DisableMSBuildAssemblyCopyCheck>true</DisableMSBuildAssemblyCopyCheck>
<MauiEnableXamlCBindingWithSourceCompilation>true</MauiEnableXamlCBindingWithSourceCompilation>
</PropertyGroup>

<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<RuntimeIdentifier Condition="$(TargetFramework.Contains('-maccatalyst'))">maccatalyst-x64</RuntimeIdentifier>
<RuntimeIdentifier Condition="$(TargetFramework.Contains('-maccatalyst')) and '$([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture)' == 'arm64'">maccatalyst-arm64</RuntimeIdentifier>
<ExcludeMicrosoftNetTestSdk>true</ExcludeMicrosoftNetTestSdk>
<MauiEnableXamlCBindingWithSourceCompilation>true</MauiEnableXamlCBindingWithSourceCompilation>
</PropertyGroup>

<PropertyGroup>
Expand Down
1 change: 1 addition & 0 deletions src/Controls/tests/TestCases.HostApp/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<UseMaui Condition=" '$(UseWorkload)' == 'true' ">true</UseMaui>
<DefaultXamlRuntime Condition=" '$(UseWorkload)' != 'true' ">Maui</DefaultXamlRuntime>
<WarningsNotAsErrors>$(WarningsNotAsErrors);XC0022;XC0023</WarningsNotAsErrors>
<MauiEnableXamlCBindingWithSourceCompilation>true</MauiEnableXamlCBindingWithSourceCompilation>
</PropertyGroup>
<Import Project="../../../../Directory.Build.props" />
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@
<AssemblyName>Microsoft.Maui.Controls.Xaml.UnitTests</AssemblyName>
<WarningLevel>4</WarningLevel>
<NoWarn>$(NoWarn);0672;0219;0414;CS0436;CS0618</NoWarn>
<WarningsNotAsErrors>$(WarningsNotAsErrors);XC0618;XC0022;XC0023;XC0045</WarningsNotAsErrors>
<WarningsNotAsErrors>$(WarningsNotAsErrors);XC0618;XC0022;XC0023;XC0025;XC0045</WarningsNotAsErrors>
<IsPackable>false</IsPackable>
<DisableMSBuildAssemblyCopyCheck>true</DisableMSBuildAssemblyCopyCheck>
<MauiEnableXamlCBindingWithSourceCompilation>true</MauiEnableXamlCBindingWithSourceCompilation>
</PropertyGroup>

<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
Expand Down
9 changes: 9 additions & 0 deletions src/Controls/tests/Xaml.UnitTests/Issues/Maui23711.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?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:local="clr-namespace:Microsoft.Maui.Controls.Xaml.UnitTests"
x:Class="Microsoft.Maui.Controls.Xaml.UnitTests.Maui23711"
x:DataType="local:DeclaredModel"
x:Name="root">
<Label x:Name="label" Text="{Binding Value, Source={x:Reference root}}" />
</ContentPage>
66 changes: 66 additions & 0 deletions src/Controls/tests/Xaml.UnitTests/Issues/Maui23711.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using System.Linq;
using Microsoft.Maui.ApplicationModel;
using Microsoft.Maui.Controls.Core.UnitTests;
using Microsoft.Maui.Devices;
using Microsoft.Maui.Dispatching;

using Microsoft.Maui.UnitTests;
using NUnit.Framework;

using Mono.Cecil;
using Mono.Cecil.Cil;

namespace Microsoft.Maui.Controls.Xaml.UnitTests;

[XamlCompilation(XamlCompilationOptions.Skip)]
public partial class Maui23711 : ContentPage
{
public Maui23711()
{
InitializeComponent();
}

public Maui23711(bool useCompiledXaml)
{
//this stub will be replaced at compile time
}

[TestFixture]
class Test
{
MockDeviceInfo mockDeviceInfo;

[SetUp]
public void Setup()
{
Application.SetCurrentApplication(new MockApplication());
DeviceInfo.SetCurrent(mockDeviceInfo = new MockDeviceInfo());
DispatcherProvider.SetCurrent(new DispatcherProviderStub());
}


[TearDown] public void TearDown()
{
AppInfo.SetCurrent(null);
DeviceInfo.SetCurrent(null);
}

[Test]
public void UsesReflectionBasedBindingsWhenCompilationOfBindingsWithSourceIsDisabled([Values(false, true)] bool compileBindingsWithSource)
{
MockCompiler.Compile(typeof(Maui23711), out MethodDefinition methodDefinition, compileBindingsWithSource: compileBindingsWithSource);
Assert.AreEqual(compileBindingsWithSource, ContainsTypedBindingInstantiation(methodDefinition));
}

static bool ContainsTypedBindingInstantiation(MethodDefinition methodDef)
=> methodDef.Body.Instructions.Any(instruction =>
instruction.OpCode == OpCodes.Newobj
&& instruction.Operand is MethodReference methodRef
&& methodRef.DeclaringType.Name == "TypedBinding`2");
}
}

public class DeclaredModel
{
public string Value { get; set; }
}
16 changes: 13 additions & 3 deletions src/Controls/tests/Xaml.UnitTests/MockCompiler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,21 @@ namespace Microsoft.Maui.Controls.Xaml.UnitTests
{
public static class MockCompiler
{
public static void Compile(Type type, string targetFramework = null, bool treatWarningsAsErrors = false)
public static void Compile(
Type type,
string targetFramework = null,
bool treatWarningsAsErrors = false,
bool compileBindingsWithSource = true)
{
Compile(type, out _, targetFramework, treatWarningsAsErrors);
Compile(type, out _, targetFramework, treatWarningsAsErrors, compileBindingsWithSource);
}

public static void Compile(Type type, out MethodDefinition methodDefinition, string targetFramework = null, bool treatWarningsAsErrors = false)
public static void Compile(
Type type,
out MethodDefinition methodDefinition,
string targetFramework = null,
bool treatWarningsAsErrors = false,
bool compileBindingsWithSource = true)
{
methodDefinition = null;
var assembly = type.Assembly.Location;
Expand All @@ -33,6 +42,7 @@ public static void Compile(Type type, out MethodDefinition methodDefinition, str
Type = type.FullName,
TargetFramework = targetFramework,
TreatWarningsAsErrors = treatWarningsAsErrors,
CompileBindingsWithSource = compileBindingsWithSource,
BuildEngine = new MSBuild.UnitTests.DummyBuildEngine()
};

Expand Down
Loading

0 comments on commit 0d0f25a

Please sign in to comment.