Skip to content

Commit

Permalink
Merge pull request #12808 from unoplatform/dev/nr/issue12807
Browse files Browse the repository at this point in the history
feat: Adding ElementMetadataUpdateHandlerAttribute
  • Loading branch information
nickrandolph authored Jul 10, 2023
2 parents a61e4ba + 70c3512 commit 661b53b
Show file tree
Hide file tree
Showing 3 changed files with 369 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,25 @@ partial class ClientHotReloadProcessor : IRemoteControlProcessor
{
private bool _linkerEnabled;
private HotReloadAgent _agent;
private ElementUpdateAgent? _elementAgent;
private static ClientHotReloadProcessor? _instance;

private ElementUpdateAgent ElementAgent
{
get
{
_elementAgent ??= new ElementUpdateAgent(s =>
{
if (this.Log().IsEnabled(LogLevel.Trace))
{
this.Log().Trace(s);
}
});

return _elementAgent;
}
}

[MemberNotNull(nameof(_agent))]
partial void InitializeMetadataUpdater()
{
Expand Down Expand Up @@ -152,24 +169,52 @@ private static void ReloadWithUpdatedTypes(Type[] updatedTypes)
return;
}

foreach (var (element, elementMappedType) in EnumerateHotReloadInstances(Window.Current.Content,
fe =>
{
var originalType = fe.GetType().GetOriginalType() ?? fe.GetType();
var handlerActions = _instance?.ElementAgent?.ElementHandlerActions;

// Action: BeforeVisualTreeUpdate
// This is called before the visual tree is updated
handlerActions?.Do(h => h.Value.BeforeVisualTreeUpdate(updatedTypes));

var mappedType = originalType.GetMappedType();
return (mappedType is not null) ? (fe, mappedType) : default;
}, enumerateChildrenAfterMatch: true))
// Iterate through the visual tree and either invole ElementUpdate,
// or replace the element with a new one
foreach (
var (element, elementHandler, elementMappedType) in
EnumerateHotReloadInstances(
Window.Current.Content,
fe =>
{
// Get the original type of the element, in case it's been replaced
var originalType = fe.GetType().GetOriginalType() ?? fe.GetType();

// Get the handler for the type specified
var handler = (from h in handlerActions
where originalType == h.Key ||
originalType.IsSubclassOf(h.Key)
select h.Value).FirstOrDefault();

// Get the replacement type, or null if not replaced
var mappedType = originalType.GetMappedType();

return (handler is not null || mappedType is not null) ? (fe, handler, mappedType) : default;
},
enumerateChildrenAfterMatch: true))
{

// Action: ElementUpdate
// This is invoked for each existing element that is in the tree that needs to be replaced
elementHandler?.ElementUpdate(element, updatedTypes);

if (elementMappedType is not null)
{
ReplaceViewInstance(element, elementMappedType);
ReplaceViewInstance(element, elementMappedType, elementHandler);
}
}

// Action: AfterVisualTreeUpdate
handlerActions?.Do(h => h.Value.AfterVisualTreeUpdate(updatedTypes));
}

private static void ReplaceViewInstance(UIElement instance, Type replacementType, Type[]? updatedTypes = default)
private static void ReplaceViewInstance(UIElement instance, Type replacementType, ElementUpdateAgent.ElementUpdateHandlerActions? handler = default, Type[]? updatedTypes = default)
{
if (replacementType.GetConstructor(Array.Empty<Type>()) is { } creator)
{
Expand All @@ -181,6 +226,11 @@ private static void ReplaceViewInstance(UIElement instance, Type replacementType
var newInstance = Activator.CreateInstance(replacementType);
var instanceFE = instance as FrameworkElement;
var newInstanceFE = newInstance as FrameworkElement;
if (instanceFE is not null &&
newInstanceFE is not null)
{
handler?.BeforeElementReplaced(instanceFE, newInstanceFE, updatedTypes);
}
switch (instance)
{
#if __IOS__
Expand All @@ -198,6 +248,12 @@ private static void ReplaceViewInstance(UIElement instance, Type replacementType
}
break;
}

if (instanceFE is not null &&
newInstanceFE is not null)
{
handler?.AfterElementReplaced(instanceFE, newInstanceFE, updatedTypes);
}
}
else
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,284 @@
// The structure of the ElementUpdaterAgent has been kept similar to the HotReloadAgent,
// which is based on the implementation in https://github.com/dotnet/aspnetcore/blob/26e3dfc7f3f3a91ba445ec0f8b1598d12542fb9f/src/Components/WebAssembly/WebAssembly/src/HotReload/HotReloadAgent.cs



#if NET6_0_OR_GREATER || __WASM__ || __SKIA__
#nullable enable

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using System.Reflection.Metadata;
using Windows.UI.Xaml;
using Uno;


namespace Uno.UI.RemoteControl.HotReload.MetadataUpdater;

internal sealed class ElementUpdateAgent : IDisposable
{
/// Flags for hot reload handler Types like MVC's HotReloadService.
private const DynamicallyAccessedMemberTypes HotReloadHandlerLinkerFlags = DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods;

private readonly Action<string> _log;
private readonly AssemblyLoadEventHandler _assemblyLoad;
private readonly ConcurrentDictionary<Type, ElementUpdateHandlerActions> _elementHandlerActions = new();

internal const string MetadataUpdaterType = "System.Reflection.Metadata.MetadataUpdater";

public ElementUpdateAgent(Action<string> log)
{
_log = log;
_assemblyLoad = OnAssemblyLoad;
AppDomain.CurrentDomain.AssemblyLoad += _assemblyLoad;
LoadElementUpdateHandlerActions();
}

public ImmutableDictionary<Type, ElementUpdateHandlerActions> ElementHandlerActions => _elementHandlerActions.ToImmutableDictionary();

private void OnAssemblyLoad(object? _, AssemblyLoadEventArgs eventArgs) =>
// This should only be invoked on the (rare) occasion that assemblies
// haven't been loaded when the agent is initialized. Since the agent
// is initialized when the first UpdateApplication call is invoked on
// the ClientHotReloadProcessor, most assemblies should already be loaded.
// For this reason, we don't worry about incrementally loading handlers
// we just reload from all assemblies
LoadElementUpdateHandlerActions();

internal sealed class ElementUpdateHandlerActions
{
/// <summary>
/// This will get invoked whenever UpdateApplication is invoked
/// but before any updates are applied to the visual tree.
/// This is only invoked once per UpdateApplication,
/// irrespective of the number of types the handler is registered for
/// </summary>
public Action<Type[]?> BeforeVisualTreeUpdate { get; set; } = _ => { };

/// <summary>
/// This will get invoked whenever UpdateApplication is invoked
/// after all updates have been applied to the visual tree.
/// This is only invoked once per UpdateApplication,
/// irrespective of the number of types the handler is registered for
/// </summary>
public Action<Type[]?> AfterVisualTreeUpdate { get; set; } = _ => { };

/// <summary>
/// This is invoked when a specific element is found in the tree.
/// This would be useful if the element holds references to controls
/// that aren't in the visual tree and need to be updated
/// (eg pages in the backstack of a frame)
/// </summary>
public Action<FrameworkElement, Type[]?> ElementUpdate { get; set; } = (_, _) => { };

/// <summary>
/// This is invoked whenever UpdateApplication is invoked,
/// before an element is replaced in the visual three.
/// This is invoked for each element in the visual tree that
/// matches a type that has been updated.
/// The oldElement is attached to the visual tree and existing datacontext.
/// The newElement is not attached to the visual tree and won't yet have a data context
/// </summary>
public Action<FrameworkElement, FrameworkElement, Type[]?> BeforeElementReplaced { get; set; } = (_, _, _) => { };

/// <summary>
/// This is invoked whenever UpdateApplication is invoked,
/// after an element is replaced in the visual three.
/// This is invoked for each element in the visual tree that
/// matches a type that has been updated.
/// The oldElement is no longer attached to the visual tree and datacontext will be null.
/// The newElement is attached to the visual tree and will have data context update, either inherited from parent or copies from the oldElement.
/// </summary>
public Action<FrameworkElement, FrameworkElement, Type[]?> AfterElementReplaced { get; set; } = (_, _, _) => { };
}

#if NET6_0_OR_GREATER
[UnconditionalSuppressMessage("Trimmer", "IL2072",
Justification = "The handlerType passed to GetHandlerActions is preserved by MetadataUpdateHandlerAttribute with DynamicallyAccessedMemberTypes.All.")]
#endif
private void LoadElementUpdateHandlerActions()
{
// We need to execute MetadataUpdateHandlers in a well-defined order. For v1, the strategy that is used is to topologically
// sort assemblies so that handlers in a dependency are executed before the dependent (e.g. the reflection cache action
// in System.Private.CoreLib is executed before System.Text.Json clears it's own cache.)
// This would ensure that caches and updates more lower in the application stack are up to date
// before ones higher in the stack are recomputed.
var sortedAssemblies = TopologicalSort(AppDomain.CurrentDomain.GetAssemblies());
_elementHandlerActions.Clear();
foreach (var assembly in sortedAssemblies)
{
foreach (var attr in assembly.GetCustomAttributesData())
{
// Look up the attribute by name rather than by type. This would allow netstandard targeting libraries to
// define their own copy without having to cross-compile.
if (attr.AttributeType.FullName == "System.Reflection.Metadata.ElementMetadataUpdateHandlerAttribute")
{

var ctorArgs = attr.ConstructorArguments;
if (ctorArgs.Count != 2 ||
ctorArgs[0].Value is not Type elementType ||
ctorArgs[1].Value is not Type handlerType)
{
_log($"'{attr}' found with invalid arguments.");
continue;
}

GetElementHandlerActions(elementType, handlerType);
}
}
}
}

internal void GetElementHandlerActions(
[DynamicallyAccessedMembers(HotReloadHandlerLinkerFlags)]
Type elementType,
[DynamicallyAccessedMembers(HotReloadHandlerLinkerFlags)]
Type handlerType)
{
bool methodFound = false;

var updateActions = new ElementUpdateHandlerActions();
_elementHandlerActions[elementType] = updateActions;

if (GetUpdateMethod(handlerType, nameof(ElementUpdateHandlerActions.BeforeVisualTreeUpdate)) is MethodInfo beforeVisualTreeUpdate)
{
updateActions.BeforeVisualTreeUpdate = CreateAction(beforeVisualTreeUpdate);
methodFound = true;
}

if (GetUpdateMethod(handlerType, nameof(ElementUpdateHandlerActions.AfterVisualTreeUpdate)) is MethodInfo afterVisualTreeUpdate)
{
updateActions.AfterVisualTreeUpdate = CreateAction(afterVisualTreeUpdate);
methodFound = true;
}

if (GetHandlerMethod(handlerType, nameof(ElementUpdateHandlerActions.ElementUpdate), new[] { typeof(FrameworkElement), typeof(Type[]) }) is MethodInfo elementUpdate)
{
updateActions.ElementUpdate = CreateHandlerAction<Action<FrameworkElement, Type[]?>>(elementUpdate);
methodFound = true;
}

if (GetHandlerMethod(
handlerType,
nameof(ElementUpdateHandlerActions.BeforeElementReplaced),
new[] { typeof(FrameworkElement), typeof(FrameworkElement), typeof(Type[]) }) is MethodInfo beforeElementReplaced)
{
updateActions.BeforeElementReplaced = CreateHandlerAction<Action<FrameworkElement, FrameworkElement, Type[]?>>(beforeElementReplaced);
methodFound = true;
}

if (GetHandlerMethod(
handlerType,
nameof(ElementUpdateHandlerActions.AfterElementReplaced),
new[] { typeof(FrameworkElement), typeof(FrameworkElement), typeof(Type[]) }) is MethodInfo afterElementReplaced)
{
updateActions.AfterElementReplaced = CreateHandlerAction<Action<FrameworkElement, FrameworkElement, Type[]?>>(afterElementReplaced);
methodFound = true;
}

if (!methodFound)
{
_log($"No invokable methods found on metadata handler type '{handlerType}'. " +
$"Allowed methods are BeforeVisualTreeUpdate, AfterVisualTreeUpdate, ElementUpdate, BeforeElementReplaced, AfterElementReplaced");
}
else
{
_log($"Invokable methods found on metadata handler type '{handlerType}'. ");
}
}

private MethodInfo? GetUpdateMethod(
[DynamicallyAccessedMembers(HotReloadHandlerLinkerFlags)]
Type handlerType, string name)
=> GetHandlerMethod(handlerType, name, new[] { typeof(Type[]) });

private MethodInfo? GetHandlerMethod(
[DynamicallyAccessedMembers(HotReloadHandlerLinkerFlags)]
Type handlerType, string name, Type[] parameterTypes)
{
if (handlerType.GetMethod(name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static, null, parameterTypes, null) is MethodInfo updateMethod &&
updateMethod.ReturnType == typeof(void))
{
return updateMethod;
}

foreach (MethodInfo method in handlerType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance))
{
if (method.Name == name)
{
_log($"Type '{handlerType}' has method '{method}' that does not match the required signature.");
break;
}
}

return null;
}

private Action<Type[]?> CreateAction(MethodInfo update)
{
var action = CreateHandlerAction<Action<Type[]?>>(update);
return types =>
{
try
{
action(types);
}
catch (Exception ex)
{
_log($"Exception from '{action}': {ex}");
}
};
}

private TAction CreateHandlerAction<TAction>(MethodInfo update) where TAction : Delegate
{
TAction action = (TAction)update.CreateDelegate(typeof(TAction));
return action;
}

internal static List<Assembly> TopologicalSort(Assembly[] assemblies)
{
var sortedAssemblies = new List<Assembly>(assemblies.Length);

var visited = new HashSet<string>(StringComparer.Ordinal);

foreach (var assembly in assemblies)
{
Visit(assemblies, assembly, sortedAssemblies, visited);
}

[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Hot reload is only expected to work when trimming is disabled.")]
static void Visit(Assembly[] assemblies, Assembly assembly, List<Assembly> sortedAssemblies, HashSet<string> visited)
{
var assemblyIdentifier = assembly.GetName().Name!;
if (!visited.Add(assemblyIdentifier))
{
return;
}

foreach (var dependencyName in assembly.GetReferencedAssemblies())
{
var dependency = Array.Find(assemblies, a => a.GetName().Name == dependencyName.Name);
if (dependency is not null)
{
Visit(assemblies, dependency, sortedAssemblies, visited);
}
}

sortedAssemblies.Add(assembly);
}

return sortedAssemblies;
}

public void Dispose()
=> AppDomain.CurrentDomain.AssemblyLoad -= _assemblyLoad;

}

#endif
Loading

0 comments on commit 661b53b

Please sign in to comment.