Skip to content

Commit ce689ad

Browse files
authored
Add API to Renderer to trigger a UI refresh on hot reload (#30884)
* Add API to Renderer to trigger a UI refresh on hot reload Fixes #30816
1 parent 7211288 commit ce689ad

26 files changed

+498
-20
lines changed

src/Components/Components/src/ComponentBase.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.Threading.Tasks;
6+
using Microsoft.AspNetCore.Components.HotReload;
67
using Microsoft.AspNetCore.Components.Rendering;
78

89
namespace Microsoft.AspNetCore.Components
@@ -21,14 +22,15 @@ namespace Microsoft.AspNetCore.Components
2122
/// Optional base class for components. Alternatively, components may
2223
/// implement <see cref="IComponent"/> directly.
2324
/// </summary>
24-
public abstract class ComponentBase : IComponent, IHandleEvent, IHandleAfterRender
25+
public abstract class ComponentBase : IComponent, IHandleEvent, IHandleAfterRender, IReceiveHotReloadContext
2526
{
2627
private readonly RenderFragment _renderFragment;
2728
private RenderHandle _renderHandle;
2829
private bool _initialized;
2930
private bool _hasNeverRendered = true;
3031
private bool _hasPendingQueuedRender;
3132
private bool _hasCalledOnAfterRender;
33+
private HotReloadContext? _hotReloadContext;
3234

3335
/// <summary>
3436
/// Constructs an instance of <see cref="ComponentBase"/>.
@@ -102,7 +104,7 @@ protected void StateHasChanged()
102104
return;
103105
}
104106

105-
if (_hasNeverRendered || ShouldRender())
107+
if (_hasNeverRendered || ShouldRender() || (_hotReloadContext?.IsHotReloading ?? false))
106108
{
107109
_hasPendingQueuedRender = true;
108110

@@ -329,5 +331,10 @@ Task IHandleAfterRender.OnAfterRenderAsync()
329331
// have to use "async void" and do their own exception handling in
330332
// the case where they want to start an async task.
331333
}
334+
335+
void IReceiveHotReloadContext.Receive(HotReloadContext context)
336+
{
337+
_hotReloadContext = context;
338+
}
332339
}
333340
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
namespace Microsoft.AspNetCore.Components.HotReload
5+
{
6+
/// <summary>
7+
/// A context that indicates when a component is being rendered because of a hot reload operation.
8+
/// </summary>
9+
public sealed class HotReloadContext
10+
{
11+
/// <summary>
12+
/// Gets a value that indicates if the application is re-rendering in response to a hot-reload change.
13+
/// </summary>
14+
public bool IsHotReloading { get; internal set; }
15+
}
16+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
6+
namespace Microsoft.AspNetCore.Components.HotReload
7+
{
8+
internal class HotReloadEnvironment
9+
{
10+
public static readonly HotReloadEnvironment Instance = new(Environment.GetEnvironmentVariable("DOTNET_MODIFIABLE_ASSEMBLIES") == "debug");
11+
12+
public HotReloadEnvironment(bool isHotReloadEnabled)
13+
{
14+
IsHotReloadEnabled = isHotReloadEnabled;
15+
}
16+
17+
/// <summary>
18+
/// Gets a value that determines if HotReload is configured for this application.
19+
/// </summary>
20+
public bool IsHotReloadEnabled { get; }
21+
}
22+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Reflection;
6+
7+
[assembly: AssemblyMetadata("ReceiveHotReloadDeltaNotification", "Microsoft.AspNetCore.Components.HotReload.HotReloadManager")]
8+
9+
namespace Microsoft.AspNetCore.Components.HotReload
10+
{
11+
internal static class HotReloadManager
12+
{
13+
internal static event Action? OnDeltaApplied;
14+
15+
public static void DeltaApplied()
16+
{
17+
OnDeltaApplied?.Invoke();
18+
}
19+
}
20+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
namespace Microsoft.AspNetCore.Components.HotReload
5+
{
6+
/// <summary>
7+
/// Allows a component to receive a <see cref="HotReloadContext"/>.
8+
/// </summary>
9+
public interface IReceiveHotReloadContext : IComponent
10+
{
11+
/// <summary>
12+
/// Configures a component to use the hot reload context.
13+
/// </summary>
14+
/// <param name="context">The hot reload context.</param>
15+
void Receive(HotReloadContext context);
16+
}
17+
}

src/Components/Components/src/Microsoft.AspNetCore.Components.csproj

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22

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

36+
<ItemGroup>
37+
<EmbeddedResource Include="Properties\ILLink.Substitutions.xml" LogicalName="ILLink.Substitutions.xml" />
38+
</ItemGroup>
39+
3640
<Target Name="_GetNuspecDependencyPackageVersions">
3741
<MSBuild Targets="_GetPackageVersionInfo"
3842
BuildInParallel="$(BuildInParallel)"

src/Components/Components/src/ParameterView.cs

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,21 @@ public IReadOnlyDictionary<string, object> ToDictionary()
116116
return result;
117117
}
118118

119+
internal ParameterView Clone()
120+
{
121+
if (ReferenceEquals(_frames, _emptyFrames))
122+
{
123+
return Empty;
124+
}
125+
126+
var numEntries = GetEntryCount();
127+
var cloneBuffer = new RenderTreeFrame[1 + numEntries];
128+
cloneBuffer[0] = RenderTreeFrame.PlaceholderChildComponentWithSubtreeLength(1 + numEntries);
129+
_frames.AsSpan(1, numEntries).CopyTo(cloneBuffer.AsSpan(1));
130+
131+
return new ParameterView(Lifetime, cloneBuffer, _ownerIndex);
132+
}
133+
119134
internal ParameterView WithCascadingParameters(IReadOnlyList<CascadingParameterState> cascadingParameters)
120135
=> new ParameterView(_lifetime, _frames, _ownerIndex, cascadingParameters);
121136

@@ -189,11 +204,7 @@ internal void CaptureSnapshot(ArrayBuilder<RenderTreeFrame> builder)
189204
{
190205
builder.Clear();
191206

192-
var numEntries = 0;
193-
foreach (var entry in this)
194-
{
195-
numEntries++;
196-
}
207+
var numEntries = GetEntryCount();
197208

198209
// We need to prefix the captured frames with an "owner" frame that
199210
// describes the length of the buffer so that ParameterView
@@ -207,6 +218,17 @@ internal void CaptureSnapshot(ArrayBuilder<RenderTreeFrame> builder)
207218
}
208219
}
209220

221+
private int GetEntryCount()
222+
{
223+
var numEntries = 0;
224+
foreach (var _ in this)
225+
{
226+
numEntries++;
227+
}
228+
229+
return numEntries;
230+
}
231+
210232
/// <summary>
211233
/// Creates a new <see cref="ParameterView"/> from the given <see cref="IDictionary{TKey, TValue}"/>.
212234
/// </summary>

src/Components/Components/src/Properties/AssemblyInfo.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,6 @@
88
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Components.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
99
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Components.Web.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
1010
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Components.ProtectedBrowserStorage.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
11+
[assembly: InternalsVisibleTo("Components.TestServer, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
1112

1213
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")]
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<linker>
2+
<assembly fullname="Microsoft.AspNetCore.Components" >
3+
<!-- HotReload will not be available in a trimmed app. We'll attempt to aggressively remove all references to it. -->
4+
<type fullname="Microsoft.AspNetCore.Components.RenderTree.Renderer">
5+
<method signature="System.Void RenderRootComponentsOnHotReload()" body="remove" />
6+
<method signature="System.Void InitializeHotReload(System.IServiceProvider)" body="stub" />
7+
<method signature="System.Void InstatiateComponentForHotReload(Microsoft.AspNetCore.Components.IComponent)" body="stub" />
8+
<method signature="System.Void CaptureRootComponentForHotReload(Microsoft.AspNetCore.Components.ParameterView,Microsoft.AspNetCore.Components.Rendering.ComponentState)" body="stub" />
9+
<method signature="System.Void DisposeForHotReload()" body="stub" />
10+
</type>
11+
<type fullname="Microsoft.AspNetCore.Components.HotReload.HotReloadContext">
12+
<method signature="System.Boolean get_IsHotReloading()" body="stub" value="false" />
13+
<method signature="System.Void set_IsHotReloading(System.Boolean)" body="remove" />
14+
</type>
15+
<type fullname="Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder">
16+
<method signature="System.Boolean IsHotReloading(Microsoft.AspNetCore.Components.RenderTree.Renderer)" body="stub" value="false" />
17+
</type>
18+
</assembly>
19+
</linker>

src/Components/Components/src/PublicAPI.Unshipped.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ Microsoft.AspNetCore.Components.ComponentApplicationState.PersistAsJson<TValue>(
88
Microsoft.AspNetCore.Components.ComponentApplicationState.PersistState(string! key, byte[]! value) -> void
99
Microsoft.AspNetCore.Components.ComponentApplicationState.TryTakeAsJson<TValue>(string! key, out TValue? instance) -> bool
1010
Microsoft.AspNetCore.Components.ComponentApplicationState.TryTakePersistedState(string! key, out byte[]? value) -> bool
11+
Microsoft.AspNetCore.Components.HotReload.HotReloadContext
12+
Microsoft.AspNetCore.Components.HotReload.HotReloadContext.HotReloadContext() -> void
13+
Microsoft.AspNetCore.Components.HotReload.HotReloadContext.IsHotReloading.get -> bool
14+
Microsoft.AspNetCore.Components.HotReload.IReceiveHotReloadContext
15+
Microsoft.AspNetCore.Components.HotReload.IReceiveHotReloadContext.Receive(Microsoft.AspNetCore.Components.HotReload.HotReloadContext! context) -> void
1116
Microsoft.AspNetCore.Components.Lifetime.ComponentApplicationLifetime
1217
Microsoft.AspNetCore.Components.Lifetime.ComponentApplicationLifetime.ComponentApplicationLifetime(Microsoft.Extensions.Logging.ILogger<Microsoft.AspNetCore.Components.Lifetime.ComponentApplicationLifetime!>! logger) -> void
1318
Microsoft.AspNetCore.Components.Lifetime.ComponentApplicationLifetime.PersistStateAsync(Microsoft.AspNetCore.Components.Lifetime.IComponentApplicationStateStore! store, Microsoft.AspNetCore.Components.RenderTree.Renderer! renderer) -> System.Threading.Tasks.Task!

src/Components/Components/src/RenderTree/RenderTreeDiffBuilder.cs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@
55

66
using System;
77
using System.Collections.Generic;
8-
using System.Diagnostics;
9-
using System.Runtime.CompilerServices;
108
using Microsoft.AspNetCore.Components.Rendering;
119

1210
namespace Microsoft.AspNetCore.Components.RenderTree
@@ -535,15 +533,25 @@ private static void UpdateRetainedChildComponent(
535533
// comparisons it wants with the old values. Later we could choose to pass the
536534
// old parameter values if we wanted. By default, components always rerender
537535
// after any SetParameters call, which is safe but now always optimal for perf.
536+
537+
// When performing hot reload, we want to force all components to re-render.
538+
// We do this using two mechanisms - we call SetParametersAsync even if the parameters
539+
// are unchanged and we ignore ComponentBase.ShouldRender
540+
538541
var oldParameters = new ParameterView(ParameterViewLifetime.Unbound, oldTree, oldComponentIndex);
539542
var newParametersLifetime = new ParameterViewLifetime(diffContext.BatchBuilder);
540543
var newParameters = new ParameterView(newParametersLifetime, newTree, newComponentIndex);
541-
if (!newParameters.DefinitelyEquals(oldParameters))
544+
if (!newParameters.DefinitelyEquals(oldParameters) || IsHotReloading(diffContext.Renderer))
542545
{
543546
componentState.SetDirectParameters(newParameters);
544547
}
545548
}
546549

550+
/// <remarks>
551+
/// Intentionally authored as a separate method so we can trim this code.
552+
/// </remarks>
553+
private static bool IsHotReloading(Renderer renderer) => renderer.HotReloadContext.IsHotReloading;
554+
547555
private static int NextSiblingIndex(in RenderTreeFrame frame, int frameIndex)
548556
{
549557
switch (frame.FrameTypeField)
@@ -696,8 +704,8 @@ private static void AppendDiffEntriesForFramesWithSameSequence(
696704
break;
697705
}
698706

699-
// We don't handle attributes here, they have their own diff logic.
700-
// See AppendDiffEntriesForAttributeFrame
707+
// We don't handle attributes here, they have their own diff logic.
708+
// See AppendDiffEntriesForAttributeFrame
701709
default:
702710
throw new NotImplementedException($"Encountered unsupported frame type during diffing: {newTree[newFrameIndex].FrameTypeField}");
703711
}

0 commit comments

Comments
 (0)