Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

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

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

Filter by extension

Filter by extension


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

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

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

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

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

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

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

namespace Microsoft.AspNetCore.Components.HotReload
{
/// <summary>
/// A context that indicates when a component is being rendered 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
pranavkm marked this conversation as resolved.
Show resolved Hide resolved
{
public static readonly HotReloadEnvironment Instance = new(Environment.GetEnvironmentVariable("DOTNET_MODIFIABLE_ASSEMBLIES") == "debug");
Copy link
Member

Choose a reason for hiding this comment

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

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

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

Copy link
Member

Choose a reason for hiding this comment

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

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

Copy link
Member

Choose a reason for hiding this comment

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

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

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

Copy link
Member

Choose a reason for hiding this comment

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

I think the rule is basically:

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

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

image

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


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

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

using System;
using System.Reflection;

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

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

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

namespace Microsoft.AspNetCore.Components.HotReload
{
/// <summary>
/// Allows a component to receive a <see cref="HotReloadContext"/>.
/// </summary>
public interface IReceiveHotReloadContext : IComponent
{
/// <summary>
/// Configures a component to use the hot reload context.
/// </summary>
/// <param name="context">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")]
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>
pranavkm marked this conversation as resolved.
Show resolved Hide resolved
</linker>
5 changes: 5 additions & 0 deletions src/Components/Components/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ Microsoft.AspNetCore.Components.ComponentApplicationState.PersistAsJson<TValue>(
Microsoft.AspNetCore.Components.ComponentApplicationState.PersistState(string! key, byte[]! value) -> void
Microsoft.AspNetCore.Components.ComponentApplicationState.TryTakeAsJson<TValue>(string! key, out TValue? instance) -> bool
Microsoft.AspNetCore.Components.ComponentApplicationState.TryTakePersistedState(string! key, out byte[]? value) -> bool
Microsoft.AspNetCore.Components.HotReload.HotReloadContext
Microsoft.AspNetCore.Components.HotReload.HotReloadContext.HotReloadContext() -> void
Microsoft.AspNetCore.Components.HotReload.HotReloadContext.IsHotReloading.get -> bool
Microsoft.AspNetCore.Components.HotReload.IReceiveHotReloadContext
Copy link
Contributor Author

Choose a reason for hiding this comment

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

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

Microsoft.AspNetCore.Components.HotReload.IReceiveHotReloadContext.Receive(Microsoft.AspNetCore.Components.HotReload.HotReloadContext! context) -> void
Microsoft.AspNetCore.Components.Lifetime.ComponentApplicationLifetime
Microsoft.AspNetCore.Components.Lifetime.ComponentApplicationLifetime.ComponentApplicationLifetime(Microsoft.Extensions.Logging.ILogger<Microsoft.AspNetCore.Components.Lifetime.ComponentApplicationLifetime!>! logger) -> void
Microsoft.AspNetCore.Components.Lifetime.ComponentApplicationLifetime.PersistStateAsync(Microsoft.AspNetCore.Components.Lifetime.IComponentApplicationStateStore! store, Microsoft.AspNetCore.Components.RenderTree.Renderer! renderer) -> System.Threading.Tasks.Task!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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