Skip to content

Commit e1d9133

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

File tree

2 files changed

+42
-116
lines changed

2 files changed

+42
-116
lines changed

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

Lines changed: 20 additions & 49 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);
@@ -156,6 +155,13 @@ internal virtual void Refresh(bool isNavigationIntercepted)
156155
// for it to complete before rendering. Note: because _previousOnNavigateTask
157156
// is initialized to a CompletedTask on initialization, this will still
158157
// allow first-render to complete successfully.
158+
if (_previousOnNavigateTask.Status == TaskStatus.Faulted)
159+
{
160+
_renderHandle.Render(builder => ExceptionDispatchInfo.Throw(_previousOnNavigateTask.Exception));
161+
return;
162+
}
163+
164+
159165
if (_previousOnNavigateTask.Status != TaskStatus.RanToCompletion)
160166
{
161167
if (Navigating != null)
@@ -206,9 +212,8 @@ internal virtual void Refresh(bool isNavigationIntercepted)
206212
}
207213
}
208214

209-
private async ValueTask<bool> RunOnNavigateAsync(string path, Task previousOnNavigate)
215+
internal async ValueTask RunOnNavigateAsync(string path, bool isNavigationIntercepted)
210216
{
211-
212217
// Cancel the CTS instead of disposing it, since disposing does not
213218
// actually cancel and can cause unintended Object Disposed Exceptions.
214219
// This effectivelly cancels the previously running task and completes it.
@@ -217,67 +222,33 @@ private async ValueTask<bool> RunOnNavigateAsync(string path, Task previousOnNav
217222
// before starting the next one. This avoid race conditions where the cancellation
218223
// for the previous task was set but not fully completed by the time we get to this
219224
// invocation.
220-
await previousOnNavigate;
225+
await _previousOnNavigateTask;
221226

222-
if (OnNavigateAsync == null)
227+
if (!OnNavigateAsync.HasDelegate)
223228
{
224-
return true;
229+
Refresh(isNavigationIntercepted);
225230
}
226231

227232
_onNavigateCts = new CancellationTokenSource();
228233
var navigateContext = new NavigationContext(path, _onNavigateCts.Token);
229234

230-
try
231-
{
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-
}
246-
}
247-
catch (Exception e)
248-
{
249-
_renderHandle.Render(builder => ExceptionDispatchInfo.Throw(e));
250-
}
251-
252-
return false;
253-
}
235+
var cancellationTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
236+
navigateContext.CancellationToken.Register(state =>
237+
((TaskCompletionSource)state).SetResult(), cancellationTcs);
254238

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)
239+
if (Navigating != null)
269240
{
270-
Refresh(isNavigationIntercepted);
241+
_renderHandle.Render(Navigating);
271242
}
272-
243+
_previousOnNavigateTask = await Task.WhenAny(OnNavigateAsync.InvokeAsync(navigateContext), cancellationTcs.Task);
273244
}
274245

275246
private void OnLocationChanged(object sender, LocationChangedEventArgs args)
276247
{
277248
_locationAbsolute = args.Location;
278249
if (_renderHandle.IsInitialized && Routes != null)
279250
{
280-
_ = RunOnNavigateWithRefreshAsync(NavigationManager.ToBaseRelativePath(_locationAbsolute), args.IsNavigationIntercepted);
251+
_ = RunOnNavigateAsync(NavigationManager.ToBaseRelativePath(_locationAbsolute), args.IsNavigationIntercepted);
281252
}
282253
}
283254

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

Lines changed: 22 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -42,57 +42,34 @@ 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-
}
72+
};
9673
var refreshCalled = false;
9774
_renderer.OnUpdateDisplay = (renderBatch) =>
9875
{
@@ -103,11 +80,11 @@ async Task OnNavigateAsync(NavigationContext args)
10380
}
10481
Assert.True(false, "OnUpdateDisplay called more than once.");
10582
};
106-
_router.OnNavigateAsync = OnNavigateAsync;
83+
_router.OnNavigateAsync = new EventCallback<NavigationContext>(null, OnNavigateAsync);
10784

10885
// 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));
86+
var janTask = _renderer.Dispatcher.InvokeAsync(() => _router.RunOnNavigateAsync("http://example.com/jan", false));
87+
var febTask = _renderer.Dispatcher.InvokeAsync(() => _router.RunOnNavigateAsync("http://example.com/feb", false));
11188

11289
await janTask;
11390
await febTask;
@@ -117,34 +94,12 @@ async Task OnNavigateAsync(NavigationContext args)
11794
Assert.Equal(2, onNavigateInvoked);
11895
}
11996

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-
14297
[Fact]
14398
public async Task AlreadyCanceledOnNavigateAsyncDoesNothing()
14499
{
145100
// Arrange
146101
var triggerCancel = new TaskCompletionSource();
147-
async Task OnNavigateAsync(NavigationContext args)
102+
Action<NavigationContext> OnNavigateAsync = async (NavigationContext args) =>
148103
{
149104
if (args.Path.EndsWith("jan"))
150105
{
@@ -153,7 +108,7 @@ async Task OnNavigateAsync(NavigationContext args)
153108
tcs.TrySetCanceled();
154109
await tcs.Task;
155110
}
156-
}
111+
};
157112
var refreshCalled = false;
158113
_renderer.OnUpdateDisplay = (renderBatch) =>
159114
{
@@ -164,11 +119,11 @@ async Task OnNavigateAsync(NavigationContext args)
164119
}
165120
Assert.True(false, "OnUpdateDisplay called more than once.");
166121
};
167-
_router.OnNavigateAsync = OnNavigateAsync;
122+
_router.OnNavigateAsync = new EventCallback<NavigationContext>(null, OnNavigateAsync);
168123

169124
// 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));
125+
var jan = _renderer.Dispatcher.InvokeAsync(() => _router.RunOnNavigateAsync("http://example.com/jan", false));
126+
var feb = _renderer.Dispatcher.InvokeAsync(() => _router.RunOnNavigateAsync("http://example.com/feb", false));
172127
triggerCancel.TrySetResult();
173128

174129
await jan;
@@ -180,16 +135,16 @@ public void CanCancelPreviousOnNavigateAsync()
180135
{
181136
// Arrange
182137
var cancelled = "";
183-
async Task OnNavigateAsync(NavigationContext args)
138+
Action<NavigationContext> OnNavigateAsync = async (NavigationContext args) =>
184139
{
185140
await Task.CompletedTask;
186141
args.CancellationToken.Register(() => cancelled = args.Path);
187142
};
188-
_router.OnNavigateAsync = OnNavigateAsync;
143+
_router.OnNavigateAsync = new EventCallback<NavigationContext>(null, OnNavigateAsync);
189144

190145
// Act
191-
_ = _router.RunOnNavigateWithRefreshAsync("jan", false);
192-
_ = _router.RunOnNavigateWithRefreshAsync("feb", false);
146+
_ = _router.RunOnNavigateAsync("jan", false);
147+
_ = _router.RunOnNavigateAsync("feb", false);
193148

194149
// Assert
195150
var expected = "jan";
@@ -200,7 +155,7 @@ async Task OnNavigateAsync(NavigationContext args)
200155
public async Task RefreshesOnceOnCancelledOnNavigateAsync()
201156
{
202157
// Arrange
203-
async Task OnNavigateAsync(NavigationContext args)
158+
Action<NavigationContext> OnNavigateAsync = async (NavigationContext args) =>
204159
{
205160
if (args.Path.EndsWith("jan"))
206161
{
@@ -217,11 +172,11 @@ async Task OnNavigateAsync(NavigationContext args)
217172
}
218173
Assert.True(false, "OnUpdateDisplay called more than once.");
219174
};
220-
_router.OnNavigateAsync = OnNavigateAsync;
175+
_router.OnNavigateAsync = new EventCallback<NavigationContext>(null, OnNavigateAsync);
221176

222177
// 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));
178+
var jan = _renderer.Dispatcher.InvokeAsync(() => _router.RunOnNavigateAsync("http://example.com/jan", false));
179+
var feb = _renderer.Dispatcher.InvokeAsync(() => _router.RunOnNavigateAsync("http://example.com/feb", false));
225180

226181
await jan;
227182
await feb;

0 commit comments

Comments
 (0)