Skip to content

Follow-up for cancellation of navigation events in Blazor #43079

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Aug 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 57 additions & 17 deletions src/Components/Components/src/NavigationManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -303,14 +303,26 @@ protected async ValueTask<bool> NotifyLocationChangingAsync(string uri, string?
_locationChangingCts = cts;

var cancellationToken = cts.Token;
var context = new LocationChangingContext(uri, state, isNavigationIntercepted, cancellationToken);
var context = new LocationChangingContext
{
TargetLocation = uri,
HistoryEntryState = state,
IsNavigationIntercepted = isNavigationIntercepted,
CancellationToken = cancellationToken,
};

try
{
if (handlerCount == 1)
{
var handlerTask = InvokeLocationChangingHandlerAsync(_locationChangingHandlers[0], context);

if (handlerTask.IsFaulted)
{
await handlerTask;
return false; // Unreachable because the previous line will throw.
}

if (context.DidPreventNavigation)
{
return false;
Expand Down Expand Up @@ -398,17 +410,29 @@ protected async ValueTask<bool> NotifyLocationChangingAsync(string uri, string?
}
}

private async ValueTask InvokeLocationChangingHandlerAsync(Func<LocationChangingContext, ValueTask> handler, LocationChangingContext context)
{
try
{
await handler(context);
}
catch (OperationCanceledException)
{
// Ignore exceptions caused by cancellations.
}
catch (Exception ex)
{
HandleLocationChangingHandlerException(ex, context);
}
}

/// <summary>
/// Invokes the provided <paramref name="handler"/>, passing it the given <paramref name="context"/>.
/// This method can be overridden to analyze the state of the handler task even after
/// <see cref="NotifyLocationChangingAsync(string, string?, bool)"/> completes. For example, this can be useful for
/// processing exceptions thrown from handlers that continue running after the navigation ends.
/// Handles exceptions thrown in location changing handlers.
/// </summary>
/// <param name="handler">The handler to invoke.</param>
/// <param name="context">The context to pass to the handler.</param>
/// <returns></returns>
protected virtual ValueTask InvokeLocationChangingHandlerAsync(Func<LocationChangingContext, ValueTask> handler, LocationChangingContext context)
=> handler(context);
/// <param name="ex">The exception to handle.</param>
/// <param name="context">The context passed to the handler.</param>
protected virtual void HandleLocationChangingHandlerException(Exception ex, LocationChangingContext context)
=> throw new InvalidOperationException($"To support navigation locks, {GetType().Name} must override {nameof(HandleLocationChangingHandlerException)}");

/// <summary>
/// Sets whether navigation is currently locked. If it is, then implementations should not update <see cref="Uri"/> and call
Expand All @@ -420,10 +444,11 @@ protected virtual void SetNavigationLockState(bool value)
=> throw new NotSupportedException($"To support navigation locks, {GetType().Name} must override {nameof(SetNavigationLockState)}");

/// <summary>
/// Adds a handler to process incoming navigation events.
/// Registers a handler to process incoming navigation events.
/// </summary>
/// <param name="locationChangingHandler">The handler to process incoming navigation events.</param>
public void AddLocationChangingHandler(Func<LocationChangingContext, ValueTask> locationChangingHandler)
/// <returns>An <see cref="IDisposable"/> that can be disposed to unregister the location changing handler.</returns>
public IDisposable RegisterLocationChangingHandler(Func<LocationChangingContext, ValueTask> locationChangingHandler)
{
AssertInitialized();

Expand All @@ -435,13 +460,11 @@ public void AddLocationChangingHandler(Func<LocationChangingContext, ValueTask>
{
SetNavigationLockState(true);
}

return new LocationChangingRegistration(locationChangingHandler, this);
}

/// <summary>
/// Removes a previously-added location changing handler.
/// </summary>
/// <param name="locationChangingHandler">The handler to remove.</param>
public void RemoveLocationChangingHandler(Func<LocationChangingContext, ValueTask> locationChangingHandler)
private void RemoveLocationChangingHandler(Func<LocationChangingContext, ValueTask> locationChangingHandler)
{
AssertInitialized();

Expand Down Expand Up @@ -505,4 +528,21 @@ private static void Validate(Uri? baseUri, string uri)
throw new ArgumentException(message);
}
}

private sealed class LocationChangingRegistration : IDisposable
{
private readonly Func<LocationChangingContext, ValueTask> _handler;
private readonly NavigationManager _navigationManager;

public LocationChangingRegistration(Func<LocationChangingContext, ValueTask> handler, NavigationManager navigationManager)
{
_handler = handler;
_navigationManager = navigationManager;
}

public void Dispose()
{
_navigationManager.RemoveLocationChangingHandler(_handler);
}
}
}
10 changes: 7 additions & 3 deletions src/Components/Components/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
#nullable enable
Microsoft.AspNetCore.Components.NavigationManager.AddLocationChangingHandler(System.Func<Microsoft.AspNetCore.Components.Routing.LocationChangingContext!, System.Threading.Tasks.ValueTask>! locationChangingHandler) -> void
Microsoft.AspNetCore.Components.NavigationManager.HistoryEntryState.get -> string?
Microsoft.AspNetCore.Components.NavigationManager.HistoryEntryState.set -> void
Microsoft.AspNetCore.Components.NavigationManager.NotifyLocationChangingAsync(string! uri, string? state, bool isNavigationIntercepted) -> System.Threading.Tasks.ValueTask<bool>
Microsoft.AspNetCore.Components.NavigationManager.RemoveLocationChangingHandler(System.Func<Microsoft.AspNetCore.Components.Routing.LocationChangingContext!, System.Threading.Tasks.ValueTask>! locationChangingHandler) -> void
Microsoft.AspNetCore.Components.NavigationManager.RegisterLocationChangingHandler(System.Func<Microsoft.AspNetCore.Components.Routing.LocationChangingContext!, System.Threading.Tasks.ValueTask>! locationChangingHandler) -> System.IDisposable!
Microsoft.AspNetCore.Components.NavigationOptions.HistoryEntryState.get -> string?
Microsoft.AspNetCore.Components.NavigationOptions.HistoryEntryState.init -> void
Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder.AddContent(int sequence, Microsoft.AspNetCore.Components.MarkupString? markupContent) -> void
Microsoft.AspNetCore.Components.Routing.LocationChangedEventArgs.HistoryEntryState.get -> string?
Microsoft.AspNetCore.Components.Routing.LocationChangingContext
Microsoft.AspNetCore.Components.Routing.LocationChangingContext.CancellationToken.get -> System.Threading.CancellationToken
Microsoft.AspNetCore.Components.Routing.LocationChangingContext.CancellationToken.init -> void
Microsoft.AspNetCore.Components.Routing.LocationChangingContext.HistoryEntryState.get -> string?
Microsoft.AspNetCore.Components.Routing.LocationChangingContext.HistoryEntryState.init -> void
Microsoft.AspNetCore.Components.Routing.LocationChangingContext.IsNavigationIntercepted.get -> bool
Microsoft.AspNetCore.Components.Routing.LocationChangingContext.IsNavigationIntercepted.init -> void
Microsoft.AspNetCore.Components.Routing.LocationChangingContext.LocationChangingContext() -> void
Microsoft.AspNetCore.Components.Routing.LocationChangingContext.PreventNavigation() -> void
Microsoft.AspNetCore.Components.Routing.LocationChangingContext.TargetLocation.get -> string!
Microsoft.AspNetCore.Components.Routing.LocationChangingContext.TargetLocation.init -> void
static Microsoft.AspNetCore.Components.CompilerServices.RuntimeHelpers.CreateInferredEventCallback<T>(object! receiver, Microsoft.AspNetCore.Components.EventCallback<T> callback, T value) -> Microsoft.AspNetCore.Components.EventCallback<T>
static Microsoft.AspNetCore.Components.CompilerServices.RuntimeHelpers.InvokeAsynchronousDelegate(System.Action! callback) -> System.Threading.Tasks.Task!
static Microsoft.AspNetCore.Components.CompilerServices.RuntimeHelpers.InvokeAsynchronousDelegate(System.Func<System.Threading.Tasks.Task!>! callback) -> System.Threading.Tasks.Task!
Expand Down Expand Up @@ -50,5 +54,5 @@ static Microsoft.AspNetCore.Components.EventCallbackFactoryBinderExtensions.Crea
static Microsoft.AspNetCore.Components.EventCallbackFactoryBinderExtensions.CreateBinder(this Microsoft.AspNetCore.Components.EventCallbackFactory! factory, object! receiver, Microsoft.AspNetCore.Components.EventCallback<short?> setter, short? existingValue, System.Globalization.CultureInfo? culture = null) -> Microsoft.AspNetCore.Components.EventCallback<Microsoft.AspNetCore.Components.ChangeEventArgs!>
static Microsoft.AspNetCore.Components.EventCallbackFactoryBinderExtensions.CreateBinder(this Microsoft.AspNetCore.Components.EventCallbackFactory! factory, object! receiver, Microsoft.AspNetCore.Components.EventCallback<string?> setter, string! existingValue, System.Globalization.CultureInfo? culture = null) -> Microsoft.AspNetCore.Components.EventCallback<Microsoft.AspNetCore.Components.ChangeEventArgs!>
static Microsoft.AspNetCore.Components.EventCallbackFactoryBinderExtensions.CreateBinder<T>(this Microsoft.AspNetCore.Components.EventCallbackFactory! factory, object! receiver, Microsoft.AspNetCore.Components.EventCallback<T> setter, T existingValue, System.Globalization.CultureInfo? culture = null) -> Microsoft.AspNetCore.Components.EventCallback<Microsoft.AspNetCore.Components.ChangeEventArgs!>
virtual Microsoft.AspNetCore.Components.NavigationManager.InvokeLocationChangingHandlerAsync(System.Func<Microsoft.AspNetCore.Components.Routing.LocationChangingContext!, System.Threading.Tasks.ValueTask>! handler, Microsoft.AspNetCore.Components.Routing.LocationChangingContext! context) -> System.Threading.Tasks.ValueTask
virtual Microsoft.AspNetCore.Components.NavigationManager.HandleLocationChangingHandlerException(System.Exception! ex, Microsoft.AspNetCore.Components.Routing.LocationChangingContext! context) -> void
virtual Microsoft.AspNetCore.Components.NavigationManager.SetNavigationLockState(bool value) -> void
Original file line number Diff line number Diff line change
Expand Up @@ -6,38 +6,30 @@ namespace Microsoft.AspNetCore.Components.Routing;
/// <summary>
/// Contains context for a change to the browser's current location.
/// </summary>
public class LocationChangingContext
public sealed class LocationChangingContext
{
internal LocationChangingContext(string targetLocation, string? historyEntryState, bool isNavigationIntercepted, CancellationToken cancellationToken)
{
TargetLocation = targetLocation;
HistoryEntryState = historyEntryState;
IsNavigationIntercepted = isNavigationIntercepted;
CancellationToken = cancellationToken;
}

internal bool DidPreventNavigation { get; private set; }

/// <summary>
/// Gets the target location.
/// </summary>
public string TargetLocation { get; }
public required string TargetLocation { get; init; }

/// <summary>
/// Gets the state associated with the target history entry.
/// </summary>
public string? HistoryEntryState { get; }
public string? HistoryEntryState { get; init; }

/// <summary>
/// Gets whether this navigation was intercepted from a link.
/// </summary>
public bool IsNavigationIntercepted { get; }
public bool IsNavigationIntercepted { get; init; }

/// <summary>
/// Gets a <see cref="System.Threading.CancellationToken"/> that can be used to determine if this navigation was canceled
/// (for example, because the user has triggered a different navigation).
/// </summary>
public CancellationToken CancellationToken { get; }
public CancellationToken CancellationToken { get; init; }

/// <summary>
/// Prevents this navigation from continuing.
Expand Down
Loading