Skip to content

Commit ed383aa

Browse files
Navigate with "replace" param (#33751)
1 parent c768a46 commit ed383aa

File tree

15 files changed

+279
-37
lines changed

15 files changed

+279
-37
lines changed

src/Components/Components/src/NavigationManager.cs

+53-3
Original file line numberDiff line numberDiff line change
@@ -90,10 +90,46 @@ protected set
9090
/// <param name="uri">The destination URI. This can be absolute, or relative to the base URI
9191
/// (as returned by <see cref="BaseUri"/>).</param>
9292
/// <param name="forceLoad">If true, bypasses client-side routing and forces the browser to load the new page from the server, whether or not the URI would normally be handled by the client-side router.</param>
93-
public void NavigateTo(string uri, bool forceLoad = false)
93+
public void NavigateTo(string uri, bool forceLoad) // This overload is for binary back-compat with < 6.0
94+
=> NavigateTo(uri, forceLoad, replace: false);
95+
96+
/// <summary>
97+
/// Navigates to the specified URI.
98+
/// </summary>
99+
/// <param name="uri">The destination URI. This can be absolute, or relative to the base URI
100+
/// (as returned by <see cref="BaseUri"/>).</param>
101+
/// <param name="forceLoad">If true, bypasses client-side routing and forces the browser to load the new page from the server, whether or not the URI would normally be handled by the client-side router.</param>
102+
/// <param name="replace">If true, replaces the currently entry in the history stack. If false, appends the new entry to the history stack.</param>
103+
public void NavigateTo(string uri, bool forceLoad = false, bool replace = false)
104+
{
105+
AssertInitialized();
106+
107+
if (replace)
108+
{
109+
NavigateToCore(uri, new NavigationOptions
110+
{
111+
ForceLoad = forceLoad,
112+
ReplaceHistoryEntry = replace,
113+
});
114+
}
115+
else
116+
{
117+
// For back-compatibility, we must call the (string, bool) overload of NavigateToCore from here,
118+
// because that's the only overload guaranteed to be implemented in subclasses.
119+
NavigateToCore(uri, forceLoad);
120+
}
121+
}
122+
123+
/// <summary>
124+
/// Navigates to the specified URI.
125+
/// </summary>
126+
/// <param name="uri">The destination URI. This can be absolute, or relative to the base URI
127+
/// (as returned by <see cref="BaseUri"/>).</param>
128+
/// <param name="options">Provides additional <see cref="NavigationOptions"/>.</param>
129+
public void NavigateTo(string uri, NavigationOptions options)
94130
{
95131
AssertInitialized();
96-
NavigateToCore(uri, forceLoad);
132+
NavigateToCore(uri, options);
97133
}
98134

99135
/// <summary>
@@ -102,7 +138,21 @@ public void NavigateTo(string uri, bool forceLoad = false)
102138
/// <param name="uri">The destination URI. This can be absolute, or relative to the base URI
103139
/// (as returned by <see cref="BaseUri"/>).</param>
104140
/// <param name="forceLoad">If true, bypasses client-side routing and forces the browser to load the new page from the server, whether or not the URI would normally be handled by the client-side router.</param>
105-
protected abstract void NavigateToCore(string uri, bool forceLoad);
141+
// The reason this overload exists and is virtual is for back-compat with < 6.0. Existing NavigationManager subclasses may
142+
// already override this, so the framework needs to keep using it for the cases when only pre-6.0 options are used.
143+
// However, for anyone implementing a new NavigationManager post-6.0, we don't want them to have to override this
144+
// overload any more, so there's now a default implementation that calls the updated overload.
145+
protected virtual void NavigateToCore(string uri, bool forceLoad)
146+
=> NavigateToCore(uri, new NavigationOptions { ForceLoad = forceLoad });
147+
148+
/// <summary>
149+
/// Navigates to the specified URI.
150+
/// </summary>
151+
/// <param name="uri">The destination URI. This can be absolute, or relative to the base URI
152+
/// (as returned by <see cref="BaseUri"/>).</param>
153+
/// <param name="options">Provides additional <see cref="NavigationOptions"/>.</param>
154+
protected virtual void NavigateToCore(string uri, NavigationOptions options) =>
155+
throw new NotImplementedException($"The type {GetType().FullName} does not support supplying {nameof(NavigationOptions)}. To add support, that type should override {nameof(NavigateToCore)}(string uri, {nameof(NavigationOptions)} options).");
106156

107157
/// <summary>
108158
/// Called to initialize BaseURI and current URI before these values are used for the first time.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
namespace Microsoft.AspNetCore.Components
5+
{
6+
/// <summary>
7+
/// Additional options for navigating to another URI.
8+
/// </summary>
9+
public readonly struct NavigationOptions
10+
{
11+
/// <summary>
12+
/// If true, bypasses client-side routing and forces the browser to load the new page from the server, whether or not the URI would normally be handled by the client-side router.
13+
/// </summary>
14+
public bool ForceLoad { get; init; }
15+
16+
/// <summary>
17+
/// If true, replaces the currently entry in the history stack.
18+
/// If false, appends the new entry to the history stack.
19+
/// </summary>
20+
public bool ReplaceHistoryEntry { get; init; }
21+
}
22+
}

src/Components/Components/src/PublicAPI.Unshipped.txt

+13
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
*REMOVED*static Microsoft.AspNetCore.Components.ParameterView.FromDictionary(System.Collections.Generic.IDictionary<string!, object!>! parameters) -> Microsoft.AspNetCore.Components.ParameterView
33
*REMOVED*virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.DispatchEventAsync(ulong eventHandlerId, Microsoft.AspNetCore.Components.RenderTree.EventFieldInfo! fieldInfo, System.EventArgs! eventArgs) -> System.Threading.Tasks.Task!
44
*REMOVED*readonly Microsoft.AspNetCore.Components.RenderTree.RenderTreeEdit.RemovedAttributeName -> string!
5+
*REMOVED*Microsoft.AspNetCore.Components.NavigationManager.NavigateTo(string! uri, bool forceLoad = false) -> void
6+
*REMOVED*abstract Microsoft.AspNetCore.Components.NavigationManager.NavigateToCore(string! uri, bool forceLoad) -> void
57
Microsoft.AspNetCore.Components.ComponentApplicationState
68
Microsoft.AspNetCore.Components.ComponentApplicationState.OnPersisting -> Microsoft.AspNetCore.Components.ComponentApplicationState.OnPersistingCallback!
79
Microsoft.AspNetCore.Components.ComponentApplicationState.OnPersistingCallback
@@ -29,6 +31,15 @@ Microsoft.AspNetCore.Components.Lifetime.ComponentApplicationLifetime.State.get
2931
Microsoft.AspNetCore.Components.Lifetime.IComponentApplicationStateStore
3032
Microsoft.AspNetCore.Components.Lifetime.IComponentApplicationStateStore.GetPersistedStateAsync() -> System.Threading.Tasks.Task<System.Collections.Generic.IDictionary<string!, byte[]!>!>!
3133
Microsoft.AspNetCore.Components.Lifetime.IComponentApplicationStateStore.PersistStateAsync(System.Collections.Generic.IReadOnlyDictionary<string!, byte[]!>! state) -> System.Threading.Tasks.Task!
34+
Microsoft.AspNetCore.Components.NavigationManager.NavigateTo(string! uri, Microsoft.AspNetCore.Components.NavigationOptions options) -> void
35+
Microsoft.AspNetCore.Components.NavigationManager.NavigateTo(string! uri, bool forceLoad = false, bool replace = false) -> void
36+
Microsoft.AspNetCore.Components.NavigationManager.NavigateTo(string! uri, bool forceLoad) -> void
37+
Microsoft.AspNetCore.Components.NavigationOptions
38+
Microsoft.AspNetCore.Components.NavigationOptions.ForceLoad.get -> bool
39+
Microsoft.AspNetCore.Components.NavigationOptions.ForceLoad.init -> void
40+
Microsoft.AspNetCore.Components.NavigationOptions.NavigationOptions() -> void
41+
Microsoft.AspNetCore.Components.NavigationOptions.ReplaceHistoryEntry.get -> bool
42+
Microsoft.AspNetCore.Components.NavigationOptions.ReplaceHistoryEntry.init -> void
3243
Microsoft.AspNetCore.Components.RenderHandle.IsHotReloading.get -> bool
3344
Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder.Dispose() -> void
3445
Microsoft.AspNetCore.Components.Routing.Router.PreferExactMatches.get -> bool
@@ -49,6 +60,8 @@ Microsoft.AspNetCore.Components.RenderTree.Renderer.GetEventArgsType(ulong event
4960
abstract Microsoft.AspNetCore.Components.ErrorBoundaryBase.OnErrorAsync(System.Exception! exception) -> System.Threading.Tasks.Task!
5061
override Microsoft.AspNetCore.Components.LayoutComponentBase.SetParametersAsync(Microsoft.AspNetCore.Components.ParameterView parameters) -> System.Threading.Tasks.Task!
5162
static Microsoft.AspNetCore.Components.ParameterView.FromDictionary(System.Collections.Generic.IDictionary<string!, object?>! parameters) -> Microsoft.AspNetCore.Components.ParameterView
63+
virtual Microsoft.AspNetCore.Components.NavigationManager.NavigateToCore(string! uri, Microsoft.AspNetCore.Components.NavigationOptions options) -> void
64+
virtual Microsoft.AspNetCore.Components.NavigationManager.NavigateToCore(string! uri, bool forceLoad) -> void
5265
virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.DispatchEventAsync(ulong eventHandlerId, Microsoft.AspNetCore.Components.RenderTree.EventFieldInfo? fieldInfo, System.EventArgs! eventArgs) -> System.Threading.Tasks.Task!
5366
readonly Microsoft.AspNetCore.Components.RenderTree.RenderTreeEdit.RemovedAttributeName -> string?
5467
*REMOVED*Microsoft.AspNetCore.Components.CascadingValue<TValue>.Value.get -> TValue

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

-2
Original file line numberDiff line numberDiff line change
@@ -209,8 +209,6 @@ internal class TestNavigationManager : NavigationManager
209209
public TestNavigationManager() =>
210210
Initialize("https://www.example.com/subdir/", "https://www.example.com/subdir/jan");
211211

212-
protected override void NavigateToCore(string uri, bool forceLoad) => throw new NotImplementedException();
213-
214212
public void NotifyLocationChanged(string uri, bool intercepted)
215213
{
216214
Uri = uri;

src/Components/Server/src/Circuits/RemoteNavigationManager.cs

+9-7
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5+
using System.Diagnostics.CodeAnalysis;
56
using Microsoft.AspNetCore.Components.Routing;
67
using Microsoft.Extensions.Logging;
78
using Microsoft.JSInterop;
@@ -65,30 +66,31 @@ public void NotifyLocationChanged(string uri, bool intercepted)
6566
}
6667

6768
/// <inheritdoc />
68-
protected override void NavigateToCore(string uri, bool forceLoad)
69+
[DynamicDependency(DynamicallyAccessedMemberTypes.PublicProperties, typeof(NavigationOptions))]
70+
protected override void NavigateToCore(string uri, NavigationOptions options)
6971
{
70-
Log.RequestingNavigation(_logger, uri, forceLoad);
72+
Log.RequestingNavigation(_logger, uri, options);
7173

7274
if (_jsRuntime == null)
7375
{
7476
var absoluteUriString = ToAbsoluteUri(uri).ToString();
7577
throw new NavigationException(absoluteUriString);
7678
}
7779

78-
_jsRuntime.InvokeAsync<object>(Interop.NavigateTo, uri, forceLoad).Preserve();
80+
_jsRuntime.InvokeVoidAsync(Interop.NavigateTo, uri, options).Preserve();
7981
}
8082

8183
private static class Log
8284
{
83-
private static readonly Action<ILogger, string, bool, Exception> _requestingNavigation =
84-
LoggerMessage.Define<string, bool>(LogLevel.Debug, new EventId(1, "RequestingNavigation"), "Requesting navigation to URI {Uri} with forceLoad={ForceLoad}");
85+
private static readonly Action<ILogger, string, bool, bool, Exception> _requestingNavigation =
86+
LoggerMessage.Define<string, bool, bool>(LogLevel.Debug, new EventId(1, "RequestingNavigation"), "Requesting navigation to URI {Uri} with forceLoad={ForceLoad}, replace={Replace}");
8587

8688
private static readonly Action<ILogger, string, bool, Exception> _receivedLocationChangedNotification =
8789
LoggerMessage.Define<string, bool>(LogLevel.Debug, new EventId(2, "ReceivedLocationChangedNotification"), "Received notification that the URI has changed to {Uri} with isIntercepted={IsIntercepted}");
8890

89-
public static void RequestingNavigation(ILogger logger, string uri, bool forceLoad)
91+
public static void RequestingNavigation(ILogger logger, string uri, NavigationOptions options)
9092
{
91-
_requestingNavigation(logger, uri, forceLoad, null);
93+
_requestingNavigation(logger, uri, options.ForceLoad, options.ReplaceHistoryEntry, null);
9294
}
9395

9496
public static void ReceivedLocationChangedNotification(ILogger logger, string uri, bool isIntercepted)

src/Components/Web.JS/dist/Release/blazor.server.js

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Components/Web.JS/dist/Release/blazor.webview.js

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Components/Web.JS/src/GlobalExports.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { navigateTo, internalFunctions as navigationManagerInternalFunctions } from './Services/NavigationManager';
1+
import { navigateTo, internalFunctions as navigationManagerInternalFunctions, NavigationOptions } from './Services/NavigationManager';
22
import { domFunctions } from './DomWrapper';
33
import { Virtualize } from './Virtualize';
44
import { registerCustomEventType, EventTypeOptions } from './Rendering/Events/EventTypes';
@@ -10,7 +10,7 @@ import { WebAssemblyStartOptions } from './Platform/WebAssemblyStartOptions';
1010
import { Platform, Pointer, System_String, System_Array, System_Object, System_Boolean, System_Byte, System_Int } from './Platform/Platform';
1111

1212
interface IBlazor {
13-
navigateTo: (uri: string, forceLoad: boolean, replace: boolean) => void;
13+
navigateTo: (uri: string, options: NavigationOptions) => void;
1414
registerCustomEventType: (eventName: string, options: EventTypeOptions) => void;
1515

1616
disconnect?: () => void;

src/Components/Web.JS/src/Services/NavigationManager.ts

+39-15
Original file line numberDiff line numberDiff line change
@@ -60,46 +60,64 @@ export function attachToEventDelegator(eventDelegator: EventDelegator) {
6060

6161
if (isWithinBaseUriSpace(absoluteHref)) {
6262
event.preventDefault();
63-
performInternalNavigation(absoluteHref, true);
63+
performInternalNavigation(absoluteHref, /* interceptedLink */ true, /* replace */ false);
6464
}
6565
}
6666
});
6767
}
6868

69-
export function navigateTo(uri: string, forceLoad: boolean, replace: boolean = false) {
69+
// For back-compat, we need to accept multiple overloads
70+
export function navigateTo(uri: string, options: NavigationOptions): void;
71+
export function navigateTo(uri: string, forceLoad: boolean): void;
72+
export function navigateTo(uri: string, forceLoad: boolean, replace: boolean): void;
73+
export function navigateTo(uri: string, forceLoadOrOptions: NavigationOptions | boolean, replaceIfUsingOldOverload: boolean = false) {
7074
const absoluteUri = toAbsoluteUri(uri);
7175

72-
if (!forceLoad && isWithinBaseUriSpace(absoluteUri)) {
73-
// It's an internal URL, so do client-side navigation
74-
performInternalNavigation(absoluteUri, false, replace);
75-
} else if (forceLoad && location.href === uri) {
76-
// Force-loading the same URL you're already on requires special handling to avoid
77-
// triggering browser-specific behavior issues.
76+
// Normalize the parameters to the newer overload (i.e., using NavigationOptions)
77+
const options: NavigationOptions = forceLoadOrOptions instanceof Object
78+
? forceLoadOrOptions
79+
: { forceLoad: forceLoadOrOptions, replaceHistoryEntry: replaceIfUsingOldOverload };
80+
81+
if (!options.forceLoad && isWithinBaseUriSpace(absoluteUri)) {
82+
performInternalNavigation(absoluteUri, false, options.replaceHistoryEntry);
83+
} else {
84+
// For external navigation, we work in terms of the originally-supplied uri string,
85+
// not the computed absoluteUri. This is in case there are some special URI formats
86+
// we're unable to translate into absolute URIs.
87+
performExternalNavigation(uri, options.replaceHistoryEntry);
88+
}
89+
}
90+
91+
function performExternalNavigation(uri: string, replace: boolean) {
92+
if (location.href === uri) {
93+
// If you're already on this URL, you can't append another copy of it to the history stack,
94+
// so we can ignore the 'replace' flag. However, reloading the same URL you're already on
95+
// requires special handling to avoid triggering browser-specific behavior issues.
7896
// For details about what this fixes and why, see https://github.com/dotnet/aspnetcore/pull/10839
7997
const temporaryUri = uri + '?';
8098
history.replaceState(null, '', temporaryUri);
8199
location.replace(uri);
82-
} else if (replace){
83-
history.replaceState(null, '', absoluteUri)
100+
} else if (replace) {
101+
location.replace(uri);
84102
} else {
85-
// It's either an external URL, or forceLoad is requested, so do a full page load
86103
location.href = uri;
87104
}
88105
}
89106

90-
function performInternalNavigation(absoluteInternalHref: string, interceptedLink: boolean, replace: boolean = false) {
107+
function performInternalNavigation(absoluteInternalHref: string, interceptedLink: boolean, replace: boolean) {
91108
// Since this was *not* triggered by a back/forward gesture (that goes through a different
92109
// code path starting with a popstate event), we don't want to preserve the current scroll
93110
// position, so reset it.
94-
// To avoid ugly flickering effects, we don't want to change the scroll position until the
111+
// To avoid ugly flickering effects, we don't want to change the scroll position until
95112
// we render the new page. As a best approximation, wait until the next batch.
96113
resetScrollAfterNextBatch();
97114

98-
if(!replace){
115+
if (!replace) {
99116
history.pushState(null, /* ignored title */ '', absoluteInternalHref);
100-
}else{
117+
} else {
101118
history.replaceState(null, /* ignored title */ '', absoluteInternalHref);
102119
}
120+
103121
notifyLocationChanged(interceptedLink);
104122
}
105123

@@ -166,3 +184,9 @@ function canProcessAnchor(anchorTarget: HTMLAnchorElement) {
166184
const opensInSameFrame = !targetAttributeValue || targetAttributeValue === '_self';
167185
return opensInSameFrame && anchorTarget.hasAttribute('href') && !anchorTarget.hasAttribute('download');
168186
}
187+
188+
// Keep in sync with Components/src/NavigationOptions.cs
189+
export interface NavigationOptions {
190+
forceLoad: boolean;
191+
replaceHistoryEntry: boolean;
192+
}

src/Components/WebAssembly/WebAssembly/src/Services/WebAssemblyNavigationManager.cs

+4-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5+
using System.Diagnostics.CodeAnalysis;
56
using Microsoft.JSInterop;
67
using Interop = Microsoft.AspNetCore.Components.Web.BrowserNavigationManagerInterop;
78

@@ -29,14 +30,15 @@ public void SetLocation(string uri, bool isInterceptedLink)
2930
}
3031

3132
/// <inheritdoc />
32-
protected override void NavigateToCore(string uri, bool forceLoad)
33+
[DynamicDependency(DynamicallyAccessedMemberTypes.PublicProperties, typeof(NavigationOptions))]
34+
protected override void NavigateToCore(string uri, NavigationOptions options)
3335
{
3436
if (uri == null)
3537
{
3638
throw new ArgumentNullException(nameof(uri));
3739
}
3840

39-
DefaultWebAssemblyJSRuntime.Instance.InvokeVoid(Interop.NavigateTo, uri, forceLoad);
41+
DefaultWebAssemblyJSRuntime.Instance.InvokeVoid(Interop.NavigateTo, uri, options);
4042
}
4143
}
4244
}

src/Components/WebView/WebView/src/IpcSender.cs

+4-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5+
using System.Diagnostics.CodeAnalysis;
56
using System.Threading.Tasks;
67
using Microsoft.AspNetCore.Components.RenderTree;
78
using Microsoft.JSInterop;
@@ -33,9 +34,10 @@ public void ApplyRenderBatch(long batchId, RenderBatch renderBatch)
3334
DispatchMessageWithErrorHandling(message);
3435
}
3536

36-
public void Navigate(string uri, bool forceLoad)
37+
[DynamicDependency(DynamicallyAccessedMemberTypes.PublicProperties, typeof(NavigationOptions))]
38+
public void Navigate(string uri, NavigationOptions options)
3739
{
38-
DispatchMessageWithErrorHandling(IpcCommon.Serialize(IpcCommon.OutgoingMessageType.Navigate, uri, forceLoad));
40+
DispatchMessageWithErrorHandling(IpcCommon.Serialize(IpcCommon.OutgoingMessageType.Navigate, uri, options));
3941
}
4042

4143
public void AttachToDocument(int componentId, string selector)

0 commit comments

Comments
 (0)