Skip to content

Commit 14d37af

Browse files
SteveSandersonMSjaviercn
authored andcommitted
Support NavigateTo when enhanced nav is disabled (#52267)
Makes the NavigateTo API work even if enhanced nav is disabled via config. 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 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. - [ ] 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. - [ ] 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. - [x] Manual (required) - [x] Automated - [ ] Yes - [ ] No - [x] N/A
1 parent 3b674da commit 14d37af

File tree

8 files changed

+91
-11
lines changed

8 files changed

+91
-11
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ function boot(options?: Partial<WebStartOptions>) : Promise<void> {
4040
started = true;
4141
options = options || {};
4242
options.logLevel ??= LogLevel.Error;
43+
Blazor._internal.isBlazorWeb = true;
4344

4445
// Defined here to avoid inadvertently imported enhanced navigation
4546
// 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
@@ -84,6 +84,7 @@ export interface IBlazor {
8484
receiveWebAssemblyDotNetDataStream?: (streamId: number, data: any, bytesRead: number, errorMessage: string) => void;
8585
receiveWebViewDotNetDataStream?: (streamId: number, data: any, bytesRead: number, errorMessage: string) => void;
8686
attachWebRendererInterop?: typeof attachWebRendererInterop;
87+
isBlazorWeb?: boolean;
8788

8889
// JSExport APIs
8990
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, isForSamePath, isSamePageWithHash, isWithinBaseUriSpace, performProgrammaticEnhancedNavigation, performScrollToElementOnTheSamePage, scrollToElement, 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;
@@ -116,18 +117,21 @@ function navigateToFromDotNet(uri: string, options: NavigationOptions): void {
116117

117118
function navigateToCore(uri: string, options: NavigationOptions, skipLocationChangingCallback = false): void {
118119
const absoluteUri = toAbsoluteUri(uri);
120+
const pageLoadMechanism = currentPageLoadMechanism();
119121

120-
if (!options.forceLoad && isWithinBaseUriSpace(absoluteUri)) {
121-
if (shouldUseClientSideRouting()) {
122-
performInternalNavigation(absoluteUri, false, options.replaceHistoryEntry, options.historyEntryState, skipLocationChangingCallback);
123-
} else {
124-
performProgrammaticEnhancedNavigation(absoluteUri, options.replaceHistoryEntry);
125-
}
126-
} else {
122+
if (options.forceLoad || !isWithinBaseUriSpace(absoluteUri) || pageLoadMechanism === 'serverside-fullpageload') {
127123
// For external navigation, we work in terms of the originally-supplied uri string,
128124
// not the computed absoluteUri. This is in case there are some special URI formats
129125
// we're unable to translate into absolute URIs.
130126
performExternalNavigation(uri, options.replaceHistoryEntry);
127+
} else if (pageLoadMechanism === 'clientside-router') {
128+
performInternalNavigation(absoluteUri, false, options.replaceHistoryEntry, options.historyEntryState, skipLocationChangingCallback);
129+
} else if (pageLoadMechanism === 'serverside-enhanced') {
130+
performProgrammaticEnhancedNavigation(absoluteUri, options.replaceHistoryEntry);
131+
} else {
132+
// Force a compile-time error if some other case needs to be handled in the future
133+
const unreachable: never = pageLoadMechanism;
134+
throw new Error(`Unsupported page load mechanism: ${unreachable}`);
131135
}
132136
}
133137

@@ -266,7 +270,7 @@ async function notifyLocationChanged(interceptedLink: boolean, internalDestinati
266270
}
267271

268272
async function onPopState(state: PopStateEvent) {
269-
if (popStateCallback && shouldUseClientSideRouting()) {
273+
if (popStateCallback && currentPageLoadMechanism() !== 'serverside-enhanced') {
270274
await popStateCallback(state);
271275
}
272276

@@ -282,10 +286,24 @@ function getInteractiveRouterNavigationCallbacks(): NavigationCallbacks | undefi
282286
return navigationCallbacks.get(interactiveRouterRendererId);
283287
}
284288

285-
function shouldUseClientSideRouting() {
286-
return hasInteractiveRouter() || !hasProgrammaticEnhancedNavigationHandler();
289+
function currentPageLoadMechanism(): PageLoadMechanism {
290+
if (hasInteractiveRouter()) {
291+
return 'clientside-router';
292+
} else if (hasProgrammaticEnhancedNavigationHandler()) {
293+
return 'serverside-enhanced';
294+
} else {
295+
// For back-compat, in blazor.server.js or blazor.webassembly.js, we always behave as if there's an interactive
296+
// router even if there isn't one attached. This preserves a niche case where people may call Blazor.navigateTo
297+
// without a router and expect to receive a notification on the .NET side but no page load occurs.
298+
// In blazor.web.js, we explicitly recognize the case where you have neither an interactive nor enhanced SSR router
299+
// attached, and then handle Blazor.navigateTo by doing a full page load because that's more useful (issue #51636).
300+
const isBlazorWeb = (window['Blazor'] as IBlazor)._internal.isBlazorWeb;
301+
return isBlazorWeb ? 'serverside-fullpageload' : 'clientside-router';
302+
}
287303
}
288304

305+
type PageLoadMechanism = 'clientside-router' | 'serverside-enhanced' | 'serverside-fullpageload';
306+
289307
// Keep in sync with Components/src/NavigationOptions.cs
290308
export interface NavigationOptions {
291309
forceLoad: boolean;

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -989,4 +989,7 @@ private static Func<IWebDriver, bool> ElementWithTextAppears(By selector, string
989989
// Ensure we actually observed the new content, not just the presence of the element.
990990
return string.Equals(elements[0].Text, expectedText, StringComparison.Ordinal);
991991
};
992+
993+
private static bool IsElementStale(IWebElement element)
994+
=> EnhancedNavigationTestUtil.IsElementStale(element);
992995
}

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,4 +97,17 @@ public static long GetScrollY(this IWebDriver browser)
9797

9898
public static long SetScrollY(this IWebDriver browser, long value)
9999
=> Convert.ToInt64(((IJavaScriptExecutor)browser).ExecuteScript($"window.scrollTo(0, {value})"), CultureInfo.CurrentCulture);
100+
101+
public static bool IsElementStale(IWebElement element)
102+
{
103+
try
104+
{
105+
_ = element.Enabled;
106+
return false;
107+
}
108+
catch (StaleElementReferenceException)
109+
{
110+
return true;
111+
}
112+
}
100113
}

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1287,6 +1287,30 @@ void SetUpPageWithOneInteractiveServerComponent()
12871287
}
12881288
}
12891289

1290+
[Theory]
1291+
[InlineData(false, false)]
1292+
[InlineData(false, true)]
1293+
[InlineData(true, false)]
1294+
[InlineData(true, true)]
1295+
public void CanPerformNavigateToFromInteractiveEventHandler(bool suppressEnhancedNavigation, bool forceLoad)
1296+
{
1297+
EnhancedNavigationTestUtil.SuppressEnhancedNavigation(this, suppressEnhancedNavigation);
1298+
1299+
// Get to the test page
1300+
Navigate($"{ServerPathBase}/interactivity/navigateto");
1301+
Browser.Equal("Interactive NavigateTo", () => Browser.FindElement(By.TagName("h1")).Text);
1302+
var originalNavElem = Browser.FindElement(By.TagName("nav"));
1303+
1304+
// Perform the navigation
1305+
Browser.Click(By.Id(forceLoad ? "perform-navigateto-force" : "perform-navigateto"));
1306+
Browser.True(() => Browser.Url.EndsWith("/nav", StringComparison.Ordinal));
1307+
Browser.Equal("Hello", () => Browser.FindElement(By.Id("nav-home")).Text);
1308+
1309+
// Verify the elements were preserved if and only if they should be
1310+
var shouldPreserveElements = !suppressEnhancedNavigation && !forceLoad;
1311+
Assert.Equal(shouldPreserveElements, !EnhancedNavigationTestUtil.IsElementStale(originalNavElem));
1312+
}
1313+
12901314
private void BlockWebAssemblyResourceLoad()
12911315
{
12921316
// Force a WebAssembly resource cache miss so that we can fall back to using server interactivity
Lines changed: 18 additions & 0 deletions
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+
}

src/Components/test/testassets/Components.TestServer/RazorComponents/Shared/EnhancedNavLayout.razor

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,6 @@
4545
<br />
4646
</nav>
4747
<hr />
48-
@Body
48+
<main>
49+
@Body
50+
</main>

0 commit comments

Comments
 (0)