File: HotReload\HotReloadHelper.cs
Web Access
Project: src\src\Core\src\Core.csproj (Microsoft.Maui)
#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Maui.Hosting;
 
namespace Microsoft.Maui.HotReload
{
	public static class MauiHotReloadHelper
	{
		static IMauiHandlersCollection? HandlerService;
		//static IMauiHandlersServiceProvider? HandlerServiceProvider;
		public static void RegisterHandlers(IMauiHandlersCollection handlerService)
		{
			HandlerService = handlerService;
		}
		public static void AddActiveView(IHotReloadableView view) => ActiveViews.Add(view);
		public static void Reset()
		{
			replacedViews.Clear();
		}
		public static bool IsEnabled { get; set; } = Debugger.IsAttached;
 
		internal static bool IsSupported
#if !NETSTANDARD
			=> System.Reflection.Metadata.MetadataUpdater.IsSupported;
#else
			=> true;
#endif
 
		public static void Register(IHotReloadableView view, params object[] parameters)
		{
			// Check separately to avoid trim warnings
			if (!IsSupported)
				return;
 
			if (!IsEnabled)
				return;
 
			currentViews[view] = parameters;
		}
 
		public static void UnRegister(IHotReloadableView view)
		{
			// Check separately to avoid trim warnings
			if (!IsSupported)
				return;
 
			if (!IsEnabled)
				return;
 
			currentViews.Remove(view);
		}
		public static bool IsReplacedView(IHotReloadableView view, IView newView)
		{
			// Check separately to avoid trim warnings
			if (!IsSupported)
				return false;
 
			if (!IsEnabled)
				return false;
 
			if (view == null || newView == null)
				return false;
 
			if (!replacedViews.TryGetValue(view.GetType().FullName!, out var newViewType))
				return false;
			return newView.GetType() == newViewType;
		}
		public static IView GetReplacedView(IHotReloadableView view)
		{
			// Check separately to avoid trim warnings
			if (!IsSupported)
				return view;
 
			if (!IsEnabled)
				return view;
 
			var viewType = view.GetType();
			if (!replacedViews.TryGetValue(viewType.FullName!, out var newViewType) || viewType == newViewType)
				return view;
 
			currentViews.TryGetValue(view, out var parameters);
			try
			{
				//TODO: Add in a way to use IoC and DI
				var newView = (IView)(parameters?.Length > 0 ? Activator.CreateInstance(newViewType, args: parameters) : Activator.CreateInstance(newViewType))!;
				TransferState(view, newView);
				return newView;
			}
			catch (MissingMethodException)
			{
				Debug.WriteLine("You are using trying to HotReload a view that requires Parameters. Please call `HotReloadHelper.Register(this, params);` in the constructor;");
				//TODO: Notify that we couldnt hot reload.
				return view;
			}
			catch (Exception ex)
			{
				Debug.WriteLine($"Error Hotreloading type: {newViewType}");
				Debug.WriteLine(ex);
				//TODO: Notify that we couldnt hot reload.
				return view;
			}
 
		}
 
		static void TransferState(IHotReloadableView oldView, IView newView)
		{
			oldView.TransferState(newView);
		}
 
		static internal readonly WeakList<IHotReloadableView> ActiveViews = new WeakList<IHotReloadableView>();
		static Dictionary<string, Type> replacedViews = new(StringComparer.Ordinal);
		static Dictionary<IHotReloadableView, object[]> currentViews = new Dictionary<IHotReloadableView, object[]>();
		static Dictionary<string, List<KeyValuePair<Type, Type>>> replacedHandlers = new(StringComparer.Ordinal);
 
		[RequiresUnreferencedCode("Hot Reload is not trim compatible")]
#if !NETSTANDARD
		[RequiresDynamicCode("Hot Reload is not AOT compatible")]
#endif
		public static void RegisterReplacedView(string oldViewType, Type newViewType)
		{
			// Check separately to avoid trim warnings
			if (!IsSupported)
				return;
 
			if (!IsEnabled)
				return;
 
			Action<MethodInfo> executeStaticMethod = (method) =>
			{
				try
				{
					method?.Invoke(null, null);
				}
				catch (Exception ex)
				{
					Debug.WriteLine($"Error calling {method.Name} on type: {newViewType}");
					Debug.WriteLine(ex);
					//TODO: Notify that we couldnt execute OnHotReload for the Method;
				}
			};
 
			var onHotReloadMethods = newViewType.GetOnHotReloadMethods();
			onHotReloadMethods.ForEach(x => executeStaticMethod(x));
 
			if (typeof(IHotReloadableView).IsAssignableFrom(newViewType))
				replacedViews[oldViewType] = newViewType;
 
			if (typeof(IViewHandler).IsAssignableFrom(newViewType))
			{
				if (replacedHandlers.TryGetValue(oldViewType, out var vTypes))
				{
					foreach (var vType in vTypes)
						RegisterHandler(vType, newViewType);
					return;
				}
 
				_ = HandlerService ?? throw new ArgumentNullException(nameof(HandlerService));
				var assemblies = AppDomain.CurrentDomain.GetAssemblies();
				var t = assemblies.Select(x => x.GetType(oldViewType)).FirstOrDefault(x => x != null);
 
				var views = HandlerService!.Where(x => x.ImplementationType == t).Select(x => new KeyValuePair<Type, Type>(x.ServiceType, x.ImplementationType!)).ToList();
 
 
				replacedHandlers[oldViewType] = views.ToList();
				foreach (var h in views)
				{
					RegisterHandler(h, newViewType);
				}
			}
 
			static void RegisterHandler(KeyValuePair<Type, Type> pair, Type newHandler)
			{
				_ = HandlerService ?? throw new ArgumentNullException(nameof(HandlerService));
				var view = pair.Key;
				var newType = newHandler;
				if (pair.Value.IsGenericType)
					newType = pair.Value.GetGenericTypeDefinition().MakeGenericType(newHandler);
				HandlerService.AddHandler(view, newType);
			}
		}
 
		public static void TriggerReload()
		{
			List<IHotReloadableView>? roots = null;
			while (roots == null)
			{
				try
				{
					roots = ActiveViews.Where(x => x != null && x.Parent == null).ToList();
				}
				catch
				{
					//Sometimes we get list changed exception.
				}
			}
 
			foreach (var view in roots)
			{
				view!.Reload();
			}
		}
		#region Metadata Update Handler
		[RequiresUnreferencedCode("Hot Reload is not trim compatible")]
#if !NETSTANDARD
		[RequiresDynamicCode("Hot Reload is not AOT compatible")]
#endif
		public static void UpdateApplication(Type[] types)
		{
			IsEnabled = true;
			foreach (var t in types)
				RegisterReplacedView(t.FullName ?? "", t);
		}
		public static void ClearCache(Type[] types) => TriggerReload();
		#endregion
	}
}