Skip to content

Commit

Permalink
[XC] Compile StaticResource when possible
Browse files Browse the repository at this point in the history
when a resource can be compiled locally, do it.

TODO: convert OnPlatform
TODO: walk up DataTemplates

- fixes #20244
  • Loading branch information
StephaneDelcroix committed Sep 11, 2024
1 parent 0548772 commit 2209fd3
Show file tree
Hide file tree
Showing 7 changed files with 296 additions and 6 deletions.
3 changes: 2 additions & 1 deletion src/Controls/src/Build.Tasks/BuildException.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,8 @@ class BuildExceptionCode
public static BuildExceptionCode ResourceDictDuplicateKey = new BuildExceptionCode("XFC", 0125, nameof(ResourceDictDuplicateKey), "");
public static BuildExceptionCode ResourceDictMissingKey = new BuildExceptionCode("XFC", 0126, nameof(ResourceDictMissingKey), "");
public static BuildExceptionCode XKeyNotLiteral = new BuildExceptionCode("XFC", 0127, nameof(XKeyNotLiteral), "");

public static BuildExceptionCode StaticResourceSyntax = new BuildExceptionCode("XC", 0128, nameof(StaticResourceSyntax), "");

//CSC equivalents
public static BuildExceptionCode ObsoleteProperty = new BuildExceptionCode("XC", 0618, nameof(ObsoleteProperty), ""); //warning

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
using System.Collections.Generic;
using System.Linq;
using System.Xml;
using Microsoft.Maui.Controls.Xaml;
using Mono.Cecil;
using Mono.Cecil.Cil;
using static Mono.Cecil.Cil.Instruction;
using static Mono.Cecil.Cil.OpCodes;

namespace Microsoft.Maui.Controls.Build.Tasks
{
//yes, this is a ICompiledMarkupExtension, but declared as ICompiledValueProvider so it's evaluated later (in SetPropertyValue, not CreateObject)
class StaticResourceExtension : ICompiledValueProvider
{
public IEnumerable<Instruction> ProvideValue(VariableDefinitionReference vardefref, ModuleDefinition module, BaseNode node, ILContext context)
{
var name = new XmlName("", "Key");
var eNode = node as ElementNode;

if (!eNode.Properties.TryGetValue(name, out INode keyNode) && eNode.CollectionItems.Any())
keyNode = eNode.CollectionItems[0];

if (!(keyNode is ValueNode keyValueNode))
throw new BuildException(BuildExceptionCode.StaticResourceSyntax, eNode as IXmlLineInfo, null, null);

var n = eNode;
while (n != null)
{
if (n.Properties.TryGetValue(new XmlName(XamlParser.MauiUri, "Resources"), out var resourcesNode))
{
//single resource in <Resources>
if (resourcesNode is IElementNode irn
&& irn.Properties.TryGetValue(XmlName.xKey, out INode xKeyNode)
&& context.Variables.ContainsKey(irn)
&& xKeyNode is ValueNode xKeyValueNode
&& xKeyValueNode.Value as string == keyValueNode.Value as string)
{
if (context.Variables[resourcesNode as IElementNode].VariableType.FullName == "System.String") {
foreach (var instruction in TryConvert(irn.CollectionItems[0] as ValueNode, eNode, vardefref, module, context))
yield return instruction;
yield break;
}

vardefref.VariableDefinition = context.Variables[irn];
yield break;
}
//multiple resources in <Resources>
else if (resourcesNode is ListNode lr) {
foreach (var rn in lr.CollectionItems) {
if (rn is IElementNode irn2
&& irn2.Properties.TryGetValue(XmlName.xKey, out INode xKeyNode2)
&& context.Variables.ContainsKey(irn2)
&& xKeyNode2 is ValueNode xKeyValueNode2
&& xKeyValueNode2.Value as string == keyValueNode.Value as string)
{
if (irn2.CollectionItems.Count == 1 && irn2.CollectionItems[0] is ValueNode vn2 && vn2.Value is string) {
foreach (var instruction in TryConvert(vn2, eNode, vardefref, module, context))
yield return instruction;
yield break;
}

vardefref.VariableDefinition = context.Variables[irn2];
yield break;
}
}
}
//explicit ResourceDictionary in Resources
else if (resourcesNode is IElementNode resourceDictionary
&& resourceDictionary.XmlType.Name == "ResourceDictionary") {
foreach (var rn in resourceDictionary.CollectionItems) {
if (rn is IElementNode irn3
&& irn3.Properties.TryGetValue(XmlName.xKey, out INode xKeyNode3)
&& irn3.XmlType.Name != "OnPlatform"
&& context.Variables.ContainsKey(irn3)
&& xKeyNode3 is ValueNode xKeyValueNode3
&& xKeyValueNode3.Value as string == keyValueNode.Value as string)
{
if (irn3.CollectionItems.Count == 1 && irn3.CollectionItems[0] is ValueNode vn3 && vn3.Value is string) {
foreach (var instruction in TryConvert(vn3, eNode, vardefref, module, context))
yield return instruction;
yield break;
}

vardefref.VariableDefinition = context.Variables[irn3];
yield break;
}
}
}
}

n = n.Parent as ElementNode;
}


//Fallback
foreach (var instruction in FallBack(keyValueNode.Value as string, eNode, module, context).ToList())
yield return instruction;

var vardef = new VariableDefinition(module.TypeSystem.Object);
yield return Create(Stloc, vardef);
vardefref.VariableDefinition = vardef;
}

public static IEnumerable<Instruction> TryConvert(ValueNode stringResourceNode, IElementNode node, VariableDefinitionReference vardefref, ModuleDefinition module, ILContext context)
{
XmlName propertyName = XmlName.Empty;
SetPropertiesVisitor.TryGetPropertyName(node, node.Parent, out propertyName);
var localName = propertyName.LocalName;
var parentType = module.ImportReference((node.Parent as IElementNode).XmlType.GetTypeReference(context.Cache, module, (IXmlLineInfo)node));

var bpRef = SetPropertiesVisitor.GetBindablePropertyReference(parentType, propertyName.NamespaceURI, ref localName, out _, context, (IXmlLineInfo)node);
//BindableProperty
if (bpRef != null)
{
var targetTypeRef = module.ImportReference(bpRef.GetBindablePropertyType(context.Cache, node as IXmlLineInfo, module));
foreach (var instruction in stringResourceNode.PushConvertedValue(context, bpRef, stringResourceNode.PushServiceProvider(context, bpRef: bpRef), true, false))
yield return instruction;
var vardef = new VariableDefinition(targetTypeRef);
yield return Create(Stloc, vardef);
vardefref.VariableDefinition = vardef;
yield break;
}

var propertyRef = parentType.GetProperty(context.Cache, pd => pd.Name == localName, out var declaringTypeReference);
if (propertyRef != null)
{
foreach (var instruction in stringResourceNode.PushConvertedValue(context, propertyRef.PropertyType, new ICustomAttributeProvider[] { propertyRef, propertyRef.PropertyType.ResolveCached(context.Cache) }, stringResourceNode.PushServiceProvider(context, propertyRef: propertyRef), true, false))
yield return instruction;
var vardef = new VariableDefinition(propertyRef.PropertyType);
yield return Create(Stloc, vardef);
vardefref.VariableDefinition = vardef;
yield break;
}


}

public static IEnumerable<Instruction> FallBack(string key, IElementNode node, ModuleDefinition module, ILContext context)
{
var staticResourceExtensionType = module.ImportReference(context.Cache,
("Microsoft.Maui.Controls.Xaml", "Microsoft.Maui.Controls.Xaml", "StaticResourceExtension"));
yield return Create(Newobj, module.ImportCtorReference(context.Cache,
staticResourceExtensionType,
paramCount: 0));

SetPropertiesVisitor.TryGetPropertyName(node, node.Parent, out var propertyName);
var localName = propertyName.LocalName;

//Set the Key
var keyProperty = staticResourceExtensionType.GetProperty(context.Cache, pd => pd.Name == "Key", out _);
yield return Create(Dup);
yield return Create(Ldstr, key);
yield return Create(Callvirt, module.ImportReference(keyProperty.SetMethod));

FieldReference bpRef = null;
PropertyDefinition propertyRef = null;
TypeReference declaringTypeReference = null;
if (node.Parent is IElementNode parentNode && propertyName != XmlName.Empty)
{
var parentType = module.ImportReference(parentNode.XmlType.GetTypeReference(context.Cache, module, (IXmlLineInfo)node));
bpRef = SetPropertiesVisitor.GetBindablePropertyReference(parentType,
propertyName.NamespaceURI,
ref localName,
out _,
context,
(IXmlLineInfo)node);
propertyRef = parentType.GetProperty(context.Cache, pd => pd.Name == localName, out declaringTypeReference);

}
foreach (var instruction in node.PushServiceProvider(context, bpRef, propertyRef, declaringTypeReference))
yield return instruction;

yield return Create(Callvirt, module.ImportMethodReference(context.Cache,
("Microsoft.Maui.Controls.Xaml", "Microsoft.Maui.Controls.Xaml", "StaticResourceExtension"),
methodName: "ProvideValue",
parameterTypes: new[] { ("System.ComponentModel", "System", "IServiceProvider") }));
}
}
}
4 changes: 3 additions & 1 deletion src/Controls/src/Build.Tasks/ErrorMessages.resx
Original file line number Diff line number Diff line change
Expand Up @@ -257,5 +257,7 @@
<data name="XKeyNotLiteral" xml:space="preserve">
<value>x:Key expects a string literal.</value>
</data>

<data name="StaticResourceSyntax" xml:space="preserve">
<value>A key is required in {StaticResource}.</value>
</data>
</root>
32 changes: 28 additions & 4 deletions src/Controls/src/Build.Tasks/SetPropertiesVisitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,26 @@ public static IEnumerable<Instruction> ProvideValue(VariableDefinitionReference
{
var acceptEmptyServiceProvider = context.Variables[node].VariableType.GetCustomAttribute(context.Cache, module, ("Microsoft.Maui.Controls", "Microsoft.Maui.Controls.Xaml", "AcceptEmptyServiceProviderAttribute")) != null;
var markupExtensionType = ("Microsoft.Maui.Controls", "Microsoft.Maui.Controls.Xaml", "IMarkupExtension");
//some markup extensions weren't compiled earlier (on purpose), so we need to compile them now
var compiledValueProviderName = context.Variables[node].VariableType.GetCustomAttribute(context.Cache, module, ("Microsoft.Maui.Controls", "Microsoft.Maui.Controls.Xaml", "ProvideCompiledAttribute"))?.ConstructorArguments?[0].Value as string;
Type compiledValueProviderType;
ICompiledValueProvider valueProvider;

if ( compiledValueProviderName != null
&& (compiledValueProviderType = Type.GetType(compiledValueProviderName)) != null
&& (valueProvider = Activator.CreateInstance(compiledValueProviderType) as ICompiledValueProvider) != null)
{
var cProvideValue = typeof(ICompiledValueProvider).GetMethods().FirstOrDefault(md => md.Name == "ProvideValue");
var instructions = (IEnumerable<Instruction>)cProvideValue.Invoke(valueProvider, [
vardefref,
context.Body.Method.Module,
node as BaseNode,
context]);
foreach (var i in instructions)
yield return i;
yield break;
}

vardefref.VariableDefinition = new VariableDefinition(module.TypeSystem.Object);
foreach (var instruction in context.Variables[node].LoadAs(context.Cache, module.GetTypeDefinition(context.Cache, markupExtensionType), module))
yield return instruction;
Expand Down Expand Up @@ -1035,20 +1055,22 @@ public static IEnumerable<Instruction> GetPropertyValue(VariableDefinition paren
if (CanGetValue(parent, bpRef, attached, lineInfo, context, out _))
return GetValue(parent, bpRef, lineInfo, context, out propertyType);

//If it's a property, set it
//If it's a property, get it
if (CanGet(parent, localName, context, out _))
return Get(parent, localName, lineInfo, context, out propertyType);

throw new BuildException(PropertyResolution, lineInfo, null, localName, parent.VariableType.FullName);
}

static FieldReference GetBindablePropertyReference(VariableDefinition parent, string namespaceURI, ref string localName, out bool attached, ILContext context, IXmlLineInfo iXmlLineInfo)
=> GetBindablePropertyReference(parent.VariableType, namespaceURI, ref localName, out attached, context, iXmlLineInfo);

public static FieldReference GetBindablePropertyReference(TypeReference bpOwnerType, string namespaceURI, ref string localName, out bool attached, ILContext context, IXmlLineInfo iXmlLineInfo)
{
var module = context.Body.Method.Module;
TypeReference declaringTypeReference;

//If it's an attached BP, update elementType and propertyName
var bpOwnerType = parent.VariableType;
attached = GetNameAndTypeRef(ref bpOwnerType, namespaceURI, ref localName, context, iXmlLineInfo);
var name = $"{localName}Property";
FieldDefinition bpDef = bpOwnerType.GetField(context.Cache,
Expand Down Expand Up @@ -1247,10 +1269,12 @@ static bool CanSetValue(FieldReference bpRef, bool attached, INode node, IXmlLin
if (!context.Variables.TryGetValue(elementNode, out VariableDefinition varValue))
return false;



var bpTypeRef = bpRef.GetBindablePropertyType(context.Cache, iXmlLineInfo, module);
// If it's an attached BP, there's no second chance to handle IMarkupExtensions, so we try here.
// Worst case scenario ? InvalidCastException at runtime
if (attached && varValue.VariableType.FullName == "System.Object")
if (varValue.VariableType.FullName == "System.Object")
return true;
var implicitOperator = varValue.VariableType.GetImplicitOperatorTo(context.Cache, bpTypeRef, module);
if (implicitOperator != null)
Expand Down Expand Up @@ -1377,7 +1401,7 @@ static bool CanSet(VariableDefinition parent, string localName, INode node, ILCo
return false;

var valueNode = node as ValueNode;
if (valueNode != null && valueNode.CanConvertValue(context, propertyType, new ICustomAttributeProvider[] { property, propertyType.ResolveCached(context.Cache) }))
if (valueNode != null && valueNode.CanConvertValue(context, propertyType, new ICustomAttributeProvider[] { property, propertyType.ResolveCached(context.Cache) }))
return true;

var elementNode = node as IElementNode;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
namespace Microsoft.Maui.Controls.Xaml
{
[ContentProperty(nameof(Key))]
[ProvideCompiled("Microsoft.Maui.Controls.Build.Tasks.StaticResourceExtension")]
public sealed class StaticResourceExtension : IMarkupExtension
{
public string Key { get; set; }
Expand Down
20 changes: 20 additions & 0 deletions src/Controls/tests/Xaml.UnitTests/Issues/Maui20244.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?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"
xmlns:System="clr-namespace:System;assembly=mscorlib"
x:Class="Microsoft.Maui.Controls.Xaml.UnitTests.Maui20244">
<ContentPage.Resources>
<x:String x:Key="GridColDef">*, *, *, *, *, auto</x:String>
</ContentPage.Resources>
<StackLayout>
<StackLayout.Resources>
<ResourceDictionary>
<Color x:Key="reddish">#FF0000</Color>
<x:String x:Key="GridRowDef">*, *, auto</x:String>
</ResourceDictionary>
</StackLayout.Resources>

<Grid x:Name="grid" RowDefinitions="{StaticResource GridColDef}" ColumnDefinitions="{StaticResource GridRowDef}" />
</StackLayout>
</ContentPage>
63 changes: 63 additions & 0 deletions src/Controls/tests/Xaml.UnitTests/Issues/Maui20244.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using Microsoft.Maui.ApplicationModel;
using Microsoft.Maui.Controls.Core.UnitTests;
using Microsoft.Maui.Controls.Shapes;
using Microsoft.Maui.Devices;
using Microsoft.Maui.Dispatching;

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

namespace Microsoft.Maui.Controls.Xaml.UnitTests;

public partial class Maui20244 :ContentPage
{
public Maui20244()
{
InitializeComponent();
}

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

[TestFixture]
class Test
{
[SetUp]
public void Setup()
{
Application.SetCurrentApplication(new MockApplication());
DispatcherProvider.SetCurrent(new DispatcherProviderStub());
}

[TearDown] public void TearDown() => AppInfo.SetCurrent(null);

[Test]
public void RowDefStaticResource([Values(false, true)] bool useCompiledXaml)
{
if (useCompiledXaml)
MockCompiler.Compile(typeof(Maui20244));

var page = new Maui20244(useCompiledXaml);
var grid = page.grid;

Assert.That(grid.RowDefinitions.Count, Is.EqualTo(6));
Assert.That(grid.RowDefinitions[0].Height, Is.EqualTo(new GridLength(1, GridUnitType.Star)));
Assert.That(grid.RowDefinitions[1].Height, Is.EqualTo(new GridLength(1, GridUnitType.Star)));
Assert.That(grid.RowDefinitions[2].Height, Is.EqualTo(new GridLength(1, GridUnitType.Star)));
Assert.That(grid.RowDefinitions[3].Height, Is.EqualTo(new GridLength(1, GridUnitType.Star)));
Assert.That(grid.RowDefinitions[4].Height, Is.EqualTo(new GridLength(1, GridUnitType.Star)));
Assert.That(grid.RowDefinitions[5].Height, Is.EqualTo(new GridLength(1, GridUnitType.Auto)));

Assert.That(grid.ColumnDefinitions.Count, Is.EqualTo(3));
}
}

}

0 comments on commit 2209fd3

Please sign in to comment.