Skip to content

Commit 27f8e31

Browse files
committed
Ensure previous task awaited before starting next one
1 parent 7479d31 commit 27f8e31

File tree

3 files changed

+55
-63
lines changed

3 files changed

+55
-63
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: 34 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -23,19 +23,21 @@ public class Router : IComponent, IHandleAfterRender, IDisposable
2323
static readonly ReadOnlyDictionary<string, object> _emptyParametersDictionary
2424
= new ReadOnlyDictionary<string, object>(new Dictionary<string, object>());
2525

26-
internal RenderHandle _renderHandle { get; set; }
26+
RenderHandle _renderHandle;
2727
string _baseUri;
2828
string _locationAbsolute;
2929
bool _navigationInterceptionEnabled;
3030
ILogger<Router> _logger;
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;
3739

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

4042
[Inject] private INavigationInterception NavigationInterception { get; set; }
4143

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

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

7779
/// <inheritdoc />
7880
public void Attach(RenderHandle renderHandle)
@@ -109,18 +111,14 @@ public async Task SetParametersAsync(ParameterView parameters)
109111
throw new InvalidOperationException($"The {nameof(Router)} component requires a value for the parameter {nameof(NotFound)}.");
110112
}
111113

112-
var shouldRefresh = true;
113114
if (!_onNavigateCalled)
114115
{
115116
_onNavigateCalled = true;
116-
shouldRefresh = await RunOnNavigateAsync(NavigationManager.ToBaseRelativePath(_locationAbsolute));
117-
}
118-
119-
if (shouldRefresh)
120-
{
121-
Refresh(isNavigationIntercepted: false);
117+
await RunOnNavigateWithRefreshAsync(NavigationManager.ToBaseRelativePath(_locationAbsolute), isNavigationIntercepted: false);
118+
return;
122119
}
123120

121+
Refresh(isNavigationIntercepted: false);
124122
}
125123

126124
/// <inheritdoc />
@@ -151,7 +149,7 @@ private void RefreshRouteTable()
151149

152150
}
153151

154-
private void Refresh(bool isNavigationIntercepted)
152+
internal virtual void Refresh(bool isNavigationIntercepted)
155153
{
156154
RefreshRouteTable();
157155

@@ -194,7 +192,7 @@ private void Refresh(bool isNavigationIntercepted)
194192
}
195193
}
196194

197-
internal async Task<bool> RunOnNavigateAsync(string path)
195+
private async ValueTask<bool> RunOnNavigateAsync(string path, Task prevOnNavigate)
198196
{
199197
// If this router instance does not provide an OnNavigateAsync parameter
200198
// then we render the component associated with the route as per usual.
@@ -204,8 +202,11 @@ internal async Task<bool> RunOnNavigateAsync(string path)
204202
}
205203

206204
// If we've already invoked a task and stored its CTS, then
207-
// cancel the existing task.
205+
// cancel that existing task.
208206
_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 prevOnNavigate;
209210

210211
// Create a new cancellation token source for this instance
211212
_onNavigateCts = new CancellationTokenSource();
@@ -229,16 +230,31 @@ internal async Task<bool> RunOnNavigateAsync(string path)
229230
return task == completedTask;
230231
}
231232

232-
private async Task RunOnNavigateWithRefreshAsync(string path, bool isNavigationIntercepted)
233+
internal async Task RunOnNavigateWithRefreshAsync(string path, bool isNavigationIntercepted)
233234
{
234-
var shouldRefresh = await RunOnNavigateAsync(path);
235-
if (shouldRefresh)
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.
240+
var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
241+
_previousOnNavigateTask = tcs.Task;
242+
try
243+
{
244+
// And pass an indicator for the previous task to the currently running one.
245+
var shouldRefresh = await RunOnNavigateAsync(path, previousTask);
246+
if (shouldRefresh)
247+
{
248+
Refresh(isNavigationIntercepted);
249+
}
250+
}
251+
finally
236252
{
237-
Refresh(isNavigationIntercepted);
253+
tcs.SetResult();
238254
}
239255
}
240256

241-
internal void OnLocationChanged(object sender, LocationChangedEventArgs args)
257+
private void OnLocationChanged(object sender, LocationChangedEventArgs args)
242258
{
243259
_locationAbsolute = args.Location;
244260
if (_renderHandle.IsInitialized && Routes != null)

src/Components/Components/test/Routing/RouterTest.cs

Lines changed: 19 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,22 @@
44
using System;
55
using System.Collections.Generic;
66
using System.Linq;
7+
using System.Reflection;
8+
using System.Threading;
79
using System.Threading.Tasks;
810
using Microsoft.AspNetCore.Components;
911
using Microsoft.AspNetCore.Components.Routing;
1012
using Microsoft.AspNetCore.Components.Test.Helpers;
1113
using Microsoft.Extensions.DependencyModel;
14+
using Moq;
1215
using Xunit;
1316

1417
namespace Microsoft.AspNetCore.Components.Test.Routing
1518
{
1619
public class RouterTest
1720
{
1821
[Fact]
19-
public void CanRunOnNavigateViaLocationChangeAsync()
22+
public void CanRunOnNavigateAsync()
2023
{
2124
// Arrange
2225
var router = CreateMockRouter();
@@ -26,10 +29,10 @@ async Task OnNavigateAsync(NavigationContext args)
2629
await Task.CompletedTask;
2730
called = true;
2831
}
29-
router.OnNavigateAsync = new EventCallbackFactory().Create<NavigationContext>(router, OnNavigateAsync);
32+
router.Object.OnNavigateAsync = new EventCallbackFactory().Create<NavigationContext>(router, OnNavigateAsync);
3033

3134
// Act
32-
router.OnLocationChanged(null, new LocationChangedEventArgs("http://example.com/jan", false));
35+
router.Object.RunOnNavigateWithRefreshAsync("http://example.com/jan", false);
3336

3437
// Assert
3538
Assert.True(called);
@@ -46,54 +49,43 @@ async Task OnNavigateAsync(NavigationContext args)
4649
await Task.CompletedTask;
4750
args.CancellationToken.Register(() => cancelled = args.Path);
4851
};
49-
router.OnNavigateAsync = new EventCallbackFactory().Create<NavigationContext>(router, OnNavigateAsync);
52+
router.Object.OnNavigateAsync = new EventCallbackFactory().Create<NavigationContext>(router, OnNavigateAsync);
5053

5154
// Act
52-
router.OnLocationChanged(null, new LocationChangedEventArgs("http://example.com/jan", false));
53-
router.OnLocationChanged(null, new LocationChangedEventArgs("http://example.com/feb", false));
55+
router.Object.RunOnNavigateWithRefreshAsync("jan", false);
56+
router.Object.RunOnNavigateWithRefreshAsync("feb", false);
5457

5558
// Assert
5659
var expected = "jan";
5760
Assert.Equal(cancelled, expected);
5861
}
5962

6063
[Fact]
61-
public async Task RefreshesOnceOnCancelledOnNavigateAsync()
64+
public void RefreshesOnceOnCancelledOnNavigateAsync()
6265
{
6366
// Arrange
6467
var router = CreateMockRouter();
6568
async Task OnNavigateAsync(NavigationContext args)
6669
{
6770
if (args.Path.EndsWith("jan"))
6871
{
69-
await Task.Delay(5000);
70-
}
71-
if (args.Path.EndsWith("feb"))
72-
{
73-
await Task.CompletedTask;
72+
await Task.Delay(Timeout.Infinite);
7473
}
7574
};
76-
router.OnNavigateAsync = new EventCallbackFactory().Create<NavigationContext>(router, OnNavigateAsync);
75+
router.Object.OnNavigateAsync = new EventCallbackFactory().Create<NavigationContext>(router, OnNavigateAsync);
7776

7877
// Act
79-
var janTask = router.RunOnNavigateAsync("jan");
80-
var febTask = router.RunOnNavigateAsync("feb");
78+
router.Object.RunOnNavigateWithRefreshAsync("jan", false);
79+
router.Object.RunOnNavigateWithRefreshAsync("feb", false);
8180

82-
await janTask;
83-
await febTask;
84-
85-
// Assert
86-
Assert.False(janTask.Result);
87-
Assert.True(febTask.Result);
81+
// Assert refresh should've only been called once for the second route
82+
router.Verify(x => x.Refresh(It.IsAny<bool>()), Times.Once());
8883
}
8984

90-
private Router CreateMockRouter()
85+
private Mock<Router> CreateMockRouter()
9186
{
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();
87+
var router = new Mock<Router>() { CallBase = true };
88+
router.Setup(x => x.Refresh(It.IsAny<bool>())).Verifiable();
9789
return router;
9890
}
9991

@@ -102,23 +94,5 @@ private class JanComponent : ComponentBase { }
10294

10395
[Route("feb")]
10496
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-
}
12397
}
12498
}

0 commit comments

Comments
 (0)