Skip to content

Commit 0e255c5

Browse files
authored
[Blazor] Adds support state in NavigationManager (#42534)
* Exposes the history state from the page to the navigation manager. * Applications can pass in state during navigations with `NavigationManager.NavigateTo("url", new NavigationOptions{ HistoryEntryState = "entryState" })`. * Passing state is limited to internal navigations as defined by the underlying History API.
1 parent be68b79 commit 0e255c5

File tree

23 files changed

+153
-31
lines changed

23 files changed

+153
-31
lines changed

Diff for: src/Components/Components/src/NavigationManager.cs

+14-2
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ public event EventHandler<LocationChangedEventArgs> LocationChanged
3737

3838
// The URI. Always represented an absolute URI.
3939
private string? _uri;
40-
4140
private bool _isInitialized;
4241

4342
/// <summary>
@@ -85,6 +84,14 @@ protected set
8584
}
8685
}
8786

87+
/// <summary>
88+
/// Gets or sets the state associated with the current navigation.
89+
/// </summary>
90+
/// <remarks>
91+
/// Setting <see cref="HistoryEntryState" /> will not trigger the <see cref="LocationChanged" /> event.
92+
/// </remarks>
93+
public string? HistoryEntryState { get; protected set; }
94+
8895
/// <summary>
8996
/// Navigates to the specified URI.
9097
/// </summary>
@@ -254,7 +261,12 @@ protected void NotifyLocationChanged(bool isInterceptedLink)
254261
{
255262
try
256263
{
257-
_locationChanged?.Invoke(this, new LocationChangedEventArgs(_uri!, isInterceptedLink));
264+
_locationChanged?.Invoke(
265+
this,
266+
new LocationChangedEventArgs(_uri!, isInterceptedLink)
267+
{
268+
HistoryEntryState = HistoryEntryState
269+
});
258270
}
259271
catch (Exception ex)
260272
{

Diff for: src/Components/Components/src/NavigationOptions.cs

+5
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,9 @@ public readonly struct NavigationOptions
1818
/// If false, appends the new entry to the history stack.
1919
/// </summary>
2020
public bool ReplaceHistoryEntry { get; init; }
21+
22+
/// <summary>
23+
/// Gets or sets the state to append to the history entry.
24+
/// </summary>
25+
public string? HistoryEntryState { get; init; }
2126
}

Diff for: src/Components/Components/src/PublicAPI.Unshipped.txt

+5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
#nullable enable
2+
Microsoft.AspNetCore.Components.NavigationManager.HistoryEntryState.get -> string?
3+
Microsoft.AspNetCore.Components.NavigationManager.HistoryEntryState.set -> void
4+
Microsoft.AspNetCore.Components.NavigationOptions.HistoryEntryState.get -> string?
5+
Microsoft.AspNetCore.Components.NavigationOptions.HistoryEntryState.init -> void
26
Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder.AddContent(int sequence, Microsoft.AspNetCore.Components.MarkupString? markupContent) -> void
7+
Microsoft.AspNetCore.Components.Routing.LocationChangedEventArgs.HistoryEntryState.get -> string?
38
static Microsoft.AspNetCore.Components.CompilerServices.RuntimeHelpers.CreateInferredEventCallback<T>(object! receiver, Microsoft.AspNetCore.Components.EventCallback<T> callback, T value) -> Microsoft.AspNetCore.Components.EventCallback<T>
49
static Microsoft.AspNetCore.Components.CompilerServices.RuntimeHelpers.InvokeAsynchronousDelegate(System.Action! callback) -> System.Threading.Tasks.Task!
510
static Microsoft.AspNetCore.Components.CompilerServices.RuntimeHelpers.InvokeAsynchronousDelegate(System.Func<System.Threading.Tasks.Task!>! callback) -> System.Threading.Tasks.Task!

Diff for: src/Components/Components/src/Routing/LocationChangedEventArgs.cs

+5
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,9 @@ public LocationChangedEventArgs(string location, bool isNavigationIntercepted)
2828
/// Gets a value that determines if navigation for the link was intercepted.
2929
/// </summary>
3030
public bool IsNavigationIntercepted { get; }
31+
32+
/// <summary>
33+
/// Gets the state associated with the current history entry.
34+
/// </summary>
35+
public string? HistoryEntryState { get; internal init; }
3136
}

Diff for: src/Components/Components/test/Routing/RouterTest.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ internal class TestNavigationManager : NavigationManager
203203
public TestNavigationManager() =>
204204
Initialize("https://www.example.com/subdir/", "https://www.example.com/subdir/jan");
205205

206-
public void NotifyLocationChanged(string uri, bool intercepted)
206+
public void NotifyLocationChanged(string uri, bool intercepted, string state = null)
207207
{
208208
Uri = uri;
209209
NotifyLocationChanged(intercepted);

Diff for: src/Components/Server/src/Circuits/CircuitHost.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -493,7 +493,7 @@ public async Task<DotNetStreamReference> TryClaimPendingStream(long streamId)
493493

494494
// OnLocationChangedAsync is used in a fire-and-forget context, so it's responsible for its own
495495
// error handling.
496-
public async Task OnLocationChangedAsync(string uri, bool intercepted)
496+
public async Task OnLocationChangedAsync(string uri, string state, bool intercepted)
497497
{
498498
AssertInitialized();
499499
AssertNotDisposed();
@@ -504,7 +504,7 @@ await Renderer.Dispatcher.InvokeAsync(() =>
504504
{
505505
Log.LocationChange(_logger, uri, CircuitId);
506506
var navigationManager = (RemoteNavigationManager)Services.GetRequiredService<NavigationManager>();
507-
navigationManager.NotifyLocationChanged(uri, intercepted);
507+
navigationManager.NotifyLocationChanged(uri, state, intercepted);
508508
Log.LocationChangeSucceeded(_logger, uri, CircuitId);
509509
});
510510
}

Diff for: src/Components/Server/src/Circuits/RemoteNavigationManager.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,12 @@ public void AttachJsRuntime(IJSRuntime jsRuntime)
5656
_jsRuntime = jsRuntime;
5757
}
5858

59-
public void NotifyLocationChanged(string uri, bool intercepted)
59+
public void NotifyLocationChanged(string uri, string state, bool intercepted)
6060
{
6161
Log.ReceivedLocationChangedNotification(_logger, uri, intercepted);
6262

6363
Uri = uri;
64+
HistoryEntryState = state;
6465
NotifyLocationChanged(intercepted);
6566
}
6667

Diff for: src/Components/Server/src/ComponentHub.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -285,15 +285,15 @@ public async ValueTask OnRenderCompleted(long renderId, string errorMessageOrNul
285285
_ = circuitHost.OnRenderCompletedAsync(renderId, errorMessageOrNull);
286286
}
287287

288-
public async ValueTask OnLocationChanged(string uri, bool intercepted)
288+
public async ValueTask OnLocationChanged(string uri, string? state, bool intercepted)
289289
{
290290
var circuitHost = await GetActiveCircuitAsync();
291291
if (circuitHost == null)
292292
{
293293
return;
294294
}
295295

296-
_ = circuitHost.OnLocationChangedAsync(uri, intercepted);
296+
_ = circuitHost.OnLocationChangedAsync(uri, state, intercepted);
297297
}
298298

299299
// We store the CircuitHost through a *handle* here because Context.Items is tied to the lifetime

Diff for: src/Components/Server/test/Circuits/ComponentHubTest.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ public async Task CannotInvokeOnLocationChangedBeforeInitialization()
7979
{
8080
var (mockClientProxy, hub) = InitializeComponentHub();
8181

82-
await hub.OnLocationChanged("https://localhost:5000/subdir/page", false);
82+
await hub.OnLocationChanged("https://localhost:5000/subdir/page", null, false);
8383

8484
var errorMessage = "Circuit not initialized.";
8585
mockClientProxy.Verify(m => m.SendCoreAsync("JS.Error", new[] { errorMessage }, It.IsAny<CancellationToken>()), Times.Once());

Diff for: 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.

Diff for: 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.

Diff for: src/Components/Web.JS/src/Boot.Server.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,8 @@ async function boot(userOptions?: Partial<CircuitStartOptions>): Promise<void> {
6060
const circuit = new CircuitDescriptor(components, appState || '');
6161

6262
// Configure navigation via SignalR
63-
Blazor._internal.navigationManager.listenForNavigationEvents((uri: string, intercepted: boolean): Promise<void> => {
64-
return connection.send('OnLocationChanged', uri, intercepted);
63+
Blazor._internal.navigationManager.listenForNavigationEvents((uri: string, state: string | undefined, intercepted: boolean): Promise<void> => {
64+
return connection.send('OnLocationChanged', uri, state, intercepted);
6565
});
6666

6767
Blazor._internal.forceCloseConnection = () => connection.stop();

Diff for: src/Components/Web.JS/src/Boot.WebAssembly.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,12 @@ async function boot(options?: Partial<WebAssemblyStartOptions>): Promise<void> {
7979
Blazor._internal.navigationManager.getUnmarshalledBaseURI = () => BINDING.js_string_to_mono_string(getBaseUri());
8080
Blazor._internal.navigationManager.getUnmarshalledLocationHref = () => BINDING.js_string_to_mono_string(getLocationHref());
8181

82-
Blazor._internal.navigationManager.listenForNavigationEvents(async (uri: string, intercepted: boolean): Promise<void> => {
82+
Blazor._internal.navigationManager.listenForNavigationEvents(async (uri: string, state: string | undefined, intercepted: boolean): Promise<void> => {
8383
await DotNet.invokeMethodAsync(
8484
'Microsoft.AspNetCore.Components.WebAssembly',
8585
'NotifyLocationChanged',
8686
uri,
87+
state,
8788
intercepted
8889
);
8990
});

Diff for: src/Components/Web.JS/src/Platform/WebView/WebViewIpcSender.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ function base64EncodeByteArray(data: Uint8Array) {
3636
return dataBase64Encoded;
3737
}
3838

39-
export function sendLocationChanged(uri: string, intercepted: boolean): Promise<void> {
40-
send('OnLocationChanged', uri, intercepted);
39+
export function sendLocationChanged(uri: string, state: string | undefined, intercepted: boolean): Promise<void> {
40+
send('OnLocationChanged', uri, state, intercepted);
4141
return Promise.resolve(); // Like in Blazor Server, we only issue the notification here - there's no need to wait for a response
4242
}
4343

Diff for: src/Components/Web.JS/src/Services/NavigationManager.ts

+8-7
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ let hasEnabledNavigationInterception = false;
99
let hasRegisteredNavigationEventListeners = false;
1010

1111
// Will be initialized once someone registers
12-
let notifyLocationChangedCallback: ((uri: string, intercepted: boolean) => Promise<void>) | null = null;
12+
let notifyLocationChangedCallback: ((uri: string, state: string | undefined, intercepted: boolean) => Promise<void>) | null = null;
1313

1414
// These are the functions we're making available for invocation from .NET
1515
export const internalFunctions = {
@@ -20,7 +20,7 @@ export const internalFunctions = {
2020
getLocationHref: (): string => location.href,
2121
};
2222

23-
function listenForNavigationEvents(callback: (uri: string, intercepted: boolean) => Promise<void>): void {
23+
function listenForNavigationEvents(callback: (uri: string, state: string | undefined, intercepted: boolean) => Promise<void>): void {
2424
notifyLocationChangedCallback = callback;
2525

2626
if (hasRegisteredNavigationEventListeners) {
@@ -82,7 +82,7 @@ export function navigateTo(uri: string, forceLoadOrOptions: NavigationOptions |
8282
: { forceLoad: forceLoadOrOptions, replaceHistoryEntry: replaceIfUsingOldOverload };
8383

8484
if (!options.forceLoad && isWithinBaseUriSpace(absoluteUri)) {
85-
performInternalNavigation(absoluteUri, false, options.replaceHistoryEntry);
85+
performInternalNavigation(absoluteUri, false, options.replaceHistoryEntry, options.historyEntryState);
8686
} else {
8787
// For external navigation, we work in terms of the originally-supplied uri string,
8888
// not the computed absoluteUri. This is in case there are some special URI formats
@@ -107,7 +107,7 @@ function performExternalNavigation(uri: string, replace: boolean) {
107107
}
108108
}
109109

110-
function performInternalNavigation(absoluteInternalHref: string, interceptedLink: boolean, replace: boolean) {
110+
function performInternalNavigation(absoluteInternalHref: string, interceptedLink: boolean, replace: boolean, state: string | undefined = undefined) {
111111
// Since this was *not* triggered by a back/forward gesture (that goes through a different
112112
// code path starting with a popstate event), we don't want to preserve the current scroll
113113
// position, so reset it.
@@ -116,17 +116,17 @@ function performInternalNavigation(absoluteInternalHref: string, interceptedLink
116116
resetScrollAfterNextBatch();
117117

118118
if (!replace) {
119-
history.pushState(null, /* ignored title */ '', absoluteInternalHref);
119+
history.pushState(state, /* ignored title */ '', absoluteInternalHref);
120120
} else {
121-
history.replaceState(null, /* ignored title */ '', absoluteInternalHref);
121+
history.replaceState(state, /* ignored title */ '', absoluteInternalHref);
122122
}
123123

124124
notifyLocationChanged(interceptedLink);
125125
}
126126

127127
async function notifyLocationChanged(interceptedLink: boolean) {
128128
if (notifyLocationChangedCallback) {
129-
await notifyLocationChangedCallback(location.href, interceptedLink);
129+
await notifyLocationChangedCallback(location.href, history.state, interceptedLink);
130130
}
131131
}
132132

@@ -195,4 +195,5 @@ function canProcessAnchor(anchorTarget: HTMLAnchorElement) {
195195
export interface NavigationOptions {
196196
forceLoad: boolean;
197197
replaceHistoryEntry: boolean;
198+
historyEntryState?: string;
198199
}

Diff for: src/Components/WebAssembly/WebAssembly/src/Infrastructure/JSInteropMethods.cs

+9-2
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,16 @@ public static class JSInteropMethods
1717
/// <summary>
1818
/// For framework use only.
1919
/// </summary>
20-
[JSInvokable(nameof(NotifyLocationChanged))]
20+
[Obsolete("This API is for framework use only and is no longer used in the current version")]
2121
public static void NotifyLocationChanged(string uri, bool isInterceptedLink)
22+
=> WebAssemblyNavigationManager.Instance.SetLocation(uri, null, isInterceptedLink);
23+
24+
/// <summary>
25+
/// For framework use only.
26+
/// </summary>
27+
[JSInvokable(nameof(NotifyLocationChanged))]
28+
public static void NotifyLocationChanged(string uri, string? state, bool isInterceptedLink)
2229
{
23-
WebAssemblyNavigationManager.Instance.SetLocation(uri, isInterceptedLink);
30+
WebAssemblyNavigationManager.Instance.SetLocation(uri, state, isInterceptedLink);
2431
}
2532
}
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
#nullable enable
22
*REMOVED*static Microsoft.AspNetCore.Components.WebAssembly.HotReload.WebAssemblyHotReload.ApplyHotReloadDelta(string! moduleIdString, byte[]! metadataDelta, byte[]! ilDeta) -> void
33
static Microsoft.AspNetCore.Components.WebAssembly.HotReload.WebAssemblyHotReload.ApplyHotReloadDelta(string! moduleIdString, byte[]! metadataDelta, byte[]! ilDelta, byte[]! pdbBytes) -> void
4+
static Microsoft.AspNetCore.Components.WebAssembly.Infrastructure.JSInteropMethods.NotifyLocationChanged(string! uri, string? state, bool isInterceptedLink) -> void

Diff for: src/Components/WebAssembly/WebAssembly/src/Services/WebAssemblyNavigationManager.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,10 @@ public WebAssemblyNavigationManager(string baseUri, string uri)
2222
Initialize(baseUri, uri);
2323
}
2424

25-
public void SetLocation(string uri, bool isInterceptedLink)
25+
public void SetLocation(string uri, string? state, bool isInterceptedLink)
2626
{
2727
Uri = uri;
28+
HistoryEntryState = state;
2829
NotifyLocationChanged(isInterceptedLink);
2930
}
3031

Diff for: src/Components/WebView/WebView/src/IpcReceiver.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ public async Task OnMessageReceivedAsync(PageContext pageContext, string message
6161
OnRenderCompleted(pageContext, args[0].GetInt64(), args[1].GetString());
6262
break;
6363
case IpcCommon.IncomingMessageType.OnLocationChanged:
64-
OnLocationChanged(pageContext, args[0].GetString(), args[1].GetBoolean());
64+
OnLocationChanged(pageContext, args[0].GetString(), args[1].GetString(), args[2].GetBoolean());
6565
break;
6666
default:
6767
throw new InvalidOperationException($"Unknown message type '{messageType}'.");
@@ -97,8 +97,8 @@ private static void OnRenderCompleted(PageContext pageContext, long batchId, str
9797
pageContext.Renderer.NotifyRenderCompleted(batchId);
9898
}
9999

100-
private static void OnLocationChanged(PageContext pageContext, string uri, bool intercepted)
100+
private static void OnLocationChanged(PageContext pageContext, string uri, string? state, bool intercepted)
101101
{
102-
pageContext.NavigationManager.LocationUpdated(uri, intercepted);
102+
pageContext.NavigationManager.LocationUpdated(uri, state, intercepted);
103103
}
104104
}

Diff for: src/Components/WebView/WebView/src/Services/WebViewNavigationManager.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@ public void AttachToWebView(IpcSender ipcSender, string baseUrl, string initialU
1313
Initialize(baseUrl, initialUrl);
1414
}
1515

16-
public void LocationUpdated(string newUrl, bool intercepted)
16+
public void LocationUpdated(string newUrl, string? state, bool intercepted)
1717
{
1818
Uri = newUrl;
19+
HistoryEntryState = state;
1920
NotifyLocationChanged(intercepted);
2021
}
2122

Diff for: src/Components/test/E2ETest/Tests/RoutingTest.cs

+69-1
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,74 @@ public void CanNavigateProgrammaticallyWithForceLoad()
438438
});
439439
}
440440

441+
[Fact]
442+
public void CanNavigateProgrammaticallyWithStateValidateNoReplaceHistoryEntry()
443+
{
444+
// This test checks if default navigation does not replace Browser history entries
445+
SetUrlViaPushState("/");
446+
447+
var app = Browser.MountTestComponent<TestRouter>();
448+
var testSelector = Browser.WaitUntilTestSelectorReady();
449+
450+
app.FindElement(By.LinkText("Programmatic navigation cases")).Click();
451+
Browser.True(() => Browser.Url.EndsWith("/ProgrammaticNavigationCases", StringComparison.Ordinal));
452+
Browser.Contains("programmatic navigation", () => app.FindElement(By.Id("test-info")).Text);
453+
454+
// We navigate to the /Other page
455+
app.FindElement(By.Id("do-other-navigation-state")).Click();
456+
Browser.True(() => Browser.Url.EndsWith("/Other", StringComparison.Ordinal));
457+
Browser.Contains("state", () => app.FindElement(By.Id("test-state")).Text);
458+
AssertHighlightedLinks("Other", "Other with base-relative URL (matches all)");
459+
460+
// After we press back, we should end up at the "/ProgrammaticNavigationCases" page so we know browser history has not been replaced
461+
// If history had been replaced we would have ended up at the "/" page
462+
Browser.Navigate().Back();
463+
Browser.True(() => Browser.Url.EndsWith("/ProgrammaticNavigationCases", StringComparison.Ordinal));
464+
AssertHighlightedLinks("Programmatic navigation cases");
465+
466+
// When the navigation is forced, the state is ignored (we could choose to throw here).
467+
app.FindElement(By.Id("do-other-navigation-forced-state")).Click();
468+
Browser.True(() => Browser.Url.EndsWith("/Other", StringComparison.Ordinal));
469+
Browser.DoesNotExist(By.Id("test-state"));
470+
471+
// We check if we had a force load
472+
Assert.Throws<StaleElementReferenceException>(() =>
473+
testSelector.SelectedOption.GetAttribute("value"));
474+
475+
// But still we should be able to navigate back, and end up at the "/ProgrammaticNavigationCases" page
476+
Browser.Navigate().Back();
477+
Browser.True(() => Browser.Url.EndsWith("/ProgrammaticNavigationCases", StringComparison.Ordinal));
478+
Browser.WaitUntilTestSelectorReady();
479+
}
480+
481+
[Fact]
482+
public void CanNavigateProgrammaticallyWithStateReplaceHistoryEntry()
483+
{
484+
SetUrlViaPushState("/");
485+
486+
var app = Browser.MountTestComponent<TestRouter>();
487+
var testSelector = Browser.WaitUntilTestSelectorReady();
488+
489+
app.FindElement(By.LinkText("Programmatic navigation cases")).Click();
490+
Browser.True(() => Browser.Url.EndsWith("/ProgrammaticNavigationCases", StringComparison.Ordinal));
491+
Browser.Contains("programmatic navigation", () => app.FindElement(By.Id("test-info")).Text);
492+
493+
// We navigate to the /Other page, with "replace" enabled
494+
app.FindElement(By.Id("do-other-navigation-state-replacehistoryentry")).Click();
495+
Browser.True(() => Browser.Url.EndsWith("/Other", StringComparison.Ordinal));
496+
Browser.Contains("state", () => app.FindElement(By.Id("test-state")).Text);
497+
AssertHighlightedLinks("Other", "Other with base-relative URL (matches all)");
498+
499+
// After we press back, we should end up at the "/" page so we know browser history has been replaced
500+
// If history would not have been replaced we would have ended up at the "/ProgrammaticNavigationCases" page
501+
Browser.Navigate().Back();
502+
Browser.True(() => Browser.Url.EndsWith("/", StringComparison.Ordinal));
503+
AssertHighlightedLinks("Default (matches all)", "Default with base-relative URL (matches all)");
504+
505+
// Because this was all with client-side navigation, we didn't lose the state in the test selector
506+
Assert.Equal(typeof(TestRouter).FullName, testSelector.SelectedOption.GetAttribute("value"));
507+
}
508+
441509
[Fact]
442510
public void CanNavigateProgrammaticallyValidateNoReplaceHistoryEntry()
443511
{
@@ -452,7 +520,7 @@ public void CanNavigateProgrammaticallyValidateNoReplaceHistoryEntry()
452520
Browser.Contains("programmatic navigation", () => app.FindElement(By.Id("test-info")).Text);
453521

454522
// We navigate to the /Other page
455-
// This will also test our new NavigatTo(string uri) overload (it should not replace the browser history)
523+
// This will also test our new NavigateTo(string uri) overload (it should not replace the browser history)
456524
app.FindElement(By.Id("do-other-navigation")).Click();
457525
Browser.True(() => Browser.Url.EndsWith("/Other", StringComparison.Ordinal));
458526
AssertHighlightedLinks("Other", "Other with base-relative URL (matches all)");
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
@page "/Other"
2+
@inject NavigationManager Navigation
23
<div id="test-info">This is another page.</div>
4+
<div id="test-state">@Navigation.HistoryEntryState</div>

0 commit comments

Comments
 (0)