diff --git a/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Services/UriHelper.ts b/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Services/UriHelper.ts index 53442dae6217531bd50984a1d92167531978ad75..8e77f1a6b5d0e56baa5ef74ec5121109476e9d85 100644 --- a/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Services/UriHelper.ts +++ b/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Services/UriHelper.ts @@ -1,6 +1,6 @@ import { registerFunction } from '../Interop/RegisteredFunction'; import { platform } from '../Environment'; -import { MethodHandle } from '../Platform/Platform'; +import { MethodHandle, System_String } from '../Platform/Platform'; const registeredFunctionPrefix = 'Microsoft.AspNetCore.Blazor.Browser.Services.BrowserUriHelper'; let notifyLocationChangedMethod: MethodHandle; let hasRegisteredEventListeners = false; @@ -24,8 +24,7 @@ registerFunction(`${registeredFunctionPrefix}.enableNavigationInteception`, () = const href = anchorTarget.getAttribute('href'); if (isWithinBaseUriSpace(toAbsoluteUri(href))) { event.preventDefault(); - history.pushState(null, /* ignored title */ '', href); - handleInternalNavigation(); + performInternalNavigation(href); } } }); @@ -33,6 +32,20 @@ registerFunction(`${registeredFunctionPrefix}.enableNavigationInteception`, () = window.addEventListener('popstate', handleInternalNavigation); }); +registerFunction(`${registeredFunctionPrefix}.navigateTo`, (uriDotNetString: System_String) => { + const href = platform.toJavaScriptString(uriDotNetString); + if (isWithinBaseUriSpace(toAbsoluteUri(href))) { + performInternalNavigation(href); + } else { + location.href = href; + } +}); + +function performInternalNavigation(href: string) { + history.pushState(null, /* ignored title */ '', href); + handleInternalNavigation(); +} + function handleInternalNavigation() { if (!notifyLocationChangedMethod) { notifyLocationChangedMethod = platform.findMethod( diff --git a/src/Microsoft.AspNetCore.Blazor.Browser/Services/BrowserUriHelper.cs b/src/Microsoft.AspNetCore.Blazor.Browser/Services/BrowserUriHelper.cs index 28cf8e497fbf19a8dcdb01747b7189942beb543b..171679febb9a1ac4c22f856e220ac7a188fe2657 100644 --- a/src/Microsoft.AspNetCore.Blazor.Browser/Services/BrowserUriHelper.cs +++ b/src/Microsoft.AspNetCore.Blazor.Browser/Services/BrowserUriHelper.cs @@ -104,6 +104,17 @@ namespace Microsoft.AspNetCore.Blazor.Browser.Services throw new ArgumentException($"The URI '{absoluteUri}' is not contained by the base URI '{baseUriPrefix}'."); } + /// <inheritdoc /> + public void NavigateTo(string uri) + { + if (uri == null) + { + throw new ArgumentNullException(nameof(uri)); + } + + RegisteredFunction.InvokeUnmarshalled<object>($"{_functionPrefix}.navigateTo", uri); + } + private static void EnsureBaseUriPopulated() { // The <base href> is fixed for the lifetime of the page, so just cache it diff --git a/src/Microsoft.AspNetCore.Blazor/Services/IUriHelper.cs b/src/Microsoft.AspNetCore.Blazor/Services/IUriHelper.cs index 12e8fd0dade16923eaf1936e31cd12361ac18795..472b7956f56088d0b156d906e7228ab78043d6c1 100644 --- a/src/Microsoft.AspNetCore.Blazor/Services/IUriHelper.cs +++ b/src/Microsoft.AspNetCore.Blazor/Services/IUriHelper.cs @@ -13,7 +13,7 @@ namespace Microsoft.AspNetCore.Blazor.Services /// <summary> /// Gets the current absolute URI. /// </summary> - /// <returns>The browser's current absolute URI.</returns> + /// <returns>The current absolute URI.</returns> string GetAbsoluteUri(); /// <summary> @@ -44,5 +44,12 @@ namespace Microsoft.AspNetCore.Blazor.Services /// <param name="absoluteUri">An absolute URI that is within the space of the base URI prefix.</param> /// <returns>A relative URI path.</returns> string ToBaseRelativePath(string baseUriPrefix, string locationAbsolute); + + /// <summary> + /// Navigates to the specified URI. + /// </summary> + /// <param name="uri">The destination URI. This can be absolute, or relative to the base URI + /// prefix (as returned by <see cref="GetBaseUriPrefix"/>).</param> + void NavigateTo(string uri); } } diff --git a/test/Microsoft.AspNetCore.Blazor.E2ETest/Tests/RoutingTest.cs b/test/Microsoft.AspNetCore.Blazor.E2ETest/Tests/RoutingTest.cs index 8970de915cd0572ccd55b88420cfa8c5c5a5dcfc..9b6cfd157685b483ecfb51b1d151ce031f316f5e 100644 --- a/test/Microsoft.AspNetCore.Blazor.E2ETest/Tests/RoutingTest.cs +++ b/test/Microsoft.AspNetCore.Blazor.E2ETest/Tests/RoutingTest.cs @@ -110,6 +110,16 @@ namespace Microsoft.AspNetCore.Blazor.E2ETest.Tests Assert.Equal("This is the default page.", app.FindElement(By.Id("test-info")).Text); } + [Fact] + public void CanNavigateProgrammatically() + { + SetUrlViaPushState($"{ServerPathBase}/RouterTest/"); + + var app = MountTestComponent<TestRouter>(); + app.FindElement(By.TagName("button")).Click(); + Assert.Equal("This is another page.", app.FindElement(By.Id("test-info")).Text); + } + public void Dispose() { // Clear any existing state diff --git a/test/testapps/BasicTestApp/RouterTest/Links.cshtml b/test/testapps/BasicTestApp/RouterTest/Links.cshtml index e7bbca5d1205900da6c831572e6e0c7f65947d91..9490da172537e9b68ffc813d0f6c5d86f0bb3275 100644 --- a/test/testapps/BasicTestApp/RouterTest/Links.cshtml +++ b/test/testapps/BasicTestApp/RouterTest/Links.cshtml @@ -1,4 +1,6 @@ -<ul> +@inject Microsoft.AspNetCore.Blazor.Services.IUriHelper uriHelper + +<ul> <li><a href="/subdir/RouterTest/">Default</a></li> <li><a href="/subdir/RouterTest/?abc=123">Default with query</a></li> <li><a href="/subdir/RouterTest/#blah">Default with hash</a></li> @@ -7,3 +9,7 @@ <li><a href="/subdir/RouterTest/Other?abc=123">Other with query</a></li> <li><a href="/subdir/RouterTest/Other#blah">Other with hash</a></li> </ul> + +<button onclick=@{ uriHelper.NavigateTo("RouterTest/Other"); }> + Programmatic navigation +</button>