diff --git a/Xamarin.Forms.Build.Tasks/ExpandMarkupsVisitor.cs b/Xamarin.Forms.Build.Tasks/ExpandMarkupsVisitor.cs index 7e2cb5451..954bdcac7 100644 --- a/Xamarin.Forms.Build.Tasks/ExpandMarkupsVisitor.cs +++ b/Xamarin.Forms.Build.Tasks/ExpandMarkupsVisitor.cs @@ -127,27 +127,68 @@ namespace Xamarin.Forms.Build.Tasks if (split.Length > 2) throw new ArgumentException(); - string prefix, name; - if (split.Length == 2) { - prefix = split[0]; - name = split[1]; - } - else { - prefix = ""; - name = split[0]; - } + var (prefix, name) = ParseName(match); var namespaceuri = nsResolver.LookupNamespace(prefix) ?? ""; if (!string.IsNullOrEmpty(prefix) && string.IsNullOrEmpty(namespaceuri)) throw new XamlParseException($"Undeclared xmlns prefix '{prefix}'", xmlLineInfo); + + IList typeArguments = null; + var childnodes = new List<(XmlName, INode)>(); + var contentname = new XmlName(null, null); + + if (remaining.StartsWith("}", StringComparison.Ordinal)) + { + remaining = remaining.Substring(1); + } + else + { + char next; + string piece; + while ((piece = GetNextPiece(serviceProvider, ref remaining, out next)) != null) + { + var parsed = ParseProperty(piece, serviceProvider, ref remaining, next != '='); + XmlName childname; + + if (parsed.name == null) + { + childname = contentname; + } + else + { + var (propertyPrefix, propertyName) = ParseName(parsed.name); + + childname = XamlParser.ParsePropertyName(new XmlName( + propertyPrefix == "" ? "" : nsResolver.LookupNamespace(propertyPrefix), + propertyName)); + + if (childname.NamespaceURI == null && childname.LocalName == null) + continue; + } + + if (childname == XmlName.xTypeArguments) + { + typeArguments = TypeArgumentsParser.ParseExpression(parsed.strValue, nsResolver, xmlLineInfo); + childnodes.Add((childname, new ValueNode(typeArguments, nsResolver))); + } + else + { + var childnode = parsed.value as INode ?? new ValueNode(parsed.strValue, nsResolver); + childnodes.Add((childname, childnode)); + } + } + } + //The order of lookup is to look for the Extension-suffixed class name first and then look for the class name without the Extension suffix. XmlType type; - try { - type = new XmlType(namespaceuri, name + "Extension", null); + try + { + type = new XmlType(namespaceuri, name + "Extension", typeArguments); type.GetTypeReference(contextProvider.Context.Module, null); } - catch (XamlParseException) { - type = new XmlType(namespaceuri, name, null); + catch (XamlParseException) + { + type = new XmlType(namespaceuri, name, typeArguments); } if (type == null) @@ -157,30 +198,18 @@ namespace Xamarin.Forms.Build.Tasks ? new ElementNode(type, "", nsResolver) : new ElementNode(type, "", nsResolver, xmlLineInfo.LineNumber, xmlLineInfo.LinePosition); - if (remaining.StartsWith("}", StringComparison.Ordinal)) { - remaining = remaining.Substring(1); - return _node; + foreach (var (childname, childnode) in childnodes) { + if (childname == contentname) { + //ContentProperty + _node.CollectionItems.Add(childnode); + } + else { + _node.Properties[childname] = childnode; + } } - string piece; - while ((piece = GetNextPiece(serviceProvider, ref remaining, out var next)) != null) - HandleProperty(piece, serviceProvider, ref remaining, next != '='); - return _node; } - - protected override void SetPropertyValue(string prop, string strValue, object value, IServiceProvider serviceProvider) - { - if (value == null && strValue == null) - throw new XamlParseException($"No value found for property '{prop}' in markup expression", serviceProvider); - var nsResolver = serviceProvider.GetService(typeof(IXmlNamespaceResolver)) as IXmlNamespaceResolver; - if (prop != null) { - var name = new XmlName(_node.NamespaceURI, prop); - _node.Properties[name] = value as INode ?? new ValueNode(strValue, nsResolver); - } - else //ContentProperty - _node.CollectionItems.Add(value as INode ?? new ValueNode(strValue, nsResolver)); - } } } } \ No newline at end of file diff --git a/Xamarin.Forms.Xaml.UnitTests/GenericsTests.xaml b/Xamarin.Forms.Xaml.UnitTests/GenericsTests.xaml index 620ab5124..3fbfb3e56 100644 --- a/Xamarin.Forms.Xaml.UnitTests/GenericsTests.xaml +++ b/Xamarin.Forms.Xaml.UnitTests/GenericsTests.xaml @@ -3,7 +3,8 @@ xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:sys="clr-namespace:System;assembly=mscorlib" xmlns:scg="clr-namespace:System.Collections.Generic;assembly=mscorlib" - x:Class="Xamarin.Forms.Xaml.UnitTests.GenericsTests"> + x:Class="Xamarin.Forms.Xaml.UnitTests.GenericsTests" + P="{scg:List x:TypeArguments=x:String}"> diff --git a/Xamarin.Forms.Xaml.UnitTests/GenericsTests.xaml.cs b/Xamarin.Forms.Xaml.UnitTests/GenericsTests.xaml.cs index d448ba81b..6a6562e92 100644 --- a/Xamarin.Forms.Xaml.UnitTests/GenericsTests.xaml.cs +++ b/Xamarin.Forms.Xaml.UnitTests/GenericsTests.xaml.cs @@ -9,6 +9,8 @@ namespace Xamarin.Forms.Xaml.UnitTests { public partial class GenericsTests : ContentPage { + public List P { get; set; } + public GenericsTests () { InitializeComponent (); @@ -71,6 +73,9 @@ namespace Xamarin.Forms.Xaml.UnitTests public void TestGenericParsing (bool useCompiledXaml) { var layout = new GenericsTests (useCompiledXaml); + + Assert.NotNull (layout.P); + var list = layout.Resources ["list"]; Assert.NotNull (list); Assert.That (list, Is.TypeOf> ()); diff --git a/Xamarin.Forms.Xaml/ExpandMarkupsVisitor.cs b/Xamarin.Forms.Xaml/ExpandMarkupsVisitor.cs index 9f86d8f52..3f3cb89d6 100644 --- a/Xamarin.Forms.Xaml/ExpandMarkupsVisitor.cs +++ b/Xamarin.Forms.Xaml/ExpandMarkupsVisitor.cs @@ -119,26 +119,59 @@ namespace Xamarin.Forms.Xaml if (serviceProvider.GetService(typeof(IXmlLineInfoProvider)) is IXmlLineInfoProvider xmlLineInfoProvider) xmlLineInfo = xmlLineInfoProvider.XmlLineInfo; - var split = match.Split(':'); - if (split.Length > 2) - throw new ArgumentException(); + var (prefix, name) = ParseName(match); - string prefix; //, name; - if (split.Length == 2) { - prefix = split[0]; - // name = split [1]; + var namespaceuri = nsResolver.LookupNamespace(prefix) ?? ""; + + IList typeArguments = null; + var childnodes = new List<(XmlName, INode)>(); + var contentname = new XmlName(null, null); + + if (remaining.StartsWith("}", StringComparison.Ordinal)) { + remaining = remaining.Substring(1); } else { - prefix = ""; - // name = split [0]; + char next; + string piece; + while ((piece = GetNextPiece(serviceProvider, ref remaining, out next)) != null) { + var parsed = ParseProperty(piece, serviceProvider, ref remaining, next != '='); + XmlName childname; + + if (parsed.name == null) { + childname = contentname; + } + else { + var (propertyPrefix, propertyName) = ParseName(parsed.name); + + childname = XamlParser.ParsePropertyName(new XmlName( + propertyPrefix == "" ? null : nsResolver.LookupNamespace(propertyPrefix), + propertyName)); + + if (childname.NamespaceURI == null && childname.LocalName == null) + continue; + } + + if (childname == XmlName.xTypeArguments) { + typeArguments = TypeArgumentsParser.ParseExpression(parsed.strValue, nsResolver, xmlLineInfo); + childnodes.Add((childname, new ValueNode(typeArguments, nsResolver))); + } + else { + var childnode = parsed.value as INode ?? new ValueNode(parsed.strValue, nsResolver); + childnodes.Add((childname, childnode)); + } + } } - Type type; - if (!(serviceProvider.GetService(typeof(IXamlTypeResolver)) is IXamlTypeResolver typeResolver)) - type = null; - else { - //The order of lookup is to look for the Extension-suffixed class name first and then look for the class name without the Extension suffix. - if (!typeResolver.TryResolve(match + "Extension", out type) && !typeResolver.TryResolve(match, out type)) { + + if (!(serviceProvider.GetService(typeof (IXamlTypeResolver)) is XamlTypeResolver typeResolver)) + throw new NotSupportedException(); + + var xmltype = new XmlType(namespaceuri, name + "Extension", typeArguments); + + //The order of lookup is to look for the Extension-suffixed class name first and then look for the class name without the Extension suffix. + if (!typeResolver.TryResolve(xmltype, out _)) { + xmltype = new XmlType(namespaceuri, name, typeArguments); + if (!typeResolver.TryResolve(xmltype, out _)) { var ex = new XamlParseException($"MarkupExtension not found for {match}", serviceProvider); if (ExceptionHandler != null) { ExceptionHandler(ex); @@ -148,50 +181,24 @@ namespace Xamarin.Forms.Xaml } } - var namespaceuri = nsResolver.LookupNamespace(prefix) ?? ""; - var xmltype = new XmlType(namespaceuri, type.Name, null); - - if (type == null) - throw new NotSupportedException(); - _node = xmlLineInfo == null ? new ElementNode(xmltype, null, nsResolver) : new ElementNode(xmltype, null, nsResolver, xmlLineInfo.LineNumber, xmlLineInfo.LinePosition); - if (remaining.StartsWith("}", StringComparison.Ordinal)) { - remaining = remaining.Substring(1); - return _node; - } + foreach (var (childname, childnode) in childnodes) { + childnode.Parent = _node; - string piece; - while ((piece = GetNextPiece(serviceProvider, ref remaining, out var next)) != null) - HandleProperty(piece, serviceProvider, ref remaining, next != '='); + if (childname == contentname) { + //ContentProperty + _node.CollectionItems.Add(childnode); + } + else { + _node.Properties[childname] = childnode; + } + } return _node; } - - protected override void SetPropertyValue(string prop, string strValue, object value, IServiceProvider serviceProvider) - { - if (value == null && strValue == null) { - var xpe = new XamlParseException($"No value found for property '{prop}' in markup expression", serviceProvider); - if (ExceptionHandler != null) { - ExceptionHandler(xpe); - return; - } - throw xpe; - } - - var nsResolver = serviceProvider.GetService(typeof (IXmlNamespaceResolver)) as IXmlNamespaceResolver; - - var childnode = value as INode ?? new ValueNode(strValue, nsResolver); - childnode.Parent = _node; - if (prop != null) { - var name = new XmlName(_node.NamespaceURI, prop); - _node.Properties[name] = childnode; - } - else //ContentProperty - _node.CollectionItems.Add(childnode); - } } } } diff --git a/Xamarin.Forms.Xaml/MarkupExpressionParser.cs b/Xamarin.Forms.Xaml/MarkupExpressionParser.cs index d65ed0684..782a4b999 100644 --- a/Xamarin.Forms.Xaml/MarkupExpressionParser.cs +++ b/Xamarin.Forms.Xaml/MarkupExpressionParser.cs @@ -38,6 +38,13 @@ namespace Xamarin.Forms.Xaml { abstract class MarkupExpressionParser { + protected struct Property + { + public string name; + public string strValue; + public object value; + } + public object ParseExpression(ref string expression, IServiceProvider serviceProvider) { if (serviceProvider == null) @@ -112,7 +119,7 @@ namespace Xamarin.Forms.Xaml return true; } - protected void HandleProperty(string prop, IServiceProvider serviceProvider, ref string remaining, bool isImplicit) + protected Property ParseProperty(string prop, IServiceProvider serviceProvider, ref string remaining, bool isImplicit) { char next; object value = null; @@ -120,8 +127,7 @@ namespace Xamarin.Forms.Xaml if (isImplicit) { - SetPropertyValue(null, prop, null, serviceProvider); - return; + return new Property { name = null, strValue = prop, value = null }; } remaining = remaining.TrimStart(); if (remaining.StartsWith("{", StringComparison.Ordinal)) @@ -136,14 +142,16 @@ namespace Xamarin.Forms.Xaml str_value = value as string; } - else + else { str_value = GetNextPiece(serviceProvider, ref remaining, out next); + if (str_value == null) { + throw new XamlParseException($"No value found for property '{prop}' in markup expression", serviceProvider); + } + } - SetPropertyValue(prop, str_value, value, serviceProvider); + return new Property { name = prop, strValue = str_value, value = value }; } - protected abstract void SetPropertyValue(string prop, string strValue, object value, IServiceProvider serviceProvider); - protected string GetNextPiece(IServiceProvider serviceProvider, ref string remaining, out char next) { bool inString = false; @@ -225,5 +233,15 @@ namespace Xamarin.Forms.Xaml return piece.ToString(); } + + protected static (string, string) ParseName(string name) + { + var split = name.Split(':'); + + if (split.Length > 2) + throw new ArgumentException(); + + return split.Length == 2 ? (split[0], split[1]) : ("", split[0]); + } } } \ No newline at end of file diff --git a/Xamarin.Forms.Xaml/MarkupExtensionParser.cs b/Xamarin.Forms.Xaml/MarkupExtensionParser.cs index da642b05d..97929207b 100644 --- a/Xamarin.Forms.Xaml/MarkupExtensionParser.cs +++ b/Xamarin.Forms.Xaml/MarkupExtensionParser.cs @@ -43,13 +43,15 @@ namespace Xamarin.Forms.Xaml return markupExtension.ProvideValue(serviceProvider); string piece; - while ((piece = GetNextPiece(serviceProvider, ref remaining, out char next)) != null) - HandleProperty(piece, serviceProvider, ref remaining, next != '='); + while ((piece = GetNextPiece(serviceProvider, ref remaining, out char next)) != null) { + var value = ParseProperty(piece, serviceProvider, ref remaining, next != '='); + SetPropertyValue(value.name, value.strValue, value.value, serviceProvider); + } return markupExtension.ProvideValue(serviceProvider); } - protected override void SetPropertyValue(string prop, string strValue, object value, IServiceProvider serviceProvider) + private void SetPropertyValue(string prop, string strValue, object value, IServiceProvider serviceProvider) { MethodInfo setter; if (prop == null) { diff --git a/Xamarin.Forms.Xaml/XamlParser.cs b/Xamarin.Forms.Xaml/XamlParser.cs index 73fb60ae3..5fbed187d 100644 --- a/Xamarin.Forms.Xaml/XamlParser.cs +++ b/Xamarin.Forms.Xaml/XamlParser.cs @@ -219,58 +219,15 @@ namespace Xamarin.Forms.Xaml var namespaceUri = reader.NamespaceURI; if (reader.LocalName.Contains(".") && namespaceUri == "") namespaceUri = ((IXmlNamespaceResolver)reader).LookupNamespace(""); - var propertyName = new XmlName(namespaceUri, reader.LocalName); + var propertyName = ParsePropertyName(new XmlName(namespaceUri, reader.LocalName)); + + if (propertyName.NamespaceURI == null && propertyName.LocalName == null) + continue; object value = reader.Value; - if (reader.NamespaceURI == X2006Uri) - { - switch (reader.LocalName) { - case "Key": - propertyName = XmlName.xKey; - break; - case "Name": - propertyName = XmlName.xName; - break; - case "Class": - case "FieldModifier": - continue; - default: - Debug.WriteLine("Unhandled attribute {0}", reader.Name); - continue; - } - } - - if (reader.NamespaceURI == X2009Uri) - { - switch (reader.LocalName) { - case "Key": - propertyName = XmlName.xKey; - break; - case "Name": - propertyName = XmlName.xName; - break; - case "TypeArguments": - propertyName = XmlName.xTypeArguments; - value = TypeArgumentsParser.ParseExpression((string)value, (IXmlNamespaceResolver)reader, (IXmlLineInfo)reader); - break; - case "DataType": - propertyName = XmlName.xDataType; - break; - case "Class": - case "FieldModifier": - continue; - case "FactoryMethod": - propertyName = XmlName.xFactoryMethod; - break; - case "Arguments": - propertyName = XmlName.xArguments; - break; - default: - Debug.WriteLine("Unhandled attribute {0}", reader.Name); - continue; - } - } + if (propertyName == XmlName.xTypeArguments) + value = TypeArgumentsParser.ParseExpression((string)value, (IXmlNamespaceResolver)reader, (IXmlLineInfo)reader); var propertyNode = GetValueNode(value, reader); attributes.Add(new KeyValuePair(propertyName, propertyNode)); @@ -279,6 +236,51 @@ namespace Xamarin.Forms.Xaml return attributes; } + public static XmlName ParsePropertyName(XmlName name) + { + if (name.NamespaceURI == X2006Uri) + { + switch (name.LocalName) { + case "Key": + return XmlName.xKey; + case "Name": + return XmlName.xName; + case "Class": + case "FieldModifier": + return new XmlName(null, null); + default: + Debug.WriteLine("Unhandled attribute {0}", name); + return new XmlName(null, null); + } + } + + if (name.NamespaceURI == X2009Uri) + { + switch (name.LocalName) { + case "Key": + return XmlName.xKey; + case "Name": + return XmlName.xName; + case "TypeArguments": + return XmlName.xTypeArguments; + case "DataType": + return XmlName.xDataType; + case "Class": + case "FieldModifier": + return new XmlName(null, null); + case "FactoryMethod": + return XmlName.xFactoryMethod; + case "Arguments": + return XmlName.xArguments; + default: + Debug.WriteLine("Unhandled attribute {0}", name); + return new XmlName(null, null); + } + } + + return name; + } + static IList PrefixesToIgnore(IList> xmlns) { var prefixes = new List(); diff --git a/Xamarin.Forms.Xaml/XamlServiceProvider.cs b/Xamarin.Forms.Xaml/XamlServiceProvider.cs index 849085722..9eed91dd4 100644 --- a/Xamarin.Forms.Xaml/XamlServiceProvider.cs +++ b/Xamarin.Forms.Xaml/XamlServiceProvider.cs @@ -196,6 +196,13 @@ namespace Xamarin.Forms.Xaml.Internals return exception == null; } + internal bool TryResolve(XmlType xmlType, out Type type) + { + XamlParseException exception; + type = getTypeFromXmlName(xmlType, null, currentAssembly, out exception); + return exception == null; + } + Type Resolve(string qualifiedTypeName, IServiceProvider serviceProvider, out XamlParseException exception) { exception = null;