Skip to content

Commit a888bef

Browse files
committed
Address feedback from peer review
1 parent 7445f32 commit a888bef

File tree

8 files changed

+67
-53
lines changed

8 files changed

+67
-53
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -576,7 +576,7 @@ public Router() { }
576576
[Microsoft.AspNetCore.Components.ParameterAttribute]
577577
public Microsoft.AspNetCore.Components.RenderFragment NotFound { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
578578
[Microsoft.AspNetCore.Components.ParameterAttribute]
579-
public System.Func<Microsoft.AspNetCore.Components.Routing.NavigationContext, System.Threading.Tasks.Task> OnNavigateAsync { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
579+
public Microsoft.AspNetCore.Components.EventCallback<Microsoft.AspNetCore.Components.Routing.NavigationContext> OnNavigateAsync { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
580580
public void Attach(Microsoft.AspNetCore.Components.RenderHandle renderHandle) { }
581581
public void Dispose() { }
582582
System.Threading.Tasks.Task Microsoft.AspNetCore.Components.IHandleAfterRender.OnAfterRenderAsync() { throw null; }

src/Components/Components/src/Routing/NavigationContext.cs

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

4-
using System;
5-
using System.Collections.Generic;
6-
using System.Linq;
7-
using System.Text;
84
using System.Threading;
9-
using System.Threading.Tasks;
105

116
namespace Microsoft.AspNetCore.Components.Routing
127
{
13-
public class NavigationContext
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
1413
{
1514
internal NavigationContext(string path, CancellationToken cancellationToken)
1615
{

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

Lines changed: 26 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
using System.Reflection;
1111
using System.Threading;
1212
using System.Threading.Tasks;
13-
using Microsoft.AspNetCore.Components.Rendering;
1413
using Microsoft.Extensions.Logging;
1514

1615
namespace Microsoft.AspNetCore.Components.Routing
@@ -34,6 +33,8 @@ static readonly ReadOnlyDictionary<string, object> _emptyParametersDictionary
3433

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

36+
private bool _onNavigateCalled = false;
37+
3738
[Inject] private NavigationManager NavigationManager { get; set; }
3839

3940
[Inject] private INavigationInterception NavigationInterception { get; set; }
@@ -69,7 +70,7 @@ static readonly ReadOnlyDictionary<string, object> _emptyParametersDictionary
6970
/// <summary>
7071
/// Gets or sets a handler that should be called before navigating to a new page.
7172
/// </summary>
72-
[Parameter] public Func<NavigationContext, Task> OnNavigateAsync { get; set; }
73+
[Parameter] public EventCallback<NavigationContext> OnNavigateAsync { get; set; }
7374

7475
private RouteTable Routes { get; set; }
7576

@@ -108,13 +109,20 @@ public async Task SetParametersAsync(ParameterView parameters)
108109
throw new InvalidOperationException($"The {nameof(Router)} component requires a value for the parameter {nameof(NotFound)}.");
109110
}
110111

111-
await RunOnNavigateAsync(NavigationManager.ToBaseRelativePath(_locationAbsolute), isNavigationIntercepted: false);
112+
if (!_onNavigateCalled)
113+
{
114+
_onNavigateCalled = true;
115+
await RunOnNavigateAsync(NavigationManager.ToBaseRelativePath(_locationAbsolute));
116+
}
117+
118+
Refresh(isNavigationIntercepted: false);
112119
}
113120

114121
/// <inheritdoc />
115122
public void Dispose()
116123
{
117124
NavigationManager.LocationChanged -= OnLocationChanged;
125+
_onNavigateCts?.Dispose();
118126
}
119127

120128
private static string StringUntilAny(string str, char[] chars)
@@ -129,23 +137,14 @@ private void RefreshRouteTable()
129137
{
130138
var assemblies = AdditionalAssemblies == null ? new[] { AppAssembly } : new[] { AppAssembly }.Concat(AdditionalAssemblies);
131139
var assembliesSet = new HashSet<Assembly>(assemblies);
132-
if (_assemblies.Count != assembliesSet.Count)
140+
141+
if (!_assemblies.SetEquals(assembliesSet))
133142
{
134143
Routes = RouteTableFactory.Create(assemblies);
144+
_assemblies.Clear();
135145
_assemblies.UnionWith(assembliesSet);
136146
}
137-
// If the new assemblies set is the same length as the previous one,
138-
// we check if they are equal-by-value and refresh the route table if
139-
// not. NOTE: This is an uncommon occurrence and a cold-code path so
140-
// we shouldn't end up needing to do an equal-by-value check too frequently.
141-
if (_assemblies.Count == assembliesSet.Count)
142-
{
143-
if (!_assemblies.SetEquals(assembliesSet))
144-
{
145-
Routes = RouteTableFactory.Create(assemblies);
146-
_assemblies.UnionWith(assembliesSet);
147-
}
148-
}
147+
149148
}
150149

151150
private void Refresh(bool isNavigationIntercepted)
@@ -191,33 +190,29 @@ private void Refresh(bool isNavigationIntercepted)
191190
}
192191
}
193192

194-
private async Task RunOnNavigateAsync(string path, bool isNavigationIntercepted)
193+
private async Task RunOnNavigateAsync(string path)
195194
{
196195
// If this router instance does not provide an OnNavigateAsync parameter
197196
// then we render the component associated with the route as per usual.
198-
if (OnNavigateAsync == null)
197+
if (!OnNavigateAsync.HasDelegate)
199198
{
200-
Refresh(isNavigationIntercepted);
201199
return;
202200
}
203201

204202
// If we've already invoked a task and stored its CTS, then
205203
// cancel the existing task.
206-
if (_onNavigateCts != null && !_onNavigateCts.IsCancellationRequested)
207-
{
208-
_onNavigateCts.Cancel();
209-
}
204+
_onNavigateCts?.Dispose();
205+
210206
// Create a new cancellation token source for this instance
211207
_onNavigateCts = new CancellationTokenSource();
212-
213208
var navigateContext = new NavigationContext(path, _onNavigateCts.Token);
214-
var task = OnNavigateAsync(navigateContext);
209+
var task = OnNavigateAsync.InvokeAsync(navigateContext);
215210

216211
// Create a cancellation task based on the cancellation token
217212
// associated with the current running task.
218213
var cancellationTaskSource = new TaskCompletionSource();
219214
navigateContext.CancellationToken.Register(() =>
220-
cancellationTaskSource.TrySetCanceled(navigateContext.CancellationToken));
215+
cancellationTaskSource.SetResult());
221216

222217
// If the user provided a Navigating render fragment, then show it.
223218
if (Navigating != null && task.Status != TaskStatus.RanToCompletion)
@@ -226,7 +221,11 @@ private async Task RunOnNavigateAsync(string path, bool isNavigationIntercepted)
226221
}
227222

228223
await Task.WhenAny(task, cancellationTaskSource.Task);
224+
}
229225

226+
private async Task RunOnNavigateWithRefreshAsync(string path, bool isNavigationIntercepted)
227+
{
228+
await RunOnNavigateAsync(path);
230229
Refresh(isNavigationIntercepted);
231230
}
232231

@@ -235,7 +234,7 @@ private void OnLocationChanged(object sender, LocationChangedEventArgs args)
235234
_locationAbsolute = args.Location;
236235
if (_renderHandle.IsInitialized && Routes != null)
237236
{
238-
_ = RunOnNavigateAsync(NavigationManager.ToBaseRelativePath(_locationAbsolute), args.IsNavigationIntercepted);
237+
_ = RunOnNavigateWithRefreshAsync(NavigationManager.ToBaseRelativePath(_locationAbsolute), args.IsNavigationIntercepted);
239238
}
240239
}
241240

src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5-
using System.Collections.Generic;
65
using System.IO;
76
using System.Linq;
87
using Microsoft.AspNetCore.Components.Routing;
@@ -11,7 +10,6 @@
1110
using Microsoft.Extensions.Configuration;
1211
using Microsoft.Extensions.Configuration.Json;
1312
using Microsoft.Extensions.DependencyInjection;
14-
using Microsoft.Extensions.DependencyInjection.Extensions;
1513
using Microsoft.Extensions.Logging;
1614
using Microsoft.JSInterop;
1715

@@ -191,6 +189,7 @@ internal void InitializeDefaultServices()
191189
Services.AddSingleton<IJSRuntime>(DefaultWebAssemblyJSRuntime.Instance);
192190
Services.AddSingleton<NavigationManager>(WebAssemblyNavigationManager.Instance);
193191
Services.AddSingleton<INavigationInterception>(WebAssemblyNavigationInterception.Instance);
192+
Services.AddSingleton(provider => new LazyAssemblyLoader(provider));
194193
Services.AddLogging(builder => {
195194
builder.AddProvider(new WebAssemblyConsoleLoggerProvider(DefaultWebAssemblyJSRuntime.Instance));
196195
});

src/Components/WebAssembly/WebAssembly/src/Services/LazyAssemblyLoader.cs

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,33 +2,47 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5-
using System.IO;
65
using System.Collections.Generic;
6+
using System.IO;
77
using System.Linq;
88
using System.Reflection;
9-
using System.Threading.Tasks;
109
using System.Runtime.InteropServices;
1110
using System.Runtime.Loader;
12-
using Microsoft.JSInterop.WebAssembly;
11+
using System.Threading.Tasks;
1312
using Microsoft.Extensions.DependencyInjection;
1413
using Microsoft.JSInterop;
14+
using Microsoft.JSInterop.WebAssembly;
1515

1616
namespace Microsoft.AspNetCore.Components.WebAssembly.Services
1717
{
18+
/// <summary>
19+
/// Provides a service for loading assemblies at runtime in a browser context.
20+
///
21+
/// Supports finding pre-loaded assemblies in a server or pre-rendering context.
22+
/// </summary>
1823
public class LazyAssemblyLoader
1924
{
2025
internal const string GetDynamicAssemblies = "window.Blazor._internal.getLazyAssemblies";
2126
internal const string ReadDynamicAssemblies = "window.Blazor._internal.readLazyAssemblies";
2227

23-
private List<string> _loadedAssemblyCache = new List<string>();
28+
private List<Assembly> _loadedAssemblyCache = new List<Assembly>();
2429

2530
private readonly IServiceProvider _provider;
2631

2732
public LazyAssemblyLoader(IServiceProvider provider)
2833
{
2934
_provider = provider;
35+
_loadedAssemblyCache = AppDomain.CurrentDomain.GetAssemblies().ToList();
3036
}
3137

38+
/// <summary>
39+
/// In a browser context, calling this method will fetch the assemblies requested
40+
/// via a network call and load them into the runtime. In a server or pre-rendered
41+
/// context, this method will look for the assemblies already loaded in the runtime
42+
/// and return them.
43+
/// </summary>
44+
/// <param name="assembliesToLoad">The names of the assemblies to load (e.g. "MyAssembly.dll")</param>
45+
/// <returns>A list of the loaded <see cref="Assembly"/></returns>
3246
public async Task<IEnumerable<Assembly>> LoadAssembliesAsync(IEnumerable<string> assembliesToLoad)
3347
{
3448
if (RuntimeInformation.IsOSPlatform(OSPlatform.Browser))
@@ -41,26 +55,26 @@ public async Task<IEnumerable<Assembly>> LoadAssembliesAsync(IEnumerable<string>
4155

4256
private Task<IEnumerable<Assembly>> LoadAssembliesInServerAsync(IEnumerable<string> assembliesToLoad)
4357
{
44-
var assemblies = AppDomain.CurrentDomain.GetAssemblies().Where(assembly =>
58+
var loadedAssemblies = _loadedAssemblyCache.Where(assembly =>
4559
assembliesToLoad.Contains(assembly.GetName().Name + ".dll"));
4660

47-
if (assemblies.Count() != assembliesToLoad.Count())
61+
if (loadedAssemblies.Count() != assembliesToLoad.Count())
4862
{
49-
var unloadedAssemblies = assembliesToLoad.Except(assemblies.Select(assembly => assembly.GetName().Name + ".dll"));
50-
throw new FileNotFoundException($"Unable to find the following assemblies: {string.Join(",", unloadedAssemblies)}");
63+
var unloadedAssemblies = assembliesToLoad.Except(loadedAssemblies.Select(a => a.GetName().Name + ".dll"));
64+
throw new InvalidOperationException($"Unable to find the following assemblies: {string.Join(",", unloadedAssemblies)}. Make sure that the appplication is referencing the assemblies and that they are present in the output folder.");
5165
}
5266

53-
return Task.FromResult(assemblies);
67+
return Task.FromResult(loadedAssemblies);
5468
}
5569

5670
private async Task<IEnumerable<Assembly>> LoadAssembliesInClientAsync(IEnumerable<string> assembliesToLoad)
5771
{
58-
var _jsRuntime = _provider.GetRequiredService<IJSRuntime>();
72+
var jsRuntime = _provider.GetRequiredService<IJSRuntime>();
5973
// Only load assemblies that haven't already been lazily-loaded
60-
var newAssembliesToLoad = assembliesToLoad.Where(assembly => !_loadedAssemblyCache.Contains(assembly));
74+
var newAssembliesToLoad = assembliesToLoad.Except(_loadedAssemblyCache.Select(a => a.GetName().Name + ".dll"));
6175
var loadedAssemblies = new List<Assembly>();
6276

63-
var count = (int)await ((WebAssemblyJSRuntime)_jsRuntime).InvokeUnmarshalled<string[], object, object, Task<object>>(
77+
var count = (int)await ((WebAssemblyJSRuntime)jsRuntime).InvokeUnmarshalled<string[], object, object, Task<object>>(
6478
GetDynamicAssemblies,
6579
assembliesToLoad.ToArray(),
6680
null,
@@ -71,7 +85,7 @@ private async Task<IEnumerable<Assembly>> LoadAssembliesInClientAsync(IEnumerabl
7185
return loadedAssemblies;
7286
}
7387

74-
var assemblies = ((WebAssemblyJSRuntime)_jsRuntime).InvokeUnmarshalled<object, object, object, object[]>(
88+
var assemblies = ((WebAssemblyJSRuntime)jsRuntime).InvokeUnmarshalled<object, object, object, object[]>(
7589
ReadDynamicAssemblies,
7690
null,
7791
null,
@@ -85,7 +99,7 @@ private async Task<IEnumerable<Assembly>> LoadAssembliesInClientAsync(IEnumerabl
8599
// into the default app context.
86100
var loadedAssembly = AssemblyLoadContext.Default.LoadFromStream(new MemoryStream(assembly));
87101
loadedAssemblies.Add(loadedAssembly);
88-
_loadedAssemblyCache.Add(loadedAssembly.GetName().Name + ".dll");
102+
_loadedAssemblyCache.Add(loadedAssembly);
89103
}
90104

91105
return loadedAssemblies;

src/Components/test/E2ETest/Tests/WebAssemblyLazyLoadTest.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ public void CanLazyLoadOnFirstVisit()
6565
SetUrlViaPushState("/WithLazyAssembly");
6666
var app = Browser.MountTestComponent<TestRouterWithLazyAssembly>();
6767

68+
// Wait for the page to finish loading
69+
new WebDriverWait(Browser, TimeSpan.FromSeconds(2)).Until(
70+
driver => driver.FindElement(By.Id("use-package-button")) != null);
71+
6872
var button = app.FindElement(By.Id("use-package-button"));
6973

7074
// We should have requested the DLL

src/Components/test/testassets/TestServer/ClientStartup.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
using Microsoft.AspNetCore.Builder;
2-
using Microsoft.AspNetCore.Components.WebAssembly.Services;
32
using Microsoft.AspNetCore.Hosting;
43
using Microsoft.Extensions.Configuration;
54
using Microsoft.Extensions.DependencyInjection;
@@ -24,7 +23,6 @@ public void ConfigureServices(IServiceCollection services)
2423
{
2524
services.AddMvc();
2625
services.AddServerSideBlazor();
27-
services.AddSingleton(s => new LazyAssemblyLoader(s));
2826
}
2927

3028
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.

src/Components/test/testassets/TestServer/PrerenderedStartup.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using Microsoft.Extensions.DependencyInjection;
66
using Microsoft.Extensions.Hosting;
77
using Microsoft.AspNetCore.Components.WebAssembly.Services;
8+
using Microsoft.JSInterop;
89

910
namespace TestServer
1011
{
@@ -23,7 +24,7 @@ public void ConfigureServices(IServiceCollection services)
2324
services.AddMvc();
2425
services.AddServerSideBlazor();
2526
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie();
26-
services.AddSingleton(s => new LazyAssemblyLoader(s));
27+
services.AddSingleton<LazyAssemblyLoader>();
2728
}
2829

2930
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.

0 commit comments

Comments
 (0)