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));
+}