diff --git a/src/Components/Web.JS/src/Boot.Web.ts b/src/Components/Web.JS/src/Boot.Web.ts index 9abcfee32287..82ab6352e8f5 100644 --- a/src/Components/Web.JS/src/Boot.Web.ts +++ b/src/Components/Web.JS/src/Boot.Web.ts @@ -40,6 +40,7 @@ function boot(options?: Partial) : Promise { started = true; options = options || {}; options.logLevel ??= LogLevel.Error; + Blazor._internal.isBlazorWeb = true; // Defined here to avoid inadvertently imported enhanced navigation // related APIs in WebAssembly or Blazor Server contexts. diff --git a/src/Components/Web.JS/src/GlobalExports.ts b/src/Components/Web.JS/src/GlobalExports.ts index 02cb2bdd6095..6bd2fbe69c75 100644 --- a/src/Components/Web.JS/src/GlobalExports.ts +++ b/src/Components/Web.JS/src/GlobalExports.ts @@ -84,6 +84,7 @@ export interface IBlazor { receiveWebAssemblyDotNetDataStream?: (streamId: number, data: any, bytesRead: number, errorMessage: string) => void; receiveWebViewDotNetDataStream?: (streamId: number, data: any, bytesRead: number, errorMessage: string) => void; attachWebRendererInterop?: typeof attachWebRendererInterop; + isBlazorWeb?: boolean; // JSExport APIs dotNetExports?: { diff --git a/src/Components/Web.JS/src/Services/NavigationManager.ts b/src/Components/Web.JS/src/Services/NavigationManager.ts index 20b0340973ed..29e1d892d354 100644 --- a/src/Components/Web.JS/src/Services/NavigationManager.ts +++ b/src/Components/Web.JS/src/Services/NavigationManager.ts @@ -7,6 +7,7 @@ import { EventDelegator } from '../Rendering/Events/EventDelegator'; import { attachEnhancedNavigationListener, getInteractiveRouterRendererId, handleClickForNavigationInterception, hasInteractiveRouter, hasProgrammaticEnhancedNavigationHandler, isForSamePath, isSamePageWithHash, isWithinBaseUriSpace, performProgrammaticEnhancedNavigation, performScrollToElementOnTheSamePage, scrollToElement, setHasInteractiveRouter, toAbsoluteUri } from './NavigationUtils'; import { WebRendererId } from '../Rendering/WebRendererId'; import { isRendererAttached } from '../Rendering/WebRendererInteropMethods'; +import { IBlazor } from '../GlobalExports'; let hasRegisteredNavigationEventListeners = false; let currentHistoryIndex = 0; @@ -116,18 +117,21 @@ function navigateToFromDotNet(uri: string, options: NavigationOptions): void { function navigateToCore(uri: string, options: NavigationOptions, skipLocationChangingCallback = false): void { const absoluteUri = toAbsoluteUri(uri); + const pageLoadMechanism = currentPageLoadMechanism(); - if (!options.forceLoad && isWithinBaseUriSpace(absoluteUri)) { - if (shouldUseClientSideRouting()) { - performInternalNavigation(absoluteUri, false, options.replaceHistoryEntry, options.historyEntryState, skipLocationChangingCallback); - } else { - performProgrammaticEnhancedNavigation(absoluteUri, options.replaceHistoryEntry); - } - } else { + if (options.forceLoad || !isWithinBaseUriSpace(absoluteUri) || pageLoadMechanism === 'serverside-fullpageload') { // For external navigation, we work in terms of the originally-supplied uri string, // not the computed absoluteUri. This is in case there are some special URI formats // we're unable to translate into absolute URIs. performExternalNavigation(uri, options.replaceHistoryEntry); + } else if (pageLoadMechanism === 'clientside-router') { + performInternalNavigation(absoluteUri, false, options.replaceHistoryEntry, options.historyEntryState, skipLocationChangingCallback); + } else if (pageLoadMechanism === 'serverside-enhanced') { + performProgrammaticEnhancedNavigation(absoluteUri, options.replaceHistoryEntry); + } else { + // Force a compile-time error if some other case needs to be handled in the future + const unreachable: never = pageLoadMechanism; + throw new Error(`Unsupported page load mechanism: ${unreachable}`); } } @@ -266,7 +270,7 @@ async function notifyLocationChanged(interceptedLink: boolean, internalDestinati } async function onPopState(state: PopStateEvent) { - if (popStateCallback && shouldUseClientSideRouting()) { + if (popStateCallback && currentPageLoadMechanism() !== 'serverside-enhanced') { await popStateCallback(state); } @@ -282,10 +286,24 @@ function getInteractiveRouterNavigationCallbacks(): NavigationCallbacks | undefi return navigationCallbacks.get(interactiveRouterRendererId); } -function shouldUseClientSideRouting() { - return hasInteractiveRouter() || !hasProgrammaticEnhancedNavigationHandler(); +function currentPageLoadMechanism(): PageLoadMechanism { + if (hasInteractiveRouter()) { + return 'clientside-router'; + } else if (hasProgrammaticEnhancedNavigationHandler()) { + return 'serverside-enhanced'; + } else { + // For back-compat, in blazor.server.js or blazor.webassembly.js, we always behave as if there's an interactive + // router even if there isn't one attached. This preserves a niche case where people may call Blazor.navigateTo + // without a router and expect to receive a notification on the .NET side but no page load occurs. + // In blazor.web.js, we explicitly recognize the case where you have neither an interactive nor enhanced SSR router + // attached, and then handle Blazor.navigateTo by doing a full page load because that's more useful (issue #51636). + const isBlazorWeb = (window['Blazor'] as IBlazor)._internal.isBlazorWeb; + return isBlazorWeb ? 'serverside-fullpageload' : 'clientside-router'; + } } +type PageLoadMechanism = 'clientside-router' | 'serverside-enhanced' | 'serverside-fullpageload'; + // Keep in sync with Components/src/NavigationOptions.cs export interface NavigationOptions { forceLoad: boolean; diff --git a/src/Components/test/E2ETest/ServerRenderingTests/InteractivityTest.cs b/src/Components/test/E2ETest/ServerRenderingTests/InteractivityTest.cs index c2c4e6c66340..4fe0a5d42647 100644 --- a/src/Components/test/E2ETest/ServerRenderingTests/InteractivityTest.cs +++ b/src/Components/test/E2ETest/ServerRenderingTests/InteractivityTest.cs @@ -1287,6 +1287,30 @@ void SetUpPageWithOneInteractiveServerComponent() } } + [Theory] + [InlineData(false, false)] + [InlineData(false, true)] + [InlineData(true, false)] + [InlineData(true, true)] + public void CanPerformNavigateToFromInteractiveEventHandler(bool suppressEnhancedNavigation, bool forceLoad) + { + EnhancedNavigationTestUtil.SuppressEnhancedNavigation(this, suppressEnhancedNavigation); + + // Get to the test page + Navigate($"{ServerPathBase}/interactivity/navigateto"); + Browser.Equal("Interactive NavigateTo", () => Browser.FindElement(By.TagName("h1")).Text); + var originalNavElem = Browser.FindElement(By.TagName("nav")); + + // Perform the navigation + Browser.Click(By.Id(forceLoad ? "perform-navigateto-force" : "perform-navigateto")); + Browser.True(() => Browser.Url.EndsWith("/nav", StringComparison.Ordinal)); + Browser.Equal("Hello", () => Browser.FindElement(By.Id("nav-home")).Text); + + // Verify the elements were preserved if and only if they should be + var shouldPreserveElements = !suppressEnhancedNavigation && !forceLoad; + Assert.Equal(shouldPreserveElements, !originalNavElem.IsStale()); + } + private void BlockWebAssemblyResourceLoad() { // Force a WebAssembly resource cache miss so that we can fall back to using server interactivity diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Interactivity/InteractiveNavigateTo.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Interactivity/InteractiveNavigateTo.razor new file mode 100644 index 000000000000..82ff639c8595 --- /dev/null +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Interactivity/InteractiveNavigateTo.razor @@ -0,0 +1,18 @@ +@page "/interactivity/navigateto" +@layout Components.TestServer.RazorComponents.Shared.EnhancedNavLayout +@inject NavigationManager Nav +@rendermode RenderMode.InteractiveServer + +

Interactive NavigateTo

+ +

Shows that NavigateTo from an interactive event handler works as expected, with or without enhanced navigation.

+ + + + +@code { + void PerformNavigateTo(bool forceLoad) + { + Nav.NavigateTo("nav", forceLoad); + } +} diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Shared/EnhancedNavLayout.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Shared/EnhancedNavLayout.razor index 7e9166fabb1e..25544afef64e 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponents/Shared/EnhancedNavLayout.razor +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Shared/EnhancedNavLayout.razor @@ -45,4 +45,6 @@

-@Body +
+ @Body +