Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add API to Renderer to trigger a UI refresh on hot reload #30884

Merged
merged 11 commits into from
Mar 18, 2021
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions src/Components/Components/src/ComponentBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.HotReload;
using Microsoft.AspNetCore.Components.Rendering;

namespace Microsoft.AspNetCore.Components
Expand All @@ -21,14 +22,15 @@ namespace Microsoft.AspNetCore.Components
/// Optional base class for components. Alternatively, components may
/// implement <see cref="IComponent"/> directly.
/// </summary>
public abstract class ComponentBase : IComponent, IHandleEvent, IHandleAfterRender
public abstract class ComponentBase : IComponent, IHandleEvent, IHandleAfterRender, IReceiveHotReloadContext
{
private readonly RenderFragment _renderFragment;
private RenderHandle _renderHandle;
private bool _initialized;
private bool _hasNeverRendered = true;
private bool _hasPendingQueuedRender;
private bool _hasCalledOnAfterRender;
private HotReloadContext? _hotReloadContext;

/// <summary>
/// Constructs an instance of <see cref="ComponentBase"/>.
Expand Down Expand Up @@ -102,7 +104,7 @@ protected void StateHasChanged()
return;
}

if (_hasNeverRendered || ShouldRender())
if (_hasNeverRendered || ShouldRender() || (_hotReloadContext?.IsHotReloading ?? false))
{
_hasPendingQueuedRender = true;

Expand Down Expand Up @@ -329,5 +331,10 @@ Task IHandleAfterRender.OnAfterRenderAsync()
// have to use "async void" and do their own exception handling in
// the case where they want to start an async task.
}

void IReceiveHotReloadContext.Receive(HotReloadContext context)
{
_hotReloadContext = context;
}
}
}
16 changes: 16 additions & 0 deletions src/Components/Components/src/HotReload/HotReloadContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

namespace Microsoft.AspNetCore.Components.HotReload
{
/// <summary>
/// A context that indicates when a component is being rendered after a hot reload is applied to the application.
pranavkm marked this conversation as resolved.
Show resolved Hide resolved
/// </summary>
public sealed class HotReloadContext
{
/// <summary>
/// Gets a value that indicates if the application is re-rendering in response to a hot-reload change.
/// </summary>
public bool IsHotReloading { get; internal set; }
}
}
22 changes: 22 additions & 0 deletions src/Components/Components/src/HotReload/HotReloadEnvironment.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;

namespace Microsoft.AspNetCore.Components.HotReload
{
internal class HotReloadEnvironment
pranavkm marked this conversation as resolved.
Show resolved Hide resolved
{
public static readonly HotReloadEnvironment Instance = new(Environment.GetEnvironmentVariable("DOTNET_MODIFIABLE_ASSEMBLIES") == "debug");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any chance that a static constructor prevents the linker from removing the type? I would guess that static constructors may always have side effects and hence always have to be left in and will run.

It's probably not a big deal. I'll leave it up to you whether you think it's worth doing in a less convenient way!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually I have no idea how smart the linker is about figuring out whether the static constructor would run or not.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Static ctors are hard to trim away and guarantee correctness. I believe the only time a static ctor will be trimmed is if the whole type can be trimmed. Maybe also if it was BeforeFieldInit and all the static fields were removed?

@vitek-karas and @MichalStrehovsky would know the exact rules here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the rule is basically:

  • If it's BeforeFieldInit then linker may trim it
    • In this case the .cctor is preserved only if there's a field preserved on the type
  • Otherwise it will always keep it if it keeps the type

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was able to do some "fun" substitutions.xml to trim references to this type. Here's what the trimmed results look like now:

image

Removing the interface and the context is difficult because ComponentBase implements it. If we were determined we could turn the context in to a Func<bool> that would eliminate the context type.


public HotReloadEnvironment(bool isHotReloadEnabled)
{
IsHotReloadEnabled = isHotReloadEnabled;
}

/// <summary>
/// Gets a value that determines if HotReload is configured for this application.
/// </summary>
public bool IsHotReloadEnabled { get; }
}
}
20 changes: 20 additions & 0 deletions src/Components/Components/src/HotReload/HotReloadManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Reflection;

[assembly: AssemblyMetadata("ReceiveHotReloadDeltaNotification", "Microsoft.AspNetCore.Components.HotReload.HotReloadManager")]

namespace Microsoft.AspNetCore.Components.HotReload
{
internal static class HotReloadManager
{
internal static event Action? OnDeltaApplied;

public static void DeltaApplied()
{
OnDeltaApplied?.Invoke();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

namespace Microsoft.AspNetCore.Components.HotReload
{
/// <summary>
/// Allows a component to receive a <see cref="HotReloadContext"/>.
/// </summary>
public interface IReceiveHotReloadContext : IComponent
{
/// <summary>
/// Configures a component to use the hot reload context.
/// </summary>
/// <param name="context"></param>
pranavkm marked this conversation as resolved.
Show resolved Hide resolved
void Receive(HotReloadContext context);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
Expand Down Expand Up @@ -33,6 +33,10 @@
<SuppressBaselineReference Include="Microsoft.JSInterop" Condition=" '$(AspNetCoreMajorMinorVersion)' == '6.0' " />
</ItemGroup>

<ItemGroup>
<EmbeddedResource Include="Properties\ILLink.Substitutions.xml" LogicalName="ILLink.Substitutions.xml" />
</ItemGroup>

<Target Name="_GetNuspecDependencyPackageVersions">
<MSBuild Targets="_GetPackageVersionInfo"
BuildInParallel="$(BuildInParallel)"
Expand Down Expand Up @@ -60,5 +64,6 @@
<NuspecProperty Include="OutputPath=$(OutputPath)" />
<NuspecProperty Include="PackageThirdPartyNoticesFile=$(PackageThirdPartyNoticesFile)" />
</ItemGroup>

pranavkm marked this conversation as resolved.
Show resolved Hide resolved

</Project>
11 changes: 11 additions & 0 deletions src/Components/Components/src/ParameterView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,17 @@ public IReadOnlyDictionary<string, object> ToDictionary()
return result;
}

internal ParameterView Clone()
{
if (ReferenceEquals(_frames, _emptyFrames))
{
return Empty;
}

var dictionary = ToDictionary();
return FromDictionary((IDictionary<string, object?>)dictionary);
pranavkm marked this conversation as resolved.
Show resolved Hide resolved
}

internal ParameterView WithCascadingParameters(IReadOnlyList<CascadingParameterState> cascadingParameters)
=> new ParameterView(_lifetime, _frames, _ownerIndex, cascadingParameters);

Expand Down
1 change: 1 addition & 0 deletions src/Components/Components/src/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Components.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Components.Web.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Components.ProtectedBrowserStorage.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
[assembly: InternalsVisibleTo("Components.TestServer, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]

[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")]
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<linker>
<assembly fullname="Microsoft.AspNetCore.Components" >
<!-- Trim away hot-reload -->
<type fullname="Microsoft.AspNetCore.Components.HotReload.HotReloadEnvironment">
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need/want a feature value here? Or is the idea that whenever you are trimming this assembly, turn off HotReload?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The latter. We don't have any scenarios where we would want hot reload to be present in a published application, so it just seems prudent to trim it all the time. Do you foresee any reason to put this behind a switch?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you foresee any reason to put this behind a switch?

No, not at this time. I was just making sure my assumption was correct. Maybe sticking a quick comment in the xml file would help.

Just an FYI - in dotnet/runtime we also run the trimmer on each shared fx assembly during the build in order to trim away any dead code (which can be there because of shared source files), and in System.Private.CoreLib it will trim the IL for specific OS and architecture, since CoreLib targets each separately. If you were to ever run the trimmer in this way, this file would still take affect and it would trim away the HotReload stuff during your build. I don't think you have plans to do this, but I just wanted to call it out for informational purposes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do want these types to be present when users are developing, so we couldn't trim it as part of our build. Our codebase has fewer platform specific implementations (DataProtection is the only one that comes to mind which has lots of windows specific code).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<method signature="System.Boolean get_IsHotReloadEnabled()" body="stub" value="false" />
</type>
<type fullname="Microsoft.AspNetCore.Components.HotReload.HotReloadContext">
<method signature="System.Boolean get_IsHotReloading()" body="stub" value="false" />
<method signature="System.Void set_IsHotReloading(System.Boolean)" body="remove" />
</type>
</assembly>
pranavkm marked this conversation as resolved.
Show resolved Hide resolved
</linker>
5 changes: 5 additions & 0 deletions src/Components/Components/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ Microsoft.AspNetCore.Components.ComponentApplicationState.PersistAsJson<TValue>(
Microsoft.AspNetCore.Components.ComponentApplicationState.PersistState(string! key, byte[]! value) -> void
Microsoft.AspNetCore.Components.ComponentApplicationState.TryTakeAsJson<TValue>(string! key, out TValue? instance) -> bool
Microsoft.AspNetCore.Components.ComponentApplicationState.TryTakePersistedState(string! key, out byte[]? value) -> bool
Microsoft.AspNetCore.Components.HotReload.HotReloadContext
Microsoft.AspNetCore.Components.HotReload.HotReloadContext.HotReloadContext() -> void
Microsoft.AspNetCore.Components.HotReload.HotReloadContext.IsHotReloading.get -> bool
Microsoft.AspNetCore.Components.HotReload.IReceiveHotReloadContext
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we replace this with a property on RenderHandle? e.g. RenderHandle.IsHotReloading

Microsoft.AspNetCore.Components.HotReload.IReceiveHotReloadContext.Receive(Microsoft.AspNetCore.Components.HotReload.HotReloadContext! context) -> void
Microsoft.AspNetCore.Components.Lifetime.ComponentApplicationLifetime
Microsoft.AspNetCore.Components.Lifetime.ComponentApplicationLifetime.ComponentApplicationLifetime(Microsoft.Extensions.Logging.ILogger<Microsoft.AspNetCore.Components.Lifetime.ComponentApplicationLifetime!>! logger) -> void
Microsoft.AspNetCore.Components.Lifetime.ComponentApplicationLifetime.PersistStateAsync(Microsoft.AspNetCore.Components.Lifetime.IComponentApplicationStateStore! store, Microsoft.AspNetCore.Components.RenderTree.Renderer! renderer) -> System.Threading.Tasks.Task!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -535,10 +535,15 @@ private static void UpdateRetainedChildComponent(
// comparisons it wants with the old values. Later we could choose to pass the
// old parameter values if we wanted. By default, components always rerender
// after any SetParameters call, which is safe but now always optimal for perf.

// When performing hot reload, we want to force all components to re-render.
// We do this using two mechanisms - we call SetParametersAsync even if the parameters
// are unchanged and we ignore ComponentBase.ShouldRender

var oldParameters = new ParameterView(ParameterViewLifetime.Unbound, oldTree, oldComponentIndex);
var newParametersLifetime = new ParameterViewLifetime(diffContext.BatchBuilder);
var newParameters = new ParameterView(newParametersLifetime, newTree, newComponentIndex);
if (!newParameters.DefinitelyEquals(oldParameters))
if (!newParameters.DefinitelyEquals(oldParameters) || diffContext.Renderer.HotReloadContext.IsHotReloading)
{
componentState.SetDirectParameters(newParameters);
}
Expand Down Expand Up @@ -696,8 +701,8 @@ private static void AppendDiffEntriesForFramesWithSameSequence(
break;
}

// We don't handle attributes here, they have their own diff logic.
// See AppendDiffEntriesForAttributeFrame
// We don't handle attributes here, they have their own diff logic.
// See AppendDiffEntriesForAttributeFrame
default:
throw new NotImplementedException($"Encountered unsupported frame type during diffing: {newTree[newFrameIndex].FrameTypeField}");
}
Expand Down
57 changes: 55 additions & 2 deletions src/Components/Components/src/RenderTree/Renderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.HotReload;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
Expand All @@ -32,6 +33,8 @@ public abstract partial class Renderer : IDisposable, IAsyncDisposable
private readonly Dictionary<ulong, ulong> _eventHandlerIdReplacements = new Dictionary<ulong, ulong>();
private readonly ILogger<Renderer> _logger;
private readonly ComponentFactory _componentFactory;
private readonly HotReloadEnvironment _hotReloadEnvironment;
private List<(ComponentState, ParameterView)>? _rootComponents;

private int _nextComponentId = 0; // TODO: change to 'long' when Mono .NET->JS interop supports it
private bool _isBatchInProgress;
Expand Down Expand Up @@ -92,6 +95,10 @@ public Renderer(IServiceProvider serviceProvider, ILoggerFactory loggerFactory,
_serviceProvider = serviceProvider;
_logger = loggerFactory.CreateLogger<Renderer>();
_componentFactory = new ComponentFactory(componentActivator);

_hotReloadEnvironment = serviceProvider.GetService<HotReloadEnvironment>() ?? HotReloadEnvironment.Instance;
pranavkm marked this conversation as resolved.
Show resolved Hide resolved

HotReloadManager.OnDeltaApplied += RenderRootComponentsOnHotReload;
}

private static IComponentActivator GetComponentActivatorOrDefault(IServiceProvider serviceProvider)
Expand All @@ -101,7 +108,7 @@ private static IComponentActivator GetComponentActivatorOrDefault(IServiceProvid
}

/// <summary>
/// Gets the <see cref="Microsoft.AspNetCore.Components.Dispatcher" /> associated with this <see cref="Renderer" />.
/// Gets the <see cref="Components.Dispatcher" /> associated with this <see cref="Renderer" />.
/// </summary>
public abstract Dispatcher Dispatcher { get; }

Expand All @@ -111,13 +118,49 @@ private static IComponentActivator GetComponentActivatorOrDefault(IServiceProvid
/// </summary>
protected internal ElementReferenceContext? ElementReferenceContext { get; protected set; }

internal HotReloadContext HotReloadContext { get; } = new();

private async void RenderRootComponentsOnHotReload()
{
await Dispatcher.InvokeAsync(async () =>
{
Debug.Assert(_rootComponents is not null);
HotReloadContext.IsHotReloading = true;

try
{
_pendingTasks = new List<Task>();
foreach (var (componentState, initialParameters) in _rootComponents)
{
AddToPendingTasks(componentState.Component.SetParametersAsync(initialParameters));
}

await ProcessAsynchronousWork();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure we should really be doing this asynchronously? That could take a completely arbitrary amount of time, maybe minutes (waiting for any component in the hierarchy to load data). During this indefinite period, there could be:

  • Arbitrary user interactions, which will render weirdly because of the hot reload flag
  • Other hot reload deltas being applied, interfering with the shared _pendingTasks field

I'm suspicious that this should really only be synchronous. We only need the component tree to re-render itself once immediately to update the state in the existing UI. If components do some async data loading, it's up to them to be able to re-render themselves afterwards.

Debug.Assert(_pendingTasks.Count == 0);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also brings up the point that, in some applications, hot reload refreshes might not be as state-preserving as you'd want. Consider if a root (or deep) component always does some async data loading while showing Loading... instead of its child content, then instantiates its descendant subtree. In that case, each code delta is going to retrigger this flow, and you won't get the instant update in the UI you're actually working on (you'll see "Loading..."), and any transient state in descendants will be discarded.

I'm not disputing that you're doing the right thing by re-rendering the whole tree (I'm disputing the async thing, but that's separate). I'm just realising that while our demo cases hot-reloaded really well, there will be some other cases that don't. It will be up to app developers to make their component hierarchies refresh in a nondestructive way to get the best experience. For example they should try to do data loading in OnInitializedAsync rather than OnParametersSetAsync (except if they manually compare old and new parameters) so that each hot reload doesn't rebuild the descendant tree from scratch. We should think about how to set this expectation in how we document and communicate the feature (cc @danroth27).

finally
{
_pendingTasks = null;
HotReloadContext.IsHotReloading = false;
}
});
}

/// <summary>
/// Constructs a new component of the specified type.
/// </summary>
/// <param name="componentType">The type of the component to instantiate.</param>
/// <returns>The component instance.</returns>
protected IComponent InstantiateComponent([DynamicallyAccessedMembers(Component)] Type componentType)
=> _componentFactory.InstantiateComponent(_serviceProvider, componentType);
{
var component = _componentFactory.InstantiateComponent(_serviceProvider, componentType);
if (_hotReloadEnvironment.IsHotReloadEnabled && component is IReceiveHotReloadContext receiveHotReloadContext)
{
receiveHotReloadContext.Receive(HotReloadContext);
}

return component;
}

/// <summary>
/// Associates the <see cref="IComponent"/> with the <see cref="Renderer"/>, assigning
Expand Down Expand Up @@ -182,6 +225,14 @@ protected async Task RenderRootComponentAsync(int componentId, ParameterView ini
// During the asynchronous rendering process we want to wait up until all components have
// finished rendering so that we can produce the complete output.
var componentState = GetRequiredComponentState(componentId);
if (_hotReloadEnvironment.IsHotReloadEnabled)
{
// when we're doing hot-reload, stash away the parameters used while rendering root components.
// We'll use this to trigger re-renders on hot reload updates.
_rootComponents ??= new();
_rootComponents.Add((componentState, initialParameters.Clone()));
}

componentState.SetDirectParameters(initialParameters);

try
Expand Down Expand Up @@ -884,6 +935,8 @@ public void Dispose()
/// <inheritdoc />
public async ValueTask DisposeAsync()
{
HotReloadManager.OnDeltaApplied -= RenderRootComponentsOnHotReload;

if (_disposed)
{
return;
Expand Down
4 changes: 4 additions & 0 deletions src/Components/Web.JS/src/Boot.WebAssembly.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ async function boot(options?: Partial<WebAssemblyStartOptions>): Promise<void> {

Blazor._internal.InputFile = WasmInputFile;

Blazor._internal.applyHotReload = (id: string, metadataDelta: string, ilDeta: string) => {
DotNet.invokeMethod('Microsoft.AspNetCore.Components.WebAssembly', 'ApplyHotReloadDelta', id, metadataDelta, ilDeta);
};

// Configure JS interop
Blazor._internal.invokeJSFromDotNet = invokeJSFromDotNet;

Expand Down
3 changes: 2 additions & 1 deletion src/Components/Web.JS/src/GlobalExports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ interface IBlazor {
readSatelliteAssemblies?: () => System_Array<System_Object>,
getLazyAssemblies?: any
dotNetCriticalError?: any
getSatelliteAssemblies?: any
getSatelliteAssemblies?: any,
applyHotReload?: (id: string, metadataDelta: string, ilDeta: string) => void
pranavkm marked this conversation as resolved.
Show resolved Hide resolved
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.HotReload;
using Microsoft.AspNetCore.Components.RenderTree;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Rendering;
Expand Down Expand Up @@ -40,5 +44,23 @@ public static Task DispatchEvent(WebEventDescriptor eventDescriptor, string even
webEvent.EventFieldInfo,
webEvent.EventArgs);
}

/// <summary>
/// For framework use only.
/// </summary>
[JSInvokable(nameof(ApplyHotReloadDelta))]
public static void ApplyHotReloadDelta(string moduleId, byte[] metadataDelta, byte[] ilDeta)
{
var moduleIdGuid = Guid.Parse(moduleId);
var assembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => a.Modules.FirstOrDefault() is Module m && m.ModuleVersionId == moduleIdGuid);

if (assembly is not null)
{
System.Reflection.Metadata.AssemblyExtensions.ApplyUpdate(assembly, metadataDelta, ilDeta, ReadOnlySpan<byte>.Empty);
}

// Remove this once there's a runtime API to subscribe to.
typeof(ComponentBase).Assembly.GetType("Microsoft.AspNetCore.Components.HotReload.HotReloadManager")!.GetMethod("DeltaApplied", BindingFlags.NonPublic | BindingFlags.Static)!.Invoke(null, null);
}
}
}
Loading