File: Routing.cs
Web Access
Project: src\src\Controls\src\Core\Controls.Core.csproj (Microsoft.Maui.Controls)
#nullable disable
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
 
namespace Microsoft.Maui.Controls
{
	/// <include file="../../docs/Microsoft.Maui.Controls/Routing.xml" path="Type[@FullName='Microsoft.Maui.Controls.Routing']/Docs/*" />
	public static class Routing
	{
		static int s_routeCount = 0;
		static Dictionary<string, RouteFactory> s_routes = new(StringComparer.Ordinal);
		static Dictionary<string, Page> s_implicitPageRoutes = new(StringComparer.Ordinal);
		static HashSet<string> s_routeKeys;
 
		const string ImplicitPrefix = "IMPL_";
		const string DefaultPrefix = "D_FAULT_";
		internal const string PathSeparator = "/";
 
		// We only need these while a navigation is happening 
		internal static void ClearImplicitPageRoutes()
		{
			s_implicitPageRoutes.Clear();
			s_routeKeys = null;
		}
 
		internal static void RegisterImplicitPageRoute(Page page)
		{
			var route = GetRoute(page);
			if (!IsUserDefined(route))
			{
				s_implicitPageRoutes[route] = page;
				s_routeKeys = null;
			}
		}
 
		// Shell works much better if the entire nav stack can be represented by a string
		// If the users pushes pages without using routes we want these page keys tracked
		internal static void RegisterImplicitPageRoutes(Shell shell)
		{
			foreach (var item in shell.Items)
			{
				foreach (var section in item.Items)
				{
					var navigationStackCount = section.Navigation.NavigationStack.Count;
					for (int i = 1; i < navigationStackCount; i++)
					{
						RegisterImplicitPageRoute(section.Navigation.NavigationStack[i]);
					}
					var navigationModalStackCount = section.Navigation.ModalStack.Count;
					for (int i = 0; i < navigationModalStackCount; i++)
					{
						var page = section.Navigation.ModalStack[i];
						RegisterImplicitPageRoute(page);
 
						if (page is INavigationPageController np)
						{
							foreach (var npPages in np.Pages)
							{
								RegisterImplicitPageRoute(npPages);
							}
						}
					}
				}
			}
		}
 
		internal static string GenerateImplicitRoute(string source)
		{
			if (IsImplicit(source))
				return source;
 
			return String.Concat(ImplicitPrefix, source);
		}
 
		internal static bool IsImplicit(string source)
		{
			return source.StartsWith(ImplicitPrefix, StringComparison.Ordinal);
		}
 
		internal static bool IsImplicit(BindableObject source)
		{
			return IsImplicit(GetRoute(source));
		}
 
		internal static bool IsDefault(string source)
		{
			return source.StartsWith(DefaultPrefix, StringComparison.Ordinal);
		}
 
		internal static bool IsDefault(BindableObject source)
		{
			return IsDefault(GetRoute(source));
		}
 
		internal static bool IsUserDefined(BindableObject source)
		{
			if (source == null)
				return false;
 
			return IsUserDefined(GetRoute(source));
		}
 
		internal static bool IsUserDefined(string route)
		{
			if (route == null)
				return false;
 
			return !(IsDefault(route) || IsImplicit(route));
		}
 
		internal static void Clear()
		{
			s_implicitPageRoutes.Clear();
			s_routes.Clear();
			s_routeKeys = null;
		}
 
		/// <summary>Bindable property for attached property <c>Route</c>.</summary>
		public static readonly BindableProperty RouteProperty = CreateRouteProperty();
 
		[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2111:ReflectionToDynamicallyAccessedMembers",
			Justification = "The CreateAttached method has a DynamicallyAccessedMembers annotation for all public methods"
			+ "on the declaring type. This includes the Routing.RegisterRoute(string, Type) method which also has a "
			+ "DynamicallyAccessedMembers annotation and the trimmer can't guarantee the availability of the requirements"
			+ "of the method. `BindableProperty` only needs methods starting with `Get`, so `RegisterRoute` is never accessed via reflection.")]
		private static BindableProperty CreateRouteProperty()
			=> BindableProperty.CreateAttached("Route", typeof(string), typeof(Routing), null,
				defaultValueCreator: CreateDefaultRoute);
 
		static object CreateDefaultRoute(BindableObject bindable)
		{
			return $"{DefaultPrefix}{bindable.GetType().Name}{++s_routeCount}";
		}
 
		internal static HashSet<string> GetRouteKeys()
		{
			var keys = s_routeKeys;
			if (keys != null)
				return keys;
 
			keys = new HashSet<string>(StringComparer.Ordinal);
			foreach (var key in s_routes.Keys)
			{
				keys.Add(ShellUriHandler.FormatUri(key));
			}
			foreach (var key in s_implicitPageRoutes.Keys)
			{
				keys.Add(ShellUriHandler.FormatUri(key));
			}
			return s_routeKeys = keys;
		}
 
		/// <include file="../../docs/Microsoft.Maui.Controls/Routing.xml" path="//Member[@MemberName='GetOrCreateContent']/Docs/*" />
#pragma warning disable CS1573 // Parameter has no matching param tag in the XML comment (but other parameters do)
		public static Element GetOrCreateContent(string route, IServiceProvider services = null)
#pragma warning restore CS1573 // Parameter has no matching param tag in the XML comment (but other parameters do)
		{
			Element result = null;
 
			if (s_implicitPageRoutes.TryGetValue(route, out var page))
			{
				return page;
			}
 
			if (s_routes.TryGetValue(route, out var content))
				result = content.GetOrCreate(services);
 
			if (result != null)
				SetRoute(result, route);
 
			return result;
		}
 
		/// <include file="../../docs/Microsoft.Maui.Controls/Routing.xml" path="//Member[@MemberName='GetRoute']/Docs/*" />
		public static string GetRoute(BindableObject obj)
		{
			return (string)obj.GetValue(RouteProperty);
		}
 
		internal static string GetRoutePathIfNotImplicit(Element obj)
		{
			var source = GetRoute(obj);
			if (IsImplicit(source))
				return String.Empty;
 
			return $"{source}/";
		}
 
		/// <include file="../../docs/Microsoft.Maui.Controls/Routing.xml" path="//Member[@MemberName='FormatRoute'][1]/Docs/*" />
		public static string FormatRoute(List<string> segments)
		{
			var route = FormatRoute(String.Join(PathSeparator, segments));
			return route;
		}
 
		/// <include file="../../docs/Microsoft.Maui.Controls/Routing.xml" path="//Member[@MemberName='FormatRoute'][2]/Docs/*" />
		public static string FormatRoute(string route)
		{
			return route;
		}
 
		/// <include file="../../docs/Microsoft.Maui.Controls/Routing.xml" path="//Member[@MemberName='RegisterRoute'][2]/Docs/*" />
		public static void RegisterRoute(string route, RouteFactory factory)
		{
			if (!String.IsNullOrWhiteSpace(route))
				route = FormatRoute(route);
			ValidateRoute(route, factory);
 
			s_routes[route] = factory;
			s_routeKeys = null;
		}
 
		/// <include file="../../docs/Microsoft.Maui.Controls/Routing.xml" path="//Member[@MemberName='UnRegisterRoute']/Docs/*" />
		public static void UnRegisterRoute(string route)
		{
			if (s_routes.Remove(route))
			{
				s_routeKeys = null;
			}
		}
 
		/// <include file="../../docs/Microsoft.Maui.Controls/Routing.xml" path="//Member[@MemberName='RegisterRoute'][1]/Docs/*" />
		public static void RegisterRoute(
			string route,
			[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type type)
		{
			RegisterRoute(route, new TypeRouteFactory(type));
		}
 
		/// <include file="../../docs/Microsoft.Maui.Controls/Routing.xml" path="//Member[@MemberName='SetRoute']/Docs/*" />
		public static void SetRoute(Element obj, string value)
		{
			obj.SetValue(RouteProperty, value);
		}
 
		static void ValidateRoute(string route, RouteFactory routeFactory)
		{
			if (string.IsNullOrWhiteSpace(route))
				throw new ArgumentNullException(nameof(route), "Route cannot be an empty string");
 
			routeFactory = routeFactory ?? throw new ArgumentNullException(nameof(routeFactory), "Route Factory cannot be null");
 
			var uri = new Uri(route, UriKind.RelativeOrAbsolute);
 
			var parts = uri.OriginalString.Split(new[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries);
			foreach (var part in parts)
			{
				if (IsImplicit(part))
					throw new ArgumentException($"Route contains invalid characters in \"{part}\"");
			}
 
			RouteFactory existingRegistration = null;
			if (s_routes.TryGetValue(route, out existingRegistration) && !existingRegistration.Equals(routeFactory))
				throw new ArgumentException($"Duplicated Route: \"{route}\"");
		}
 
		class TypeRouteFactory : RouteFactory
		{
			[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]
			readonly Type _type;
 
			public TypeRouteFactory(
				[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type type)
			{
				_type = type;
			}
 
			public override Element GetOrCreate()
			{
				return (Element)Activator.CreateInstance(_type);
			}
 
			public override Element GetOrCreate(IServiceProvider services)
			{
				if (services != null)
				{
					return (Element)Extensions.DependencyInjection.ActivatorUtilities.GetServiceOrCreateInstance(services, _type);
				}
 
				return (Element)Activator.CreateInstance(_type);
			}
 
			public override bool Equals(object obj)
			{
				if ((obj is TypeRouteFactory typeRouteFactory))
					return typeRouteFactory._type == _type;
 
				return false;
			}
 
			public override int GetHashCode()
			{
				return _type.GetHashCode();
			}
		}
	}
}