Skip to content

Commit 303a9bf

Browse files
captainsafiagfoidlpranavkm
authored
Spruce up async handling in OnNavigateAsync callback in Blazor router (#23835)
* Spruce up async handling in OnNavigateAsync * Apply suggestions from code review Co-authored-by: Günther Foidl <[email protected]> * Ensure previous task awaited before starting next one * Apply suggestions from code review Co-authored-by: Pranav K <[email protected]> * Validate no exceptions throw on multiple invocations * Address feedback from peer review Co-authored-by: Günther Foidl <[email protected]> Co-authored-by: Pranav K <[email protected]>
1 parent d054275 commit 303a9bf

File tree

3 files changed

+145
-13
lines changed

3 files changed

+145
-13
lines changed

src/Components/Components/src/Properties/AssemblyInfo.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,5 @@
88
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Components.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
99
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Components.Web.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
1010
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Components.Web.Extensions.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
11+
12+
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")]

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

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ static readonly ReadOnlyDictionary<string, object> _emptyParametersDictionary
3131

3232
private CancellationTokenSource _onNavigateCts;
3333

34+
private Task _previousOnNavigateTask = Task.CompletedTask;
35+
3436
private readonly HashSet<Assembly> _assemblies = new HashSet<Assembly>();
3537

3638
private bool _onNavigateCalled = false;
@@ -112,7 +114,8 @@ public async Task SetParametersAsync(ParameterView parameters)
112114
if (!_onNavigateCalled)
113115
{
114116
_onNavigateCalled = true;
115-
await RunOnNavigateAsync(NavigationManager.ToBaseRelativePath(_locationAbsolute));
117+
await RunOnNavigateWithRefreshAsync(NavigationManager.ToBaseRelativePath(_locationAbsolute), isNavigationIntercepted: false);
118+
return;
116119
}
117120

118121
Refresh(isNavigationIntercepted: false);
@@ -122,7 +125,6 @@ public async Task SetParametersAsync(ParameterView parameters)
122125
public void Dispose()
123126
{
124127
NavigationManager.LocationChanged -= OnLocationChanged;
125-
_onNavigateCts?.Dispose();
126128
}
127129

128130
private static string StringUntilAny(string str, char[] chars)
@@ -147,7 +149,7 @@ private void RefreshRouteTable()
147149

148150
}
149151

150-
private void Refresh(bool isNavigationIntercepted)
152+
internal virtual void Refresh(bool isNavigationIntercepted)
151153
{
152154
RefreshRouteTable();
153155

@@ -190,28 +192,31 @@ private void Refresh(bool isNavigationIntercepted)
190192
}
191193
}
192194

193-
private async Task RunOnNavigateAsync(string path)
195+
private async ValueTask<bool> RunOnNavigateAsync(string path, Task previousOnNavigate)
194196
{
195197
// If this router instance does not provide an OnNavigateAsync parameter
196198
// then we render the component associated with the route as per usual.
197199
if (!OnNavigateAsync.HasDelegate)
198200
{
199-
return;
201+
return true;
200202
}
201203

202204
// If we've already invoked a task and stored its CTS, then
203-
// cancel the existing task.
204-
_onNavigateCts?.Dispose();
205+
// cancel that existing CTS.
206+
_onNavigateCts?.Cancel();
207+
// Then make sure that the task has been completed cancelled or
208+
// completed before continuing with the execution of this current task.
209+
await previousOnNavigate;
205210

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

210215
// Create a cancellation task based on the cancellation token
211216
// associated with the current running task.
212-
var cancellationTaskSource = new TaskCompletionSource();
217+
var cancellationTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
213218
navigateContext.CancellationToken.Register(state =>
214-
((TaskCompletionSource)state).SetResult(), cancellationTaskSource);
219+
((TaskCompletionSource)state).SetResult(), cancellationTcs);
215220

216221
var task = OnNavigateAsync.InvokeAsync(navigateContext);
217222

@@ -221,13 +226,34 @@ private async Task RunOnNavigateAsync(string path)
221226
_renderHandle.Render(Navigating);
222227
}
223228

224-
await Task.WhenAny(task, cancellationTaskSource.Task);
229+
var completedTask = await Task.WhenAny(task, cancellationTcs.Task);
230+
return task == completedTask;
225231
}
226232

227-
private async Task RunOnNavigateWithRefreshAsync(string path, bool isNavigationIntercepted)
233+
internal async Task RunOnNavigateWithRefreshAsync(string path, bool isNavigationIntercepted)
228234
{
229-
await RunOnNavigateAsync(path);
230-
Refresh(isNavigationIntercepted);
235+
// We cache the Task representing the previously invoked RunOnNavigateWithRefreshAsync
236+
// that is stored
237+
var previousTask = _previousOnNavigateTask;
238+
// Then we create a new one that represents our current invocation and store it
239+
// globally for the next invocation. Note to the developer, if the WASM runtime
240+
// support multi-threading then we'll need to implement the appropriate locks
241+
// here to ensure that the cached previous task is overwritten incorrectly.
242+
var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
243+
_previousOnNavigateTask = tcs.Task;
244+
try
245+
{
246+
// And pass an indicator for the previous task to the currently running one.
247+
var shouldRefresh = await RunOnNavigateAsync(path, previousTask);
248+
if (shouldRefresh)
249+
{
250+
Refresh(isNavigationIntercepted);
251+
}
252+
}
253+
finally
254+
{
255+
tcs.SetResult();
256+
}
231257
}
232258

233259
private void OnLocationChanged(object sender, LocationChangedEventArgs args)
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
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;
5+
using System.Collections.Generic;
6+
using System.Linq;
7+
using System.Reflection;
8+
using System.Threading;
9+
using System.Threading.Tasks;
10+
using Microsoft.AspNetCore.Components;
11+
using Microsoft.AspNetCore.Components.Routing;
12+
using Microsoft.AspNetCore.Components.Test.Helpers;
13+
using Microsoft.Extensions.DependencyModel;
14+
using Moq;
15+
using Xunit;
16+
17+
namespace Microsoft.AspNetCore.Components.Test.Routing
18+
{
19+
public class RouterTest
20+
{
21+
[Fact]
22+
public async Task CanRunOnNavigateAsync()
23+
{
24+
// Arrange
25+
var router = CreateMockRouter();
26+
var called = false;
27+
async Task OnNavigateAsync(NavigationContext args)
28+
{
29+
await Task.CompletedTask;
30+
called = true;
31+
}
32+
router.Object.OnNavigateAsync = new EventCallbackFactory().Create<NavigationContext>(router, OnNavigateAsync);
33+
34+
// Act
35+
await router.Object.RunOnNavigateWithRefreshAsync("http://example.com/jan", false);
36+
37+
// Assert
38+
Assert.True(called);
39+
}
40+
41+
[Fact]
42+
public async Task CanCancelPreviousOnNavigateAsync()
43+
{
44+
// Arrange
45+
var router = CreateMockRouter();
46+
var cancelled = "";
47+
async Task OnNavigateAsync(NavigationContext args)
48+
{
49+
await Task.CompletedTask;
50+
args.CancellationToken.Register(() => cancelled = args.Path);
51+
};
52+
router.Object.OnNavigateAsync = new EventCallbackFactory().Create<NavigationContext>(router, OnNavigateAsync);
53+
54+
// Act
55+
await router.Object.RunOnNavigateWithRefreshAsync("jan", false);
56+
await router.Object.RunOnNavigateWithRefreshAsync("feb", false);
57+
58+
// Assert
59+
var expected = "jan";
60+
Assert.Equal(cancelled, expected);
61+
}
62+
63+
[Fact]
64+
public async Task RefreshesOnceOnCancelledOnNavigateAsync()
65+
{
66+
// Arrange
67+
var router = CreateMockRouter();
68+
async Task OnNavigateAsync(NavigationContext args)
69+
{
70+
if (args.Path.EndsWith("jan"))
71+
{
72+
await Task.Delay(Timeout.Infinite);
73+
}
74+
};
75+
router.Object.OnNavigateAsync = new EventCallbackFactory().Create<NavigationContext>(router, OnNavigateAsync);
76+
77+
// Act
78+
var janTask = router.Object.RunOnNavigateWithRefreshAsync("jan", false);
79+
var febTask = router.Object.RunOnNavigateWithRefreshAsync("feb", false);
80+
81+
var janTaskException = await Record.ExceptionAsync(() => janTask);
82+
var febTaskException = await Record.ExceptionAsync(() => febTask);
83+
84+
// Assert neither exceution threw an exception
85+
Assert.Null(janTaskException);
86+
Assert.Null(febTaskException);
87+
// Assert refresh should've only been called once for the second route
88+
router.Verify(x => x.Refresh(false), Times.Once());
89+
}
90+
91+
private Mock<Router> CreateMockRouter()
92+
{
93+
var router = new Mock<Router>() { CallBase = true };
94+
router.Setup(x => x.Refresh(It.IsAny<bool>())).Verifiable();
95+
return router;
96+
}
97+
98+
[Route("jan")]
99+
private class JanComponent : ComponentBase { }
100+
101+
[Route("feb")]
102+
private class FebComponent : ComponentBase { }
103+
}
104+
}

0 commit comments

Comments
 (0)