Skip to content

Commit bbc1162

Browse files
Add framework support for lazy-loading assemblies on route change (#23290)
* Add framework support for lazy-loading assemblies on route change * Configure lazy-loaded assemblies in WebAssemblyLazyLoadDefinition * Move tests to WebAssembly-only scenarios * Refactor RouteTableFactory and add WebAssemblyDynamicResourceLoader * Address feedback from peer review * Rename 'dynamicAssembly' to 'lazyAssembly' and address peer review * Add sample with loading state * Update Router API and assembly loading tests * Support and test cancellation and pre-rendering * Apply suggestions from code review Co-authored-by: Steve Sanderson <[email protected]> * Spurce up API and add tests for pre-rendering scenario * Use CT instead of CTS in NavigationContext * Address feedback from peer review * Remove extra test file and update Router Co-authored-by: Steve Sanderson <[email protected]>
1 parent 37c2003 commit bbc1162

28 files changed

+652
-206
lines changed

src/Components/Components/ref/Microsoft.AspNetCore.Components.netcoreapp.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -556,6 +556,12 @@ public LocationChangedEventArgs(string location, bool isNavigationIntercepted) {
556556
public bool IsNavigationIntercepted { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } }
557557
public string Location { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } }
558558
}
559+
public sealed partial class NavigationContext
560+
{
561+
internal NavigationContext() { }
562+
public System.Threading.CancellationToken CancellationToken { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } }
563+
public string Path { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } }
564+
}
559565
public partial class Router : Microsoft.AspNetCore.Components.IComponent, Microsoft.AspNetCore.Components.IHandleAfterRender, System.IDisposable
560566
{
561567
public Router() { }
@@ -566,10 +572,15 @@ public Router() { }
566572
[Microsoft.AspNetCore.Components.ParameterAttribute]
567573
public Microsoft.AspNetCore.Components.RenderFragment<Microsoft.AspNetCore.Components.RouteData> Found { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
568574
[Microsoft.AspNetCore.Components.ParameterAttribute]
575+
public Microsoft.AspNetCore.Components.RenderFragment Navigating { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
576+
[Microsoft.AspNetCore.Components.ParameterAttribute]
569577
public Microsoft.AspNetCore.Components.RenderFragment NotFound { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
578+
[Microsoft.AspNetCore.Components.ParameterAttribute]
579+
public Microsoft.AspNetCore.Components.EventCallback<Microsoft.AspNetCore.Components.Routing.NavigationContext> OnNavigateAsync { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
570580
public void Attach(Microsoft.AspNetCore.Components.RenderHandle renderHandle) { }
571581
public void Dispose() { }
572582
System.Threading.Tasks.Task Microsoft.AspNetCore.Components.IHandleAfterRender.OnAfterRenderAsync() { throw null; }
583+
[System.Diagnostics.DebuggerStepThroughAttribute]
573584
public System.Threading.Tasks.Task SetParametersAsync(Microsoft.AspNetCore.Components.ParameterView parameters) { throw null; }
574585
}
575586
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.Threading;
5+
6+
namespace Microsoft.AspNetCore.Components.Routing
7+
{
8+
/// <summary>
9+
/// Provides information about the current asynchronous navigation event
10+
/// including the target path and the cancellation token.
11+
/// </summary>
12+
public sealed class NavigationContext
13+
{
14+
internal NavigationContext(string path, CancellationToken cancellationToken)
15+
{
16+
Path = path;
17+
CancellationToken = cancellationToken;
18+
}
19+
20+
public string Path { get; }
21+
22+
public CancellationToken CancellationToken { get; }
23+
}
24+
}

src/Components/Components/src/Routing/Router.cs

Lines changed: 81 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
using System.Collections.ObjectModel;
99
using System.Linq;
1010
using System.Reflection;
11+
using System.Threading;
1112
using System.Threading.Tasks;
12-
using Microsoft.AspNetCore.Components.Rendering;
1313
using Microsoft.Extensions.Logging;
1414

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

32+
private CancellationTokenSource _onNavigateCts;
33+
34+
private readonly HashSet<Assembly> _assemblies = new HashSet<Assembly>();
35+
36+
private bool _onNavigateCalled = false;
37+
3238
[Inject] private NavigationManager NavigationManager { get; set; }
3339

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

65+
/// <summary>
66+
/// Get or sets the content to display when asynchronous navigation is in progress.
67+
/// </summary>
68+
[Parameter] public RenderFragment Navigating { get; set; }
69+
70+
/// <summary>
71+
/// Gets or sets a handler that should be called before navigating to a new page.
72+
/// </summary>
73+
[Parameter] public EventCallback<NavigationContext> OnNavigateAsync { get; set; }
74+
5975
private RouteTable Routes { get; set; }
6076

6177
/// <inheritdoc />
@@ -69,7 +85,7 @@ public void Attach(RenderHandle renderHandle)
6985
}
7086

7187
/// <inheritdoc />
72-
public Task SetParametersAsync(ParameterView parameters)
88+
public async Task SetParametersAsync(ParameterView parameters)
7389
{
7490
parameters.SetParameterProperties(this);
7591

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

112+
if (!_onNavigateCalled)
113+
{
114+
_onNavigateCalled = true;
115+
await RunOnNavigateAsync(NavigationManager.ToBaseRelativePath(_locationAbsolute));
116+
}
96117

97-
var assemblies = AdditionalAssemblies == null ? new[] { AppAssembly } : new[] { AppAssembly }.Concat(AdditionalAssemblies);
98-
Routes = RouteTableFactory.Create(assemblies);
99118
Refresh(isNavigationIntercepted: false);
100-
return Task.CompletedTask;
101119
}
102120

103121
/// <inheritdoc />
104122
public void Dispose()
105123
{
106124
NavigationManager.LocationChanged -= OnLocationChanged;
125+
_onNavigateCts?.Dispose();
107126
}
108127

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

136+
private void RefreshRouteTable()
137+
{
138+
var assemblies = AdditionalAssemblies == null ? new[] { AppAssembly } : new[] { AppAssembly }.Concat(AdditionalAssemblies);
139+
var assembliesSet = new HashSet<Assembly>(assemblies);
140+
141+
if (!_assemblies.SetEquals(assembliesSet))
142+
{
143+
Routes = RouteTableFactory.Create(assemblies);
144+
_assemblies.Clear();
145+
_assemblies.UnionWith(assembliesSet);
146+
}
147+
148+
}
149+
117150
private void Refresh(bool isNavigationIntercepted)
118151
{
152+
RefreshRouteTable();
153+
119154
var locationPath = NavigationManager.ToBaseRelativePath(_locationAbsolute);
120155
locationPath = StringUntilAny(locationPath, _queryOrHashStartChar);
121156
var context = new RouteContext(locationPath);
@@ -155,12 +190,52 @@ private void Refresh(bool isNavigationIntercepted)
155190
}
156191
}
157192

193+
private async Task RunOnNavigateAsync(string path)
194+
{
195+
// If this router instance does not provide an OnNavigateAsync parameter
196+
// then we render the component associated with the route as per usual.
197+
if (!OnNavigateAsync.HasDelegate)
198+
{
199+
return;
200+
}
201+
202+
// If we've already invoked a task and stored its CTS, then
203+
// cancel the existing task.
204+
_onNavigateCts?.Dispose();
205+
206+
// Create a new cancellation token source for this instance
207+
_onNavigateCts = new CancellationTokenSource();
208+
var navigateContext = new NavigationContext(path, _onNavigateCts.Token);
209+
210+
// Create a cancellation task based on the cancellation token
211+
// associated with the current running task.
212+
var cancellationTaskSource = new TaskCompletionSource();
213+
navigateContext.CancellationToken.Register(state =>
214+
((TaskCompletionSource)state).SetResult(), cancellationTaskSource);
215+
216+
var task = OnNavigateAsync.InvokeAsync(navigateContext);
217+
218+
// If the user provided a Navigating render fragment, then show it.
219+
if (Navigating != null && task.Status != TaskStatus.RanToCompletion)
220+
{
221+
_renderHandle.Render(Navigating);
222+
}
223+
224+
await Task.WhenAny(task, cancellationTaskSource.Task);
225+
}
226+
227+
private async Task RunOnNavigateWithRefreshAsync(string path, bool isNavigationIntercepted)
228+
{
229+
await RunOnNavigateAsync(path);
230+
Refresh(isNavigationIntercepted);
231+
}
232+
158233
private void OnLocationChanged(object sender, LocationChangedEventArgs args)
159234
{
160235
_locationAbsolute = args.Location;
161236
if (_renderHandle.IsInitialized && Routes != null)
162237
{
163-
Refresh(args.IsNavigationIntercepted);
238+
_ = RunOnNavigateWithRefreshAsync(NavigationManager.ToBaseRelativePath(_locationAbsolute), args.IsNavigationIntercepted);
164239
}
165240
}
166241

src/Components/Web.JS/dist/Release/blazor.webassembly.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Components/Web.JS/src/Platform/BootConfig.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export interface BootJsonData {
3030

3131
export interface ResourceGroups {
3232
readonly assembly: ResourceList;
33+
readonly lazyAssembly: ResourceList;
3334
readonly pdb?: ResourceList;
3435
readonly runtime: ResourceList;
3536
readonly satelliteResources?: { [cultureName: string] : ResourceList };

src/Components/Web.JS/src/Platform/Mono/MonoPlatform.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,34 @@ function createEmscriptenModuleInstance(resourceLoader: WebAssemblyResourceLoade
293293
}
294294
return BINDING.js_to_mono_obj(Promise.resolve(0));
295295
}
296+
297+
window['Blazor']._internal.getLazyAssemblies = (assembliesToLoadDotNetArray: System_Array<System_String>) : System_Object => {
298+
const assembliesToLoad = BINDING.mono_array_to_js_array<System_String, string>(assembliesToLoadDotNetArray);
299+
const lazyAssemblies = resourceLoader.bootConfig.resources.lazyAssembly;
300+
301+
if (lazyAssemblies) {
302+
const resourcePromises = Promise.all(assembliesToLoad
303+
.filter(assembly => lazyAssemblies.hasOwnProperty(assembly))
304+
.map(assembly => resourceLoader.loadResource(assembly, `_framework/${assembly}`, lazyAssemblies[assembly], 'assembly'))
305+
.map(async resource => (await resource.response).arrayBuffer()));
306+
307+
return BINDING.js_to_mono_obj(
308+
resourcePromises.then(resourcesToLoad => {
309+
if (resourcesToLoad.length) {
310+
window['Blazor']._internal.readLazyAssemblies = () => {
311+
const array = BINDING.mono_obj_array_new(resourcesToLoad.length);
312+
for (var i = 0; i < resourcesToLoad.length; i++) {
313+
BINDING.mono_obj_array_set(array, i, BINDING.js_typed_array_to_array(new Uint8Array(resourcesToLoad[i])));
314+
}
315+
return array;
316+
};
317+
}
318+
319+
return resourcesToLoad.length;
320+
}));
321+
}
322+
return BINDING.js_to_mono_obj(Promise.resolve(0));
323+
}
296324
});
297325

298326
module.postRun.push(() => {

0 commit comments

Comments
 (0)