From 55e88afe3d711df1bab6d84d767e5fee2b43d541 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Fri, 12 Mar 2021 09:45:44 -0800 Subject: [PATCH 01/10] Add API to Renderer to trigger a UI refresh on hot reload --- .../Components/src/ComponentBase.cs | 11 +++- .../src/HotReload/HotReloadContext.cs | 19 ++++++ .../src/HotReload/HotReloadManager.cs | 25 ++++++++ .../src/HotReload/IReceiveHotReloadContext.cs | 17 ++++++ .../Components/src/PublicAPI.Unshipped.txt | 5 ++ .../src/RenderTree/RenderTreeDiffBuilder.cs | 29 +++++++--- .../Components/src/RenderTree/Renderer.cs | 58 ++++++++++++++++++- .../src/Rendering/ComponentState.cs | 9 +++ 8 files changed, 161 insertions(+), 12 deletions(-) create mode 100644 src/Components/Components/src/HotReload/HotReloadContext.cs create mode 100644 src/Components/Components/src/HotReload/HotReloadManager.cs create mode 100644 src/Components/Components/src/HotReload/IReceiveHotReloadContext.cs diff --git a/src/Components/Components/src/ComponentBase.cs b/src/Components/Components/src/ComponentBase.cs index 1780a78a6a84..35b193100097 100644 --- a/src/Components/Components/src/ComponentBase.cs +++ b/src/Components/Components/src/ComponentBase.cs @@ -3,6 +3,7 @@ using System; using System.Threading.Tasks; +using Microsoft.AspNetCore.Components.HotReload; using Microsoft.AspNetCore.Components.Rendering; namespace Microsoft.AspNetCore.Components @@ -21,7 +22,7 @@ namespace Microsoft.AspNetCore.Components /// Optional base class for components. Alternatively, components may /// implement directly. /// - public abstract class ComponentBase : IComponent, IHandleEvent, IHandleAfterRender + public abstract class ComponentBase : IComponent, IHandleEvent, IHandleAfterRender, IReceiveHotReloadContext { private readonly RenderFragment _renderFragment; private RenderHandle _renderHandle; @@ -29,6 +30,7 @@ public abstract class ComponentBase : IComponent, IHandleEvent, IHandleAfterRend private bool _hasNeverRendered = true; private bool _hasPendingQueuedRender; private bool _hasCalledOnAfterRender; + private HotReloadContext? _hotReloadContext; /// /// Constructs an instance of . @@ -102,7 +104,7 @@ protected void StateHasChanged() return; } - if (_hasNeverRendered || ShouldRender()) + if (_hasNeverRendered || (_hotReloadContext?.IsHotReloading() ?? false) || ShouldRender()) { _hasPendingQueuedRender = true; @@ -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; + } } } diff --git a/src/Components/Components/src/HotReload/HotReloadContext.cs b/src/Components/Components/src/HotReload/HotReloadContext.cs new file mode 100644 index 000000000000..6d9469ec1abe --- /dev/null +++ b/src/Components/Components/src/HotReload/HotReloadContext.cs @@ -0,0 +1,19 @@ +// 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 +{ + /// + /// A context that indicates when a component is being rendered after a hot reload is applied to the application. + /// + public sealed class HotReloadContext + { + internal bool HotReloading { get; set; } + + /// + /// Gets a value that indicates if the application is re-rendering in response to a hot-reload change. + /// + /// + public bool IsHotReloading() => HotReloading; + } +} diff --git a/src/Components/Components/src/HotReload/HotReloadManager.cs b/src/Components/Components/src/HotReload/HotReloadManager.cs new file mode 100644 index 000000000000..bee574e1bdba --- /dev/null +++ b/src/Components/Components/src/HotReload/HotReloadManager.cs @@ -0,0 +1,25 @@ +// 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 +{ + // Not to be confused with the HR Manager. + internal static class HotReloadManager + { + // Hot reload stuff + internal static bool IsHotReloadEnabled = Environment.GetEnvironmentVariable("COMPLUS_ForceEnc") == "1"; + + // For hotreload + internal static event Action? OnDeltaApplied; + + public static void DeltaApplied() + { + OnDeltaApplied?.Invoke(); + } + } +} diff --git a/src/Components/Components/src/HotReload/IReceiveHotReloadContext.cs b/src/Components/Components/src/HotReload/IReceiveHotReloadContext.cs new file mode 100644 index 000000000000..0eaf0be99f6e --- /dev/null +++ b/src/Components/Components/src/HotReload/IReceiveHotReloadContext.cs @@ -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 +{ + /// + /// Allows a component to receive a . + /// + public interface IReceiveHotReloadContext : IComponent + { + /// + /// Configures a component to use the hot reload context. + /// + /// + void Receive(HotReloadContext context); + } +} diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt index 3032b67cb877..9efc565316b7 100644 --- a/src/Components/Components/src/PublicAPI.Unshipped.txt +++ b/src/Components/Components/src/PublicAPI.Unshipped.txt @@ -8,6 +8,11 @@ Microsoft.AspNetCore.Components.ComponentApplicationState.PersistAsJson( Microsoft.AspNetCore.Components.ComponentApplicationState.PersistState(string! key, byte[]! value) -> void Microsoft.AspNetCore.Components.ComponentApplicationState.TryTakeAsJson(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() -> 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! logger) -> void Microsoft.AspNetCore.Components.Lifetime.ComponentApplicationLifetime.PersistStateAsync(Microsoft.AspNetCore.Components.Lifetime.IComponentApplicationStateStore! store, Microsoft.AspNetCore.Components.RenderTree.Renderer! renderer) -> System.Threading.Tasks.Task! diff --git a/src/Components/Components/src/RenderTree/RenderTreeDiffBuilder.cs b/src/Components/Components/src/RenderTree/RenderTreeDiffBuilder.cs index 80bf99724e0b..a03cec132953 100644 --- a/src/Components/Components/src/RenderTree/RenderTreeDiffBuilder.cs +++ b/src/Components/Components/src/RenderTree/RenderTreeDiffBuilder.cs @@ -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 @@ -31,7 +29,7 @@ public static RenderTreeDiff ComputeDiff( var editsBufferStartLength = editsBuffer.Count; var diffContext = new DiffContext(renderer, batchBuilder, componentId, oldTree.Array, newTree.Array); - AppendDiffEntriesForRange(ref diffContext, 0, oldTree.Count, 0, newTree.Count); + AppendDiffEntriesForRange(renderer, ref diffContext, 0, oldTree.Count, 0, newTree.Count); var editsSegment = editsBuffer.ToSegment(editsBufferStartLength, editsBuffer.Count); var result = new RenderTreeDiff(componentId, editsSegment); @@ -42,6 +40,7 @@ public static void DisposeFrames(RenderBatchBuilder batchBuilder, ArrayRange DisposeFramesInRange(batchBuilder, frames.Array, 0, frames.Count); private static void AppendDiffEntriesForRange( + Renderer renderer, ref DiffContext diffContext, int oldStartIndex, int oldEndIndexExcl, int newStartIndex, int newEndIndexExcl) @@ -239,7 +238,7 @@ private static void AppendDiffEntriesForRange( switch (action) { case DiffAction.Match: - AppendDiffEntriesForFramesWithSameSequence(ref diffContext, oldStartIndex, matchWithNewTreeIndex); + AppendDiffEntriesForFramesWithSameSequence(renderer, ref diffContext, oldStartIndex, matchWithNewTreeIndex); oldStartIndex = NextSiblingIndex(oldTree[oldStartIndex], oldStartIndex); newStartIndex = NextSiblingIndex(newTree[newStartIndex], newStartIndex); hasMoreOld = oldEndIndexExcl > oldStartIndex; @@ -511,6 +510,7 @@ private static void AppendAttributeDiffEntriesForRangeSlow( } private static void UpdateRetainedChildComponent( + Renderer renderer, ref DiffContext diffContext, int oldComponentIndex, int newComponentIndex) @@ -525,6 +525,17 @@ private static void UpdateRetainedChildComponent( newComponentFrame.ComponentStateField = componentState; newComponentFrame.ComponentIdField = componentState.ComponentId; + var newParametersLifetime = new ParameterViewLifetime(diffContext.BatchBuilder); + var newParameters = new ParameterView(newParametersLifetime, newTree, newComponentIndex); + if (renderer.HotReloadContext.IsHotReloading()) + { + // When performing hot reload, we want to force all components. + // We do this using two mechanisms - we call SetParametersAsync even if the parameters + // are unchanged and we ignore ComponentBase.ShouldRender + componentState.SetDirectParameters(newParameters); + return; + } + // As an important rendering optimization, we want to skip parameter update // notifications if we know for sure they haven't changed/mutated. The // "MayHaveChangedSince" logic is conservative, in that it returns true if @@ -536,8 +547,6 @@ private static void UpdateRetainedChildComponent( // old parameter values if we wanted. By default, components always rerender // after any SetParameters call, which is safe but now always optimal for perf. 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)) { componentState.SetDirectParameters(newParameters); @@ -560,6 +569,7 @@ private static int NextSiblingIndex(in RenderTreeFrame frame, int frameIndex) } private static void AppendDiffEntriesForFramesWithSameSequence( + Renderer renderer, ref DiffContext diffContext, int oldFrameIndex, int newFrameIndex) @@ -638,6 +648,7 @@ private static void AppendDiffEntriesForFramesWithSameSequence( var prevSiblingIndex = diffContext.SiblingIndex; diffContext.SiblingIndex = 0; AppendDiffEntriesForRange( + renderer, ref diffContext, oldFrameAttributesEndIndexExcl, oldFrameChildrenEndIndexExcl, newFrameAttributesEndIndexExcl, newFrameChildrenEndIndexExcl); @@ -661,6 +672,7 @@ private static void AppendDiffEntriesForFramesWithSameSequence( case RenderTreeFrameType.Region: { AppendDiffEntriesForRange( + renderer, ref diffContext, oldFrameIndex + 1, oldFrameIndex + oldFrame.RegionSubtreeLengthField, newFrameIndex + 1, newFrameIndex + newFrame.RegionSubtreeLengthField); @@ -672,6 +684,7 @@ private static void AppendDiffEntriesForFramesWithSameSequence( if (oldFrame.ComponentTypeField == newFrame.ComponentTypeField) { UpdateRetainedChildComponent( + renderer, ref diffContext, oldFrameIndex, newFrameIndex); @@ -696,8 +709,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}"); } diff --git a/src/Components/Components/src/RenderTree/Renderer.cs b/src/Components/Components/src/RenderTree/Renderer.cs index 38276aeaf426..160485ff395e 100644 --- a/src/Components/Components/src/RenderTree/Renderer.cs +++ b/src/Components/Components/src/RenderTree/Renderer.cs @@ -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; @@ -92,6 +93,8 @@ public Renderer(IServiceProvider serviceProvider, ILoggerFactory loggerFactory, _serviceProvider = serviceProvider; _logger = loggerFactory.CreateLogger(); _componentFactory = new ComponentFactory(componentActivator); + + HotReloadManager.OnDeltaApplied += RenderRootComponentsOnHotReload; } private static IComponentActivator GetComponentActivatorOrDefault(IServiceProvider serviceProvider) @@ -101,7 +104,7 @@ private static IComponentActivator GetComponentActivatorOrDefault(IServiceProvid } /// - /// Gets the associated with this . + /// Gets the associated with this . /// public abstract Dispatcher Dispatcher { get; } @@ -111,13 +114,54 @@ private static IComponentActivator GetComponentActivatorOrDefault(IServiceProvid /// protected internal ElementReferenceContext? ElementReferenceContext { get; protected set; } + internal HotReloadContext HotReloadContext { get; } = new(); + + private async void RenderRootComponentsOnHotReload() + { + await Dispatcher.InvokeAsync(async () => + { + HotReloadContext.HotReloading = true; + + try + { + _pendingTasks = new List(); + foreach (var componentState in _componentStateById.Values) + { + if (componentState.IsRootComponent) + { + var parameterView = componentState.InitialParameters is null ? + ParameterView.Empty : + ParameterView.FromDictionary((IDictionary)componentState.InitialParameters); + AddToPendingTasks(componentState.Component.SetParametersAsync(parameterView)); + } + } + + await ProcessAsynchronousWork(); + Debug.Assert(_pendingTasks.Count == 0); + } + finally + { + _pendingTasks = null; + HotReloadContext.HotReloading = false; + } + }); + } + /// /// Constructs a new component of the specified type. /// /// The type of the component to instantiate. /// The component instance. protected IComponent InstantiateComponent([DynamicallyAccessedMembers(Component)] Type componentType) - => _componentFactory.InstantiateComponent(_serviceProvider, componentType); + { + var component = _componentFactory.InstantiateComponent(_serviceProvider, componentType); + if (component is IReceiveHotReloadContext receiveHotReloadContext) + { + receiveHotReloadContext.Receive(HotReloadContext); + } + + return component; + } /// /// Associates the with the , assigning @@ -182,8 +226,16 @@ 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); + componentState.IsRootComponent = true; componentState.SetDirectParameters(initialParameters); + if (HotReloadManager.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. + componentState.InitialParameters = initialParameters.ToDictionary(); + } + try { await ProcessAsynchronousWork(); @@ -884,6 +936,8 @@ public void Dispose() /// public async ValueTask DisposeAsync() { + HotReloadManager.OnDeltaApplied -= RenderRootComponentsOnHotReload; + if (_disposed) { return; diff --git a/src/Components/Components/src/Rendering/ComponentState.cs b/src/Components/Components/src/Rendering/ComponentState.cs index de620ccf8ffe..968b1e8fb093 100644 --- a/src/Components/Components/src/Rendering/ComponentState.cs +++ b/src/Components/Components/src/Rendering/ComponentState.cs @@ -53,6 +53,15 @@ public ComponentState(Renderer renderer, int componentId, IComponent component, public IComponent Component { get; } public ComponentState ParentComponentState { get; } public RenderTreeBuilder CurrentRenderTree { get; private set; } + public bool IsRootComponent { get; set; } + + + /// + /// A snapshot of the used to initially render a component. + /// This is used to capture parameters specified via + /// when hot reload is enabled. + /// + public IReadOnlyDictionary? InitialParameters { get; set; } public void RenderIntoBatch(RenderBatchBuilder batchBuilder, RenderFragment renderFragment) { From 7311d25ba07a9cd4535b41ec81c42e4d37c882b8 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Mon, 15 Mar 2021 12:03:18 -0700 Subject: [PATCH 02/10] Changes per PR comments --- .../Components/src/ComponentBase.cs | 2 +- .../src/HotReload/HotReloadContext.cs | 5 +-- .../src/HotReload/HotReloadManager.cs | 8 ++--- .../Microsoft.AspNetCore.Components.csproj | 7 +++- .../Components/src/ParameterView.cs | 11 +++++++ .../src/Properties/ILLink.Substitutions.xml | 13 ++++++++ .../Components/src/PublicAPI.Unshipped.txt | 2 +- .../src/RenderTree/RenderTreeDiffBuilder.cs | 32 +++++++------------ .../Components/src/RenderTree/Renderer.cs | 27 +++++++--------- .../src/Rendering/ComponentState.cs | 9 ------ src/Components/Web.JS/src/Boot.WebAssembly.ts | 4 +++ src/Components/Web.JS/src/GlobalExports.ts | 3 +- .../src/Infrastructure/JSInteropMethods.cs | 22 +++++++++++++ .../WebAssembly/src/PublicAPI.Unshipped.txt | 1 + 14 files changed, 90 insertions(+), 56 deletions(-) create mode 100644 src/Components/Components/src/Properties/ILLink.Substitutions.xml diff --git a/src/Components/Components/src/ComponentBase.cs b/src/Components/Components/src/ComponentBase.cs index 35b193100097..56cf2f3d6138 100644 --- a/src/Components/Components/src/ComponentBase.cs +++ b/src/Components/Components/src/ComponentBase.cs @@ -104,7 +104,7 @@ protected void StateHasChanged() return; } - if (_hasNeverRendered || (_hotReloadContext?.IsHotReloading() ?? false) || ShouldRender()) + if (_hasNeverRendered || ShouldRender() || (_hotReloadContext?.IsHotReloading ?? false)) { _hasPendingQueuedRender = true; diff --git a/src/Components/Components/src/HotReload/HotReloadContext.cs b/src/Components/Components/src/HotReload/HotReloadContext.cs index 6d9469ec1abe..3d012a07061f 100644 --- a/src/Components/Components/src/HotReload/HotReloadContext.cs +++ b/src/Components/Components/src/HotReload/HotReloadContext.cs @@ -8,12 +8,9 @@ namespace Microsoft.AspNetCore.Components.HotReload /// public sealed class HotReloadContext { - internal bool HotReloading { get; set; } - /// /// Gets a value that indicates if the application is re-rendering in response to a hot-reload change. /// - /// - public bool IsHotReloading() => HotReloading; + public bool IsHotReloading { get; internal set; } } } diff --git a/src/Components/Components/src/HotReload/HotReloadManager.cs b/src/Components/Components/src/HotReload/HotReloadManager.cs index bee574e1bdba..03155aae98dc 100644 --- a/src/Components/Components/src/HotReload/HotReloadManager.cs +++ b/src/Components/Components/src/HotReload/HotReloadManager.cs @@ -8,13 +8,13 @@ namespace Microsoft.AspNetCore.Components.HotReload { - // Not to be confused with the HR Manager. internal static class HotReloadManager { - // Hot reload stuff - internal static bool IsHotReloadEnabled = Environment.GetEnvironmentVariable("COMPLUS_ForceEnc") == "1"; + /// + /// Gets a value that determines if HotReload is configured for this application. + /// + public static bool IsHotReloadEnabled { get; } = Environment.GetEnvironmentVariable("DOTNET_MODIFIABLE_ASSEMBLIES") == "debug"; - // For hotreload internal static event Action? OnDeltaApplied; public static void DeltaApplied() diff --git a/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj b/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj index 2ee1e93ba030..4bc16263a37b 100644 --- a/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj +++ b/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj @@ -1,4 +1,4 @@ - + $(DefaultNetCoreTargetFramework) @@ -33,6 +33,10 @@ + + + + + diff --git a/src/Components/Components/src/ParameterView.cs b/src/Components/Components/src/ParameterView.cs index 89adfaa4fc85..47034fd0d45d 100644 --- a/src/Components/Components/src/ParameterView.cs +++ b/src/Components/Components/src/ParameterView.cs @@ -116,6 +116,17 @@ public IReadOnlyDictionary ToDictionary() return result; } + internal ParameterView Clone() + { + if (ReferenceEquals(_frames, _emptyFrames)) + { + return Empty; + } + + var dictionary = ToDictionary(); + return FromDictionary((IDictionary)dictionary); + } + internal ParameterView WithCascadingParameters(IReadOnlyList cascadingParameters) => new ParameterView(_lifetime, _frames, _ownerIndex, cascadingParameters); diff --git a/src/Components/Components/src/Properties/ILLink.Substitutions.xml b/src/Components/Components/src/Properties/ILLink.Substitutions.xml new file mode 100644 index 000000000000..66d614c085a4 --- /dev/null +++ b/src/Components/Components/src/Properties/ILLink.Substitutions.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt index 9efc565316b7..ab4b3f271702 100644 --- a/src/Components/Components/src/PublicAPI.Unshipped.txt +++ b/src/Components/Components/src/PublicAPI.Unshipped.txt @@ -10,7 +10,7 @@ Microsoft.AspNetCore.Components.ComponentApplicationState.TryTakeAsJson( 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() -> bool +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 diff --git a/src/Components/Components/src/RenderTree/RenderTreeDiffBuilder.cs b/src/Components/Components/src/RenderTree/RenderTreeDiffBuilder.cs index a03cec132953..29e4abed131e 100644 --- a/src/Components/Components/src/RenderTree/RenderTreeDiffBuilder.cs +++ b/src/Components/Components/src/RenderTree/RenderTreeDiffBuilder.cs @@ -5,6 +5,8 @@ using System; using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.CompilerServices; using Microsoft.AspNetCore.Components.Rendering; namespace Microsoft.AspNetCore.Components.RenderTree @@ -29,7 +31,7 @@ public static RenderTreeDiff ComputeDiff( var editsBufferStartLength = editsBuffer.Count; var diffContext = new DiffContext(renderer, batchBuilder, componentId, oldTree.Array, newTree.Array); - AppendDiffEntriesForRange(renderer, ref diffContext, 0, oldTree.Count, 0, newTree.Count); + AppendDiffEntriesForRange(ref diffContext, 0, oldTree.Count, 0, newTree.Count); var editsSegment = editsBuffer.ToSegment(editsBufferStartLength, editsBuffer.Count); var result = new RenderTreeDiff(componentId, editsSegment); @@ -40,7 +42,6 @@ public static void DisposeFrames(RenderBatchBuilder batchBuilder, ArrayRange DisposeFramesInRange(batchBuilder, frames.Array, 0, frames.Count); private static void AppendDiffEntriesForRange( - Renderer renderer, ref DiffContext diffContext, int oldStartIndex, int oldEndIndexExcl, int newStartIndex, int newEndIndexExcl) @@ -238,7 +239,7 @@ private static void AppendDiffEntriesForRange( switch (action) { case DiffAction.Match: - AppendDiffEntriesForFramesWithSameSequence(renderer, ref diffContext, oldStartIndex, matchWithNewTreeIndex); + AppendDiffEntriesForFramesWithSameSequence(ref diffContext, oldStartIndex, matchWithNewTreeIndex); oldStartIndex = NextSiblingIndex(oldTree[oldStartIndex], oldStartIndex); newStartIndex = NextSiblingIndex(newTree[newStartIndex], newStartIndex); hasMoreOld = oldEndIndexExcl > oldStartIndex; @@ -510,7 +511,6 @@ private static void AppendAttributeDiffEntriesForRangeSlow( } private static void UpdateRetainedChildComponent( - Renderer renderer, ref DiffContext diffContext, int oldComponentIndex, int newComponentIndex) @@ -525,17 +525,6 @@ private static void UpdateRetainedChildComponent( newComponentFrame.ComponentStateField = componentState; newComponentFrame.ComponentIdField = componentState.ComponentId; - var newParametersLifetime = new ParameterViewLifetime(diffContext.BatchBuilder); - var newParameters = new ParameterView(newParametersLifetime, newTree, newComponentIndex); - if (renderer.HotReloadContext.IsHotReloading()) - { - // When performing hot reload, we want to force all components. - // We do this using two mechanisms - we call SetParametersAsync even if the parameters - // are unchanged and we ignore ComponentBase.ShouldRender - componentState.SetDirectParameters(newParameters); - return; - } - // As an important rendering optimization, we want to skip parameter update // notifications if we know for sure they haven't changed/mutated. The // "MayHaveChangedSince" logic is conservative, in that it returns true if @@ -546,8 +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); - if (!newParameters.DefinitelyEquals(oldParameters)) + var newParametersLifetime = new ParameterViewLifetime(diffContext.BatchBuilder); + var newParameters = new ParameterView(newParametersLifetime, newTree, newComponentIndex); + if (!newParameters.DefinitelyEquals(oldParameters) || diffContext.Renderer.HotReloadContext.IsHotReloading) { componentState.SetDirectParameters(newParameters); } @@ -569,7 +565,6 @@ private static int NextSiblingIndex(in RenderTreeFrame frame, int frameIndex) } private static void AppendDiffEntriesForFramesWithSameSequence( - Renderer renderer, ref DiffContext diffContext, int oldFrameIndex, int newFrameIndex) @@ -648,7 +643,6 @@ private static void AppendDiffEntriesForFramesWithSameSequence( var prevSiblingIndex = diffContext.SiblingIndex; diffContext.SiblingIndex = 0; AppendDiffEntriesForRange( - renderer, ref diffContext, oldFrameAttributesEndIndexExcl, oldFrameChildrenEndIndexExcl, newFrameAttributesEndIndexExcl, newFrameChildrenEndIndexExcl); @@ -672,7 +666,6 @@ private static void AppendDiffEntriesForFramesWithSameSequence( case RenderTreeFrameType.Region: { AppendDiffEntriesForRange( - renderer, ref diffContext, oldFrameIndex + 1, oldFrameIndex + oldFrame.RegionSubtreeLengthField, newFrameIndex + 1, newFrameIndex + newFrame.RegionSubtreeLengthField); @@ -684,7 +677,6 @@ private static void AppendDiffEntriesForFramesWithSameSequence( if (oldFrame.ComponentTypeField == newFrame.ComponentTypeField) { UpdateRetainedChildComponent( - renderer, ref diffContext, oldFrameIndex, newFrameIndex); diff --git a/src/Components/Components/src/RenderTree/Renderer.cs b/src/Components/Components/src/RenderTree/Renderer.cs index 160485ff395e..d9f1584f59c2 100644 --- a/src/Components/Components/src/RenderTree/Renderer.cs +++ b/src/Components/Components/src/RenderTree/Renderer.cs @@ -33,6 +33,7 @@ public abstract partial class Renderer : IDisposable, IAsyncDisposable private readonly Dictionary _eventHandlerIdReplacements = new Dictionary(); private readonly ILogger _logger; private readonly ComponentFactory _componentFactory; + private List<(ComponentState, ParameterView)>? _rootComponents; private int _nextComponentId = 0; // TODO: change to 'long' when Mono .NET->JS interop supports it private bool _isBatchInProgress; @@ -120,20 +121,15 @@ private async void RenderRootComponentsOnHotReload() { await Dispatcher.InvokeAsync(async () => { - HotReloadContext.HotReloading = true; + Debug.Assert(_rootComponents is not null); + HotReloadContext.IsHotReloading = true; try { _pendingTasks = new List(); - foreach (var componentState in _componentStateById.Values) + foreach (var (componentState, initialParameters) in _rootComponents) { - if (componentState.IsRootComponent) - { - var parameterView = componentState.InitialParameters is null ? - ParameterView.Empty : - ParameterView.FromDictionary((IDictionary)componentState.InitialParameters); - AddToPendingTasks(componentState.Component.SetParametersAsync(parameterView)); - } + AddToPendingTasks(componentState.Component.SetParametersAsync(initialParameters)); } await ProcessAsynchronousWork(); @@ -142,7 +138,7 @@ await Dispatcher.InvokeAsync(async () => finally { _pendingTasks = null; - HotReloadContext.HotReloading = false; + HotReloadContext.IsHotReloading = false; } }); } @@ -155,7 +151,7 @@ await Dispatcher.InvokeAsync(async () => protected IComponent InstantiateComponent([DynamicallyAccessedMembers(Component)] Type componentType) { var component = _componentFactory.InstantiateComponent(_serviceProvider, componentType); - if (component is IReceiveHotReloadContext receiveHotReloadContext) + if (HotReloadManager.IsHotReloadEnabled && component is IReceiveHotReloadContext receiveHotReloadContext) { receiveHotReloadContext.Receive(HotReloadContext); } @@ -226,16 +222,17 @@ 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); - componentState.IsRootComponent = true; - componentState.SetDirectParameters(initialParameters); - if (HotReloadManager.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. - componentState.InitialParameters = initialParameters.ToDictionary(); + _rootComponents ??= new(); + _rootComponents.Add((componentState, initialParameters.Clone())); + } + componentState.SetDirectParameters(initialParameters); + try { await ProcessAsynchronousWork(); diff --git a/src/Components/Components/src/Rendering/ComponentState.cs b/src/Components/Components/src/Rendering/ComponentState.cs index 968b1e8fb093..de620ccf8ffe 100644 --- a/src/Components/Components/src/Rendering/ComponentState.cs +++ b/src/Components/Components/src/Rendering/ComponentState.cs @@ -53,15 +53,6 @@ public ComponentState(Renderer renderer, int componentId, IComponent component, public IComponent Component { get; } public ComponentState ParentComponentState { get; } public RenderTreeBuilder CurrentRenderTree { get; private set; } - public bool IsRootComponent { get; set; } - - - /// - /// A snapshot of the used to initially render a component. - /// This is used to capture parameters specified via - /// when hot reload is enabled. - /// - public IReadOnlyDictionary? InitialParameters { get; set; } public void RenderIntoBatch(RenderBatchBuilder batchBuilder, RenderFragment renderFragment) { diff --git a/src/Components/Web.JS/src/Boot.WebAssembly.ts b/src/Components/Web.JS/src/Boot.WebAssembly.ts index 1fa5f26023c6..5fd2f2bffc46 100644 --- a/src/Components/Web.JS/src/Boot.WebAssembly.ts +++ b/src/Components/Web.JS/src/Boot.WebAssembly.ts @@ -38,6 +38,10 @@ async function boot(options?: Partial): Promise { 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; diff --git a/src/Components/Web.JS/src/GlobalExports.ts b/src/Components/Web.JS/src/GlobalExports.ts index 7cc3c3747280..00e9758087a7 100644 --- a/src/Components/Web.JS/src/GlobalExports.ts +++ b/src/Components/Web.JS/src/GlobalExports.ts @@ -45,7 +45,8 @@ interface IBlazor { readSatelliteAssemblies?: () => System_Array, getLazyAssemblies?: any dotNetCriticalError?: any - getSatelliteAssemblies?: any + getSatelliteAssemblies?: any, + applyHotReload?: (id: string, metadataDelta: string, ilDeta: string) => void } } diff --git a/src/Components/WebAssembly/WebAssembly/src/Infrastructure/JSInteropMethods.cs b/src/Components/WebAssembly/WebAssembly/src/Infrastructure/JSInteropMethods.cs index c6eb32b09229..c4c486c8e17b 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Infrastructure/JSInteropMethods.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Infrastructure/JSInteropMethods.cs @@ -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; @@ -40,5 +44,23 @@ public static Task DispatchEvent(WebEventDescriptor eventDescriptor, string even webEvent.EventFieldInfo, webEvent.EventArgs); } + + /// + /// For framework use only. + /// + [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.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); + } } } diff --git a/src/Components/WebAssembly/WebAssembly/src/PublicAPI.Unshipped.txt b/src/Components/WebAssembly/WebAssembly/src/PublicAPI.Unshipped.txt index 1de9e94bc802..bcdda6e1d927 100644 --- a/src/Components/WebAssembly/WebAssembly/src/PublicAPI.Unshipped.txt +++ b/src/Components/WebAssembly/WebAssembly/src/PublicAPI.Unshipped.txt @@ -38,5 +38,6 @@ static Microsoft.AspNetCore.Components.WebAssembly.Http.WebAssemblyHttpRequestMe static Microsoft.AspNetCore.Components.WebAssembly.Http.WebAssemblyHttpRequestMessageExtensions.SetBrowserRequestMode(this System.Net.Http.HttpRequestMessage! requestMessage, Microsoft.AspNetCore.Components.WebAssembly.Http.BrowserRequestMode requestMode) -> System.Net.Http.HttpRequestMessage! static Microsoft.AspNetCore.Components.WebAssembly.Http.WebAssemblyHttpRequestMessageExtensions.SetBrowserRequestOption(this System.Net.Http.HttpRequestMessage! requestMessage, string! name, object! value) -> System.Net.Http.HttpRequestMessage! static Microsoft.AspNetCore.Components.WebAssembly.Http.WebAssemblyHttpRequestMessageExtensions.SetBrowserResponseStreamingEnabled(this System.Net.Http.HttpRequestMessage! requestMessage, bool streamingEnabled) -> System.Net.Http.HttpRequestMessage! +static Microsoft.AspNetCore.Components.WebAssembly.Infrastructure.JSInteropMethods.ApplyHotReloadDelta(string! moduleId, byte[]! metadataDelta, byte[]! ilDeta) -> void static Microsoft.AspNetCore.Components.WebAssembly.Infrastructure.JSInteropMethods.DispatchEvent(Microsoft.AspNetCore.Components.RenderTree.WebEventDescriptor! eventDescriptor, string! eventArgsJson) -> System.Threading.Tasks.Task! static Microsoft.AspNetCore.Components.WebAssembly.Infrastructure.JSInteropMethods.NotifyLocationChanged(string! uri, bool isInterceptedLink) -> void From eec977a616cde07ad51e725f934a93decde40957 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Mon, 15 Mar 2021 13:32:01 -0700 Subject: [PATCH 03/10] Fixup --- ...re.Components.WebAssembly.WarningSuppressions.xml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/Components/WebAssembly/WebAssembly/src/Microsoft.AspNetCore.Components.WebAssembly.WarningSuppressions.xml b/src/Components/WebAssembly/WebAssembly/src/Microsoft.AspNetCore.Components.WebAssembly.WarningSuppressions.xml index 732104ae1516..986fa0232bcd 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Microsoft.AspNetCore.Components.WebAssembly.WarningSuppressions.xml +++ b/src/Components/WebAssembly/WebAssembly/src/Microsoft.AspNetCore.Components.WebAssembly.WarningSuppressions.xml @@ -19,6 +19,12 @@ member M:Microsoft.AspNetCore.Components.WebAssembly.Hosting.WebAssemblyHostBuilder.InitializeRegisteredRootComponents(Microsoft.JSInterop.IJSUnmarshalledRuntime) + + ILLink + IL2026 + member + M:Microsoft.AspNetCore.Components.WebAssembly.Infrastructure.JSInteropMethods.ApplyHotReloadDelta(System.String,System.Byte[],System.Byte[]) + ILLink IL2026 @@ -43,5 +49,11 @@ member M:Microsoft.AspNetCore.Components.WebAssemblyComponentParameterDeserializer.DeserializeParameters(System.Collections.Generic.IList{Microsoft.AspNetCore.Components.ComponentParameter},System.Collections.Generic.IList{System.Object}) + + ILLink + IL2075 + member + M:Microsoft.AspNetCore.Components.WebAssembly.Infrastructure.JSInteropMethods.ApplyHotReloadDelta(System.String,System.Byte[],System.Byte[]) + \ No newline at end of file From 89b9b4bb1d7d2e7a3f371eb000383519402a8020 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Mon, 15 Mar 2021 22:53:05 -0700 Subject: [PATCH 04/10] Add a E2E test --- .../src/HotReload/HotReloadEnvironment.cs | 22 ++++++++ .../src/HotReload/HotReloadManager.cs | 7 +-- .../Components/src/Properties/AssemblyInfo.cs | 1 + .../src/Properties/ILLink.Substitutions.xml | 3 +- .../Components/src/RenderTree/Renderer.cs | 8 ++- .../ServerExecutionTests/HotReloadTest.cs | 55 +++++++++++++++++++ .../HotReload/ComponentWithParameter.razor | 6 ++ .../HotReload/ComponentWithShouldRender.razor | 8 +++ .../HotReload/RenderOnHotReload.razor | 10 ++++ .../test/testassets/BasicTestApp/Index.razor | 1 + .../Controllers/ReloadController.cs | 20 +++++++ .../testassets/TestServer/HotReloadStartup.cs | 45 +++++++++++++++ .../test/testassets/TestServer/Program.cs | 1 + 13 files changed, 176 insertions(+), 11 deletions(-) create mode 100644 src/Components/Components/src/HotReload/HotReloadEnvironment.cs create mode 100644 src/Components/test/E2ETest/ServerExecutionTests/HotReloadTest.cs create mode 100644 src/Components/test/testassets/BasicTestApp/HotReload/ComponentWithParameter.razor create mode 100644 src/Components/test/testassets/BasicTestApp/HotReload/ComponentWithShouldRender.razor create mode 100644 src/Components/test/testassets/BasicTestApp/HotReload/RenderOnHotReload.razor create mode 100644 src/Components/test/testassets/TestServer/Controllers/ReloadController.cs create mode 100644 src/Components/test/testassets/TestServer/HotReloadStartup.cs diff --git a/src/Components/Components/src/HotReload/HotReloadEnvironment.cs b/src/Components/Components/src/HotReload/HotReloadEnvironment.cs new file mode 100644 index 000000000000..fef2cd9255cd --- /dev/null +++ b/src/Components/Components/src/HotReload/HotReloadEnvironment.cs @@ -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; + } + + /// + /// Gets a value that determines if HotReload is configured for this application. + /// + public bool IsHotReloadEnabled { get; } + } +} diff --git a/src/Components/Components/src/HotReload/HotReloadManager.cs b/src/Components/Components/src/HotReload/HotReloadManager.cs index 03155aae98dc..bcf59b7ea6b5 100644 --- a/src/Components/Components/src/HotReload/HotReloadManager.cs +++ b/src/Components/Components/src/HotReload/HotReloadManager.cs @@ -10,12 +10,7 @@ namespace Microsoft.AspNetCore.Components.HotReload { internal static class HotReloadManager { - /// - /// Gets a value that determines if HotReload is configured for this application. - /// - public static bool IsHotReloadEnabled { get; } = Environment.GetEnvironmentVariable("DOTNET_MODIFIABLE_ASSEMBLIES") == "debug"; - - internal static event Action? OnDeltaApplied; + internal static event Action? OnDeltaApplied; public static void DeltaApplied() { diff --git a/src/Components/Components/src/Properties/AssemblyInfo.cs b/src/Components/Components/src/Properties/AssemblyInfo.cs index 8ff5dba8445c..66e087922b0d 100644 --- a/src/Components/Components/src/Properties/AssemblyInfo.cs +++ b/src/Components/Components/src/Properties/AssemblyInfo.cs @@ -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")] diff --git a/src/Components/Components/src/Properties/ILLink.Substitutions.xml b/src/Components/Components/src/Properties/ILLink.Substitutions.xml index 66d614c085a4..258b67f1f0d9 100644 --- a/src/Components/Components/src/Properties/ILLink.Substitutions.xml +++ b/src/Components/Components/src/Properties/ILLink.Substitutions.xml @@ -1,9 +1,8 @@ - + - diff --git a/src/Components/Components/src/RenderTree/Renderer.cs b/src/Components/Components/src/RenderTree/Renderer.cs index d9f1584f59c2..9e1d357ebb5b 100644 --- a/src/Components/Components/src/RenderTree/Renderer.cs +++ b/src/Components/Components/src/RenderTree/Renderer.cs @@ -33,6 +33,7 @@ public abstract partial class Renderer : IDisposable, IAsyncDisposable private readonly Dictionary _eventHandlerIdReplacements = new Dictionary(); private readonly ILogger _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 @@ -95,6 +96,8 @@ public Renderer(IServiceProvider serviceProvider, ILoggerFactory loggerFactory, _logger = loggerFactory.CreateLogger(); _componentFactory = new ComponentFactory(componentActivator); + _hotReloadEnvironment = serviceProvider.GetService() ?? HotReloadEnvironment.Instance; + HotReloadManager.OnDeltaApplied += RenderRootComponentsOnHotReload; } @@ -151,7 +154,7 @@ await Dispatcher.InvokeAsync(async () => protected IComponent InstantiateComponent([DynamicallyAccessedMembers(Component)] Type componentType) { var component = _componentFactory.InstantiateComponent(_serviceProvider, componentType); - if (HotReloadManager.IsHotReloadEnabled && component is IReceiveHotReloadContext receiveHotReloadContext) + if (_hotReloadEnvironment.IsHotReloadEnabled && component is IReceiveHotReloadContext receiveHotReloadContext) { receiveHotReloadContext.Receive(HotReloadContext); } @@ -222,13 +225,12 @@ 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 (HotReloadManager.IsHotReloadEnabled) + 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); diff --git a/src/Components/test/E2ETest/ServerExecutionTests/HotReloadTest.cs b/src/Components/test/E2ETest/ServerExecutionTests/HotReloadTest.cs new file mode 100644 index 000000000000..e813e232b83b --- /dev/null +++ b/src/Components/test/E2ETest/ServerExecutionTests/HotReloadTest.cs @@ -0,0 +1,55 @@ +// 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.Threading.Tasks; +using BasicTestApp.HotReload; +using Microsoft.AspNetCore.Components.E2ETest.Infrastructure; +using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; +using Microsoft.AspNetCore.E2ETesting; +using OpenQA.Selenium; +using TestServer; +using Xunit; +using Xunit.Abstractions; +using System.Net.Http; + +namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests +{ + public class HotReloadTest : ServerTestBase> + { + public HotReloadTest( + BrowserFixture browserFixture, + BasicTestAppServerSiteFixture serverFixture, + ITestOutputHelper output) + : base(browserFixture, serverFixture, output) + { + } + + public override async Task InitializeAsync() + { + await base.InitializeAsync(Guid.NewGuid().ToString()); + } + + protected override void InitializeAsyncCore() + { + Navigate(ServerPathBase, noReload: false); + Browser.MountTestComponent(); + } + + [Fact] + public async Task InvokingRender_CausesComponentToRender() + { + Browser.Equal("This component was rendered 1 time(s).", () => Browser.Exists(By.TagName("h2")).Text); + Browser.Equal("Initial title", () => Browser.Exists(By.TagName("h3")).Text); + Browser.Equal("Component with ShouldRender=false was rendered 1 time(s).", () => Browser.Exists(By.TagName("h4")).Text); + + using var client = new HttpClient { BaseAddress = _serverFixture.RootUri }; + var response = await client.GetAsync("/rerender"); + response.EnsureSuccessStatusCode(); + + Browser.Equal("This component was rendered 2 time(s).", () => Browser.Exists(By.TagName("h2")).Text); + Browser.Equal("Initial title", () => Browser.Exists(By.TagName("h3")).Text); + Browser.Equal("Component with ShouldRender=false was rendered 2 time(s).", () => Browser.Exists(By.TagName("h4")).Text); + } + } +} diff --git a/src/Components/test/testassets/BasicTestApp/HotReload/ComponentWithParameter.razor b/src/Components/test/testassets/BasicTestApp/HotReload/ComponentWithParameter.razor new file mode 100644 index 000000000000..ee99fbdb6db3 --- /dev/null +++ b/src/Components/test/testassets/BasicTestApp/HotReload/ComponentWithParameter.razor @@ -0,0 +1,6 @@ +

@Title

+ +@code +{ + [Parameter] public string Title { get; set; } +} \ No newline at end of file diff --git a/src/Components/test/testassets/BasicTestApp/HotReload/ComponentWithShouldRender.razor b/src/Components/test/testassets/BasicTestApp/HotReload/ComponentWithShouldRender.razor new file mode 100644 index 000000000000..1cea3335f9da --- /dev/null +++ b/src/Components/test/testassets/BasicTestApp/HotReload/ComponentWithShouldRender.razor @@ -0,0 +1,8 @@ +

Component with ShouldRender=false was rendered @(++RenderCount) time(s).

+ +@code +{ + int RenderCount = 0; + + protected override bool ShouldRender() => false; +} \ No newline at end of file diff --git a/src/Components/test/testassets/BasicTestApp/HotReload/RenderOnHotReload.razor b/src/Components/test/testassets/BasicTestApp/HotReload/RenderOnHotReload.razor new file mode 100644 index 000000000000..00f35e56c292 --- /dev/null +++ b/src/Components/test/testassets/BasicTestApp/HotReload/RenderOnHotReload.razor @@ -0,0 +1,10 @@ +

This component was rendered @(++RenderCount) time(s).

+ + + + + +@code +{ + int RenderCount = 0; +} \ No newline at end of file diff --git a/src/Components/test/testassets/BasicTestApp/Index.razor b/src/Components/test/testassets/BasicTestApp/Index.razor index ba51153b4733..34a05f91dac4 100644 --- a/src/Components/test/testassets/BasicTestApp/Index.razor +++ b/src/Components/test/testassets/BasicTestApp/Index.razor @@ -92,6 +92,7 @@ + @System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription diff --git a/src/Components/test/testassets/TestServer/Controllers/ReloadController.cs b/src/Components/test/testassets/TestServer/Controllers/ReloadController.cs new file mode 100644 index 000000000000..162f4540099d --- /dev/null +++ b/src/Components/test/testassets/TestServer/Controllers/ReloadController.cs @@ -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 Microsoft.AspNetCore.Components.HotReload; +using Microsoft.AspNetCore.Mvc; + +namespace ComponentsApp.Server +{ + [ApiController] + public class ReloadController : ControllerBase + { + [HttpGet("/rerender")] + public IActionResult Rerender() + { + HotReloadManager.DeltaApplied(); + + return Ok(); + } + } +} diff --git a/src/Components/test/testassets/TestServer/HotReloadStartup.cs b/src/Components/test/testassets/TestServer/HotReloadStartup.cs new file mode 100644 index 000000000000..f9b0061c9d41 --- /dev/null +++ b/src/Components/test/testassets/TestServer/HotReloadStartup.cs @@ -0,0 +1,45 @@ +// 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.Globalization; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Components.HotReload; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace TestServer +{ + public class HotReloadStartup + { + public void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(new HotReloadEnvironment(isHotReloadEnabled: true)); + services.AddControllers(); + services.AddRazorPages(); + services.AddServerSideBlazor(); + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + var enUs = new CultureInfo("en-US"); + CultureInfo.DefaultThreadCurrentCulture = enUs; + CultureInfo.DefaultThreadCurrentUICulture = enUs; + + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseBlazorFrameworkFiles(); + app.UseStaticFiles(); + app.UseRouting(); + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + endpoints.MapBlazorHub(); + endpoints.MapFallbackToPage("/_ServerHost"); + }); + } + } +} diff --git a/src/Components/test/testassets/TestServer/Program.cs b/src/Components/test/testassets/TestServer/Program.cs index 6351ada5b656..df8993c4c28d 100644 --- a/src/Components/test/testassets/TestServer/Program.cs +++ b/src/Components/test/testassets/TestServer/Program.cs @@ -29,6 +29,7 @@ public static async Task Main(string[] args) ["Globalization + Localization (Server-side)"] = (BuildWebHost(CreateAdditionalArgs(args)), "/subdir"), ["Server-side blazor"] = (BuildWebHost(CreateAdditionalArgs(args)), "/subdir"), ["Hosted client-side blazor"] = (BuildWebHost(CreateAdditionalArgs(args)), "/subdir"), + ["Hot Reload"] = (BuildWebHost(CreateAdditionalArgs(args)), "/subdir"), ["Dev server client-side blazor"] = CreateDevServerHost(CreateAdditionalArgs(args)) }; From ee77867d4978a171cea9b3f15f269150d238f9dc Mon Sep 17 00:00:00 2001 From: Pranav K Date: Tue, 16 Mar 2021 10:08:05 -0700 Subject: [PATCH 05/10] Apply suggestions from code review Co-authored-by: Steve Sanderson --- src/Components/Components/src/HotReload/HotReloadContext.cs | 2 +- .../Components/src/HotReload/IReceiveHotReloadContext.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Components/Components/src/HotReload/HotReloadContext.cs b/src/Components/Components/src/HotReload/HotReloadContext.cs index 3d012a07061f..35948bdc6859 100644 --- a/src/Components/Components/src/HotReload/HotReloadContext.cs +++ b/src/Components/Components/src/HotReload/HotReloadContext.cs @@ -4,7 +4,7 @@ namespace Microsoft.AspNetCore.Components.HotReload { /// - /// A context that indicates when a component is being rendered after a hot reload is applied to the application. + /// A context that indicates when a component is being rendered because of a hot reload operation. /// public sealed class HotReloadContext { diff --git a/src/Components/Components/src/HotReload/IReceiveHotReloadContext.cs b/src/Components/Components/src/HotReload/IReceiveHotReloadContext.cs index 0eaf0be99f6e..8de60e382310 100644 --- a/src/Components/Components/src/HotReload/IReceiveHotReloadContext.cs +++ b/src/Components/Components/src/HotReload/IReceiveHotReloadContext.cs @@ -11,7 +11,7 @@ public interface IReceiveHotReloadContext : IComponent /// /// Configures a component to use the hot reload context. /// - /// + /// The hot reload context. void Receive(HotReloadContext context); } } From cebf3bf1fc4dfacd468f08845a741f3689c9094a Mon Sep 17 00:00:00 2001 From: Pranav K Date: Tue, 16 Mar 2021 15:57:17 -0700 Subject: [PATCH 06/10] Changes per PR --- .../Components/src/ParameterView.cs | 19 +++++++++---- .../src/Properties/ILLink.Substitutions.xml | 3 ++ .../Components/src/RenderTree/Renderer.cs | 28 ++++++++++++------- 3 files changed, 35 insertions(+), 15 deletions(-) diff --git a/src/Components/Components/src/ParameterView.cs b/src/Components/Components/src/ParameterView.cs index 47034fd0d45d..0ca0dd036eef 100644 --- a/src/Components/Components/src/ParameterView.cs +++ b/src/Components/Components/src/ParameterView.cs @@ -123,6 +123,8 @@ internal ParameterView Clone() return Empty; } + var numEntries = GetEntryCount(); + var dictionary = ToDictionary(); return FromDictionary((IDictionary)dictionary); } @@ -200,11 +202,7 @@ internal void CaptureSnapshot(ArrayBuilder 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 @@ -218,6 +216,17 @@ internal void CaptureSnapshot(ArrayBuilder builder) } } + private int GetEntryCount() + { + var numEntries = 0; + foreach (var _ in this) + { + numEntries++; + } + + return numEntries; + } + /// /// Creates a new from the given . /// diff --git a/src/Components/Components/src/Properties/ILLink.Substitutions.xml b/src/Components/Components/src/Properties/ILLink.Substitutions.xml index 258b67f1f0d9..3ad3d5ad0523 100644 --- a/src/Components/Components/src/Properties/ILLink.Substitutions.xml +++ b/src/Components/Components/src/Properties/ILLink.Substitutions.xml @@ -1,6 +1,9 @@ + + + diff --git a/src/Components/Components/src/RenderTree/Renderer.cs b/src/Components/Components/src/RenderTree/Renderer.cs index 9e1d357ebb5b..98274c7bec5c 100644 --- a/src/Components/Components/src/RenderTree/Renderer.cs +++ b/src/Components/Components/src/RenderTree/Renderer.cs @@ -96,9 +96,14 @@ public Renderer(IServiceProvider serviceProvider, ILoggerFactory loggerFactory, _logger = loggerFactory.CreateLogger(); _componentFactory = new ComponentFactory(componentActivator); + // HotReloadEnvironment is a test-specific feature and may not be available in a running app. We'll fallback to the default instance + // if the test fixture does not provide one. _hotReloadEnvironment = serviceProvider.GetService() ?? HotReloadEnvironment.Instance; - HotReloadManager.OnDeltaApplied += RenderRootComponentsOnHotReload; + if (_hotReloadEnvironment.IsHotReloadEnabled) + { + HotReloadManager.OnDeltaApplied += RenderRootComponentsOnHotReload; + } } private static IComponentActivator GetComponentActivatorOrDefault(IServiceProvider serviceProvider) @@ -122,25 +127,25 @@ private static IComponentActivator GetComponentActivatorOrDefault(IServiceProvid private async void RenderRootComponentsOnHotReload() { - await Dispatcher.InvokeAsync(async () => + await Dispatcher.InvokeAsync(() => { - Debug.Assert(_rootComponents is not null); - HotReloadContext.IsHotReloading = true; + if (_rootComponents is null) + { + return; + } + HotReloadContext.IsHotReloading = true; try { - _pendingTasks = new List(); foreach (var (componentState, initialParameters) in _rootComponents) { - AddToPendingTasks(componentState.Component.SetParametersAsync(initialParameters)); + componentState.SetDirectParameters(initialParameters); } - await ProcessAsynchronousWork(); - Debug.Assert(_pendingTasks.Count == 0); + ProcessPendingRender(); } finally { - _pendingTasks = null; HotReloadContext.IsHotReloading = false; } }); @@ -935,7 +940,10 @@ public void Dispose() /// public async ValueTask DisposeAsync() { - HotReloadManager.OnDeltaApplied -= RenderRootComponentsOnHotReload; + if (_hotReloadEnvironment.IsHotReloadEnabled) + { + HotReloadManager.OnDeltaApplied -= RenderRootComponentsOnHotReload; + } if (_disposed) { From b9e9b5a2cfbed9cfa1b89926f17d0ed4e15694d6 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Tue, 16 Mar 2021 16:16:02 -0700 Subject: [PATCH 07/10] Add tests for Clone --- .../Components/src/ParameterView.cs | 6 +- .../Components/test/ParameterViewTest.cs | 61 +++++++++++++++++++ 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/src/Components/Components/src/ParameterView.cs b/src/Components/Components/src/ParameterView.cs index 0ca0dd036eef..2ab1404357d7 100644 --- a/src/Components/Components/src/ParameterView.cs +++ b/src/Components/Components/src/ParameterView.cs @@ -124,9 +124,11 @@ internal ParameterView Clone() } var numEntries = GetEntryCount(); + var cloneBuffer = new RenderTreeFrame[1 + numEntries]; + cloneBuffer[0] = RenderTreeFrame.PlaceholderChildComponentWithSubtreeLength(1 + numEntries); + _frames.AsSpan(1, numEntries).CopyTo(cloneBuffer.AsSpan(1)); - var dictionary = ToDictionary(); - return FromDictionary((IDictionary)dictionary); + return new ParameterView(Lifetime, cloneBuffer, _ownerIndex); } internal ParameterView WithCascadingParameters(IReadOnlyList cascadingParameters) diff --git a/src/Components/Components/test/ParameterViewTest.cs b/src/Components/Components/test/ParameterViewTest.cs index 5193e8fe296c..6cf26a7a7ee3 100644 --- a/src/Components/Components/test/ParameterViewTest.cs +++ b/src/Components/Components/test/ParameterViewTest.cs @@ -348,6 +348,67 @@ public void CannotReadAfterLifetimeExpiry() Assert.Equal($"The {nameof(ParameterView)} instance can no longer be read because it has expired. {nameof(ParameterView)} can only be read synchronously and must not be stored for later use.", ex.Message); } + [Fact] + public void Clone_EmptyParameterView() + { + // Arrange + var initial = ParameterView.Empty; + + // Act + var cloned = initial.Clone(); + + // Assert + Assert.Empty(ToEnumerable(cloned)); + } + + [Fact] + public void Clone_ParameterViewSingleParameter() + { + // Arrange + var attribute1Value = new object(); + var initial = new ParameterView(ParameterViewLifetime.Unbound, new[] + { + RenderTreeFrame.Element(0, "some element").WithElementSubtreeLength(2), + RenderTreeFrame.Attribute(1, "attribute 1", attribute1Value), + }, 0); + + + // Act + var cloned = initial.Clone(); + + // Assert + Assert.Collection( + ToEnumerable(cloned), + p => AssertParameter("attribute 1", attribute1Value, expectedIsCascading: false)); + } + + [Fact] + public void Clone_ParameterPreservesOrder() + { + // Arrange + var attribute1Value = new object(); + var attribute2Value = new object(); + var attribute3Value = new object(); + var initial = new ParameterView(ParameterViewLifetime.Unbound, new[] + { + RenderTreeFrame.Element(0, "some element").WithElementSubtreeLength(4), + RenderTreeFrame.Attribute(1, "attribute 1", attribute1Value), + RenderTreeFrame.Attribute(1, "attribute 2", attribute2Value), + RenderTreeFrame.Attribute(1, "attribute 3", attribute3Value), + }, 0); + + + // Act + var cloned = initial.Clone(); + + // Assert + Assert.Collection( + ToEnumerable(cloned), + p => AssertParameter("attribute 1", attribute1Value, expectedIsCascading: false), + p => AssertParameter("attribute 2", attribute2Value, expectedIsCascading: false), + p => AssertParameter("attribute 3", attribute3Value, expectedIsCascading: false)); + } + private Action AssertParameter(string expectedName, object expectedValue, bool expectedIsCascading) { return parameter => From faadec8ad94d84153b28cebb6995905368cecdb8 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Tue, 16 Mar 2021 16:50:06 -0700 Subject: [PATCH 08/10] Apply suggestions from code review Co-authored-by: Steve Sanderson --- .../Components/src/Microsoft.AspNetCore.Components.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj b/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj index 4bc16263a37b..3c4e0bc9a706 100644 --- a/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj +++ b/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj @@ -64,6 +64,5 @@ -
From aea65f3a7762bb8c9756b684003463402b1ee9cb Mon Sep 17 00:00:00 2001 From: Pranav K Date: Wed, 17 Mar 2021 11:42:35 -0700 Subject: [PATCH 09/10] More changes --- .../src/Properties/ILLink.Substitutions.xml | 9 ++- .../src/RenderTree/RenderTreeDiffBuilder.cs | 9 ++- .../Components/src/RenderTree/Renderer.cs | 71 +++++++++++++------ 3 files changed, 64 insertions(+), 25 deletions(-) diff --git a/src/Components/Components/src/Properties/ILLink.Substitutions.xml b/src/Components/Components/src/Properties/ILLink.Substitutions.xml index 3ad3d5ad0523..5a7e104232e2 100644 --- a/src/Components/Components/src/Properties/ILLink.Substitutions.xml +++ b/src/Components/Components/src/Properties/ILLink.Substitutions.xml @@ -1,8 +1,12 @@ - + + + + + @@ -11,5 +15,8 @@ + + + diff --git a/src/Components/Components/src/RenderTree/RenderTreeDiffBuilder.cs b/src/Components/Components/src/RenderTree/RenderTreeDiffBuilder.cs index 29e4abed131e..674684a65367 100644 --- a/src/Components/Components/src/RenderTree/RenderTreeDiffBuilder.cs +++ b/src/Components/Components/src/RenderTree/RenderTreeDiffBuilder.cs @@ -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 @@ -543,12 +541,17 @@ private static void UpdateRetainedChildComponent( 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) || diffContext.Renderer.HotReloadContext.IsHotReloading) + if (!newParameters.DefinitelyEquals(oldParameters) || IsHotReloading(diffContext.Renderer)) { componentState.SetDirectParameters(newParameters); } } + /// + /// Intentionally authored as a separate method so we can trim this code. + /// + private static bool IsHotReloading(Renderer renderer) => renderer.HotReloadContext.IsHotReloading; + private static int NextSiblingIndex(in RenderTreeFrame frame, int frameIndex) { switch (frame.FrameTypeField) diff --git a/src/Components/Components/src/RenderTree/Renderer.cs b/src/Components/Components/src/RenderTree/Renderer.cs index 98274c7bec5c..0748d1675754 100644 --- a/src/Components/Components/src/RenderTree/Renderer.cs +++ b/src/Components/Components/src/RenderTree/Renderer.cs @@ -33,15 +33,16 @@ public abstract partial class Renderer : IDisposable, IAsyncDisposable private readonly Dictionary _eventHandlerIdReplacements = new Dictionary(); private readonly ILogger _logger; private readonly ComponentFactory _componentFactory; - private readonly HotReloadEnvironment _hotReloadEnvironment; + private HotReloadContext _hotReloadContext; + private 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; private ulong _lastEventHandlerId; - private List _pendingTasks; + private List? _pendingTasks; private bool _disposed; - private Task _disposeTask; + private Task? _disposeTask; /// /// Allows the caller to handle exceptions from the SynchronizationContext when one is available. @@ -96,6 +97,13 @@ public Renderer(IServiceProvider serviceProvider, ILoggerFactory loggerFactory, _logger = loggerFactory.CreateLogger(); _componentFactory = new ComponentFactory(componentActivator); + InitializeHotReload(serviceProvider); + } + + private void InitializeHotReload(IServiceProvider serviceProvider) + { + _hotReloadContext = new(); + // HotReloadEnvironment is a test-specific feature and may not be available in a running app. We'll fallback to the default instance // if the test fixture does not provide one. _hotReloadEnvironment = serviceProvider.GetService() ?? HotReloadEnvironment.Instance; @@ -123,7 +131,7 @@ private static IComponentActivator GetComponentActivatorOrDefault(IServiceProvid /// protected internal ElementReferenceContext? ElementReferenceContext { get; protected set; } - internal HotReloadContext HotReloadContext { get; } = new(); + internal HotReloadContext HotReloadContext => _hotReloadContext; private async void RenderRootComponentsOnHotReload() { @@ -141,8 +149,6 @@ await Dispatcher.InvokeAsync(() => { componentState.SetDirectParameters(initialParameters); } - - ProcessPendingRender(); } finally { @@ -159,12 +165,19 @@ await Dispatcher.InvokeAsync(() => protected IComponent InstantiateComponent([DynamicallyAccessedMembers(Component)] Type componentType) { var component = _componentFactory.InstantiateComponent(_serviceProvider, componentType); - if (_hotReloadEnvironment.IsHotReloadEnabled && component is IReceiveHotReloadContext receiveHotReloadContext) + InstatiateComponentForHotReload(component); + return component; + } + + /// + /// Intentionally authored as a separate method call so we can trim this code. + /// + private void InstatiateComponentForHotReload(IComponent component) + { + if (_hotReloadEnvironment is not null && _hotReloadEnvironment.IsHotReloadEnabled && component is IReceiveHotReloadContext receiveHotReloadContext) { receiveHotReloadContext.Receive(HotReloadContext); } - - return component; } /// @@ -230,13 +243,7 @@ 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())); - } + CaptureRootComponentForHotReload(initialParameters, componentState); componentState.SetDirectParameters(initialParameters); @@ -251,6 +258,20 @@ protected async Task RenderRootComponentAsync(int componentId, ParameterView ini } } + /// + /// Intentionally authored as a separate method call so we can trim this code. + /// + private void CaptureRootComponentForHotReload(ParameterView initialParameters, ComponentState componentState) + { + if (_hotReloadEnvironment?.IsHotReloadEnabled ?? false) + { + // 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())); + } + } + /// /// Allows derived types to handle exceptions during rendering. Defaults to rethrowing the original exception. /// @@ -316,7 +337,7 @@ public virtual Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo? fie UpdateRenderTreeToMatchClientState(latestEquivalentEventHandlerId, fieldInfo); } - Task task = null; + Task? task = null; try { // The event handler might request multiple renders in sequence. Capture them @@ -940,10 +961,7 @@ public void Dispose() /// public async ValueTask DisposeAsync() { - if (_hotReloadEnvironment.IsHotReloadEnabled) - { - HotReloadManager.OnDeltaApplied -= RenderRootComponentsOnHotReload; - } + DisposeForHotReload(); if (_disposed) { @@ -967,5 +985,16 @@ public async ValueTask DisposeAsync() } } } + + /// + /// Intentionally authored as a separate method call so we can trim this code. + /// + private void DisposeForHotReload() + { + if (_hotReloadEnvironment?.IsHotReloadEnabled ?? false) + { + HotReloadManager.OnDeltaApplied -= RenderRootComponentsOnHotReload; + } + } } } From 5bbe39693fcac8b2c7a9e20b1b80432832dc9494 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Wed, 17 Mar 2021 11:42:47 -0700 Subject: [PATCH 10/10] Update src/Components/Web.JS/src/GlobalExports.ts Co-authored-by: Steve Sanderson --- src/Components/Web.JS/src/GlobalExports.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Components/Web.JS/src/GlobalExports.ts b/src/Components/Web.JS/src/GlobalExports.ts index 00e9758087a7..43af8414820b 100644 --- a/src/Components/Web.JS/src/GlobalExports.ts +++ b/src/Components/Web.JS/src/GlobalExports.ts @@ -46,7 +46,7 @@ interface IBlazor { getLazyAssemblies?: any dotNetCriticalError?: any getSatelliteAssemblies?: any, - applyHotReload?: (id: string, metadataDelta: string, ilDeta: string) => void + applyHotReload?: (id: string, metadataDelta: string, ilDelta: string) => void } } @@ -62,4 +62,4 @@ export const Blazor: IBlazor = { }; // Make the following APIs available in global scope for invocation from JS -window['Blazor'] = Blazor; \ No newline at end of file +window['Blazor'] = Blazor;