Skip to content

Commit 39914ac

Browse files
committed
Make OnNavigateAsync EventCallback and cancel previous navigation
1 parent bbb851e commit 39914ac

File tree

2 files changed

+40
-121
lines changed

2 files changed

+40
-121
lines changed

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

Lines changed: 19 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ static readonly ReadOnlyDictionary<string, object> _emptyParametersDictionary
7373
/// <summary>
7474
/// Gets or sets a handler that should be called before navigating to a new page.
7575
/// </summary>
76-
[Parameter] public Func<NavigationContext, Task>? OnNavigateAsync { get; set; }
76+
[Parameter] public EventCallback<NavigationContext> OnNavigateAsync { get; set; }
7777

7878
private RouteTable Routes { get; set; }
7979

@@ -115,8 +115,7 @@ public async Task SetParametersAsync(ParameterView parameters)
115115
if (!_onNavigateCalled)
116116
{
117117
_onNavigateCalled = true;
118-
await RunOnNavigateWithRefreshAsync(NavigationManager.ToBaseRelativePath(_locationAbsolute), isNavigationIntercepted: false);
119-
return;
118+
await RunOnNavigateAsync(NavigationManager.ToBaseRelativePath(_locationAbsolute), isNavigationIntercepted: false);
120119
}
121120

122121
Refresh(isNavigationIntercepted: false);
@@ -206,9 +205,8 @@ internal virtual void Refresh(bool isNavigationIntercepted)
206205
}
207206
}
208207

209-
private async ValueTask<bool> RunOnNavigateAsync(string path, Task previousOnNavigate)
208+
internal async ValueTask RunOnNavigateAsync(string path, bool isNavigationIntercepted)
210209
{
211-
212210
// Cancel the CTS instead of disposing it, since disposing does not
213211
// actually cancel and can cause unintended Object Disposed Exceptions.
214212
// This effectivelly cancels the previously running task and completes it.
@@ -217,67 +215,43 @@ private async ValueTask<bool> RunOnNavigateAsync(string path, Task previousOnNav
217215
// before starting the next one. This avoid race conditions where the cancellation
218216
// for the previous task was set but not fully completed by the time we get to this
219217
// invocation.
220-
await previousOnNavigate;
218+
await _previousOnNavigateTask;
221219

222-
if (OnNavigateAsync == null)
220+
var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
221+
_previousOnNavigateTask = tcs.Task;
222+
223+
if (!OnNavigateAsync.HasDelegate)
223224
{
224-
return true;
225+
Refresh(isNavigationIntercepted);
225226
}
226227

227228
_onNavigateCts = new CancellationTokenSource();
228229
var navigateContext = new NavigationContext(path, _onNavigateCts.Token);
229230

231+
var cancellationTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
232+
navigateContext.CancellationToken.Register(state =>
233+
((TaskCompletionSource)state).SetResult(), cancellationTcs);
234+
230235
try
231236
{
232-
if (Navigating != null)
233-
{
234-
_renderHandle.Render(Navigating);
235-
}
236-
await OnNavigateAsync(navigateContext);
237-
return true;
238-
}
239-
catch (OperationCanceledException e)
240-
{
241-
if (e.CancellationToken != navigateContext.CancellationToken)
242-
{
243-
var rethrownException = new InvalidOperationException("OnNavigateAsync can only be cancelled via NavigateContext.CancellationToken.", e);
244-
_renderHandle.Render(builder => ExceptionDispatchInfo.Throw(rethrownException));
245-
}
237+
// Task.WhenAny returns a Task<Task> so we need to await twice to unwrap the exception
238+
var task = await Task.WhenAny(OnNavigateAsync.InvokeAsync(navigateContext), cancellationTcs.Task);
239+
await task;
240+
tcs.SetResult();
241+
Refresh(isNavigationIntercepted);
246242
}
247243
catch (Exception e)
248244
{
249245
_renderHandle.Render(builder => ExceptionDispatchInfo.Throw(e));
250246
}
251-
252-
return false;
253-
}
254-
255-
internal async Task RunOnNavigateWithRefreshAsync(string path, bool isNavigationIntercepted)
256-
{
257-
// We cache the Task representing the previously invoked RunOnNavigateWithRefreshAsync
258-
// that is stored. Then we create a new one that represents our current invocation and store it
259-
// globally for the next invocation. This allows us to check inside `RunOnNavigateAsync` if the
260-
// previous OnNavigateAsync task has fully completed before starting the next one.
261-
var previousTask = _previousOnNavigateTask;
262-
var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
263-
_previousOnNavigateTask = tcs.Task;
264-
265-
// And pass an indicator for the previous task to the currently running one.
266-
var shouldRefresh = await RunOnNavigateAsync(path, previousTask);
267-
tcs.SetResult();
268-
if (shouldRefresh)
269-
{
270-
Refresh(isNavigationIntercepted);
271-
}
272-
273247
}
274248

275249
private void OnLocationChanged(object sender, LocationChangedEventArgs args)
276250
{
277251
_locationAbsolute = args.Location;
278252
if (_renderHandle.IsInitialized && Routes != null)
279253
{
280-
_ = RunOnNavigateWithRefreshAsync(NavigationManager.ToBaseRelativePath(_locationAbsolute), args.IsNavigationIntercepted);
254+
_ = RunOnNavigateAsync(NavigationManager.ToBaseRelativePath(_locationAbsolute), args.IsNavigationIntercepted);
281255
}
282256
}
283257

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

Lines changed: 21 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -42,72 +42,39 @@ public async Task CanRunOnNavigateAsync()
4242
{
4343
// Arrange
4444
var called = false;
45-
async Task OnNavigateAsync(NavigationContext args)
45+
Action<NavigationContext> OnNavigateAsync = async (NavigationContext args) =>
4646
{
4747
await Task.CompletedTask;
4848
called = true;
49-
}
50-
_router.OnNavigateAsync = OnNavigateAsync;
51-
52-
// Act
53-
await _renderer.Dispatcher.InvokeAsync(() => _router.RunOnNavigateWithRefreshAsync("http://example.com/jan", false));
54-
55-
// Assert
56-
Assert.True(called);
57-
}
58-
59-
[Fact]
60-
public async Task CanHandleSingleFailedOnNavigateAsync()
61-
{
62-
// Arrange
63-
var called = false;
64-
async Task OnNavigateAsync(NavigationContext args)
65-
{
66-
called = true;
67-
await Task.CompletedTask;
68-
throw new Exception("This is an uncaught exception.");
69-
}
70-
_router.OnNavigateAsync = OnNavigateAsync;
49+
};
50+
_router.OnNavigateAsync = new EventCallback<NavigationContext>(null, OnNavigateAsync);
7151

7252
// Act
73-
await _renderer.Dispatcher.InvokeAsync(() => _router.RunOnNavigateWithRefreshAsync("http://example.com/jan", false));
53+
await _renderer.Dispatcher.InvokeAsync(() => _router.RunOnNavigateAsync("http://example.com/jan", false));
7454

7555
// Assert
7656
Assert.True(called);
77-
Assert.Single(_renderer.HandledExceptions);
78-
var unhandledException = _renderer.HandledExceptions[0];
79-
Assert.Equal("This is an uncaught exception.", unhandledException.Message);
8057
}
8158

8259
[Fact]
8360
public async Task CanceledFailedOnNavigateAsyncDoesNothing()
8461
{
8562
// Arrange
8663
var onNavigateInvoked = 0;
87-
async Task OnNavigateAsync(NavigationContext args)
64+
Action<NavigationContext> OnNavigateAsync = async (NavigationContext args) =>
8865
{
8966
onNavigateInvoked += 1;
9067
if (args.Path.EndsWith("jan"))
9168
{
9269
await Task.Delay(Timeout.Infinite, args.CancellationToken);
9370
throw new Exception("This is an uncaught exception.");
9471
}
95-
}
96-
var refreshCalled = false;
97-
_renderer.OnUpdateDisplay = (renderBatch) =>
98-
{
99-
if (!refreshCalled)
100-
{
101-
refreshCalled = true;
102-
return;
103-
}
104-
Assert.True(false, "OnUpdateDisplay called more than once.");
10572
};
106-
_router.OnNavigateAsync = OnNavigateAsync;
73+
_router.OnNavigateAsync = new EventCallback<NavigationContext>(null, OnNavigateAsync);
10774

10875
// Act
109-
var janTask = _renderer.Dispatcher.InvokeAsync(() => _router.RunOnNavigateWithRefreshAsync("http://example.com/jan", false));
110-
var febTask = _renderer.Dispatcher.InvokeAsync(() => _router.RunOnNavigateWithRefreshAsync("http://example.com/feb", false));
76+
var janTask = _renderer.Dispatcher.InvokeAsync(() => _router.RunOnNavigateAsync("http://example.com/jan", false));
77+
var febTask = _renderer.Dispatcher.InvokeAsync(() => _router.RunOnNavigateAsync("http://example.com/feb", false));
11178

11279
await janTask;
11380
await febTask;
@@ -117,34 +84,12 @@ async Task OnNavigateAsync(NavigationContext args)
11784
Assert.Equal(2, onNavigateInvoked);
11885
}
11986

120-
[Fact]
121-
public async Task CanHandleSingleCancelledOnNavigateAsync()
122-
{
123-
// Arrange
124-
async Task OnNavigateAsync(NavigationContext args)
125-
{
126-
var tcs = new TaskCompletionSource<int>();
127-
tcs.TrySetCanceled();
128-
await tcs.Task;
129-
}
130-
_renderer.OnUpdateDisplay = (renderBatch) => Assert.True(false, "OnUpdateDisplay called more than once.");
131-
_router.OnNavigateAsync = OnNavigateAsync;
132-
133-
// Act
134-
await _renderer.Dispatcher.InvokeAsync(() => _router.RunOnNavigateWithRefreshAsync("http://example.com/jan", false));
135-
136-
// Assert
137-
Assert.Single(_renderer.HandledExceptions);
138-
var unhandledException = _renderer.HandledExceptions[0];
139-
Assert.Equal("OnNavigateAsync can only be cancelled via NavigateContext.CancellationToken.", unhandledException.Message);
140-
}
141-
14287
[Fact]
14388
public async Task AlreadyCanceledOnNavigateAsyncDoesNothing()
14489
{
14590
// Arrange
14691
var triggerCancel = new TaskCompletionSource();
147-
async Task OnNavigateAsync(NavigationContext args)
92+
Action<NavigationContext> OnNavigateAsync = async (NavigationContext args) =>
14893
{
14994
if (args.Path.EndsWith("jan"))
15095
{
@@ -153,7 +98,7 @@ async Task OnNavigateAsync(NavigationContext args)
15398
tcs.TrySetCanceled();
15499
await tcs.Task;
155100
}
156-
}
101+
};
157102
var refreshCalled = false;
158103
_renderer.OnUpdateDisplay = (renderBatch) =>
159104
{
@@ -164,11 +109,11 @@ async Task OnNavigateAsync(NavigationContext args)
164109
}
165110
Assert.True(false, "OnUpdateDisplay called more than once.");
166111
};
167-
_router.OnNavigateAsync = OnNavigateAsync;
112+
_router.OnNavigateAsync = new EventCallback<NavigationContext>(null, OnNavigateAsync);
168113

169114
// Act (start the operations then await them)
170-
var jan = _renderer.Dispatcher.InvokeAsync(() => _router.RunOnNavigateWithRefreshAsync("http://example.com/jan", false));
171-
var feb = _renderer.Dispatcher.InvokeAsync(() => _router.RunOnNavigateWithRefreshAsync("http://example.com/feb", false));
115+
var jan = _renderer.Dispatcher.InvokeAsync(() => _router.RunOnNavigateAsync("http://example.com/jan", false));
116+
var feb = _renderer.Dispatcher.InvokeAsync(() => _router.RunOnNavigateAsync("http://example.com/feb", false));
172117
triggerCancel.TrySetResult();
173118

174119
await jan;
@@ -180,16 +125,16 @@ public void CanCancelPreviousOnNavigateAsync()
180125
{
181126
// Arrange
182127
var cancelled = "";
183-
async Task OnNavigateAsync(NavigationContext args)
128+
Action<NavigationContext> OnNavigateAsync = async (NavigationContext args) =>
184129
{
185130
await Task.CompletedTask;
186131
args.CancellationToken.Register(() => cancelled = args.Path);
187132
};
188-
_router.OnNavigateAsync = OnNavigateAsync;
133+
_router.OnNavigateAsync = new EventCallback<NavigationContext>(null, OnNavigateAsync);
189134

190135
// Act
191-
_ = _router.RunOnNavigateWithRefreshAsync("jan", false);
192-
_ = _router.RunOnNavigateWithRefreshAsync("feb", false);
136+
_ = _router.RunOnNavigateAsync("jan", false);
137+
_ = _router.RunOnNavigateAsync("feb", false);
193138

194139
// Assert
195140
var expected = "jan";
@@ -200,7 +145,7 @@ async Task OnNavigateAsync(NavigationContext args)
200145
public async Task RefreshesOnceOnCancelledOnNavigateAsync()
201146
{
202147
// Arrange
203-
async Task OnNavigateAsync(NavigationContext args)
148+
Action<NavigationContext> OnNavigateAsync = async (NavigationContext args) =>
204149
{
205150
if (args.Path.EndsWith("jan"))
206151
{
@@ -217,11 +162,11 @@ async Task OnNavigateAsync(NavigationContext args)
217162
}
218163
Assert.True(false, "OnUpdateDisplay called more than once.");
219164
};
220-
_router.OnNavigateAsync = OnNavigateAsync;
165+
_router.OnNavigateAsync = new EventCallback<NavigationContext>(null, OnNavigateAsync);
221166

222167
// Act
223-
var jan = _renderer.Dispatcher.InvokeAsync(() => _router.RunOnNavigateWithRefreshAsync("http://example.com/jan", false));
224-
var feb = _renderer.Dispatcher.InvokeAsync(() => _router.RunOnNavigateWithRefreshAsync("http://example.com/feb", false));
168+
var jan = _renderer.Dispatcher.InvokeAsync(() => _router.RunOnNavigateAsync("http://example.com/jan", false));
169+
var feb = _renderer.Dispatcher.InvokeAsync(() => _router.RunOnNavigateAsync("http://example.com/feb", false));
225170

226171
await jan;
227172
await feb;

0 commit comments

Comments
 (0)