Skip to content
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

Ability to cancel a Navigation event #24417

Closed
Closed
Show file tree
Hide file tree
Changes from 16 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
69 changes: 69 additions & 0 deletions src/Components/Components/src/NavigationManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,29 @@ public event EventHandler<LocationChangedEventArgs> LocationChanged

private EventHandler<LocationChangedEventArgs>? _locationChanged;

/// <summary>
/// An event that fires when the navigation location is about to change
/// </summary>
public event EventHandler<LocationChangingEventArgs> LocationChanging
{
add
{
AssertInitialized();
_locationChanging += value;
UpdateHasLocationChangingEventHandlers();
}
remove
{
AssertInitialized();
_locationChanging -= value;
UpdateHasLocationChangingEventHandlers();
}
}

private EventHandler<LocationChangingEventArgs>? _locationChanging;

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 +234,52 @@ protected void NotifyLocationChanged(bool isInterceptedLink)
}
}

/// <summary>
/// Triggers the <see cref="LocationChanging"/> event with the current URI value.
/// </summary>
protected bool NotifyLocationChanging(string uri, bool isInterceptedLink, bool forceLoad)
{
try
{
var evt = new LocationChangingEventArgs(uri, isInterceptedLink, forceLoad);
_locationChanging?.Invoke(this, evt);
return evt.Cancel;
}
catch (Exception ex)
{
throw new LocationChangeException("An exception occurred while dispatching a location changing event.", ex);
}
}

/// <summary>
/// Called when <see cref="LocationChanging"/> 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="LocationChanging"/>
/// </summary>
protected void UpdateHasLocationChangingEventHandlers()
{
var value = _locationChanging != null;
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;
}
}
}

private void AssertInitialized()
{
if (!_isInitialized)
Expand Down
11 changes: 11 additions & 0 deletions src/Components/Components/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -119,11 +119,14 @@ 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.LocationChanging -> System.EventHandler<Microsoft.AspNetCore.Components.Routing.LocationChangingEventArgs!>!
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) -> 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 @@ -273,6 +276,13 @@ 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.LocationChangingEventArgs
Microsoft.AspNetCore.Components.Routing.LocationChangingEventArgs.Cancel.get -> bool
Microsoft.AspNetCore.Components.Routing.LocationChangingEventArgs.Cancel.set -> void
Microsoft.AspNetCore.Components.Routing.LocationChangingEventArgs.ForceLoad.get -> bool
Microsoft.AspNetCore.Components.Routing.LocationChangingEventArgs.IsNavigationIntercepted.get -> bool
Microsoft.AspNetCore.Components.Routing.LocationChangingEventArgs.Location.get -> string!
Microsoft.AspNetCore.Components.Routing.LocationChangingEventArgs.LocationChangingEventArgs(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 @@ -409,6 +419,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
46 changes: 46 additions & 0 deletions src/Components/Components/src/Routing/LocationChangingEventArgs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// 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;

namespace Microsoft.AspNetCore.Components.Routing
{
/// <summary>
/// <see cref="EventArgs" /> for <see cref="NavigationManager.LocationChanging" />.
/// </summary>
public class LocationChangingEventArgs : EventArgs
{
/// <summary>
/// Initializes a new instance of <see cref="LocationChangingEventArgs" />.
/// </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 LocationChangingEventArgs(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; }

/// <summary>
/// Gets or sets a value to cancel the current navigation
/// </summary>
public bool Cancel { get; set; }
}
}
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 @@ -474,6 +474,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(() =>
{
Log.LocationChanging(_logger, uri, CircuitId);
var navigationManager = (RemoteNavigationManager)Services.GetRequiredService<NavigationManager>();
return 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 @@ -609,6 +651,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 @@ -644,6 +687,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 @@ -797,6 +841,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 @@ -852,6 +901,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
37 changes: 35 additions & 2 deletions src/Components/Server/src/Circuits/RemoteNavigationManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ public void AttachJsRuntime(IJSRuntime jsRuntime)
{
throw new InvalidOperationException("JavaScript runtime already initialized.");
}

_jsRuntime = jsRuntime;
UpdateHasLocationChangingEventHandlers();
}

public void NotifyLocationChanged(string uri, bool intercepted)
Expand All @@ -64,9 +64,19 @@ public void NotifyLocationChanged(string uri, bool intercepted)
NotifyLocationChanged(intercepted);
}

public bool HandleLocationChanging(string uri, bool intercepted, bool forceLoad)
{
return NotifyLocationChanging(uri, intercepted, forceLoad);
}

/// <inheritdoc />
protected override void NavigateToCore(string uri, bool forceLoad)
{
if (uri == null)
{
throw new ArgumentNullException(nameof(uri));
}

Log.RequestingNavigation(_logger, uri, forceLoad);

if (_jsRuntime == null)
Expand All @@ -75,7 +85,22 @@ protected override void NavigateToCore(string uri, bool forceLoad)
throw new NavigationException(absoluteUriString);
}

_jsRuntime.InvokeAsync<object>(Interop.NavigateTo, uri, forceLoad);
//In serverside blazor we call the locationChanging event here to avoid an extra browser / server roundtrip
if (!NotifyLocationChanging(uri, false, forceLoad))
{
_jsRuntime.InvokeAsync<object>(Interop.NavigateTo, uri, forceLoad);
}
else
{
Log.NavigationCanceled(_logger, uri);
}
}

/// <inheritdoc />
protected override bool SetHasLocationChangingEventHandlers(bool value)
{
_jsRuntime?.InvokeAsync<object>(Interop.SetHasLocationChangingListeners, value);
return _jsRuntime != null;
}

private static class Log
Expand All @@ -86,6 +111,9 @@ private static class Log
private static readonly Action<ILogger, string, bool, Exception> _receivedLocationChangedNotification =
LoggerMessage.Define<string, bool>(LogLevel.Debug, new EventId(2, "ReceivedLocationChangedNotification"), "Received notification that the URI has changed to {Uri} with isIntercepted={IsIntercepted}");

private static readonly Action<ILogger, string, Exception> _navigationCanceled =
LoggerMessage.Define<string>(LogLevel.Debug, new EventId(3, "NavigationCanceled"), "Navigation to URI {Uri} canceled");

public static void RequestingNavigation(ILogger logger, string uri, bool forceLoad)
{
_requestingNavigation(logger, uri, forceLoad, null);
Expand All @@ -95,6 +123,11 @@ public static void ReceivedLocationChangedNotification(ILogger logger, string ur
{
_receivedLocationChangedNotification(logger, uri, isIntercepted, null);
}

public static void NavigationCanceled(ILogger logger, string uri)
{
_navigationCanceled(logger, uri, null);
}
}
}
}
10 changes: 10 additions & 0 deletions src/Components/Server/src/ComponentHub.cs
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,16 @@ public async ValueTask OnLocationChanged(string uri, bool intercepted)
_ = circuitHost.OnLocationChangedAsync(uri, intercepted);
}

public async ValueTask<bool> OnLocationChanging(string uri, bool intercepted)
{
var circuitHost = await GetActiveCircuitAsync();
if (circuitHost == null)
{
return false;
}
return await circuitHost.OnLocationChangingAsync(uri, intercepted);
}

// We store the CircuitHost through a *handle* here because Context.Items is tied to the lifetime
// of the connection. It's possible that a misbehaving client could cause disposal of a CircuitHost
// but keep a connection open indefinitely, preventing GC of the Circuit and related application state.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,7 @@ internal static class BrowserNavigationManagerInterop
public static readonly string GetBaseUri = Prefix + "getUnmarshalledBaseURI";

public static readonly string NavigateTo = Prefix + "navigateTo";

public static readonly string SetHasLocationChangingListeners = Prefix + "setHasLocationChangingListeners";
}
}
4 changes: 2 additions & 2 deletions src/Components/Web.JS/dist/Release/blazor.server.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/Components/Web.JS/dist/Release/blazor.webassembly.js

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions src/Components/Web.JS/src/Boot.Server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ async function initializeConnection(options: CircuitStartOptions, logger: Logger
// Configure navigation via SignalR
window['Blazor']._internal.navigationManager.listenForNavigationEvents((uri: string, intercepted: boolean): Promise<void> => {
return connection.send('OnLocationChanged', uri, intercepted);
},
(uri: string, intercepted: boolean): Promise<boolean> => {
return connection.invoke('OnLocationChanging', uri, intercepted);
});

connection.on('JS.AttachComponent', (componentId, selector) => attachRootComponentToLogicalElement(0, circuit.resolveElement(selector), componentId));
Expand Down
13 changes: 10 additions & 3 deletions src/Components/Web.JS/src/Boot.WebAssembly.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,15 +59,22 @@ async function boot(options?: Partial<WebAssemblyStartOptions>): Promise<void> {
const getLocationHref = window['Blazor']._internal.navigationManager.getLocationHref;
window['Blazor']._internal.navigationManager.getUnmarshalledBaseURI = () => BINDING.js_string_to_mono_string(getBaseUri());
window['Blazor']._internal.navigationManager.getUnmarshalledLocationHref = () => BINDING.js_string_to_mono_string(getLocationHref());

window['Blazor']._internal.navigationManager.listenForNavigationEvents(async (uri: string, intercepted: boolean): Promise<void> => {
await DotNet.invokeMethodAsync(
'Microsoft.AspNetCore.Components.WebAssembly',
'NotifyLocationChanged',
uri,
intercepted
);
});
)
}, async (uri: string, intercepted: boolean): Promise<boolean> => {
return await DotNet.invokeMethodAsync<boolean>(
'Microsoft.AspNetCore.Components.WebAssembly',
'NotifyLocationChanging',
uri,
intercepted
)}
);

// Get the custom environment setting if defined
const environment = options?.environment;
Expand Down
Loading