Skip to content

Add framework support for lazy-loading assemblies on route change #23290

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

Merged
merged 14 commits into from
Jul 9, 2020
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
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,12 @@ public LocationChangedEventArgs(string location, bool isNavigationIntercepted) {
public bool IsNavigationIntercepted { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } }
public string Location { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } }
}
public sealed partial class NavigationContext
{
internal NavigationContext() { }
public System.Threading.CancellationToken CancellationToken { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } }
public string Path { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } }
}
public partial class Router : Microsoft.AspNetCore.Components.IComponent, Microsoft.AspNetCore.Components.IHandleAfterRender, System.IDisposable
{
public Router() { }
Expand All @@ -566,10 +572,15 @@ public Router() { }
[Microsoft.AspNetCore.Components.ParameterAttribute]
public Microsoft.AspNetCore.Components.RenderFragment<Microsoft.AspNetCore.Components.RouteData> Found { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
[Microsoft.AspNetCore.Components.ParameterAttribute]
public Microsoft.AspNetCore.Components.RenderFragment Navigating { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
[Microsoft.AspNetCore.Components.ParameterAttribute]
public Microsoft.AspNetCore.Components.RenderFragment NotFound { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
[Microsoft.AspNetCore.Components.ParameterAttribute]
public Microsoft.AspNetCore.Components.EventCallback<Microsoft.AspNetCore.Components.Routing.NavigationContext> OnNavigateAsync { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
public void Attach(Microsoft.AspNetCore.Components.RenderHandle renderHandle) { }
public void Dispose() { }
System.Threading.Tasks.Task Microsoft.AspNetCore.Components.IHandleAfterRender.OnAfterRenderAsync() { throw null; }
[System.Diagnostics.DebuggerStepThroughAttribute]
public System.Threading.Tasks.Task SetParametersAsync(Microsoft.AspNetCore.Components.ParameterView parameters) { throw null; }
}
}
24 changes: 24 additions & 0 deletions src/Components/Components/src/Routing/NavigationContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// 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.Threading;

namespace Microsoft.AspNetCore.Components.Routing
{
/// <summary>
/// Provides information about the current asynchronous navigation event
/// including the target path and the cancellation token.
/// </summary>
public sealed class NavigationContext
{
internal NavigationContext(string path, CancellationToken cancellationToken)
{
Path = path;
CancellationToken = cancellationToken;
}

public string Path { get; }

public CancellationToken CancellationToken { get; }
}
}
87 changes: 81 additions & 6 deletions src/Components/Components/src/Routing/Router.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
using System.Collections.ObjectModel;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.Extensions.Logging;

namespace Microsoft.AspNetCore.Components.Routing
Expand All @@ -29,6 +29,12 @@ static readonly ReadOnlyDictionary<string, object> _emptyParametersDictionary
bool _navigationInterceptionEnabled;
ILogger<Router> _logger;

private CancellationTokenSource _onNavigateCts;

private readonly HashSet<Assembly> _assemblies = new HashSet<Assembly>();

private bool _onNavigateCalled = false;

[Inject] private NavigationManager NavigationManager { get; set; }

[Inject] private INavigationInterception NavigationInterception { get; set; }
Expand Down Expand Up @@ -56,6 +62,16 @@ static readonly ReadOnlyDictionary<string, object> _emptyParametersDictionary
/// </summary>
[Parameter] public RenderFragment<RouteData> Found { get; set; }

/// <summary>
/// Get or sets the content to display when asynchronous navigation is in progress.
/// </summary>
[Parameter] public RenderFragment Navigating { get; set; }

/// <summary>
/// Gets or sets a handler that should be called before navigating to a new page.
/// </summary>
[Parameter] public EventCallback<NavigationContext> OnNavigateAsync { get; set; }

private RouteTable Routes { get; set; }

/// <inheritdoc />
Expand All @@ -69,7 +85,7 @@ public void Attach(RenderHandle renderHandle)
}

/// <inheritdoc />
public Task SetParametersAsync(ParameterView parameters)
public async Task SetParametersAsync(ParameterView parameters)
{
parameters.SetParameterProperties(this);

Expand All @@ -93,17 +109,20 @@ public Task SetParametersAsync(ParameterView parameters)
throw new InvalidOperationException($"The {nameof(Router)} component requires a value for the parameter {nameof(NotFound)}.");
}

if (!_onNavigateCalled)
{
_onNavigateCalled = true;
await RunOnNavigateAsync(NavigationManager.ToBaseRelativePath(_locationAbsolute));
}

var assemblies = AdditionalAssemblies == null ? new[] { AppAssembly } : new[] { AppAssembly }.Concat(AdditionalAssemblies);
Routes = RouteTableFactory.Create(assemblies);
Refresh(isNavigationIntercepted: false);
return Task.CompletedTask;
}

/// <inheritdoc />
public void Dispose()
{
NavigationManager.LocationChanged -= OnLocationChanged;
_onNavigateCts?.Dispose();
Copy link
Member

Choose a reason for hiding this comment

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

Can this ever be called when RunOnNavigateAsync is running? If so, this could lead to ODEs in RunOnNavigateAsync.

}

private static string StringUntilAny(string str, char[] chars)
Expand All @@ -114,8 +133,24 @@ private static string StringUntilAny(string str, char[] chars)
: str.Substring(0, firstIndex);
}

private void RefreshRouteTable()
{
var assemblies = AdditionalAssemblies == null ? new[] { AppAssembly } : new[] { AppAssembly }.Concat(AdditionalAssemblies);
var assembliesSet = new HashSet<Assembly>(assemblies);

if (!_assemblies.SetEquals(assembliesSet))
{
Routes = RouteTableFactory.Create(assemblies);
_assemblies.Clear();
_assemblies.UnionWith(assembliesSet);
}

}

private void Refresh(bool isNavigationIntercepted)
{
RefreshRouteTable();

var locationPath = NavigationManager.ToBaseRelativePath(_locationAbsolute);
locationPath = StringUntilAny(locationPath, _queryOrHashStartChar);
var context = new RouteContext(locationPath);
Expand Down Expand Up @@ -155,12 +190,52 @@ private void Refresh(bool isNavigationIntercepted)
}
}

private async Task RunOnNavigateAsync(string path)
{
// If this router instance does not provide an OnNavigateAsync parameter
// then we render the component associated with the route as per usual.
if (!OnNavigateAsync.HasDelegate)
{
return;
}

// If we've already invoked a task and stored its CTS, then
// cancel the existing task.
_onNavigateCts?.Dispose();

// Create a new cancellation token source for this instance
_onNavigateCts = new CancellationTokenSource();
var navigateContext = new NavigationContext(path, _onNavigateCts.Token);

// Create a cancellation task based on the cancellation token
// associated with the current running task.
var cancellationTaskSource = new TaskCompletionSource();
navigateContext.CancellationToken.Register(state =>
((TaskCompletionSource)state).SetResult(), cancellationTaskSource);

var task = OnNavigateAsync.InvokeAsync(navigateContext);

// If the user provided a Navigating render fragment, then show it.
if (Navigating != null && task.Status != TaskStatus.RanToCompletion)
{
_renderHandle.Render(Navigating);
}

await Task.WhenAny(task, cancellationTaskSource.Task);
}

private async Task RunOnNavigateWithRefreshAsync(string path, bool isNavigationIntercepted)
{
await RunOnNavigateAsync(path);
Refresh(isNavigationIntercepted);
}

private void OnLocationChanged(object sender, LocationChangedEventArgs args)
{
_locationAbsolute = args.Location;
if (_renderHandle.IsInitialized && Routes != null)
{
Refresh(args.IsNavigationIntercepted);
_ = RunOnNavigateWithRefreshAsync(NavigationManager.ToBaseRelativePath(_locationAbsolute), args.IsNavigationIntercepted);
Copy link
Member

Choose a reason for hiding this comment

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

Is it ok to ignore this task?

Copy link
Member

Choose a reason for hiding this comment

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

We have a separate workitem to surface exceptions in this task, we don't want to tackle it on the current PR

Copy link
Member Author

Choose a reason for hiding this comment

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

Not really. We have an open issue to track how we capture unhandled expectations and dispatch them to the renderer. It's tracked because the change is orthogonal to what we have here.

#23763

}
}

Expand Down
2 changes: 1 addition & 1 deletion src/Components/Web.JS/dist/Release/blazor.webassembly.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/Components/Web.JS/src/Platform/BootConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export interface BootJsonData {

export interface ResourceGroups {
readonly assembly: ResourceList;
readonly lazyAssembly: ResourceList;
readonly pdb?: ResourceList;
readonly runtime: ResourceList;
readonly satelliteResources?: { [cultureName: string] : ResourceList };
Expand Down
28 changes: 28 additions & 0 deletions src/Components/Web.JS/src/Platform/Mono/MonoPlatform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,34 @@ function createEmscriptenModuleInstance(resourceLoader: WebAssemblyResourceLoade
}
return BINDING.js_to_mono_obj(Promise.resolve(0));
}

window['Blazor']._internal.getLazyAssemblies = (assembliesToLoadDotNetArray: System_Array<System_String>) : System_Object => {
const assembliesToLoad = BINDING.mono_array_to_js_array<System_String, string>(assembliesToLoadDotNetArray);
const lazyAssemblies = resourceLoader.bootConfig.resources.lazyAssembly;

if (lazyAssemblies) {
const resourcePromises = Promise.all(assembliesToLoad
.filter(assembly => lazyAssemblies.hasOwnProperty(assembly))
.map(assembly => resourceLoader.loadResource(assembly, `_framework/${assembly}`, lazyAssemblies[assembly], 'assembly'))
.map(async resource => (await resource.response).arrayBuffer()));

return BINDING.js_to_mono_obj(
resourcePromises.then(resourcesToLoad => {
if (resourcesToLoad.length) {
window['Blazor']._internal.readLazyAssemblies = () => {
const array = BINDING.mono_obj_array_new(resourcesToLoad.length);
for (var i = 0; i < resourcesToLoad.length; i++) {
BINDING.mono_obj_array_set(array, i, BINDING.js_typed_array_to_array(new Uint8Array(resourcesToLoad[i])));
}
return array;
};
}

return resourcesToLoad.length;
}));
}
return BINDING.js_to_mono_obj(Promise.resolve(0));
}
});

module.postRun.push(() => {
Expand Down
Loading