File: XamlLoader.cs
Web Access
Project: src\src\Controls\src\Xaml\Controls.Xaml.csproj (Microsoft.Maui.Controls.Xaml)
//
// XamlLoader.cs
//
// Author:
//       Stephane Delcroix <stephane@mi8.be>
//
// Copyright (c) 2013 Mobile Inception
// Copyright (c) 2013-2014 Xamarin, Inc
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
 
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Xml;
using Microsoft.Maui.Controls.Internals;
using Microsoft.Maui.Controls.Xaml.Diagnostics;
 
namespace Microsoft.Maui.Controls.Xaml
{
	[RequiresUnreferencedCode(TrimmerConstants.XamlRuntimeParsingNotSupportedWarning)]
#if !NETSTANDARD
	[RequiresDynamicCode(TrimmerConstants.XamlRuntimeParsingNotSupportedWarning)]
#endif
	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<Exception>)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<Exception>)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;
		}
 
		public 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<Exception>)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 SimplifyTypeExtensionVisitor(), 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 SimplifyTypeExtensionVisitor(), 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<Type, string> XamlResources = new Dictionary<Type, string>();
		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();
 
				if (ContainsXClass(xaml, type.FullName))
				{
					return xaml;
				}
			}
			return null;
 
			// Equivalent to regex $"x:Class *= *\"{fullName}\""
			static bool ContainsXClass(string xaml, string fullName)
			{
				int index = 0;
				while (index >= 0 && index < xaml.Length)
				{
					if (FindNextXClass()
						&& SkipWhitespaces()
						&& NextCharacter('=')
						&& SkipWhitespaces()
						&& NextCharacter('"')
						&& NextFullName()
						&& NextCharacter('"'))
					{
						return true;
					}
				}
 
				return false;
 
				bool FindNextXClass()
				{
					const string xClass = "x:Class";
 
					index = xaml.IndexOf(xClass, startIndex: index);
					if (index < 0)
					{
						return false;
					}
 
					index += xClass.Length;
					return true;
				}
 
				bool SkipWhitespaces()
				{
					while (index < xaml.Length && xaml[index] == ' ')
					{
						index++;
					}
 
					return true;
				}
 
				bool NextCharacter(char character)
				{
					if (index < xaml.Length && xaml[index] != character)
					{
						return false;
					}
 
					index++;
					return true;
				}
 
				bool NextFullName()
				{
					if (index >= xaml.Length - fullName.Length)
					{
						return false;
					}
 
					var slice = xaml.AsSpan().Slice(index, fullName.Length);
					if (!MemoryExtensions.Equals(slice, fullName.AsSpan(), StringComparison.Ordinal))
					{
						return false;
					}
 
					index += fullName.Length;
					return true;
				}
			}
		}
	}
}