Skip to content

Commit f2cb4a2

Browse files
committed
Spruce up async handling in OnNavigateAsync
1 parent 0c5c177 commit f2cb4a2

File tree

2 files changed

+151
-15
lines changed

2 files changed

+151
-15
lines changed

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

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ public class Router : IComponent, IHandleAfterRender, IDisposable
2323
static readonly ReadOnlyDictionary<string, object> _emptyParametersDictionary
2424
= new ReadOnlyDictionary<string, object>(new Dictionary<string, object>());
2525

26-
RenderHandle _renderHandle;
26+
internal RenderHandle _renderHandle { get; set; }
2727
string _baseUri;
2828
string _locationAbsolute;
2929
bool _navigationInterceptionEnabled;
@@ -35,7 +35,7 @@ static readonly ReadOnlyDictionary<string, object> _emptyParametersDictionary
3535

3636
private bool _onNavigateCalled = false;
3737

38-
[Inject] private NavigationManager NavigationManager { get; set; }
38+
[Inject] internal NavigationManager NavigationManager { get; set; }
3939

4040
[Inject] private INavigationInterception NavigationInterception { get; set; }
4141

@@ -72,7 +72,7 @@ static readonly ReadOnlyDictionary<string, object> _emptyParametersDictionary
7272
/// </summary>
7373
[Parameter] public EventCallback<NavigationContext> OnNavigateAsync { get; set; }
7474

75-
private RouteTable Routes { get; set; }
75+
internal RouteTable Routes { get; set; }
7676

7777
/// <inheritdoc />
7878
public void Attach(RenderHandle renderHandle)
@@ -109,20 +109,24 @@ public async Task SetParametersAsync(ParameterView parameters)
109109
throw new InvalidOperationException($"The {nameof(Router)} component requires a value for the parameter {nameof(NotFound)}.");
110110
}
111111

112+
var shouldRefresh = true;
112113
if (!_onNavigateCalled)
113114
{
114115
_onNavigateCalled = true;
115-
await RunOnNavigateAsync(NavigationManager.ToBaseRelativePath(_locationAbsolute));
116+
shouldRefresh = await RunOnNavigateAsync(NavigationManager.ToBaseRelativePath(_locationAbsolute));
117+
}
118+
119+
if (shouldRefresh)
120+
{
121+
Refresh(isNavigationIntercepted: false);
116122
}
117123

118-
Refresh(isNavigationIntercepted: false);
119124
}
120125

121126
/// <inheritdoc />
122127
public void Dispose()
123128
{
124129
NavigationManager.LocationChanged -= OnLocationChanged;
125-
_onNavigateCts?.Dispose();
126130
}
127131

128132
private static string StringUntilAny(string str, char[] chars)
@@ -190,28 +194,28 @@ private void Refresh(bool isNavigationIntercepted)
190194
}
191195
}
192196

193-
private async Task RunOnNavigateAsync(string path)
197+
internal async Task<bool> RunOnNavigateAsync(string path)
194198
{
195199
// If this router instance does not provide an OnNavigateAsync parameter
196200
// then we render the component associated with the route as per usual.
197201
if (!OnNavigateAsync.HasDelegate)
198202
{
199-
return;
203+
return await Task.FromResult(true);
200204
}
201205

202206
// If we've already invoked a task and stored its CTS, then
203207
// cancel the existing task.
204-
_onNavigateCts?.Dispose();
208+
_onNavigateCts?.Cancel();
205209

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

210214
// Create a cancellation task based on the cancellation token
211215
// associated with the current running task.
212-
var cancellationTaskSource = new TaskCompletionSource();
216+
var cancellationTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
213217
navigateContext.CancellationToken.Register(state =>
214-
((TaskCompletionSource)state).SetResult(), cancellationTaskSource);
218+
((TaskCompletionSource)state).SetResult(), cancellationTcs);
215219

216220
var task = OnNavigateAsync.InvokeAsync(navigateContext);
217221

@@ -221,16 +225,24 @@ private async Task RunOnNavigateAsync(string path)
221225
_renderHandle.Render(Navigating);
222226
}
223227

224-
await Task.WhenAny(task, cancellationTaskSource.Task);
228+
var completedTask = await Task.WhenAny(task, cancellationTcs.Task);
229+
if (completedTask == task)
230+
{
231+
return await Task.FromResult(true);
232+
}
233+
return await Task.FromResult(false);
225234
}
226235

227236
private async Task RunOnNavigateWithRefreshAsync(string path, bool isNavigationIntercepted)
228237
{
229-
await RunOnNavigateAsync(path);
230-
Refresh(isNavigationIntercepted);
238+
var shouldRefresh = await RunOnNavigateAsync(path);
239+
if (shouldRefresh)
240+
{
241+
Refresh(isNavigationIntercepted);
242+
}
231243
}
232244

233-
private void OnLocationChanged(object sender, LocationChangedEventArgs args)
245+
internal void OnLocationChanged(object sender, LocationChangedEventArgs args)
234246
{
235247
_locationAbsolute = args.Location;
236248
if (_renderHandle.IsInitialized && Routes != null)
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
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.Threading.Tasks;
8+
using Microsoft.AspNetCore.Components;
9+
using Microsoft.AspNetCore.Components.Routing;
10+
using Microsoft.AspNetCore.Components.Test.Helpers;
11+
using Microsoft.Extensions.DependencyModel;
12+
using Xunit;
13+
14+
namespace Microsoft.AspNetCore.Components.Test.Routing
15+
{
16+
public class RouterTest
17+
{
18+
[Fact]
19+
public void CanRunOnNavigateViaLocationChangeAsync()
20+
{
21+
// Arrange
22+
var router = CreateMockRouter();
23+
var called = false;
24+
async Task OnNavigateAsync(NavigationContext args)
25+
{
26+
await Task.CompletedTask;
27+
called = true;
28+
}
29+
router.OnNavigateAsync = new EventCallbackFactory().Create<NavigationContext>(router, OnNavigateAsync);
30+
31+
// Act
32+
router.OnLocationChanged(null, new LocationChangedEventArgs("http://example.com/jan", false));
33+
34+
// Assert
35+
Assert.True(called);
36+
}
37+
38+
[Fact]
39+
public void CanCancelPreviousOnNavigateAsync()
40+
{
41+
// Arrange
42+
var router = CreateMockRouter();
43+
var cancelled = "";
44+
async Task OnNavigateAsync(NavigationContext args)
45+
{
46+
await Task.CompletedTask;
47+
args.CancellationToken.Register(() => cancelled = args.Path);
48+
};
49+
router.OnNavigateAsync = new EventCallbackFactory().Create<NavigationContext>(router, OnNavigateAsync);
50+
51+
// Act
52+
router.OnLocationChanged(null, new LocationChangedEventArgs("http://example.com/jan", false));
53+
router.OnLocationChanged(null, new LocationChangedEventArgs("http://example.com/feb", false));
54+
55+
// Assert
56+
var expected = "jan";
57+
Assert.Equal(cancelled, expected);
58+
}
59+
60+
[Fact]
61+
public async Task RefreshesOnceOnCancelledOnNavigateAsync()
62+
{
63+
// Arrange
64+
var router = CreateMockRouter();
65+
async Task OnNavigateAsync(NavigationContext args)
66+
{
67+
if (args.Path.EndsWith("jan"))
68+
{
69+
await Task.Delay(5000);
70+
}
71+
if (args.Path.EndsWith("feb"))
72+
{
73+
await Task.CompletedTask;
74+
}
75+
};
76+
router.OnNavigateAsync = new EventCallbackFactory().Create<NavigationContext>(router, OnNavigateAsync);
77+
78+
// Act
79+
var janTask = router.RunOnNavigateAsync("jan");
80+
var febTask = router.RunOnNavigateAsync("feb");
81+
82+
await janTask;
83+
await febTask;
84+
85+
// Assert
86+
Assert.False(janTask.Result);
87+
Assert.True(febTask.Result);
88+
}
89+
90+
private Router CreateMockRouter()
91+
{
92+
var router = new Router();
93+
var renderer = new TestRenderer();
94+
router._renderHandle = new RenderHandle(renderer, 0);
95+
router.Routes = RouteTableFactory.Create(new[] { typeof(JanComponent), typeof(FebComponent) });
96+
router.NavigationManager = new TestNavigationManager();
97+
return router;
98+
}
99+
100+
[Route("jan")]
101+
private class JanComponent : ComponentBase { }
102+
103+
[Route("feb")]
104+
private class FebComponent : ComponentBase { }
105+
106+
private class TestNavigationManager : NavigationManager
107+
{
108+
public TestNavigationManager()
109+
{
110+
Initialize("http://example.com/", "http://example.com/months");
111+
}
112+
113+
public new void Initialize(string baseUri, string uri)
114+
{
115+
base.Initialize(baseUri, uri);
116+
}
117+
118+
protected override void NavigateToCore(string uri, bool forceLoad)
119+
{
120+
throw new System.NotImplementedException();
121+
}
122+
}
123+
}
124+
}

0 commit comments

Comments
 (0)