diff --git a/Maui.ServerDrivenUI.sln b/Maui.ServerDrivenUI.sln index df0d0d9..8abccad 100644 --- a/Maui.ServerDrivenUI.sln +++ b/Maui.ServerDrivenUI.sln @@ -16,6 +16,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Itens de Solução", "Itens README.md = README.md EndProjectSection EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Maui.Controls.DeviceTests", "Microsoft.Maui.Core.DeviceTests\Microsoft.Maui.Controls.DeviceTests.csproj", "{28109973-FDEC-4CA8-9E37-D16AE77F9B4C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -26,6 +28,10 @@ Global {E67A0AF3-8CB0-4E40-99A0-6C2001D7E0D1}.Debug|Any CPU.Build.0 = Debug|Any CPU {E67A0AF3-8CB0-4E40-99A0-6C2001D7E0D1}.Release|Any CPU.ActiveCfg = Release|Any CPU {E67A0AF3-8CB0-4E40-99A0-6C2001D7E0D1}.Release|Any CPU.Build.0 = Release|Any CPU + {28109973-FDEC-4CA8-9E37-D16AE77F9B4C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {28109973-FDEC-4CA8-9E37-D16AE77F9B4C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {28109973-FDEC-4CA8-9E37-D16AE77F9B4C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {28109973-FDEC-4CA8-9E37-D16AE77F9B4C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Maui.ServerDrivenUI/Maui.ServerDrivenUI.csproj b/Maui.ServerDrivenUI/Maui.ServerDrivenUI.csproj index 759b502..af57730 100644 --- a/Maui.ServerDrivenUI/Maui.ServerDrivenUI.csproj +++ b/Maui.ServerDrivenUI/Maui.ServerDrivenUI.csproj @@ -26,4 +26,9 @@ + + + + + diff --git a/Maui.ServerDrivenUI/Views/ServerDrivenVisualElement.cs b/Maui.ServerDrivenUI/Views/ServerDrivenVisualElement.cs index 9b75927..cc3ec0b 100644 --- a/Maui.ServerDrivenUI/Views/ServerDrivenVisualElement.cs +++ b/Maui.ServerDrivenUI/Views/ServerDrivenVisualElement.cs @@ -1,5 +1,6 @@ using Maui.ServerDrivenUI.Models.Exceptions; using Maui.ServerDrivenUI.Services; +using Maui.ServerDrivenUI.Xaml; namespace Maui.ServerDrivenUI.Views; @@ -35,7 +36,7 @@ internal static async Task InitializeComponentAsync(IServerDrivenVisualElement e try { - visualElement?.LoadFromXaml(xaml); + visualElement?.LoadXaml(xaml); errorMessage = string.Empty; if (XamlConverterService.LabelsSpans.Any()) diff --git a/Microsoft.Maui.Core.DeviceTests/Extensions.cs b/Microsoft.Maui.Core.DeviceTests/Extensions.cs new file mode 100644 index 0000000..2a57177 --- /dev/null +++ b/Microsoft.Maui.Core.DeviceTests/Extensions.cs @@ -0,0 +1,24 @@ +using System.Reflection; + +namespace Maui.ServerDrivenUI.Xaml; + +public static class Extensions +{ + public static TXaml LoadXaml(this TXaml view, Type callingType) + { + XamlLoader.Load(view, callingType); + return view; + } + + public static TXaml LoadXaml(this TXaml view, string xaml) + { + XamlLoader.Load(view, xaml); + return view; + } + + internal static TXaml LoadXaml(this TXaml view, string xaml, Assembly rootAssembly) + { + XamlLoader.Load(view, xaml, rootAssembly); + return view; + } +} diff --git a/Microsoft.Maui.Core.DeviceTests/GlobalUsings.cs b/Microsoft.Maui.Core.DeviceTests/GlobalUsings.cs new file mode 100644 index 0000000..93fa548 --- /dev/null +++ b/Microsoft.Maui.Core.DeviceTests/GlobalUsings.cs @@ -0,0 +1 @@ +global using Microsoft.Maui.Controls.Internals; \ No newline at end of file diff --git a/Microsoft.Maui.Core.DeviceTests/Microsoft.Maui.Controls.DeviceTests.csproj b/Microsoft.Maui.Core.DeviceTests/Microsoft.Maui.Controls.DeviceTests.csproj new file mode 100644 index 0000000..d72f0c6 --- /dev/null +++ b/Microsoft.Maui.Core.DeviceTests/Microsoft.Maui.Controls.DeviceTests.csproj @@ -0,0 +1,16 @@ + + + + net8.0-android;net8.0-ios17;net8.0-maccatalyst + $(TargetFrameworks);net8.0-windows10.0.19041.0 + enable + enable + true + + + + + + + + diff --git a/Microsoft.Maui.Core.DeviceTests/XamlLoader.cs b/Microsoft.Maui.Core.DeviceTests/XamlLoader.cs new file mode 100644 index 0000000..46aa57a --- /dev/null +++ b/Microsoft.Maui.Core.DeviceTests/XamlLoader.cs @@ -0,0 +1,340 @@ +using System.Diagnostics; +using System.Reflection; +using System.Text.RegularExpressions; +using System.Xml; + +namespace Maui.ServerDrivenUI.Xaml; + +internal static partial class XamlLoader +{ + public static void Load(object view, Type callingType) + { + var xaml = GetXamlForType(callingType, view, out var useDesignProperties); + if (string.IsNullOrEmpty(xaml)) + throw new XamlParseException(string.Format("No embeddedresource found for {0}", callingType), new XmlLineInfo()); + Load(view, xaml, useDesignProperties); + } + + public static void Load(object view, string xaml) => Load(view, xaml, false); + public static void Load(object view, string xaml, bool useDesignProperties) => Load(view, xaml, null, useDesignProperties); + public static void Load(object view, string xaml, Assembly rootAssembly) => Load(view, xaml, rootAssembly, false); + + public static void Load(object view, string xaml, Assembly rootAssembly, bool useDesignProperties) + { + using (var textReader = new StringReader(xaml)) + using (var reader = XmlReader.Create(textReader)) + { + while (reader.Read()) + { + //Skip until element + if (reader.NodeType == XmlNodeType.Whitespace) + continue; + if (reader.NodeType == XmlNodeType.XmlDeclaration) + continue; + if (reader.NodeType != XmlNodeType.Element) + { + Debug.WriteLine("Unhandled node {0} {1} {2}", reader.NodeType, reader.Name, reader.Value); + continue; + } + + var rootnode = new RuntimeRootNode(new XmlType(reader.NamespaceURI, reader.Name, null), view, (IXmlNamespaceResolver)reader) { LineNumber = ((IXmlLineInfo)reader).LineNumber, LinePosition = ((IXmlLineInfo)reader).LinePosition }; + XamlParser.ParseXaml(rootnode, reader); + var doNotThrow = ResourceLoader.ExceptionHandler2 != null; + void ehandler(Exception e) => ResourceLoader.ExceptionHandler2?.Invoke((e, XamlFilePathAttribute.GetFilePathForObject(view))); + Visit(rootnode, new HydrationContext { + RootElement = view, + RootAssembly = rootAssembly ?? view.GetType().Assembly, + ExceptionHandler = doNotThrow ? ehandler : (Action)null + }, useDesignProperties); + + VisualDiagnostics.OnChildAdded(null, view as Element); + + break; + } + } + } + + public static object Create(string xaml, bool doNotThrow = false) => Create(xaml, doNotThrow, false); + + public static object Create(string xaml, bool doNotThrow, bool useDesignProperties) + { + doNotThrow = doNotThrow || ResourceLoader.ExceptionHandler2 != null; + void ehandler(Exception e) => ResourceLoader.ExceptionHandler2?.Invoke((e, null)); + + object inflatedView = null; + using (var textreader = new StringReader(xaml)) + using (var reader = XmlReader.Create(textreader)) + { + while (reader.Read()) + { + //Skip until element + if (reader.NodeType == XmlNodeType.Whitespace) + continue; + if (reader.NodeType == XmlNodeType.XmlDeclaration) + continue; + if (reader.NodeType != XmlNodeType.Element) + { + Debug.WriteLine("Unhandled node {0} {1} {2}", reader.NodeType, reader.Name, reader.Value); + continue; + } + + var typeArguments = XamlParser.GetTypeArguments(reader); + var rootnode = new RuntimeRootNode(new XmlType(reader.NamespaceURI, reader.Name, typeArguments), null, (IXmlNamespaceResolver)reader) { LineNumber = ((IXmlLineInfo)reader).LineNumber, LinePosition = ((IXmlLineInfo)reader).LinePosition }; + + XamlParser.ParseXaml(rootnode, reader); + var visitorContext = new HydrationContext { + ExceptionHandler = doNotThrow ? ehandler : (Action)null, + }; + var cvv = new CreateValuesVisitor(visitorContext); + cvv.Visit((ElementNode)rootnode, null); + inflatedView = rootnode.Root = visitorContext.Values[rootnode]; + visitorContext.RootElement = inflatedView as BindableObject; + + Visit(rootnode, visitorContext, useDesignProperties); + VisualDiagnostics.OnChildAdded(null, inflatedView as Element); + break; + } + } + return inflatedView; + } + + internal static IResourceDictionary LoadResources(string xaml, IResourcesProvider rootView) + { + void ehandler(Exception e) => ResourceLoader.ExceptionHandler2?.Invoke((e, XamlFilePathAttribute.GetFilePathForObject(rootView))); + + using (var textReader = new StringReader(xaml)) + using (var reader = XmlReader.Create(textReader)) + { + while (reader.Read()) + { + //Skip until element + if (reader.NodeType == XmlNodeType.Whitespace) + continue; + if (reader.NodeType == XmlNodeType.XmlDeclaration) + continue; + if (reader.NodeType != XmlNodeType.Element) + { + Debug.WriteLine("Unhandled node {0} {1} {2}", reader.NodeType, reader.Name, reader.Value); + continue; + } + + //the root is set to null, and not to rootView, on purpose as we don't want to erase the current Resources of the view + RootNode rootNode = new RuntimeRootNode(new XmlType(reader.NamespaceURI, reader.Name, null), null, (IXmlNamespaceResolver)reader) { LineNumber = ((IXmlLineInfo)reader).LineNumber, LinePosition = ((IXmlLineInfo)reader).LinePosition }; + XamlParser.ParseXaml(rootNode, reader); + var rNode = (IElementNode)rootNode; + if (!rNode.Properties.TryGetValue(new XmlName(XamlParser.MauiUri, "Resources"), out var resources)) + return null; + + var visitorContext = new HydrationContext { + ExceptionHandler = ResourceLoader.ExceptionHandler2 != null ? ehandler : (Action)null, + }; + var cvv = new CreateValuesVisitor(visitorContext); + if (resources is ElementNode resourcesEN && (resourcesEN.XmlType.NamespaceUri != XamlParser.MauiUri || resourcesEN.XmlType.Name != nameof(ResourceDictionary))) + { //single implicit resource + resources = new ElementNode(new XmlType(XamlParser.MauiUri, nameof(ResourceDictionary), null), XamlParser.MauiUri, rootNode.NamespaceResolver); + ((ElementNode)resources).CollectionItems.Add(resourcesEN); + } + else if (resources is ListNode resourcesLN) + { //multiple implicit resources + resources = new ElementNode(new XmlType(XamlParser.MauiUri, nameof(ResourceDictionary), null), XamlParser.MauiUri, rootNode.NamespaceResolver); + foreach (var n in resourcesLN.CollectionItems) + ((ElementNode)resources).CollectionItems.Add(n); + } + cvv.Visit((ElementNode)resources, null); + + visitorContext.RootElement = rootView; + + resources.Accept(new XamlNodeVisitor((node, parent) => node.Parent = parent), null); //set parents for {StaticResource} + resources.Accept(new ExpandMarkupsVisitor(visitorContext), null); + resources.Accept(new PruneIgnoredNodesVisitor(false), null); + resources.Accept(new NamescopingVisitor(visitorContext), null); //set namescopes for {x:Reference} + resources.Accept(new CreateValuesVisitor(visitorContext), null); + resources.Accept(new RegisterXNamesVisitor(visitorContext), null); + resources.Accept(new FillResourceDictionariesVisitor(visitorContext), null); + resources.Accept(new ApplyPropertiesVisitor(visitorContext, true), null); + + return visitorContext.Values[resources] as IResourceDictionary; + } + } + return null; + } + + static void Visit(RootNode rootnode, HydrationContext visitorContext, bool useDesignProperties) + { + rootnode.Accept(new XamlNodeVisitor((node, parent) => node.Parent = parent), null); //set parents for {StaticResource} + rootnode.Accept(new ExpandMarkupsVisitor(visitorContext), null); + rootnode.Accept(new PruneIgnoredNodesVisitor(useDesignProperties), null); + if (useDesignProperties) + rootnode.Accept(new RemoveDuplicateDesignNodes(), null); + rootnode.Accept(new NamescopingVisitor(visitorContext), null); //set namescopes for {x:Reference} + rootnode.Accept(new CreateValuesVisitor(visitorContext), null); + rootnode.Accept(new RegisterXNamesVisitor(visitorContext), null); + rootnode.Accept(new FillResourceDictionariesVisitor(visitorContext), null); + rootnode.Accept(new ApplyPropertiesVisitor(visitorContext, true), null); + } + + static string GetXamlForType(Type type, object instance, out bool useDesignProperties) + { + useDesignProperties = false; + //the Previewer might want to provide it's own xaml for this... let them do that + //the check at the end is preferred (using ResourceLoader). keep this until all the previewers are updated + + string xaml; + var assembly = type.Assembly; + var resourceId = XamlResourceIdAttribute.GetResourceIdForType(type); + + var rlr = ResourceLoader.ResourceProvider2?.Invoke(new ResourceLoader.ResourceLoadingQuery { + AssemblyName = assembly.GetName(), + ResourcePath = XamlResourceIdAttribute.GetPathForType(type), + Instance = instance, + }); + var alternateXaml = rlr?.ResourceContent; + + if (alternateXaml != null) + { + useDesignProperties = rlr.UseDesignProperties; + return alternateXaml; + } + + if (resourceId == null) + return LegacyGetXamlForType(type); + + using (var stream = assembly.GetManifestResourceStream(resourceId)) + { + if (stream != null) + using (var reader = new StreamReader(stream)) + xaml = reader.ReadToEnd(); + else + xaml = null; + } + + return xaml; + } + + //if the assembly was generated using a version of XamlG that doesn't outputs XamlResourceIdAttributes, we still need to find the resource, and load it + static readonly Dictionary XamlResources = new Dictionary(); + static string LegacyGetXamlForType(Type type) + { + var assembly = type.Assembly; + + string resourceId; + if (XamlResources.TryGetValue(type, out resourceId)) + { + var result = ReadResourceAsXaml(type, assembly, resourceId); + if (result != null) + return result; + } + + var likelyResourceName = type.Name + ".xaml"; + var resourceNames = assembly.GetManifestResourceNames(); + string resourceName = null; + + // first pass, pray to find it because the user named it correctly + + foreach (var resource in resourceNames) + { + if (ResourceMatchesFilename(assembly, resource, likelyResourceName)) + { + resourceName = resource; + var xaml = ReadResourceAsXaml(type, assembly, resource); + if (xaml != null) + return xaml; + } + } + + // okay maybe they at least named it .xaml + + foreach (var resource in resourceNames) + { + if (!resource.EndsWith(".xaml", StringComparison.OrdinalIgnoreCase)) + continue; + + resourceName = resource; + var xaml = ReadResourceAsXaml(type, assembly, resource); + if (xaml != null) + return xaml; + } + + foreach (var resource in resourceNames) + { + if (resource.EndsWith(".xaml", StringComparison.OrdinalIgnoreCase)) + continue; + + resourceName = resource; + var xaml = ReadResourceAsXaml(type, assembly, resource, true); + if (xaml != null) + return xaml; + } + + return null; + } + + //legacy... + static bool ResourceMatchesFilename(Assembly assembly, string resource, string filename) + { + try + { + var info = assembly.GetManifestResourceInfo(resource); + + if (!string.IsNullOrEmpty(info.FileName) && + string.Compare(info.FileName, filename, StringComparison.OrdinalIgnoreCase) == 0) + return true; + } + catch (PlatformNotSupportedException) + { + // Because Win10 + .NET Native + } + + if (resource.EndsWith("." + filename, StringComparison.OrdinalIgnoreCase) || + string.Compare(resource, filename, StringComparison.OrdinalIgnoreCase) == 0) + return true; + + return false; + } + + //part of the legacy as well... + static string ReadResourceAsXaml(Type type, Assembly assembly, string likelyTargetName, bool validate = false) + { + using (var stream = assembly.GetManifestResourceStream(likelyTargetName)) + using (var reader = new StreamReader(stream)) + { + if (validate) + { + // terrible validation of XML. Unfortunately it will probably work most of the time since comments + // also start with a <. We can't bring in any real deps. + + var firstNonWhitespace = (char)reader.Read(); + while (char.IsWhiteSpace(firstNonWhitespace)) + firstNonWhitespace = (char)reader.Read(); + + if (firstNonWhitespace != '<') + return null; + + stream.Seek(0, SeekOrigin.Begin); + } + + var xaml = reader.ReadToEnd(); + + var pattern = $"x:Class *= *\"{type.FullName}\""; + var regex = new Regex(pattern, RegexOptions.ECMAScript); + if (regex.IsMatch(xaml) || xaml.IndexOf($"x:Class=\"{type.FullName}\"") != -1) + return xaml; + } + return null; + } +} +internal static partial class XamlLoader +{ + internal class RuntimeRootNode : RootNode + { + public RuntimeRootNode(XmlType xmlType, object root, IXmlNamespaceResolver resolver) : base(xmlType, resolver) + { + Root = root; + } + + public object Root + { + get; internal set; + } + } +} \ No newline at end of file diff --git a/Microsoft.Maui.Core.DeviceTests/XamlParser.Namespaces.cs b/Microsoft.Maui.Core.DeviceTests/XamlParser.Namespaces.cs new file mode 100644 index 0000000..eec709c --- /dev/null +++ b/Microsoft.Maui.Core.DeviceTests/XamlParser.Namespaces.cs @@ -0,0 +1,12 @@ +namespace Maui.ServerDrivenUI.Xaml; + +static partial class XamlParser +{ + [Obsolete("Should not be used except for migration/error message purposes")] + public const string FormsUri = "http://xamarin.com/schemas/2014/forms"; + public const string MauiUri = "http://schemas.microsoft.com/dotnet/2021/maui"; + public const string MauiDesignUri = "http://schemas.microsoft.com/dotnet/2021/maui/design"; + public const string X2006Uri = "http://schemas.microsoft.com/winfx/2006/xaml"; + public const string X2009Uri = "http://schemas.microsoft.com/winfx/2009/xaml"; + public const string McUri = "http://schemas.openxmlformats.org/markup-compatibility/2006"; +} \ No newline at end of file diff --git a/Microsoft.Maui.Core.DeviceTests/XamlParser.cs b/Microsoft.Maui.Core.DeviceTests/XamlParser.cs new file mode 100644 index 0000000..3d3cdf2 --- /dev/null +++ b/Microsoft.Maui.Core.DeviceTests/XamlParser.cs @@ -0,0 +1,411 @@ +using System.Diagnostics; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Xml; + +namespace Maui.ServerDrivenUI.Xaml; + +static partial class XamlParser +{ + public static void ParseXaml(RootNode rootNode, XmlReader reader) + { + var attributes = ParseXamlAttributes(reader, out IList> xmlns); + var prefixes = PrefixesToIgnore(xmlns); + (rootNode.IgnorablePrefixes ?? (rootNode.IgnorablePrefixes = new List())).AddRange(prefixes); + rootNode.Properties.AddRange(attributes); + ParseXamlElementFor(rootNode, reader); + } + + static void ParseXamlElementFor(IElementNode node, XmlReader reader) + { + Debug.Assert(reader.NodeType == XmlNodeType.Element); + + var elementName = reader.Name; + var isEmpty = reader.IsEmptyElement; + + if (isEmpty) + return; + + while (reader.Read()) + { + switch (reader.NodeType) + { + case XmlNodeType.EndElement: + Debug.Assert(reader.Name == elementName); //make sure we close the right element + return; + case XmlNodeType.Element: + // 1. Property Element. + if (reader.Name.IndexOf(".", StringComparison.Ordinal) != -1) + { + XmlName name; + if (reader.Name.StartsWith(elementName + ".", StringComparison.Ordinal)) + name = new XmlName(reader.NamespaceURI, reader.Name.Substring(elementName.Length + 1)); + else //Attached BP + name = new XmlName(reader.NamespaceURI, reader.LocalName); + + if (node.Properties.ContainsKey(name)) + throw new XamlParseException($"'{reader.Name}' is a duplicate property name.", (IXmlLineInfo)reader); + + INode prop = null; + if (reader.IsEmptyElement) + Debug.WriteLine($"Unexpected empty element '<{reader.Name} />'", (IXmlLineInfo)reader); + else + prop = ReadNode(reader); + + if (prop != null) + node.Properties.Add(name, prop); + } + // 2. Xaml2009 primitives, x:Arguments, ... + else if (reader.NamespaceURI == X2009Uri && reader.LocalName == "Arguments") + { + if (node.Properties.ContainsKey(XmlName.xArguments)) + throw new XamlParseException($"'x:Arguments' is a duplicate directive name.", (IXmlLineInfo)reader); + + var prop = ReadNode(reader); + if (prop != null) + node.Properties.Add(XmlName.xArguments, prop); + } + // 3. DataTemplate (should be handled by 4.) + else if (node.XmlType.NamespaceUri == MauiUri && + (node.XmlType.Name == "DataTemplate" || node.XmlType.Name == "ControlTemplate")) + { + if (node.Properties.ContainsKey(XmlName._CreateContent)) + throw new XamlParseException($"Multiple child elements in {node.XmlType.Name}", (IXmlLineInfo)reader); + + var prop = ReadNode(reader, true); + if (prop != null) + node.Properties.Add(XmlName._CreateContent, prop); + } + // 4. Implicit content, implicit collection, or collection syntax. Add to CollectionItems, resolve case later. + else + { + var item = ReadNode(reader, true); + if (item != null) + node.CollectionItems.Add(item); + } + break; + case XmlNodeType.Whitespace: + break; + case XmlNodeType.Text: + case XmlNodeType.CDATA: + if (node.CollectionItems.Count == 1 && node.CollectionItems[0] is ValueNode) + ((ValueNode)node.CollectionItems[0]).Value += reader.Value.Trim(); + else + node.CollectionItems.Add(new ValueNode(reader.Value.Trim(), (IXmlNamespaceResolver)reader)); + break; + default: + Debug.WriteLine("Unhandled node {0} {1} {2}", reader.NodeType, reader.Name, reader.Value); + break; + } + } + } + + static INode ReadNode(XmlReader reader, bool nested = false) + { + var skipFirstRead = nested; + Debug.Assert(reader.NodeType == XmlNodeType.Element); + var name = reader.Name; + var nodes = new List(); + + while (skipFirstRead || reader.Read()) + { + skipFirstRead = false; + + INode node; + switch (reader.NodeType) + { + case XmlNodeType.EndElement: + Debug.Assert(reader.Name == name); + if (nodes.Count == 0) //Empty element + return null; + if (nodes.Count == 1) + return nodes[0]; + return new ListNode(nodes, (IXmlNamespaceResolver)reader, ((IXmlLineInfo)reader).LineNumber, + ((IXmlLineInfo)reader).LinePosition); + case XmlNodeType.Element: + var isEmpty = reader.IsEmptyElement && reader.Name == name; + var elementName = reader.Name; + var elementNsUri = reader.NamespaceURI; + var elementXmlInfo = (IXmlLineInfo)reader; + IList> xmlns; + + var attributes = ParseXamlAttributes(reader, out xmlns); + var prefixes = PrefixesToIgnore(xmlns); + var typeArguments = GetTypeArguments(attributes); + + node = new ElementNode(new XmlType(elementNsUri, elementName, typeArguments), elementNsUri, + reader as IXmlNamespaceResolver, elementXmlInfo.LineNumber, elementXmlInfo.LinePosition); + ((IElementNode)node).Properties.AddRange(attributes); + (node.IgnorablePrefixes ?? (node.IgnorablePrefixes = new List())).AddRange(prefixes); + + ParseXamlElementFor((IElementNode)node, reader); + nodes.Add(node); + if (isEmpty || nested) + return node; + break; + case XmlNodeType.Text: + case XmlNodeType.CDATA: + node = new ValueNode(reader.Value.Trim(), (IXmlNamespaceResolver)reader, ((IXmlLineInfo)reader).LineNumber, + ((IXmlLineInfo)reader).LinePosition); + nodes.Add(node); + break; + case XmlNodeType.Whitespace: + break; + default: + Debug.WriteLine("Unhandled node {0} {1} {2}", reader.NodeType, reader.Name, reader.Value); + break; + } + } + throw new XamlParseException("Closing PropertyElement expected", (IXmlLineInfo)reader); + } + + internal static IList GetTypeArguments(XmlReader reader) => GetTypeArguments(ParseXamlAttributes(reader, out _)); + + static IList GetTypeArguments(IList> attributes) + { + return attributes.Any(kvp => kvp.Key == XmlName.xTypeArguments) + ? ((ValueNode)attributes.First(kvp => kvp.Key == XmlName.xTypeArguments).Value).Value as IList + : null; + } + + static IList> ParseXamlAttributes(XmlReader reader, out IList> xmlns) + { + Debug.Assert(reader.NodeType == XmlNodeType.Element); + var attributes = new List>(); + xmlns = new List>(); + for (var i = 0; i < reader.AttributeCount; i++) + { + reader.MoveToAttribute(i); + + //skip xmlns + if (reader.NamespaceURI == "http://www.w3.org/2000/xmlns/") + { + xmlns.Add(new KeyValuePair(reader.LocalName, reader.Value)); + continue; + } + + var namespaceUri = reader.NamespaceURI; + if (reader.LocalName.IndexOf(".", StringComparison.Ordinal) != -1 && namespaceUri == "") + namespaceUri = ((IXmlNamespaceResolver)reader).LookupNamespace(""); + var propertyName = ParsePropertyName(new XmlName(namespaceUri, reader.LocalName)); + + if (propertyName.NamespaceURI == null && propertyName.LocalName == null) + continue; + + object value = reader.Value; + + if (propertyName == XmlName.xTypeArguments) + value = TypeArgumentsParser.ParseExpression((string)value, (IXmlNamespaceResolver)reader, (IXmlLineInfo)reader); + + var propertyNode = GetValueNode(value, reader); + attributes.Add(new KeyValuePair(propertyName, propertyNode)); + } + reader.MoveToElement(); + 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(); + foreach (var kvp in xmlns) + { + var prefix = kvp.Key; + + XmlnsHelper.ParseXmlns(kvp.Value, out _, out _, out _, out var targetPlatform); + if (targetPlatform == null) + continue; + + try + { + if (targetPlatform != DeviceInfo.Platform.ToString()) + { + // Special case for Windows backward compatibility + if (targetPlatform == "Windows" && DeviceInfo.Platform == DevicePlatform.WinUI) + continue; + + prefixes.Add(prefix); + } + } + catch (InvalidOperationException) + { + prefixes.Add(prefix); + } + } + return prefixes; + } + + static IValueNode GetValueNode(object value, XmlReader reader) + { + var valueString = value as string; + if (valueString != null && valueString.Trim().StartsWith("{}", StringComparison.Ordinal)) + { + return new ValueNode(valueString.Substring(2), (IXmlNamespaceResolver)reader, ((IXmlLineInfo)reader).LineNumber, + ((IXmlLineInfo)reader).LinePosition); + } + if (valueString != null && valueString.Trim().StartsWith("{", StringComparison.Ordinal)) + { + return new MarkupNode(valueString.Trim(), reader as IXmlNamespaceResolver, ((IXmlLineInfo)reader).LineNumber, + ((IXmlLineInfo)reader).LinePosition); + } + return new ValueNode(value, (IXmlNamespaceResolver)reader, ((IXmlLineInfo)reader).LineNumber, + ((IXmlLineInfo)reader).LinePosition); + } + + static IList s_xmlnsDefinitions; + + static void GatherXmlnsDefinitionAttributes() + { + Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); + s_xmlnsDefinitions = new List(); + + foreach (var assembly in assemblies) + { + try + { + foreach (XmlnsDefinitionAttribute attribute in assembly.GetCustomAttributes(typeof(XmlnsDefinitionAttribute))) + { + s_xmlnsDefinitions.Add(attribute); + attribute.AssemblyName = attribute.AssemblyName ?? assembly.FullName; + } + } + catch (Exception ex) + { + // If we can't load the custom attribute for whatever reason from the assembly, + // We can ignore it and keep going. + Debug.WriteLine($"Failed to parse Assembly Attribute: {ex.ToString()}"); + } + } + } + + public static Type GetElementType(XmlType xmlType, IXmlLineInfo xmlInfo, Assembly currentAssembly, + out XamlParseException exception) + { + bool hasRetriedNsSearch = false; + + retry: + if (s_xmlnsDefinitions == null) + GatherXmlnsDefinitionAttributes(); + + Type type = xmlType.GetTypeReference( + s_xmlnsDefinitions, + currentAssembly?.FullName, + (typeInfo) => { + var t = Type.GetType($"{typeInfo.clrNamespace}.{typeInfo.typeName}, {typeInfo.assemblyName}"); + if (t is not null && t.IsPublicOrVisibleInternal(currentAssembly)) + return t; + return null; + }); + + var typeArguments = xmlType.TypeArguments; + exception = null; + + if (type == null) + { + // This covers the scenario where the AppDomain's loaded + // assemblies might have changed since this method was first + // called. This occurred during unit test runs and could + // conceivably occur in the field. + if (!hasRetriedNsSearch) + { + hasRetriedNsSearch = true; + s_xmlnsDefinitions = null; + goto retry; + } + } + + if (type != null && typeArguments != null) + { + XamlParseException innerexception = null; + var args = typeArguments.Select(delegate (XmlType xmltype) { + var t = GetElementType(xmltype, xmlInfo, currentAssembly, out XamlParseException xpe); + if (xpe != null) + { + innerexception = xpe; + return null; + } + return t; + }).ToArray(); + if (innerexception != null) + { + exception = innerexception; + return null; + } + + try + { + type = type.MakeGenericType(args); + } + catch (InvalidOperationException) + { + exception = new XamlParseException($"Type {type} is not a GenericTypeDefinition", xmlInfo); + } + } + + if (type == null) + exception = new XamlParseException($"Type {xmlType.Name} not found in xmlns {xmlType.NamespaceUri}", xmlInfo); + + return type; + } + + public static bool IsPublicOrVisibleInternal(this Type type, Assembly assembly) + { + if (type.IsPublic || type.IsNestedPublic) + return true; + if (type.Assembly == assembly) + return true; + if (type.Assembly.IsVisibleInternal(assembly)) + return true; + return false; + } + + public static bool IsVisibleInternal(this Assembly from, Assembly to) => + from.GetCustomAttributes().Any(ca => + ca.AssemblyName.StartsWith(to.GetName().Name, StringComparison.InvariantCulture)); +}