Skip to content

Commit b445dd6

Browse files
Support NavigateTo when enhanced nav is disabled (#52267)
# Support NavigateTo when enhanced nav is disabled Makes the NavigateTo API work even if enhanced nav is disabled via config. ## Description By default, Blazor apps have either enhanced nav or an interactive router . In these default cases, the `NavigateTo` API works correctly. However there's also an obscure way to disable both of these via config. It's niche, but it's supported, so the rest of the system should work with that. Unfortunately `NavigateTo` assumes that either enhanced nav or an interactive router will be enabled and doesn't account for the case when neither is. Fixes #51636 ## Customer Impact Without this fix, anyone who uses the `ssr: { disableDomPreservation: true }` config option will be unable to use the `NavigateTo` API, as it will do nothing. This behavior isn't desirable. ## Regression? - [ ] Yes - [x] No No because existing code can't use `ssr: { disableDomPreservation: true }` as the option didn't exist prior to .NET 8. Someone else might argue that it's a regression in the sense that, if you're migrating existing code to use newer .NET 8 patterns (and are using `disableDomPreservation` for some reason, even though you wouldn't normally), your existing uses of `NavigateTo` could stop working. That's not how we normally define "regression" but I'm trying to give the fullest explanation. ## Risk - [ ] High - [ ] Medium - [x] Low The fix explicitly retains the old code path if you're coming from .NET 7 or earlier (i.e., if you are using `blazor.webassembly/server/webview.js`. The fixed code path is only applied in `blazor.web.js`, so it should not affect existing apps that are simply moving to the `net8.0` TFM without other code changes. ## Verification - [x] Manual (required) - [x] Automated ## Packaging changes reviewed? - [ ] Yes - [ ] No - [x] N/A
1 parent c0e3747 commit b445dd6

File tree

11 files changed

+92
-25
lines changed

11 files changed

+92
-25
lines changed

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

Lines changed: 1 addition & 1 deletion
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.web.js

Lines changed: 1 addition & 1 deletion
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

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Components/Web.JS/src/Boot.Web.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ function boot(options?: Partial<WebStartOptions>) : Promise<void> {
3838
options = options || {};
3939
options.logLevel ??= LogLevel.Error;
4040
Blazor._internal.loadWebAssemblyQuicklyTimeout = 3000;
41+
Blazor._internal.isBlazorWeb = true;
4142

4243
// Defined here to avoid inadvertently imported enhanced navigation
4344
// related APIs in WebAssembly or Blazor Server contexts.

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ export interface IBlazor {
8080
receiveWebViewDotNetDataStream?: (streamId: number, data: any, bytesRead: number, errorMessage: string) => void;
8181
attachWebRendererInterop?: typeof attachWebRendererInterop;
8282
loadWebAssemblyQuicklyTimeout?: number;
83+
isBlazorWeb?: boolean;
8384

8485
// JSExport APIs
8586
dotNetExports?: {

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

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { EventDelegator } from '../Rendering/Events/EventDelegator';
77
import { attachEnhancedNavigationListener, getInteractiveRouterRendererId, handleClickForNavigationInterception, hasInteractiveRouter, hasProgrammaticEnhancedNavigationHandler, isWithinBaseUriSpace, performProgrammaticEnhancedNavigation, setHasInteractiveRouter, toAbsoluteUri } from './NavigationUtils';
88
import { WebRendererId } from '../Rendering/WebRendererId';
99
import { isRendererAttached } from '../Rendering/WebRendererInteropMethods';
10+
import { IBlazor } from '../GlobalExports';
1011

1112
let hasRegisteredNavigationEventListeners = false;
1213
let currentHistoryIndex = 0;
@@ -142,18 +143,21 @@ function navigateToFromDotNet(uri: string, options: NavigationOptions): void {
142143

143144
function navigateToCore(uri: string, options: NavigationOptions, skipLocationChangingCallback = false): void {
144145
const absoluteUri = toAbsoluteUri(uri);
146+
const pageLoadMechanism = currentPageLoadMechanism();
145147

146-
if (!options.forceLoad && isWithinBaseUriSpace(absoluteUri)) {
147-
if (shouldUseClientSideRouting()) {
148-
performInternalNavigation(absoluteUri, false, options.replaceHistoryEntry, options.historyEntryState, skipLocationChangingCallback);
149-
} else {
150-
performProgrammaticEnhancedNavigation(absoluteUri, options.replaceHistoryEntry);
151-
}
152-
} else {
148+
if (options.forceLoad || !isWithinBaseUriSpace(absoluteUri) || pageLoadMechanism === 'serverside-fullpageload') {
153149
// For external navigation, we work in terms of the originally-supplied uri string,
154150
// not the computed absoluteUri. This is in case there are some special URI formats
155151
// we're unable to translate into absolute URIs.
156152
performExternalNavigation(uri, options.replaceHistoryEntry);
153+
} else if (pageLoadMechanism === 'clientside-router') {
154+
performInternalNavigation(absoluteUri, false, options.replaceHistoryEntry, options.historyEntryState, skipLocationChangingCallback);
155+
} else if (pageLoadMechanism === 'serverside-enhanced') {
156+
performProgrammaticEnhancedNavigation(absoluteUri, options.replaceHistoryEntry);
157+
} else {
158+
// Force a compile-time error if some other case needs to be handled in the future
159+
const unreachable: never = pageLoadMechanism;
160+
throw new Error(`Unsupported page load mechanism: ${unreachable}`);
157161
}
158162
}
159163

@@ -287,7 +291,7 @@ async function notifyLocationChanged(interceptedLink: boolean, internalDestinati
287291
}
288292

289293
async function onPopState(state: PopStateEvent) {
290-
if (popStateCallback && shouldUseClientSideRouting()) {
294+
if (popStateCallback && currentPageLoadMechanism() !== 'serverside-enhanced') {
291295
await popStateCallback(state);
292296
}
293297

@@ -303,10 +307,24 @@ function getInteractiveRouterNavigationCallbacks(): NavigationCallbacks | undefi
303307
return navigationCallbacks.get(interactiveRouterRendererId);
304308
}
305309

306-
function shouldUseClientSideRouting() {
307-
return hasInteractiveRouter() || !hasProgrammaticEnhancedNavigationHandler();
310+
function currentPageLoadMechanism(): PageLoadMechanism {
311+
if (hasInteractiveRouter()) {
312+
return 'clientside-router';
313+
} else if (hasProgrammaticEnhancedNavigationHandler()) {
314+
return 'serverside-enhanced';
315+
} else {
316+
// For back-compat, in blazor.server.js or blazor.webassembly.js, we always behave as if there's an interactive
317+
// router even if there isn't one attached. This preserves a niche case where people may call Blazor.navigateTo
318+
// without a router and expect to receive a notification on the .NET side but no page load occurs.
319+
// In blazor.web.js, we explicitly recognize the case where you have neither an interactive nor enhanced SSR router
320+
// attached, and then handle Blazor.navigateTo by doing a full page load because that's more useful (issue #51636).
321+
const isBlazorWeb = (window['Blazor'] as IBlazor)._internal.isBlazorWeb;
322+
return isBlazorWeb ? 'serverside-fullpageload' : 'clientside-router';
323+
}
308324
}
309325

326+
type PageLoadMechanism = 'clientside-router' | 'serverside-enhanced' | 'serverside-fullpageload';
327+
310328
// Keep in sync with Components/src/NavigationOptions.cs
311329
export interface NavigationOptions {
312330
forceLoad: boolean;

src/Components/test/E2ETest/ServerRenderingTests/EnhancedNavigationTest.cs

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -558,15 +558,5 @@ private void AssertEnhancedUpdateCountEquals(long count)
558558
=> Browser.Equal(count, () => ((IJavaScriptExecutor)Browser).ExecuteScript("return window.enhancedPageUpdateCount;"));
559559

560560
private static bool IsElementStale(IWebElement element)
561-
{
562-
try
563-
{
564-
_ = element.Enabled;
565-
return false;
566-
}
567-
catch (StaleElementReferenceException)
568-
{
569-
return true;
570-
}
571-
}
561+
=> EnhancedNavigationTestUtil.IsElementStale(element);
572562
}

src/Components/test/E2ETest/ServerRenderingTests/EnhancedNavigationTestUtil.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,17 @@ public static void SuppressEnhancedNavigation<TServerFixture>(ServerTestBase<TSe
3232

3333
public static long GetScrollY(this IWebDriver browser)
3434
=> Convert.ToInt64(((IJavaScriptExecutor)browser).ExecuteScript("return window.scrollY"), CultureInfo.CurrentCulture);
35+
36+
public static bool IsElementStale(IWebElement element)
37+
{
38+
try
39+
{
40+
_ = element.Enabled;
41+
return false;
42+
}
43+
catch (StaleElementReferenceException)
44+
{
45+
return true;
46+
}
47+
}
3548
}

src/Components/test/E2ETest/ServerRenderingTests/InteractivityTest.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1110,6 +1110,30 @@ public void CanPersistPrerenderedState_WebAssemblyPrerenderedStateAvailableOnlyO
11101110
Browser.Equal("not restored", () => Browser.FindElement(By.Id("wasm")).Text);
11111111
}
11121112

1113+
[Theory]
1114+
[InlineData(false, false)]
1115+
[InlineData(false, true)]
1116+
[InlineData(true, false)]
1117+
[InlineData(true, true)]
1118+
public void CanPerformNavigateToFromInteractiveEventHandler(bool suppressEnhancedNavigation, bool forceLoad)
1119+
{
1120+
EnhancedNavigationTestUtil.SuppressEnhancedNavigation(this, suppressEnhancedNavigation);
1121+
1122+
// Get to the test page
1123+
Navigate($"{ServerPathBase}/interactivity/navigateto");
1124+
Browser.Equal("Interactive NavigateTo", () => Browser.FindElement(By.TagName("h1")).Text);
1125+
var originalNavElem = Browser.FindElement(By.TagName("nav"));
1126+
1127+
// Perform the navigation
1128+
Browser.Click(By.Id(forceLoad ? "perform-navigateto-force" : "perform-navigateto"));
1129+
Browser.True(() => Browser.Url.EndsWith("/nav", StringComparison.Ordinal));
1130+
Browser.Equal("Hello", () => Browser.FindElement(By.Id("nav-home")).Text);
1131+
1132+
// Verify the elements were preserved if and only if they should be
1133+
var shouldPreserveElements = !suppressEnhancedNavigation && !forceLoad;
1134+
Assert.Equal(shouldPreserveElements, !EnhancedNavigationTestUtil.IsElementStale(originalNavElem));
1135+
}
1136+
11131137
private void BlockWebAssemblyResourceLoad()
11141138
{
11151139
((IJavaScriptExecutor)Browser).ExecuteScript("sessionStorage.setItem('block-load-boot-resource', 'true')");
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
@page "/interactivity/navigateto"
2+
@layout Components.TestServer.RazorComponents.Shared.EnhancedNavLayout
3+
@inject NavigationManager Nav
4+
@rendermode RenderMode.InteractiveServer
5+
6+
<h1>Interactive NavigateTo</h1>
7+
8+
<p>Shows that NavigateTo from an interactive event handler works as expected, with or without enhanced navigation.</p>
9+
10+
<button id="perform-navigateto" @onclick="@(() => PerformNavigateTo(false))">Navigate</button>
11+
<button id="perform-navigateto-force" @onclick="@(() => PerformNavigateTo(true))">Navigate (force load)</button>
12+
13+
@code {
14+
void PerformNavigateTo(bool forceLoad)
15+
{
16+
Nav.NavigateTo("nav", forceLoad);
17+
}
18+
}

0 commit comments

Comments
 (0)