[Xaml] support arrays as x:Arguments (#545)
authorStephane Delcroix <stephane@delcroix.org>
Sun, 4 Dec 2016 21:08:11 +0000 (22:08 +0100)
committerGitHub <noreply@github.com>
Sun, 4 Dec 2016 21:08:11 +0000 (22:08 +0100)
* [Xaml] port some FactoryMethod tests to XamlC

* [Xaml] support array parameters in factory ctors

* [XamlC] support arrays as x:Arguments

* fix build

16 files changed:
Xamarin.Forms.Build.Tasks/CompiledMarkupExtensions/ArrayExtension.cs [new file with mode: 0644]
Xamarin.Forms.Build.Tasks/CompiledMarkupExtensions/ICompiledMarkupExtension.cs
Xamarin.Forms.Build.Tasks/CompiledMarkupExtensions/StaticExtension.cs
Xamarin.Forms.Build.Tasks/CreateObjectVisitor.cs
Xamarin.Forms.Build.Tasks/TypeReferenceExtensions.cs
Xamarin.Forms.Build.Tasks/Xamarin.Forms.Build.Tasks.csproj
Xamarin.Forms.Xaml.UnitTests/FactoryMethodMissingCtor.xaml [new file with mode: 0644]
Xamarin.Forms.Xaml.UnitTests/FactoryMethodMissingCtor.xaml.cs [new file with mode: 0644]
Xamarin.Forms.Xaml.UnitTests/FactoryMethodMissingMethod.xaml [new file with mode: 0644]
Xamarin.Forms.Xaml.UnitTests/FactoryMethodMissingMethod.xaml.cs [new file with mode: 0644]
Xamarin.Forms.Xaml.UnitTests/FactoryMethodTests.cs [deleted file]
Xamarin.Forms.Xaml.UnitTests/FactoryMethods.xaml
Xamarin.Forms.Xaml.UnitTests/FactoryMethods.xaml.cs
Xamarin.Forms.Xaml.UnitTests/SetValue.xaml
Xamarin.Forms.Xaml.UnitTests/Xamarin.Forms.Xaml.UnitTests.csproj
Xamarin.Forms.Xaml/CreateValuesVisitor.cs

diff --git a/Xamarin.Forms.Build.Tasks/CompiledMarkupExtensions/ArrayExtension.cs b/Xamarin.Forms.Build.Tasks/CompiledMarkupExtensions/ArrayExtension.cs
new file mode 100644 (file)
index 0000000..0626114
--- /dev/null
@@ -0,0 +1,31 @@
+using System.Collections.Generic;
+using Mono.Cecil;
+using Mono.Cecil.Cil;
+using Mono.Cecil.Rocks;
+using Xamarin.Forms.Xaml;
+
+namespace Xamarin.Forms.Build.Tasks
+{
+       class ArrayExtension : ICompiledMarkupExtension
+       {
+               public IEnumerable<Instruction> ProvideValue(IElementNode node, ModuleDefinition module, ILContext context, out TypeReference memberRef)
+               {
+                       var typeNode = node.Properties[new XmlName("", "Type")] as IElementNode;
+                       var typeTypeRef = context.TypeExtensions[typeNode];
+                       var n = node.CollectionItems.Count;
+
+                       var instructions = new List<Instruction>();
+                       instructions.Add(Instruction.Create(OpCodes.Ldc_I4, n));
+                       instructions.Add(Instruction.Create(OpCodes.Newarr, typeTypeRef));
+
+                       memberRef = typeTypeRef.MakeArrayType();
+                       for (var i = 0; i < n; i++) {
+                               instructions.Add(Instruction.Create(OpCodes.Dup));
+                               instructions.Add(Instruction.Create(OpCodes.Ldc_I4, i));
+                               instructions.Add(Instruction.Create(OpCodes.Ldloc, context.Variables[node.CollectionItems[i] as IElementNode]));
+                               instructions.Add(Instruction.Create(OpCodes.Stelem_Ref));
+                       }
+                       return instructions;
+               }
+       }
+}
\ No newline at end of file
index b18d956..285a536 100644 (file)
@@ -7,6 +7,6 @@ namespace Xamarin.Forms.Build.Tasks
 {
        interface ICompiledMarkupExtension
        {
-               IEnumerable<Instruction> ProvideValue(IElementNode node, ModuleDefinition module, out TypeReference typeRef);
+               IEnumerable<Instruction> ProvideValue(IElementNode node, ModuleDefinition module, ILContext context, out TypeReference typeRef);
        }
 }
\ No newline at end of file
index 14797c5..014fb82 100644 (file)
@@ -1,5 +1,4 @@
-using System;
-using System.Collections.Generic;
+using System.Collections.Generic;
 using Mono.Cecil;
 using Mono.Cecil.Cil;
 using Xamarin.Forms.Xaml;
@@ -11,11 +10,11 @@ namespace Xamarin.Forms.Build.Tasks
 {
        class StaticExtension : ICompiledMarkupExtension
        {
-               public IEnumerable<Instruction> ProvideValue(IElementNode node, ModuleDefinition module, out TypeReference memberRef)
+               public IEnumerable<Instruction> ProvideValue(IElementNode node, ModuleDefinition module, ILContext context, out TypeReference memberRef)
                {
                        INode ntype;
                        if (!node.Properties.TryGetValue(new XmlName("", "Member"), out ntype))
-                               ntype = node.CollectionItems [0];
+                               ntype = node.CollectionItems[0];
                        var member = ((ValueNode)ntype).Value as string;
 
                        if (IsNullOrEmpty(member) || !member.Contains(".")) {
index 4e1372c..ac53021 100644 (file)
@@ -64,7 +64,7 @@ namespace Xamarin.Forms.Build.Tasks
                        if (typeref.FullName == "Xamarin.Forms.Xaml.StaticExtension") {
                                var markupProvider = new StaticExtension();
 
-                               var il = markupProvider.ProvideValue(node, Module, out typeref);
+                               var il = markupProvider.ProvideValue(node, Module, Context, out typeref);
 
                                var vardef = new VariableDefinition(typeref);
                                Context.Variables [node] = vardef;
@@ -180,7 +180,6 @@ namespace Xamarin.Forms.Build.Tasks
                                        Context.IL.Emit(OpCodes.Initobj, Module.Import(typedef));
                                }
 
-                               //if/when we land the compiled converters, those 2 blocks could be greatly simplified
                                if (typeref.FullName == "Xamarin.Forms.Xaml.TypeExtension") {
                                        var visitor = new SetPropertiesVisitor(Context);
                                        foreach (var cnode in node.Properties.Values.ToList())
@@ -212,6 +211,30 @@ namespace Xamarin.Forms.Build.Tasks
                                                Context.Body.Variables.Add(vardefref.VariableDefinition);
                                        }
                                }
+
+                               if (typeref.FullName == "Xamarin.Forms.Xaml.ArrayExtension")
+                               {
+                                       var visitor = new SetPropertiesVisitor(Context);
+                                       foreach (var cnode in node.Properties.Values.ToList())
+                                               cnode.Accept(visitor, node);
+                                       foreach (var cnode in node.CollectionItems)
+                                               cnode.Accept(visitor, node);
+
+                                       var markupProvider = new ArrayExtension();
+
+                                       var il = markupProvider.ProvideValue(node, Module, Context, out typeref);
+
+                                       vardef = new VariableDefinition(typeref);
+                                       Context.Variables[node] = vardef;
+                                       Context.Body.Variables.Add(vardef);
+
+                                       Context.IL.Append(il);
+                                       Context.IL.Emit(OpCodes.Stloc, vardef);
+
+                                       //clean the node as it has been fully exhausted
+                                       node.Properties.Remove(new XmlName("","Type"));
+                                       node.CollectionItems.Clear();
+                               }
                        }
                }
 
@@ -316,8 +339,10 @@ namespace Xamarin.Forms.Build.Tasks
 
                static bool IsXaml2009LanguagePrimitive(IElementNode node)
                {
-                       if (node.NamespaceURI == "http://schemas.microsoft.com/winfx/2009/xaml")
-                               return true;
+                       if (node.NamespaceURI == "http://schemas.microsoft.com/winfx/2009/xaml") {
+                               var n = node.XmlType.Name.Split(':') [1];
+                               return n != "Array";
+                       }
                        if (node.NamespaceURI != "clr-namespace:System;assembly=mscorlib")
                                return false;
                        var name = node.XmlType.Name.Split(':')[1];
index d0ccbc5..613b0cc 100644 (file)
@@ -65,12 +65,9 @@ namespace Xamarin.Forms.Build.Tasks
                        genericArguments = null;
                        var typeDef = typeRef.Resolve();
                        TypeReference iface;
-                       if (
-                               (iface =
-                                       typeDef.Interfaces.FirstOrDefault(
-                                               tr =>
-                                                       tr.FullName.StartsWith(@interface) && tr.IsGenericInstance && (tr as GenericInstanceType).HasGenericArguments)) !=
-                               null)
+                       if ((iface = typeDef.Interfaces.FirstOrDefault(tr =>
+                                                       tr.FullName.StartsWith(@interface, StringComparison.Ordinal) &&
+                                                       tr.IsGenericInstance && (tr as GenericInstanceType).HasGenericArguments)) != null)
                        {
                                interfaceReference = iface as GenericInstanceType;
                                genericArguments = (iface as GenericInstanceType).GenericArguments;
@@ -89,20 +86,25 @@ namespace Xamarin.Forms.Build.Tasks
 
                        var arrayInterfaces = new[]
                        {
-                               "System.IEnumerable",
+                               "System.Collections.IEnumerable",
                                "System.Collections.IList",
                                "System.Collections.Collection"
                        };
 
                        var arrayGenericInterfaces = new[]
                        {
-                               "System.IEnumerable`1",
+                               "System.Collections.IEnumerable`1",
                                "System.Collections.Generic.IList`1",
                                "System.Collections.Generic.IReadOnlyCollection<T>",
                                "System.Collections.Generic.IReadOnlyList<T>",
                                "System.Collections.Generic.Collection<T>"
                        };
 
+                       if (typeRef.IsArray && baseClass.IsArray) {
+                               typeRef = typeRef.Resolve();
+                               baseClass = baseClass.Resolve();
+                       }
+
                        if (typeRef.IsArray)
                        {
                                var arrayType = typeRef.Resolve();
@@ -112,7 +114,9 @@ namespace Xamarin.Forms.Build.Tasks
                                    baseClass.IsGenericInstance &&
                                    (baseClass as GenericInstanceType).GenericArguments[0].FullName == arrayType.FullName)
                                        return true;
+                               return false;
                        }
+
                        var typeDef = typeRef.Resolve();
                        if (typeDef.FullName == baseClass.FullName)
                                return true;
index 515319e..46d4f23 100644 (file)
     <Compile Include="CompiledConverters\RectangleTypeConverter.cs" />
     <Compile Include="Logger.cs" />
     <Compile Include="XamlTask.cs" />
+    <Compile Include="CompiledMarkupExtensions\ArrayExtension.cs" />
   </ItemGroup>
   <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
   <Target Name="AfterBuild">
diff --git a/Xamarin.Forms.Xaml.UnitTests/FactoryMethodMissingCtor.xaml b/Xamarin.Forms.Xaml.UnitTests/FactoryMethodMissingCtor.xaml
new file mode 100644 (file)
index 0000000..f49108c
--- /dev/null
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<local:MockView xmlns="http://xamarin.com/schemas/2014/forms"
+               xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
+               xmlns:local="clr-namespace:Xamarin.Forms.Xaml.UnitTests;assembly=Xamarin.Forms.Xaml.UnitTests"
+               x:Class="Xamarin.Forms.Xaml.UnitTests.FactoryMethodMissingCtor">
+       <local:MockView.Content>
+               <local:MockFactory>
+                       <x:Arguments>
+                               <x:Object/>
+                               <x:String>bar</x:String>
+                               <x:Int32>42</x:Int32>
+                       </x:Arguments>
+               </local:MockFactory>
+       </local:MockView.Content>
+</local:MockView>
\ No newline at end of file
diff --git a/Xamarin.Forms.Xaml.UnitTests/FactoryMethodMissingCtor.xaml.cs b/Xamarin.Forms.Xaml.UnitTests/FactoryMethodMissingCtor.xaml.cs
new file mode 100644 (file)
index 0000000..6771edf
--- /dev/null
@@ -0,0 +1,35 @@
+using System;
+using NUnit.Framework;
+using Xamarin.Forms.Core.UnitTests;
+
+namespace Xamarin.Forms.Xaml.UnitTests
+{
+       [XamlCompilation(XamlCompilationOptions.Skip)]
+       public partial class FactoryMethodMissingCtor : MockView
+       {
+               public FactoryMethodMissingCtor()
+               {
+                       InitializeComponent();
+               }
+
+               [TestFixture]
+               public class Tests
+               {
+                       [SetUp]
+                       public void SetUp()
+                       {
+                               Device.PlatformServices = new MockPlatformServices();
+                       }
+
+                       [TestCase(false)]
+                       [TestCase(true)]
+                       public void Throw(bool useCompiledXaml)
+                       {
+                               if (useCompiledXaml)
+                                       Assert.Throws(new XamlParseExceptionConstraint(7, 4), () => MockCompiler.Compile(typeof(FactoryMethodMissingCtor)));
+                               else
+                                       Assert.Throws<MissingMethodException>(() => new FactoryMethodMissingCtor());
+                       }
+               }
+       }
+}
\ No newline at end of file
diff --git a/Xamarin.Forms.Xaml.UnitTests/FactoryMethodMissingMethod.xaml b/Xamarin.Forms.Xaml.UnitTests/FactoryMethodMissingMethod.xaml
new file mode 100644 (file)
index 0000000..85e279d
--- /dev/null
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<local:MockView
+               xmlns="http://xamarin.com/schemas/2014/forms"
+               xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
+               xmlns:local="clr-namespace:Xamarin.Forms.Xaml.UnitTests;assembly=Xamarin.Forms.Xaml.UnitTests"
+               x:Class="Xamarin.Forms.Xaml.UnitTests.FactoryMethodMissingMethod">
+       <local:MockView.Content>
+               <local:MockFactory x:FactoryMethod="Factory">
+                       <x:Arguments>
+                               <x:Object/>
+                               <x:String>bar</x:String>
+                               <x:Int32>42</x:Int32>
+                       </x:Arguments>
+               </local:MockFactory>
+       </local:MockView.Content>
+</local:MockView>
\ No newline at end of file
diff --git a/Xamarin.Forms.Xaml.UnitTests/FactoryMethodMissingMethod.xaml.cs b/Xamarin.Forms.Xaml.UnitTests/FactoryMethodMissingMethod.xaml.cs
new file mode 100644 (file)
index 0000000..ecc4048
--- /dev/null
@@ -0,0 +1,35 @@
+using System;
+using NUnit.Framework;
+using Xamarin.Forms.Core.UnitTests;
+
+namespace Xamarin.Forms.Xaml.UnitTests
+{
+       [XamlCompilation(XamlCompilationOptions.Skip)]
+       public partial class FactoryMethodMissingMethod : MockView
+       {
+               public FactoryMethodMissingMethod()
+               {
+                       InitializeComponent();
+               }
+
+               [TestFixture]
+               public class Tests
+               {
+                       [SetUp]
+                       public void SetUp()
+                       {
+                               Device.PlatformServices = new MockPlatformServices();
+                       }
+
+                       [TestCase(false)]
+                       [TestCase(true)]
+                       public void Throw(bool useCompiledXaml)
+                       {
+                               if (useCompiledXaml)
+                                       Assert.Throws(new XamlParseExceptionConstraint(8, 4), () => MockCompiler.Compile(typeof(FactoryMethodMissingMethod)));
+                               else
+                                       Assert.Throws<MissingMemberException>(() => new FactoryMethodMissingMethod());
+                       }
+               }
+       }
+}
diff --git a/Xamarin.Forms.Xaml.UnitTests/FactoryMethodTests.cs b/Xamarin.Forms.Xaml.UnitTests/FactoryMethodTests.cs
deleted file mode 100644 (file)
index d6ee188..0000000
+++ /dev/null
@@ -1,53 +0,0 @@
-using System;
-using NUnit.Framework;
-
-using Xamarin.Forms.Core.UnitTests;
-
-namespace Xamarin.Forms.Xaml.UnitTests
-{
-       [TestFixture]
-       public class FactoryMethodTests : BaseTestFixture
-       {
-               [Test]
-               public void ThrowOnMissingCtor ()
-               {
-                       var xaml = @"
-                       <local:MockView
-                               xmlns=""http://xamarin.com/schemas/2014/forms""
-                               xmlns:x=""http://schemas.microsoft.com/winfx/2009/xaml""
-                               xmlns:local=""clr-namespace:Xamarin.Forms.Xaml.UnitTests;assembly=Xamarin.Forms.Xaml.UnitTests"" >
-                               <local:MockView.Content>
-                                       <local:MockFactory>
-                                               <x:Arguments>
-                                                       <x:Object/>
-                                                       <x:String>bar</x:String>
-                                                       <x:Int32>42</x:Int32>
-                                               </x:Arguments>
-                                       </local:MockFactory>
-                               </local:MockView.Content>
-                       </local:MockView>";
-                       Assert.Throws<MissingMethodException> (()=>new MockView ().LoadFromXaml (xaml));
-               }
-
-               [Test]
-               public void ThrowOnMissingMethod ()
-               {
-                       var xaml = @"
-                       <local:MockView
-                               xmlns=""http://xamarin.com/schemas/2014/forms""
-                               xmlns:x=""http://schemas.microsoft.com/winfx/2009/xaml""
-                               xmlns:local=""clr-namespace:Xamarin.Forms.Xaml.UnitTests;assembly=Xamarin.Forms.Xaml.UnitTests"" >
-                               <local:MockView.Content>
-                                       <local:MockFactory x:FactoryMethod=""Factory"">
-                                               <x:Arguments>
-                                                       <x:Object/>
-                                                       <x:String>bar</x:String>
-                                                       <x:Int32>42</x:Int32>
-                                               </x:Arguments>
-                                       </local:MockFactory>
-                               </local:MockView.Content>
-                       </local:MockView>";
-                       Assert.Throws<MissingMemberException> (()=>new MockView ().LoadFromXaml (xaml));
-               }
-       }
-}
index 5225c7d..9ff91de 100644 (file)
@@ -1,4 +1,4 @@
-<?xml version="1.0" encoding="UTF-8"?>
+<?xml version="1.0" encoding="UTF-8"?>
 <ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
                         xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                         xmlns:local="clr-namespace:Xamarin.Forms.Xaml.UnitTests"
@@ -32,7 +32,7 @@
                        <local:MockView.Content>
                                <local:MockFactory x:FactoryMethod="ParameterlessFactory">
                                </local:MockFactory>
-                               </local:MockView.Content>
+                       </local:MockView.Content>
                </local:MockView>
                <local:MockView x:Name="v4">
                        <local:MockView.Content>
                                <local:MockFactory x:Arguments="{x:Static local:MockxStatic.MockStaticProperty}"/>
                        </local:MockView.Content>
                </local:MockView>
+               <local:MockView x:Name="v8">
+                       <local:MockView.Content>
+                               <local:MockFactory >
+                                       <x:Arguments>
+                                               <x:Array Type="{x:Type x:Object}">
+                                          <x:String>Foo</x:String>
+                                          <x:String>Bar</x:String>
+                                        </x:Array>
+                                       </x:Arguments>
+                               </local:MockFactory>
+                       </local:MockView.Content>
+               </local:MockView>
        </StackLayout>
 </ContentPage>
\ No newline at end of file
index 9fa4e92..f579038 100644 (file)
@@ -32,6 +32,11 @@ namespace Xamarin.Forms.Xaml.UnitTests
                        Content = "int ctor " + arg.ToString ();
                }
 
+               public MockFactory(object [] args)
+               {
+                       Content = string.Join(" ", args);
+               }
+
                public static  MockFactory ParameterlessFactory ()
                {
                        return new MockFactory {
@@ -138,6 +143,14 @@ namespace Xamarin.Forms.Xaml.UnitTests
                                var layout = new FactoryMethods(useCompiledXaml);
                                Assert.AreEqual("alternate ctor Property", layout.v7.Content.Content);
                        }
+
+                       [TestCase(false)]
+                       [TestCase(true)]
+                       public void TestCtorWithArrayParameter(bool useCompiledXaml)
+                       {
+                               var layout = new FactoryMethods(useCompiledXaml);
+                               Assert.AreEqual("Foo Bar", layout.v8.Content.Content);
+                       }
                }
        }
 }
\ No newline at end of file
index fd11bd5..5ebc4f4 100644 (file)
@@ -1,4 +1,4 @@
-<?xml version="1.0" encoding="UTF-8"?>
+<?xml version="1.0" encoding="UTF-8"?>
 <ContentPage 
        xmlns="http://xamarin.com/schemas/2014/forms"
        xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
index 9c2776e..4977edb 100644 (file)
@@ -94,7 +94,6 @@
     <Compile Include="OnPlatformTests.cs" />
     <Compile Include="NullExtensionTests.cs" />
     <Compile Include="TypeExtensionTests.cs" />
-    <Compile Include="FactoryMethodTests.cs" />
     <Compile Include="XamlgTests.cs" />
     <Compile Include="Issues\TestCases.cs" />
     <Compile Include="Issues\Issue1493.cs" />
     <Compile Include="Issues\Bz47703.xaml.cs">
       <DependentUpon>Bz47703.xaml</DependentUpon>
     </Compile>
+     <Compile Include="FactoryMethodMissingCtor.xaml.cs">
+       <DependentUpon>FactoryMethodMissingCtor.xaml</DependentUpon>
+     </Compile>
+     <Compile Include="FactoryMethodMissingMethod.xaml.cs">
+       <DependentUpon>FactoryMethodMissingMethod.xaml</DependentUpon>
+     </Compile>
   </ItemGroup>
   <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
   <Import Project="..\.nuspec\Xamarin.Forms.Debug.targets" />
     <EmbeddedResource Include="Issues\Bz47703.xaml">
       <Generator>MSBuild:UpdateDesignTimeXaml</Generator>
     </EmbeddedResource>
+    <EmbeddedResource Include="FactoryMethodMissingCtor.xaml">
+      <Generator>MSBuild:UpdateDesignTimeXaml</Generator>
+    </EmbeddedResource>
+    <EmbeddedResource Include="FactoryMethodMissingMethod.xaml">
+      <Generator>MSBuild:UpdateDesignTimeXaml</Generator>
+    </EmbeddedResource>
   </ItemGroup>
   <ItemGroup>
     <Service Include="{82A7F48D-3B50-4B1E-B82E-3ADA8210C358}" />
index c2331bf..817b62c 100644 (file)
@@ -107,7 +107,7 @@ namespace Xamarin.Forms.Xaml
                        Values[node] = value;
 
                        var markup = value as IMarkupExtension;
-                       if (markup != null && (value is TypeExtension || value is StaticExtension))
+                       if (markup != null && (value is TypeExtension || value is StaticExtension || value is ArrayExtension))
                        {
                                var serviceProvider = new XamlServiceProvider(node, Context);
 
@@ -119,9 +119,16 @@ namespace Xamarin.Forms.Xaml
 
                                value = markup.ProvideValue(serviceProvider);
 
+                               INode xKey;
+                               if (!node.Properties.TryGetValue(XmlName.xKey, out xKey))
+                                       xKey = null;
+                               
                                node.Properties.Clear();
                                node.CollectionItems.Clear();
 
+                               if (xKey != null)
+                                       node.Properties.Add(XmlName.xKey, xKey);
+
                                Values[node] = value;
                        }