Skip to content

Commit

Permalink
Add API to Renderer to trigger a UI refresh on hot reload (#30884)
Browse files Browse the repository at this point in the history
* Add API to Renderer to trigger a UI refresh on hot reload

Fixes #30816
  • Loading branch information
pranavkm authored Mar 18, 2021
1 parent 7211288 commit ce689ad
Show file tree
Hide file tree
Showing 26 changed files with 498 additions and 20 deletions.
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 because of a hot reload operation.
/// </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
{
public static readonly HotReloadEnvironment Instance = new(Environment.GetEnvironmentVariable("DOTNET_MODIFIABLE_ASSEMBLIES") == "debug");

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">The hot reload context.</param>
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
32 changes: 27 additions & 5 deletions src/Components/Components/src/ParameterView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,21 @@ public IReadOnlyDictionary<string, object> ToDictionary()
return result;
}

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

var numEntries = GetEntryCount();
var cloneBuffer = new RenderTreeFrame[1 + numEntries];
cloneBuffer[0] = RenderTreeFrame.PlaceholderChildComponentWithSubtreeLength(1 + numEntries);
_frames.AsSpan(1, numEntries).CopyTo(cloneBuffer.AsSpan(1));

return new ParameterView(Lifetime, cloneBuffer, _ownerIndex);
}

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

Expand Down Expand Up @@ -189,11 +204,7 @@ internal void CaptureSnapshot(ArrayBuilder<RenderTreeFrame> builder)
{
builder.Clear();

var numEntries = 0;
foreach (var entry in this)
{
numEntries++;
}
var numEntries = GetEntryCount();

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

private int GetEntryCount()
{
var numEntries = 0;
foreach (var _ in this)
{
numEntries++;
}

return numEntries;
}

/// <summary>
/// Creates a new <see cref="ParameterView"/> from the given <see cref="IDictionary{TKey, TValue}"/>.
/// </summary>
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")]
19 changes: 19 additions & 0 deletions src/Components/Components/src/Properties/ILLink.Substitutions.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<linker>
<assembly fullname="Microsoft.AspNetCore.Components" >
<!-- HotReload will not be available in a trimmed app. We'll attempt to aggressively remove all references to it. -->
<type fullname="Microsoft.AspNetCore.Components.RenderTree.Renderer">
<method signature="System.Void RenderRootComponentsOnHotReload()" body="remove" />
<method signature="System.Void InitializeHotReload(System.IServiceProvider)" body="stub" />
<method signature="System.Void InstatiateComponentForHotReload(Microsoft.AspNetCore.Components.IComponent)" body="stub" />
<method signature="System.Void CaptureRootComponentForHotReload(Microsoft.AspNetCore.Components.ParameterView,Microsoft.AspNetCore.Components.Rendering.ComponentState)" body="stub" />
<method signature="System.Void DisposeForHotReload()" body="stub" />
</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>
<type fullname="Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder">
<method signature="System.Boolean IsHotReloading(Microsoft.AspNetCore.Components.RenderTree.Renderer)" body="stub" value="false" />
</type>
</assembly>
</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
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
18 changes: 13 additions & 5 deletions src/Components/Components/src/RenderTree/RenderTreeDiffBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using Microsoft.AspNetCore.Components.Rendering;

namespace Microsoft.AspNetCore.Components.RenderTree
Expand Down Expand Up @@ -535,15 +533,25 @@ 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) || IsHotReloading(diffContext.Renderer))
{
componentState.SetDirectParameters(newParameters);
}
}

/// <remarks>
/// Intentionally authored as a separate method so we can trim this code.
/// </remarks>
private static bool IsHotReloading(Renderer renderer) => renderer.HotReloadContext.IsHotReloading;

private static int NextSiblingIndex(in RenderTreeFrame frame, int frameIndex)
{
switch (frame.FrameTypeField)
Expand Down Expand Up @@ -696,8 +704,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
Loading

0 comments on commit ce689ad

Please sign in to comment.