Skip to content

Ability to cancel a Navigation event #24417

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

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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
92 changes: 92 additions & 0 deletions src/Components/Components/src/NavigationManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Routing;

namespace Microsoft.AspNetCore.Components
Expand Down Expand Up @@ -30,6 +33,11 @@ public event EventHandler<LocationChangedEventArgs> LocationChanged

private EventHandler<LocationChangedEventArgs>? _locationChanged;

private readonly IList<IHandleLocationChanging> _locationChangingListeners = new List<IHandleLocationChanging>();


private bool _hasLocationChangingEventHandlers;

// For the baseUri it's worth storing as a System.Uri so we can do operations
// on that type. System.Uri gives us access to the original string anyway.
private Uri? _baseUri;
Expand Down Expand Up @@ -211,6 +219,90 @@ protected void NotifyLocationChanged(bool isInterceptedLink)
}
}

/// <summary>
/// Calls all registered <see cref="IHandleLocationChanging"/> handlers, to see if navigation should be canceled.
/// Returns true if navigation should be canceled.
/// </summary>
protected async ValueTask<bool> NotifyLocationChanging(string uri, bool isInterceptedLink, bool forceLoad)
{
try
{
if (_locationChangingListeners.Count > 0)
{
var context = new LocationChangingContext(uri, isInterceptedLink, forceLoad);
//Copy List to local array, to prevent exception when user removes handler during execution
foreach (var listener in _locationChangingListeners.ToArray())
{
if (await listener.OnLocationChanging(context))
{
return true;
}
}
}
return false;
}
catch (Exception ex)
{
throw new LocationChangeException("An exception occurred while dispatching a location changing event.", ex);
}
}

/// <summary>
/// Called when <see cref="IHandleLocationChanging"/> the fact that any event handlers are present or not changes.
/// this can be used by descendants to inform the JSRuntime that there are locationchanging event handlers
/// </summary>
/// <param name="value">true if there are event handlers</param>
/// <returns>true when the navigation subsystem could be informed that we have event handlers</returns>
protected virtual bool SetHasLocationChangingEventHandlers(bool value)
{
return true;
}

/// <summary>
/// Calls <see cref="SetHasLocationChangingEventHandlers"/> when needed
/// This function is normally called when event handlers are added or removed from <see cref="NavigationManager"/>
/// </summary>
protected void UpdateHasLocationChangingEventHandlers()
{
var value = _locationChangingListeners.Any();
if (_hasLocationChangingEventHandlers != value)
{
//If SetHasLocationChangingEventHandlers returns false, we won't update the _hasLocationChangingEventHandlers.
//This way we can call this function again at a later time (for example when JSRuntime is initialized, See RemoteNavigationManager)
if (SetHasLocationChangingEventHandlers(value))
{
_hasLocationChangingEventHandlers = value;
}
}
}

/// <summary>
/// Will add a <see cref="IHandleLocationChanging"/> handler to be called during a navigation Location change
/// </summary>
/// <param name="locationChangingHandler"> <see cref="IHandleLocationChanging"/> handler to be added.</param>
public void AddLocationChangingHandler(IHandleLocationChanging locationChangingHandler)
{
AssertInitialized();
_locationChangingListeners.Add(locationChangingHandler);
UpdateHasLocationChangingEventHandlers();
}

/// <summary>
/// Will remove a <see cref="IHandleLocationChanging"/> handler that was previously added by <see cref="AddLocationChangingHandler"/>.
/// </summary>
/// <param name="locationChangingHandler"> <see cref="IHandleLocationChanging"/> handler to be removed.</param>
/// <returns></returns>
public bool RemoveLocationChangingHandler(IHandleLocationChanging locationChangingHandler)
{
AssertInitialized();
var isRemoved = _locationChangingListeners.Remove(locationChangingHandler);
if (isRemoved)
{
UpdateHasLocationChangingEventHandlers();
}
return isRemoved;
}

private void AssertInitialized()
{
if (!_isInitialized)
Expand Down
14 changes: 13 additions & 1 deletion src/Components/Components/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#nullable enable
#nullable enable
Microsoft.AspNetCore.Components.BindConverter
Microsoft.AspNetCore.Components.BindElementAttribute
Microsoft.AspNetCore.Components.BindElementAttribute.BindElementAttribute(string! element, string? suffix, string! valueAttribute, string! changeAttribute) -> void
Expand Down Expand Up @@ -115,15 +115,19 @@ Microsoft.AspNetCore.Components.NavigationException
Microsoft.AspNetCore.Components.NavigationException.Location.get -> string!
Microsoft.AspNetCore.Components.NavigationException.NavigationException(string! uri) -> void
Microsoft.AspNetCore.Components.NavigationManager
Microsoft.AspNetCore.Components.NavigationManager.AddLocationChangingHandler(Microsoft.AspNetCore.Components.Routing.IHandleLocationChanging! locationChangingHandler) -> void
Microsoft.AspNetCore.Components.NavigationManager.BaseUri.get -> string!
Microsoft.AspNetCore.Components.NavigationManager.BaseUri.set -> void
Microsoft.AspNetCore.Components.NavigationManager.Initialize(string! baseUri, string! uri) -> void
Microsoft.AspNetCore.Components.NavigationManager.LocationChanged -> System.EventHandler<Microsoft.AspNetCore.Components.Routing.LocationChangedEventArgs!>!
Microsoft.AspNetCore.Components.NavigationManager.NavigateTo(string! uri, bool forceLoad = false) -> void
Microsoft.AspNetCore.Components.NavigationManager.NavigationManager() -> void
Microsoft.AspNetCore.Components.NavigationManager.NotifyLocationChanged(bool isInterceptedLink) -> void
Microsoft.AspNetCore.Components.NavigationManager.NotifyLocationChanging(string! uri, bool isInterceptedLink, bool forceLoad) -> System.Threading.Tasks.ValueTask<bool>
Microsoft.AspNetCore.Components.NavigationManager.RemoveLocationChangingHandler(Microsoft.AspNetCore.Components.Routing.IHandleLocationChanging! locationChangingHandler) -> bool
Microsoft.AspNetCore.Components.NavigationManager.ToAbsoluteUri(string! relativeUri) -> System.Uri!
Microsoft.AspNetCore.Components.NavigationManager.ToBaseRelativePath(string! uri) -> string!
Microsoft.AspNetCore.Components.NavigationManager.UpdateHasLocationChangingEventHandlers() -> void
Microsoft.AspNetCore.Components.NavigationManager.Uri.get -> string!
Microsoft.AspNetCore.Components.NavigationManager.Uri.set -> void
Microsoft.AspNetCore.Components.OwningComponentBase
Expand Down Expand Up @@ -266,6 +270,8 @@ Microsoft.AspNetCore.Components.RouteView.RouteData.get -> Microsoft.AspNetCore.
Microsoft.AspNetCore.Components.RouteView.RouteData.set -> void
Microsoft.AspNetCore.Components.RouteView.RouteView() -> void
Microsoft.AspNetCore.Components.RouteView.SetParametersAsync(Microsoft.AspNetCore.Components.ParameterView parameters) -> System.Threading.Tasks.Task!
Microsoft.AspNetCore.Components.Routing.IHandleLocationChanging
Microsoft.AspNetCore.Components.Routing.IHandleLocationChanging.OnLocationChanging(Microsoft.AspNetCore.Components.Routing.LocationChangingContext! context) -> System.Threading.Tasks.ValueTask<bool>
Microsoft.AspNetCore.Components.Routing.IHostEnvironmentNavigationManager
Microsoft.AspNetCore.Components.Routing.IHostEnvironmentNavigationManager.Initialize(string! baseUri, string! uri) -> void
Microsoft.AspNetCore.Components.Routing.INavigationInterception
Expand All @@ -274,6 +280,11 @@ Microsoft.AspNetCore.Components.Routing.LocationChangedEventArgs
Microsoft.AspNetCore.Components.Routing.LocationChangedEventArgs.IsNavigationIntercepted.get -> bool
Microsoft.AspNetCore.Components.Routing.LocationChangedEventArgs.Location.get -> string!
Microsoft.AspNetCore.Components.Routing.LocationChangedEventArgs.LocationChangedEventArgs(string! location, bool isNavigationIntercepted) -> void
Microsoft.AspNetCore.Components.Routing.LocationChangingContext
Microsoft.AspNetCore.Components.Routing.LocationChangingContext.ForceLoad.get -> bool
Microsoft.AspNetCore.Components.Routing.LocationChangingContext.IsNavigationIntercepted.get -> bool
Microsoft.AspNetCore.Components.Routing.LocationChangingContext.Location.get -> string!
Microsoft.AspNetCore.Components.Routing.LocationChangingContext.LocationChangingContext(string! location, bool isNavigationIntercepted, bool forceLoad) -> void
Microsoft.AspNetCore.Components.Routing.NavigationContext
Microsoft.AspNetCore.Components.Routing.NavigationContext.CancellationToken.get -> System.Threading.CancellationToken
Microsoft.AspNetCore.Components.Routing.NavigationContext.Path.get -> string!
Expand Down Expand Up @@ -410,6 +421,7 @@ virtual Microsoft.AspNetCore.Components.ComponentBase.OnParametersSetAsync() ->
virtual Microsoft.AspNetCore.Components.ComponentBase.SetParametersAsync(Microsoft.AspNetCore.Components.ParameterView parameters) -> System.Threading.Tasks.Task!
virtual Microsoft.AspNetCore.Components.ComponentBase.ShouldRender() -> bool
virtual Microsoft.AspNetCore.Components.NavigationManager.EnsureInitialized() -> void
virtual Microsoft.AspNetCore.Components.NavigationManager.SetHasLocationChangingEventHandlers(bool value) -> bool
virtual Microsoft.AspNetCore.Components.OwningComponentBase.Dispose(bool disposing) -> void
virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.DispatchEventAsync(ulong eventHandlerId, Microsoft.AspNetCore.Components.RenderTree.EventFieldInfo! fieldInfo, System.EventArgs! eventArgs) -> System.Threading.Tasks.Task!
virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.Dispose(bool disposing) -> void
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Threading.Tasks;

namespace Microsoft.AspNetCore.Components.Routing
{
/// <summary>
/// Interface implemented by objects that want to react to a Location change in <see cref="NavigationManager"/>
/// </summary>
public interface IHandleLocationChanging
{
/// <summary>
/// This function is called whenever the <see cref="NavigationManager"/> wants to change it's Location
/// </summary>
/// <param name="context"></param>
/// <returns>A <see cref="ValueTask"/> whose result is a boolean, which if true will cancel the current Location change</returns>
ValueTask<bool> OnLocationChanging(LocationChangingContext context);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

namespace Microsoft.AspNetCore.Components.Routing
{

/// <summary>
/// context used by <see cref="IHandleLocationChanging" /> to see what kind of navigation is occuring.
/// </summary>
public class LocationChangingContext
{
/// <summary>
/// Initializes a new instance of <see cref="LocationChangingContext" />.
/// </summary>
/// <param name="location">The location.</param>
/// <param name="isNavigationIntercepted">A value that determines if navigation for the link was intercepted.</param>
/// <param name="forceLoad">A value that shows if the forceLoad flag was set during a call to <see cref="NavigationManager.NavigateTo" /> </param>
public LocationChangingContext(string location, bool isNavigationIntercepted, bool forceLoad)
{
Location = location;
IsNavigationIntercepted = isNavigationIntercepted;
ForceLoad = forceLoad;
}

/// <summary>
/// Gets the location we are about to change to.
/// </summary>
public string Location { get; }

/// <summary>
/// Gets a value that determines if navigation for the link was intercepted.
/// </summary>
public bool IsNavigationIntercepted { get; }

/// <summary>
/// Gets a value if the Forceload flag was set during a call to <see cref="NavigationManager.NavigateTo" />
/// </summary>
public bool ForceLoad { get; }
}
}
50 changes: 50 additions & 0 deletions src/Components/Server/src/Circuits/CircuitHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,48 @@ await Renderer.Dispatcher.InvokeAsync(() =>
}
}

public async Task<bool> OnLocationChangingAsync(string uri, bool intercepted)
{
AssertInitialized();
AssertNotDisposed();

var cancel = false;

try
{
cancel = await Renderer.Dispatcher.InvokeAsync( async () =>
{
Log.LocationChanging(_logger, uri, CircuitId);
var navigationManager = (RemoteNavigationManager)Services.GetRequiredService<NavigationManager>();
return await navigationManager.HandleLocationChanging(uri, intercepted, false);
});
}

// In either case, a well-behaved client will not send invalid URIs, and we don't really
// want to continue processing with the circuit if setting the URI failed inside application
// code. The safest thing to do is consider it a critical failure since URI is global state,
// and a failure means that an update to global state was partially applied.
catch (LocationChangeException nex)
{
// LocationChangeException means that it failed in user-code. Treat this like an unhandled
// exception in user-code.
Log.LocationChangeFailedInCircuit(_logger, uri, CircuitId, nex);
await TryNotifyClientErrorAsync(Client, GetClientErrorMessage(nex, "Location changing failed."));
UnhandledException?.Invoke(this, new UnhandledExceptionEventArgs(nex, isTerminating: false));
}
catch (Exception ex)
{
// Any other exception means that it failed inside the NavigationManager. Treat
// this like bad data.
Log.LocationChangeFailed(_logger, uri, CircuitId, ex);
await TryNotifyClientErrorAsync(Client, GetClientErrorMessage(ex, $"Location changing to '{uri}' failed."));
UnhandledException?.Invoke(this, new UnhandledExceptionEventArgs(ex, isTerminating: false));
}
return cancel;
}



public void SetCircuitUser(ClaimsPrincipal user)
{
// This can be called before the circuit is initialized.
Expand Down Expand Up @@ -610,6 +652,7 @@ private static class Log
private static readonly Action<ILogger, string, CircuitId, Exception> _locationChangeFailed;
private static readonly Action<ILogger, string, CircuitId, Exception> _locationChangeFailedInCircuit;
private static readonly Action<ILogger, long, CircuitId, Exception> _onRenderCompletedFailed;
private static readonly Action<ILogger, string, CircuitId, Exception> _locationChanging;

private static class EventIds
{
Expand Down Expand Up @@ -645,6 +688,7 @@ private static class EventIds
public static readonly EventId LocationChangeFailed = new EventId(210, "LocationChangeFailed");
public static readonly EventId LocationChangeFailedInCircuit = new EventId(211, "LocationChangeFailedInCircuit");
public static readonly EventId OnRenderCompletedFailed = new EventId(212, "OnRenderCompletedFailed");
public static readonly EventId LocationChanging = new EventId(213, "LocationChanging");
}

static Log()
Expand Down Expand Up @@ -798,6 +842,11 @@ static Log()
LogLevel.Debug,
EventIds.OnRenderCompletedFailed,
"Failed to complete render batch '{RenderId}' in circuit host '{CircuitId}'.");

_locationChanging = LoggerMessage.Define<string, CircuitId>(
LogLevel.Debug,
EventIds.LocationChanging,
"Location is about to change to {URI} in circuit '{CircuitId}'.");
}

public static void InitializationStarted(ILogger logger) => _initializationStarted(logger, null);
Expand Down Expand Up @@ -853,6 +902,7 @@ public static void BeginInvokeDotNetFailed(ILogger logger, string callId, string
}
}

public static void LocationChanging(ILogger logger, string uri, CircuitId circuitId) => _locationChanging(logger, uri, circuitId, null);
public static void LocationChange(ILogger logger, string uri, CircuitId circuitId) => _locationChange(logger, uri, circuitId, null);
public static void LocationChangeSucceeded(ILogger logger, string uri, CircuitId circuitId) => _locationChangeSucceeded(logger, uri, circuitId, null);
public static void LocationChangeFailed(ILogger logger, string uri, CircuitId circuitId, Exception exception) => _locationChangeFailed(logger, uri, circuitId, exception);
Expand Down
Loading