Defer link interception until Router is initialized (#10062)

* Defer link interception until Router is initialized

Fixes https://github.com/aspnet/AspNetCore/issues/9834
This commit is contained in:
Pranav K 2019-05-17 15:33:35 -07:00 committed by GitHub
parent 64152c9180
commit b9546df5d4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 439 additions and 121 deletions

View File

@ -76,7 +76,7 @@ namespace Microsoft.AspNetCore.Blazor.Services
protected override void EnsureInitialized() { } protected override void EnsureInitialized() { }
protected override void NavigateToCore(string uri, bool forceLoad) { } protected override void NavigateToCore(string uri, bool forceLoad) { }
[Microsoft.JSInterop.JSInvokableAttribute("NotifyLocationChanged")] [Microsoft.JSInterop.JSInvokableAttribute("NotifyLocationChanged")]
public static void NotifyLocationChanged(string newAbsoluteUri) { } public static void NotifyLocationChanged(string newAbsoluteUri, bool isInterceptedLink) { }
} }
} }
namespace Microsoft.AspNetCore.Components.Builder namespace Microsoft.AspNetCore.Components.Builder

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Net.Http; using System.Net.Http;
using Microsoft.AspNetCore.Blazor.Services; using Microsoft.AspNetCore.Blazor.Services;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Routing;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.JSInterop; using Microsoft.JSInterop;
@ -90,6 +91,7 @@ namespace Microsoft.AspNetCore.Blazor.Hosting
services.AddSingleton<IJSRuntime>(WebAssemblyJSRuntime.Instance); services.AddSingleton<IJSRuntime>(WebAssemblyJSRuntime.Instance);
services.AddSingleton<IComponentContext, WebAssemblyComponentContext>(); services.AddSingleton<IComponentContext, WebAssemblyComponentContext>();
services.AddSingleton<IUriHelper>(WebAssemblyUriHelper.Instance); services.AddSingleton<IUriHelper>(WebAssemblyUriHelper.Instance);
services.AddSingleton<INavigationInterception>(WebAssemblyNavigationInterception.Instance);
services.AddSingleton<HttpClient>(s => services.AddSingleton<HttpClient>(s =>
{ {
// Creating the URI helper needs to wait until the JS Runtime is initialized, so defer it. // Creating the URI helper needs to wait until the JS Runtime is initialized, so defer it.

View File

@ -0,0 +1,20 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Routing;
using Interop = Microsoft.AspNetCore.Components.Browser.BrowserUriHelperInterop;
namespace Microsoft.AspNetCore.Blazor.Services
{
internal sealed class WebAssemblyNavigationInterception : INavigationInterception
{
public static readonly WebAssemblyNavigationInterception Instance = new WebAssemblyNavigationInterception();
public Task EnableNavigationInterceptionAsync()
{
WebAssemblyJSRuntime.Instance.Invoke<object>(Interop.EnableNavigationInterception);
return Task.CompletedTask;
}
}
}

View File

@ -28,7 +28,7 @@ namespace Microsoft.AspNetCore.Blazor.Services
protected override void EnsureInitialized() protected override void EnsureInitialized()
{ {
WebAssemblyJSRuntime.Instance.Invoke<object>( WebAssemblyJSRuntime.Instance.Invoke<object>(
Interop.EnableNavigationInterception, Interop.ListenForNavigationEvents,
typeof(WebAssemblyUriHelper).Assembly.GetName().Name, typeof(WebAssemblyUriHelper).Assembly.GetName().Name,
nameof(NotifyLocationChanged)); nameof(NotifyLocationChanged));
@ -54,10 +54,10 @@ namespace Microsoft.AspNetCore.Blazor.Services
/// For framework use only. /// For framework use only.
/// </summary> /// </summary>
[JSInvokable(nameof(NotifyLocationChanged))] [JSInvokable(nameof(NotifyLocationChanged))]
public static void NotifyLocationChanged(string newAbsoluteUri) public static void NotifyLocationChanged(string newAbsoluteUri, bool isInterceptedLink)
{ {
Instance.SetAbsoluteUri(newAbsoluteUri); Instance.SetAbsoluteUri(newAbsoluteUri);
Instance.TriggerOnLocationChanged(); Instance.TriggerOnLocationChanged(isInterceptedLink);
} }
/// <summary> /// <summary>

View File

@ -14885,62 +14885,76 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
}; };
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
__webpack_require__(/*! @dotnet/jsinterop */ "../node_modules/@dotnet/jsinterop/dist/Microsoft.JSInterop.js"); __webpack_require__(/*! @dotnet/jsinterop */ "../node_modules/@dotnet/jsinterop/dist/Microsoft.JSInterop.js");
var hasRegisteredEventListeners = false; var hasRegisteredNavigationInterception = false;
var hasRegisteredNavigationEventListeners = false;
// Will be initialized once someone registers // Will be initialized once someone registers
var notifyLocationChangedCallback = null; var notifyLocationChangedCallback = null;
// These are the functions we're making available for invocation from .NET // These are the functions we're making available for invocation from .NET
exports.internalFunctions = { exports.internalFunctions = {
listenForNavigationEvents: listenForNavigationEvents,
enableNavigationInterception: enableNavigationInterception, enableNavigationInterception: enableNavigationInterception,
navigateTo: navigateTo, navigateTo: navigateTo,
getBaseURI: function () { return document.baseURI; }, getBaseURI: function () { return document.baseURI; },
getLocationHref: function () { return location.href; }, getLocationHref: function () { return location.href; },
}; };
function enableNavigationInterception(assemblyName, functionName) { function listenForNavigationEvents(assemblyName, functionName) {
if (hasRegisteredEventListeners || assemblyName === undefined || functionName === undefined) { if (hasRegisteredNavigationEventListeners) {
return; return;
} }
notifyLocationChangedCallback = { assemblyName: assemblyName, functionName: functionName }; notifyLocationChangedCallback = { assemblyName: assemblyName, functionName: functionName };
hasRegisteredEventListeners = true; hasRegisteredNavigationEventListeners = true;
window.addEventListener('popstate', function () { return notifyLocationChanged(false); });
}
function enableNavigationInterception() {
if (hasRegisteredNavigationInterception) {
return;
}
hasRegisteredNavigationInterception = true;
document.addEventListener('click', function (event) { document.addEventListener('click', function (event) {
if (event.button !== 0 || eventHasSpecialKey(event)) {
// Don't stop ctrl/meta-click (etc) from opening links in new tabs/windows
return;
}
// Intercept clicks on all <a> elements where the href is within the <base href> URI space // Intercept clicks on all <a> elements where the href is within the <base href> URI space
// We must explicitly check if it has an 'href' attribute, because if it doesn't, the result might be null or an empty string depending on the browser // We must explicitly check if it has an 'href' attribute, because if it doesn't, the result might be null or an empty string depending on the browser
var anchorTarget = findClosestAncestor(event.target, 'A'); var anchorTarget = findClosestAncestor(event.target, 'A');
var hrefAttributeName = 'href'; var hrefAttributeName = 'href';
if (anchorTarget && anchorTarget.hasAttribute(hrefAttributeName) && event.button === 0) { if (anchorTarget && anchorTarget.hasAttribute(hrefAttributeName)) {
var href = anchorTarget.getAttribute(hrefAttributeName);
var absoluteHref = toAbsoluteUri(href);
var targetAttributeValue = anchorTarget.getAttribute('target'); var targetAttributeValue = anchorTarget.getAttribute('target');
var opensInSameFrame = !targetAttributeValue || targetAttributeValue === '_self'; var opensInSameFrame = !targetAttributeValue || targetAttributeValue === '_self';
// Don't stop ctrl/meta-click (etc) from opening links in new tabs/windows if (!opensInSameFrame) {
if (isWithinBaseUriSpace(absoluteHref) && !eventHasSpecialKey(event) && opensInSameFrame) { return;
}
var href = anchorTarget.getAttribute(hrefAttributeName);
var absoluteHref = toAbsoluteUri(href);
if (isWithinBaseUriSpace(absoluteHref)) {
event.preventDefault(); event.preventDefault();
performInternalNavigation(absoluteHref); performInternalNavigation(absoluteHref, true);
} }
} }
}); });
window.addEventListener('popstate', handleInternalNavigation);
} }
function navigateTo(uri, forceLoad) { function navigateTo(uri, forceLoad) {
var absoluteUri = toAbsoluteUri(uri); var absoluteUri = toAbsoluteUri(uri);
if (!forceLoad && isWithinBaseUriSpace(absoluteUri)) { if (!forceLoad && isWithinBaseUriSpace(absoluteUri)) {
performInternalNavigation(absoluteUri); performInternalNavigation(absoluteUri, false);
} }
else { else {
location.href = uri; location.href = uri;
} }
} }
exports.navigateTo = navigateTo; exports.navigateTo = navigateTo;
function performInternalNavigation(absoluteInternalHref) { function performInternalNavigation(absoluteInternalHref, interceptedLink) {
history.pushState(null, /* ignored title */ '', absoluteInternalHref); history.pushState(null, /* ignored title */ '', absoluteInternalHref);
handleInternalNavigation(); notifyLocationChanged(interceptedLink);
} }
function handleInternalNavigation() { function notifyLocationChanged(interceptedLink) {
return __awaiter(this, void 0, void 0, function () { return __awaiter(this, void 0, void 0, function () {
return __generator(this, function (_a) { return __generator(this, function (_a) {
switch (_a.label) { switch (_a.label) {
case 0: case 0:
if (!notifyLocationChangedCallback) return [3 /*break*/, 2]; if (!notifyLocationChangedCallback) return [3 /*break*/, 2];
return [4 /*yield*/, DotNet.invokeMethodAsync(notifyLocationChangedCallback.assemblyName, notifyLocationChangedCallback.functionName, location.href)]; return [4 /*yield*/, DotNet.invokeMethodAsync(notifyLocationChangedCallback.assemblyName, notifyLocationChangedCallback.functionName, location.href, interceptedLink)];
case 1: case 1:
_a.sent(); _a.sent();
_a.label = 2; _a.label = 2;

View File

@ -2450,62 +2450,76 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
}; };
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
__webpack_require__(/*! @dotnet/jsinterop */ "../node_modules/@dotnet/jsinterop/dist/Microsoft.JSInterop.js"); __webpack_require__(/*! @dotnet/jsinterop */ "../node_modules/@dotnet/jsinterop/dist/Microsoft.JSInterop.js");
var hasRegisteredEventListeners = false; var hasRegisteredNavigationInterception = false;
var hasRegisteredNavigationEventListeners = false;
// Will be initialized once someone registers // Will be initialized once someone registers
var notifyLocationChangedCallback = null; var notifyLocationChangedCallback = null;
// These are the functions we're making available for invocation from .NET // These are the functions we're making available for invocation from .NET
exports.internalFunctions = { exports.internalFunctions = {
listenForNavigationEvents: listenForNavigationEvents,
enableNavigationInterception: enableNavigationInterception, enableNavigationInterception: enableNavigationInterception,
navigateTo: navigateTo, navigateTo: navigateTo,
getBaseURI: function () { return document.baseURI; }, getBaseURI: function () { return document.baseURI; },
getLocationHref: function () { return location.href; }, getLocationHref: function () { return location.href; },
}; };
function enableNavigationInterception(assemblyName, functionName) { function listenForNavigationEvents(assemblyName, functionName) {
if (hasRegisteredEventListeners || assemblyName === undefined || functionName === undefined) { if (hasRegisteredNavigationEventListeners) {
return; return;
} }
notifyLocationChangedCallback = { assemblyName: assemblyName, functionName: functionName }; notifyLocationChangedCallback = { assemblyName: assemblyName, functionName: functionName };
hasRegisteredEventListeners = true; hasRegisteredNavigationEventListeners = true;
window.addEventListener('popstate', function () { return notifyLocationChanged(false); });
}
function enableNavigationInterception() {
if (hasRegisteredNavigationInterception) {
return;
}
hasRegisteredNavigationInterception = true;
document.addEventListener('click', function (event) { document.addEventListener('click', function (event) {
if (event.button !== 0 || eventHasSpecialKey(event)) {
// Don't stop ctrl/meta-click (etc) from opening links in new tabs/windows
return;
}
// Intercept clicks on all <a> elements where the href is within the <base href> URI space // Intercept clicks on all <a> elements where the href is within the <base href> URI space
// We must explicitly check if it has an 'href' attribute, because if it doesn't, the result might be null or an empty string depending on the browser // We must explicitly check if it has an 'href' attribute, because if it doesn't, the result might be null or an empty string depending on the browser
var anchorTarget = findClosestAncestor(event.target, 'A'); var anchorTarget = findClosestAncestor(event.target, 'A');
var hrefAttributeName = 'href'; var hrefAttributeName = 'href';
if (anchorTarget && anchorTarget.hasAttribute(hrefAttributeName) && event.button === 0) { if (anchorTarget && anchorTarget.hasAttribute(hrefAttributeName)) {
var href = anchorTarget.getAttribute(hrefAttributeName);
var absoluteHref = toAbsoluteUri(href);
var targetAttributeValue = anchorTarget.getAttribute('target'); var targetAttributeValue = anchorTarget.getAttribute('target');
var opensInSameFrame = !targetAttributeValue || targetAttributeValue === '_self'; var opensInSameFrame = !targetAttributeValue || targetAttributeValue === '_self';
// Don't stop ctrl/meta-click (etc) from opening links in new tabs/windows if (!opensInSameFrame) {
if (isWithinBaseUriSpace(absoluteHref) && !eventHasSpecialKey(event) && opensInSameFrame) { return;
}
var href = anchorTarget.getAttribute(hrefAttributeName);
var absoluteHref = toAbsoluteUri(href);
if (isWithinBaseUriSpace(absoluteHref)) {
event.preventDefault(); event.preventDefault();
performInternalNavigation(absoluteHref); performInternalNavigation(absoluteHref, true);
} }
} }
}); });
window.addEventListener('popstate', handleInternalNavigation);
} }
function navigateTo(uri, forceLoad) { function navigateTo(uri, forceLoad) {
var absoluteUri = toAbsoluteUri(uri); var absoluteUri = toAbsoluteUri(uri);
if (!forceLoad && isWithinBaseUriSpace(absoluteUri)) { if (!forceLoad && isWithinBaseUriSpace(absoluteUri)) {
performInternalNavigation(absoluteUri); performInternalNavigation(absoluteUri, false);
} }
else { else {
location.href = uri; location.href = uri;
} }
} }
exports.navigateTo = navigateTo; exports.navigateTo = navigateTo;
function performInternalNavigation(absoluteInternalHref) { function performInternalNavigation(absoluteInternalHref, interceptedLink) {
history.pushState(null, /* ignored title */ '', absoluteInternalHref); history.pushState(null, /* ignored title */ '', absoluteInternalHref);
handleInternalNavigation(); notifyLocationChanged(interceptedLink);
} }
function handleInternalNavigation() { function notifyLocationChanged(interceptedLink) {
return __awaiter(this, void 0, void 0, function () { return __awaiter(this, void 0, void 0, function () {
return __generator(this, function (_a) { return __generator(this, function (_a) {
switch (_a.label) { switch (_a.label) {
case 0: case 0:
if (!notifyLocationChangedCallback) return [3 /*break*/, 2]; if (!notifyLocationChangedCallback) return [3 /*break*/, 2];
return [4 /*yield*/, DotNet.invokeMethodAsync(notifyLocationChangedCallback.assemblyName, notifyLocationChangedCallback.functionName, location.href)]; return [4 /*yield*/, DotNet.invokeMethodAsync(notifyLocationChangedCallback.assemblyName, notifyLocationChangedCallback.functionName, location.href, interceptedLink)];
case 1: case 1:
_a.sent(); _a.sent();
_a.label = 2; _a.label = 2;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -5,6 +5,7 @@
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"build": "yarn run build:debug && yarn run build:production",
"build:debug": "cd src && webpack --mode development --config ./webpack.config.js", "build:debug": "cd src && webpack --mode development --config ./webpack.config.js",
"build:production": "cd src && webpack --mode production --config ./webpack.config.js", "build:production": "cd src && webpack --mode production --config ./webpack.config.js",
"test": "jest" "test": "jest"

View File

@ -1,69 +1,88 @@
import '@dotnet/jsinterop'; import '@dotnet/jsinterop';
let hasRegisteredEventListeners = false; let hasRegisteredNavigationInterception = false;
let hasRegisteredNavigationEventListeners = false;
// Will be initialized once someone registers // Will be initialized once someone registers
let notifyLocationChangedCallback: { assemblyName: string; functionName: string } | null = null; let notifyLocationChangedCallback: { assemblyName: string; functionName: string } | null = null;
// These are the functions we're making available for invocation from .NET // These are the functions we're making available for invocation from .NET
export const internalFunctions = { export const internalFunctions = {
listenForNavigationEvents,
enableNavigationInterception, enableNavigationInterception,
navigateTo, navigateTo,
getBaseURI: () => document.baseURI, getBaseURI: () => document.baseURI,
getLocationHref: () => location.href, getLocationHref: () => location.href,
}; };
function enableNavigationInterception(assemblyName: string, functionName: string) { function listenForNavigationEvents(assemblyName: string, functionName: string) {
if (hasRegisteredEventListeners || assemblyName === undefined || functionName === undefined) { if (hasRegisteredNavigationEventListeners) {
return; return;
} }
notifyLocationChangedCallback = { assemblyName, functionName }; notifyLocationChangedCallback = { assemblyName, functionName };
hasRegisteredEventListeners = true;
hasRegisteredNavigationEventListeners = true;
window.addEventListener('popstate', () => notifyLocationChanged(false));
}
function enableNavigationInterception() {
if (hasRegisteredNavigationInterception) {
return;
}
hasRegisteredNavigationInterception = true;
document.addEventListener('click', event => { document.addEventListener('click', event => {
if (event.button !== 0 || eventHasSpecialKey(event)) {
// Don't stop ctrl/meta-click (etc) from opening links in new tabs/windows
return;
}
// Intercept clicks on all <a> elements where the href is within the <base href> URI space // Intercept clicks on all <a> elements where the href is within the <base href> URI space
// We must explicitly check if it has an 'href' attribute, because if it doesn't, the result might be null or an empty string depending on the browser // We must explicitly check if it has an 'href' attribute, because if it doesn't, the result might be null or an empty string depending on the browser
const anchorTarget = findClosestAncestor(event.target as Element | null, 'A') as HTMLAnchorElement; const anchorTarget = findClosestAncestor(event.target as Element | null, 'A') as HTMLAnchorElement;
const hrefAttributeName = 'href'; const hrefAttributeName = 'href';
if (anchorTarget && anchorTarget.hasAttribute(hrefAttributeName) && event.button === 0) { if (anchorTarget && anchorTarget.hasAttribute(hrefAttributeName)) {
const href = anchorTarget.getAttribute(hrefAttributeName)!;
const absoluteHref = toAbsoluteUri(href);
const targetAttributeValue = anchorTarget.getAttribute('target'); const targetAttributeValue = anchorTarget.getAttribute('target');
const opensInSameFrame = !targetAttributeValue || targetAttributeValue === '_self'; const opensInSameFrame = !targetAttributeValue || targetAttributeValue === '_self';
if (!opensInSameFrame) {
return;
}
// Don't stop ctrl/meta-click (etc) from opening links in new tabs/windows const href = anchorTarget.getAttribute(hrefAttributeName)!;
if (isWithinBaseUriSpace(absoluteHref) && !eventHasSpecialKey(event) && opensInSameFrame) { const absoluteHref = toAbsoluteUri(href);
if (isWithinBaseUriSpace(absoluteHref)) {
event.preventDefault(); event.preventDefault();
performInternalNavigation(absoluteHref); performInternalNavigation(absoluteHref, true);
} }
} }
}); });
window.addEventListener('popstate', handleInternalNavigation);
} }
export function navigateTo(uri: string, forceLoad: boolean) { export function navigateTo(uri: string, forceLoad: boolean) {
const absoluteUri = toAbsoluteUri(uri); const absoluteUri = toAbsoluteUri(uri);
if (!forceLoad && isWithinBaseUriSpace(absoluteUri)) { if (!forceLoad && isWithinBaseUriSpace(absoluteUri)) {
performInternalNavigation(absoluteUri); performInternalNavigation(absoluteUri, false);
} else { } else {
location.href = uri; location.href = uri;
} }
} }
function performInternalNavigation(absoluteInternalHref: string) { function performInternalNavigation(absoluteInternalHref: string, interceptedLink: boolean) {
history.pushState(null, /* ignored title */ '', absoluteInternalHref); history.pushState(null, /* ignored title */ '', absoluteInternalHref);
handleInternalNavigation(); notifyLocationChanged(interceptedLink);
} }
async function handleInternalNavigation() { async function notifyLocationChanged(interceptedLink: boolean) {
if (notifyLocationChangedCallback) { if (notifyLocationChangedCallback) {
await DotNet.invokeMethodAsync( await DotNet.invokeMethodAsync(
notifyLocationChangedCallback.assemblyName, notifyLocationChangedCallback.assemblyName,
notifyLocationChangedCallback.functionName, notifyLocationChangedCallback.functionName,
location.href location.href,
interceptedLink
); );
} }
} }

View File

@ -8,6 +8,8 @@ namespace Microsoft.AspNetCore.Components.Browser
{ {
private static readonly string Prefix = "Blazor._internal.uriHelper."; private static readonly string Prefix = "Blazor._internal.uriHelper.";
public static readonly string ListenForNavigationEvents = Prefix + "listenForNavigationEvents";
public static readonly string EnableNavigationInterception = Prefix + "enableNavigationInterception"; public static readonly string EnableNavigationInterception = Prefix + "enableNavigationInterception";
public static readonly string GetLocationHref = Prefix + "getLocationHref"; public static readonly string GetLocationHref = Prefix + "getLocationHref";

View File

@ -367,7 +367,7 @@ namespace Microsoft.AspNetCore.Components
} }
public partial interface IUriHelper public partial interface IUriHelper
{ {
event System.EventHandler<string> OnLocationChanged; event System.EventHandler<Microsoft.AspNetCore.Components.Routing.LocationChangedEventArgs> OnLocationChanged;
string GetAbsoluteUri(); string GetAbsoluteUri();
string GetBaseUri(); string GetBaseUri();
void NavigateTo(string uri); void NavigateTo(string uri);
@ -599,7 +599,7 @@ namespace Microsoft.AspNetCore.Components
public abstract partial class UriHelperBase : Microsoft.AspNetCore.Components.IUriHelper public abstract partial class UriHelperBase : Microsoft.AspNetCore.Components.IUriHelper
{ {
protected UriHelperBase() { } protected UriHelperBase() { }
public event System.EventHandler<string> OnLocationChanged { add { } remove { } } public event System.EventHandler<Microsoft.AspNetCore.Components.Routing.LocationChangedEventArgs> OnLocationChanged { add { } remove { } }
protected virtual void EnsureInitialized() { } protected virtual void EnsureInitialized() { }
public string GetAbsoluteUri() { throw null; } public string GetAbsoluteUri() { throw null; }
public virtual string GetBaseUri() { throw null; } public virtual string GetBaseUri() { throw null; }
@ -611,7 +611,7 @@ namespace Microsoft.AspNetCore.Components
protected void SetAbsoluteUri(string uri) { } protected void SetAbsoluteUri(string uri) { }
public System.Uri ToAbsoluteUri(string href) { throw null; } public System.Uri ToAbsoluteUri(string href) { throw null; }
public string ToBaseRelativePath(string baseUri, string locationAbsolute) { throw null; } public string ToBaseRelativePath(string baseUri, string locationAbsolute) { throw null; }
protected void TriggerOnLocationChanged() { } protected void TriggerOnLocationChanged(bool isinterceptedLink) { }
} }
} }
namespace Microsoft.AspNetCore.Components.Forms namespace Microsoft.AspNetCore.Components.Forms
@ -849,6 +849,19 @@ namespace Microsoft.AspNetCore.Components.RenderTree
} }
namespace Microsoft.AspNetCore.Components.Routing namespace Microsoft.AspNetCore.Components.Routing
{ {
public partial interface INavigationInterception
{
System.Threading.Tasks.Task EnableNavigationInterceptionAsync();
}
[System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)]
public readonly partial struct LocationChangedEventArgs
{
private readonly object _dummy;
private readonly int _dummyPrimitive;
public LocationChangedEventArgs(string location, bool isNavigationIntercepted) { throw null; }
public bool IsNavigationIntercepted { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
public string Location { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
}
public enum NavLinkMatch public enum NavLinkMatch
{ {
Prefix = 0, Prefix = 0,

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using Microsoft.AspNetCore.Components.Routing;
namespace Microsoft.AspNetCore.Components namespace Microsoft.AspNetCore.Components
{ {
@ -19,7 +20,7 @@ namespace Microsoft.AspNetCore.Components
/// <summary> /// <summary>
/// An event that fires when the navigation location has changed. /// An event that fires when the navigation location has changed.
/// </summary> /// </summary>
event EventHandler<string> OnLocationChanged; event EventHandler<LocationChangedEventArgs> OnLocationChanged;
/// <summary> /// <summary>
/// Converts a relative URI into an absolute one (by resolving it /// Converts a relative URI into an absolute one (by resolving it

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.Razor"> <Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup> <PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework> <TargetFramework>netstandard2.0</TargetFramework>

View File

@ -0,0 +1,19 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Threading.Tasks;
namespace Microsoft.AspNetCore.Components.Routing
{
/// <summary>
/// Contract to setup navigation interception on the client.
/// </summary>
public interface INavigationInterception
{
/// <summary>
/// Enables navigation interception on the client.
/// </summary>
/// <returns>A <see cref="Task" /> that represents the asynchronous operation.</returns>
Task EnableNavigationInterceptionAsync();
}
}

View File

@ -0,0 +1,34 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
namespace Microsoft.AspNetCore.Components.Routing
{
/// <summary>
/// <see cref="EventArgs" /> for <see cref="IUriHelper.OnLocationChanged" />.
/// </summary>
public readonly struct LocationChangedEventArgs
{
/// <summary>
/// Initializes a new instance of <see cref="LocationChangedEventArgs" />.
/// </summary>
/// <param name="location">The location.</param>
/// <param name="isNavigationIntercepted">A value that determines if navigation for the link was intercepted.</param>
public LocationChangedEventArgs(string location, bool isNavigationIntercepted)
{
Location = location;
IsNavigationIntercepted = isNavigationIntercepted;
}
/// <summary>
/// Gets the changed location.
/// </summary>
public string Location { get; }
/// <summary>
/// Gets a value that determines if navigation for the link was intercepted.
/// </summary>
public bool IsNavigationIntercepted { get; }
}
}

View File

@ -1,12 +1,11 @@
// Copyright (c) .NET Foundation. All rights reserved. // Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.RenderTree;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.RenderTree;
namespace Microsoft.AspNetCore.Components.Routing namespace Microsoft.AspNetCore.Components.Routing
{ {
@ -83,11 +82,11 @@ namespace Microsoft.AspNetCore.Components.Routing
UriHelper.OnLocationChanged -= OnLocationChanged; UriHelper.OnLocationChanged -= OnLocationChanged;
} }
private void OnLocationChanged(object sender, string newUriAbsolute) private void OnLocationChanged(object sender, LocationChangedEventArgs args)
{ {
// We could just re-render always, but for this component we know the // We could just re-render always, but for this component we know the
// only relevant state change is to the _isActive property. // only relevant state change is to the _isActive property.
var shouldBeActiveNow = ShouldMatch(newUriAbsolute); var shouldBeActiveNow = ShouldMatch(args.Location);
if (shouldBeActiveNow != _isActive) if (shouldBeActiveNow != _isActive)
{ {
_isActive = shouldBeActiveNow; _isActive = shouldBeActiveNow;

View File

@ -14,16 +14,21 @@ namespace Microsoft.AspNetCore.Components.Routing
/// A component that displays whichever other component corresponds to the /// A component that displays whichever other component corresponds to the
/// current navigation location. /// current navigation location.
/// </summary> /// </summary>
public class Router : IComponent, IDisposable public class Router : IComponent, IHandleAfterRender, IDisposable
{ {
static readonly char[] _queryOrHashStartChar = new[] { '?', '#' }; static readonly char[] _queryOrHashStartChar = new[] { '?', '#' };
RenderHandle _renderHandle; RenderHandle _renderHandle;
string _baseUri; string _baseUri;
string _locationAbsolute; string _locationAbsolute;
bool _navigationInterceptionEnabled;
[Inject] private IUriHelper UriHelper { get; set; } [Inject] private IUriHelper UriHelper { get; set; }
[Inject] private INavigationInterception NavigationInterception { get; set; }
[Inject] private IComponentContext ComponentContext { get; set; }
/// <summary> /// <summary>
/// Gets or sets the assembly that should be searched, along with its referenced /// Gets or sets the assembly that should be searched, along with its referenced
/// assemblies, for components matching the URI. /// assemblies, for components matching the URI.
@ -33,7 +38,7 @@ namespace Microsoft.AspNetCore.Components.Routing
/// <summary> /// <summary>
/// Gets or sets the type of the component that should be used as a fallback when no match is found for the requested route. /// Gets or sets the type of the component that should be used as a fallback when no match is found for the requested route.
/// </summary> /// </summary>
[Parameter] public Type FallbackComponent { get; private set; } [Parameter] public RenderFragment NotFoundContent { get; private set; }
private RouteTable Routes { get; set; } private RouteTable Routes { get; set; }
@ -52,7 +57,7 @@ namespace Microsoft.AspNetCore.Components.Routing
parameters.SetParameterProperties(this); parameters.SetParameterProperties(this);
var types = ComponentResolver.ResolveComponents(AppAssembly); var types = ComponentResolver.ResolveComponents(AppAssembly);
Routes = RouteTable.Create(types); Routes = RouteTable.Create(types);
Refresh(); Refresh(isNavigationIntercepted: false);
return Task.CompletedTask; return Task.CompletedTask;
} }
@ -79,41 +84,57 @@ namespace Microsoft.AspNetCore.Components.Routing
builder.CloseComponent(); builder.CloseComponent();
} }
private void Refresh() private void Refresh(bool isNavigationIntercepted)
{ {
var locationPath = UriHelper.ToBaseRelativePath(_baseUri, _locationAbsolute); var locationPath = UriHelper.ToBaseRelativePath(_baseUri, _locationAbsolute);
locationPath = StringUntilAny(locationPath, _queryOrHashStartChar); locationPath = StringUntilAny(locationPath, _queryOrHashStartChar);
var context = new RouteContext(locationPath); var context = new RouteContext(locationPath);
Routes.Route(context); Routes.Route(context);
if (context.Handler == null) if (context.Handler != null)
{ {
if (FallbackComponent != null) if (!typeof(IComponent).IsAssignableFrom(context.Handler))
{ {
context.Handler = FallbackComponent; throw new InvalidOperationException($"The type {context.Handler.FullName} " +
$"does not implement {typeof(IComponent).FullName}.");
}
_renderHandle.Render(builder => Render(builder, context.Handler, context.Parameters));
}
else
{
if (!isNavigationIntercepted && NotFoundContent != null)
{
// We did not find a Component that matches the route.
// Only show the NotFoundContent if the application developer programatically got us here i.e we did not
// intercept the navigation. In all other cases, force a browser navigation since this could be non-Blazor content.
_renderHandle.Render(NotFoundContent);
} }
else else
{ {
throw new InvalidOperationException($"'{nameof(Router)}' cannot find any component with a route for '/{locationPath}', and no fallback is defined."); UriHelper.NavigateTo(_locationAbsolute, forceLoad: true);
} }
} }
if (!typeof(IComponent).IsAssignableFrom(context.Handler))
{
throw new InvalidOperationException($"The type {context.Handler.FullName} " +
$"does not implement {typeof(IComponent).FullName}.");
}
_renderHandle.Render(builder => Render(builder, context.Handler, context.Parameters));
} }
private void OnLocationChanged(object sender, string newAbsoluteUri) private void OnLocationChanged(object sender, LocationChangedEventArgs args)
{ {
_locationAbsolute = newAbsoluteUri; _locationAbsolute = args.Location;
if (_renderHandle.IsInitialized) if (_renderHandle.IsInitialized && Routes != null)
{ {
Refresh(); Refresh(args.IsNavigationIntercepted);
} }
} }
Task IHandleAfterRender.OnAfterRenderAsync()
{
if (!_navigationInterceptionEnabled && ComponentContext.IsConnected)
{
_navigationInterceptionEnabled = true;
return NavigationInterception.EnableNavigationInterceptionAsync();
}
return Task.CompletedTask;
}
} }
} }

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using Microsoft.AspNetCore.Components.Routing;
namespace Microsoft.AspNetCore.Components namespace Microsoft.AspNetCore.Components
{ {
@ -10,12 +11,12 @@ namespace Microsoft.AspNetCore.Components
/// </summary> /// </summary>
public abstract class UriHelperBase : IUriHelper public abstract class UriHelperBase : IUriHelper
{ {
private EventHandler<string> _onLocationChanged; private EventHandler<LocationChangedEventArgs> _onLocationChanged;
/// <summary> /// <summary>
/// An event that fires when the navigation location has changed. /// An event that fires when the navigation location has changed.
/// </summary> /// </summary>
public event EventHandler<string> OnLocationChanged public event EventHandler<LocationChangedEventArgs> OnLocationChanged
{ {
add add
{ {
@ -205,9 +206,9 @@ namespace Microsoft.AspNetCore.Components
/// <summary> /// <summary>
/// Triggers the <see cref="OnLocationChanged"/> event with the current URI value. /// Triggers the <see cref="OnLocationChanged"/> event with the current URI value.
/// </summary> /// </summary>
protected void TriggerOnLocationChanged() protected void TriggerOnLocationChanged(bool isinterceptedLink)
{ {
_onLocationChanged?.Invoke(this, _uri); _onLocationChanged?.Invoke(this, new LocationChangedEventArgs(_uri, isinterceptedLink));
} }
private void AssertInitialized() private void AssertInitialized()

View File

@ -29,7 +29,7 @@
"test\\testassets\\ComponentsApp.App\\ComponentsApp.App.csproj", "test\\testassets\\ComponentsApp.App\\ComponentsApp.App.csproj",
"test\\testassets\\ComponentsApp.Server\\ComponentsApp.Server.csproj", "test\\testassets\\ComponentsApp.Server\\ComponentsApp.Server.csproj",
"test\\testassets\\TestContentPackage\\TestContentPackage.csproj", "test\\testassets\\TestContentPackage\\TestContentPackage.csproj",
"test\\testassets\\TestServer\\TestServer.csproj" "test\\testassets\\TestServer\\Components.TestServer.csproj"
] ]
} }
} }

View File

@ -83,7 +83,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
public override void InitializeState(string uriAbsolute, string baseUriAbsolute) { } public override void InitializeState(string uriAbsolute, string baseUriAbsolute) { }
protected override void NavigateToCore(string uri, bool forceLoad) { } protected override void NavigateToCore(string uri, bool forceLoad) { }
[Microsoft.JSInterop.JSInvokableAttribute("NotifyLocationChanged")] [Microsoft.JSInterop.JSInvokableAttribute("NotifyLocationChanged")]
public static void NotifyLocationChanged(string uriAbsolute) { } public static void NotifyLocationChanged(string uriAbsolute, bool isInterceptedLink) { }
} }
} }
namespace Microsoft.Extensions.DependencyInjection namespace Microsoft.Extensions.DependencyInjection

View File

@ -8,6 +8,7 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Browser; using Microsoft.AspNetCore.Components.Browser;
using Microsoft.AspNetCore.Components.Browser.Rendering; using Microsoft.AspNetCore.Components.Browser.Rendering;
using Microsoft.AspNetCore.Components.Rendering; using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.Routing;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.JSInterop; using Microsoft.JSInterop;
@ -120,6 +121,12 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
{ {
uriHelper.AttachJsRuntime(JSRuntime); uriHelper.AttachJsRuntime(JSRuntime);
} }
var navigationInterception = (RemoteNavigationInterception)Services.GetRequiredService<INavigationInterception>();
if (!navigationInterception.HasAttachedJSRuntime)
{
navigationInterception.AttachJSRuntime(JSRuntime);
}
} }
} }

View File

@ -8,6 +8,7 @@ using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Components.Browser; using Microsoft.AspNetCore.Components.Browser;
using Microsoft.AspNetCore.Components.Browser.Rendering; using Microsoft.AspNetCore.Components.Browser.Rendering;
using Microsoft.AspNetCore.Components.Rendering; using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.Routing;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -49,12 +50,15 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
(authenticationStateProvider as FixedAuthenticationStateProvider)?.Initialize(httpContext.User); (authenticationStateProvider as FixedAuthenticationStateProvider)?.Initialize(httpContext.User);
var uriHelper = (RemoteUriHelper)scope.ServiceProvider.GetRequiredService<IUriHelper>(); var uriHelper = (RemoteUriHelper)scope.ServiceProvider.GetRequiredService<IUriHelper>();
var navigationInterception = (RemoteNavigationInterception)scope.ServiceProvider.GetRequiredService<INavigationInterception>();
if (client.Connected) if (client.Connected)
{ {
uriHelper.AttachJsRuntime(jsRuntime); uriHelper.AttachJsRuntime(jsRuntime);
uriHelper.InitializeState( uriHelper.InitializeState(
uriAbsolute, uriAbsolute,
baseUriAbsolute); baseUriAbsolute);
navigationInterception.AttachJSRuntime(jsRuntime);
} }
else else
{ {

View File

@ -0,0 +1,43 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Routing;
using Microsoft.JSInterop;
using Interop = Microsoft.AspNetCore.Components.Browser.BrowserUriHelperInterop;
namespace Microsoft.AspNetCore.Components.Server.Circuits
{
internal sealed class RemoteNavigationInterception : INavigationInterception
{
private IJSRuntime _jsRuntime;
public void AttachJSRuntime(IJSRuntime jsRuntime)
{
if (HasAttachedJSRuntime)
{
throw new InvalidOperationException("JSRuntime has already been initialized.");
}
_jsRuntime = jsRuntime;
}
public bool HasAttachedJSRuntime => _jsRuntime != null;
public async Task EnableNavigationInterceptionAsync()
{
if (!HasAttachedJSRuntime)
{
// We should generally never get here in the ordinary case. Router will only call this API once pre-rendering is complete.
// This would guard any unusual usage of this API.
throw new InvalidOperationException("Navigation commands can not be issued at this time. This is because the component is being " +
"prerendered and the page has not yet loaded in the browser or because the circuit is currently disconnected. " +
"Components must wrap any navigation calls in conditional logic to ensure those navigation calls are not " +
"attempted during prerendering or while the client is disconnected.");
}
await _jsRuntime.InvokeAsync<object>(Interop.EnableNavigationInterception);
}
}
}

View File

@ -14,8 +14,8 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
/// </summary> /// </summary>
public class RemoteUriHelper : UriHelperBase public class RemoteUriHelper : UriHelperBase
{ {
private IJSRuntime _jsRuntime;
private readonly ILogger<RemoteUriHelper> _logger; private readonly ILogger<RemoteUriHelper> _logger;
private IJSRuntime _jsRuntime;
/// <summary> /// <summary>
/// Creates a new <see cref="RemoteUriHelper"/> instance. /// Creates a new <see cref="RemoteUriHelper"/> instance.
@ -39,7 +39,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
public override void InitializeState(string uriAbsolute, string baseUriAbsolute) public override void InitializeState(string uriAbsolute, string baseUriAbsolute)
{ {
base.InitializeState(uriAbsolute, baseUriAbsolute); base.InitializeState(uriAbsolute, baseUriAbsolute);
TriggerOnLocationChanged(); TriggerOnLocationChanged(isinterceptedLink: false);
} }
/// <summary> /// <summary>
@ -52,11 +52,13 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
{ {
throw new InvalidOperationException("JavaScript runtime already initialized."); throw new InvalidOperationException("JavaScript runtime already initialized.");
} }
_jsRuntime = jsRuntime; _jsRuntime = jsRuntime;
_jsRuntime.InvokeAsync<object>( _jsRuntime.InvokeAsync<object>(
Interop.EnableNavigationInterception, Interop.ListenForNavigationEvents,
typeof(RemoteUriHelper).Assembly.GetName().Name, typeof(RemoteUriHelper).Assembly.GetName().Name,
nameof(NotifyLocationChanged)); nameof(NotifyLocationChanged));
_logger.LogDebug($"{nameof(RemoteUriHelper)} initialized."); _logger.LogDebug($"{nameof(RemoteUriHelper)} initialized.");
} }
@ -65,7 +67,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
/// For framework use only. /// For framework use only.
/// </summary> /// </summary>
[JSInvokable(nameof(NotifyLocationChanged))] [JSInvokable(nameof(NotifyLocationChanged))]
public static void NotifyLocationChanged(string uriAbsolute) public static void NotifyLocationChanged(string uriAbsolute, bool isInterceptedLink)
{ {
var circuit = CircuitHost.Current; var circuit = CircuitHost.Current;
if (circuit == null) if (circuit == null)
@ -73,19 +75,18 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
var message = $"{nameof(NotifyLocationChanged)} called without a circuit."; var message = $"{nameof(NotifyLocationChanged)} called without a circuit.";
throw new InvalidOperationException(message); throw new InvalidOperationException(message);
} }
var uriHelper = (RemoteUriHelper)circuit.Services.GetRequiredService<IUriHelper>(); var uriHelper = (RemoteUriHelper)circuit.Services.GetRequiredService<IUriHelper>();
uriHelper.SetAbsoluteUri(uriAbsolute); uriHelper.SetAbsoluteUri(uriAbsolute);
uriHelper._logger.LogDebug($"Location changed to '{uriAbsolute}'."); uriHelper._logger.LogDebug($"Location changed to '{uriAbsolute}'.");
uriHelper.TriggerOnLocationChanged(); uriHelper.TriggerOnLocationChanged(isInterceptedLink);
} }
/// <inheritdoc /> /// <inheritdoc />
protected override void NavigateToCore(string uri, bool forceLoad) protected override void NavigateToCore(string uri, bool forceLoad)
{ {
_logger.LogDebug($"Log debug {uri} force load {forceLoad}."); _logger.LogDebug($"{uri} force load {forceLoad}.");
if (_jsRuntime == null) if (_jsRuntime == null)
{ {

View File

@ -3,6 +3,7 @@
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Routing;
using Microsoft.AspNetCore.Components.Server; using Microsoft.AspNetCore.Components.Server;
using Microsoft.AspNetCore.Components.Server.BlazorPack; using Microsoft.AspNetCore.Components.Server.BlazorPack;
using Microsoft.AspNetCore.Components.Server.Circuits; using Microsoft.AspNetCore.Components.Server.Circuits;
@ -66,6 +67,7 @@ namespace Microsoft.Extensions.DependencyInjection
// These intentionally replace the non-interactive versions included in MVC. // These intentionally replace the non-interactive versions included in MVC.
services.AddScoped<IUriHelper, RemoteUriHelper>(); services.AddScoped<IUriHelper, RemoteUriHelper>();
services.AddScoped<IJSRuntime, RemoteJSRuntime>(); services.AddScoped<IJSRuntime, RemoteJSRuntime>();
services.AddScoped<INavigationInterception, RemoteNavigationInterception>();
services.AddScoped<IComponentContext, RemoteComponentContext>(); services.AddScoped<IComponentContext, RemoteComponentContext>();
services.AddScoped<AuthenticationStateProvider, FixedAuthenticationStateProvider>(); services.AddScoped<AuthenticationStateProvider, FixedAuthenticationStateProvider>();

View File

@ -2,8 +2,6 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure; using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
@ -206,6 +204,5 @@ window.Blazor._internal.forceCloseConnection();");
Browser.True(() => Browser.Manage().Logs.GetLog(LogType.Browser) Browser.True(() => Browser.Manage().Logs.GetLog(LogType.Browser)
.Any(l => l.Level == LogLevel.Info && l.Message.Contains("Connection disconnected."))); .Any(l => l.Level == LogLevel.Info && l.Message.Contains("Connection disconnected.")));
} }
} }
} }

View File

@ -291,6 +291,16 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
AssertHighlightedLinks("Default with hash"); AssertHighlightedLinks("Default with hash");
} }
[Fact]
public void CanFollowLinkToNotAComponent()
{
SetUrlViaPushState("/");
var app = MountTestComponent<TestRouter>();
app.FindElement(By.LinkText("Not a component")).Click();
Browser.Equal("Not a component!", () => Browser.FindElement(By.Id("test-info")).Text);
}
[Fact] [Fact]
public void CanNavigateProgrammatically() public void CanNavigateProgrammatically()
{ {
@ -339,12 +349,30 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
AssertHighlightedLinks("Default (matches all)", "Default with base-relative URL (matches all)"); AssertHighlightedLinks("Default (matches all)", "Default with base-relative URL (matches all)");
} }
private void SetUrlViaPushState(string relativeUri) [Fact]
public void UsingUriHelperWithoutRouterWorks()
{
var app = MountTestComponent<UriHelperComponent>();
var initialUrl = Browser.Url;
Browser.Equal(Browser.Url, () => app.FindElement(By.Id("test-info")).Text);
var uri = SetUrlViaPushState("/mytestpath");
Browser.Equal(uri, () => app.FindElement(By.Id("test-info")).Text);
var jsExecutor = (IJavaScriptExecutor)Browser;
jsExecutor.ExecuteScript("history.back()");
Browser.Equal(initialUrl, () => app.FindElement(By.Id("test-info")).Text);
}
private string SetUrlViaPushState(string relativeUri)
{ {
var pathBaseWithoutHash = ServerPathBase.Split('#')[0]; var pathBaseWithoutHash = ServerPathBase.Split('#')[0];
var jsExecutor = (IJavaScriptExecutor)Browser; var jsExecutor = (IJavaScriptExecutor)Browser;
var absoluteUri = new Uri(_serverFixture.RootUri, $"{pathBaseWithoutHash}{relativeUri}"); var absoluteUri = new Uri(_serverFixture.RootUri, $"{pathBaseWithoutHash}{relativeUri}");
jsExecutor.ExecuteScript($"Blazor.navigateTo('{absoluteUri.ToString().Replace("'", "\\'")}')"); jsExecutor.ExecuteScript($"Blazor.navigateTo('{absoluteUri.ToString().Replace("'", "\\'")}')");
return absoluteUri.AbsoluteUri;
} }
private void AssertHighlightedLinks(params string[] linkTexts) private void AssertHighlightedLinks(params string[] linkTexts)

View File

@ -37,6 +37,7 @@
<option value="BasicTestApp.EventBubblingComponent">Event bubbling</option> <option value="BasicTestApp.EventBubblingComponent">Event bubbling</option>
<option value="BasicTestApp.EventPreventDefaultComponent">Event preventDefault</option> <option value="BasicTestApp.EventPreventDefaultComponent">Event preventDefault</option>
<option value="BasicTestApp.RouterTest.TestRouter">Router</option> <option value="BasicTestApp.RouterTest.TestRouter">Router</option>
<option value="BasicTestApp.RouterTest.TestRouterWithoutNotFoundContent">Router without NotFoundContent</option>
<option value="BasicTestApp.HtmlBlockChildContent">ChildContent HTML Block</option> <option value="BasicTestApp.HtmlBlockChildContent">ChildContent HTML Block</option>
<option value="BasicTestApp.HtmlMixedChildContent">ChildContent Mixed Block</option> <option value="BasicTestApp.HtmlMixedChildContent">ChildContent Mixed Block</option>
<option value="BasicTestApp.HtmlEncodedChildContent">ChildContent HTML Encoded Block</option> <option value="BasicTestApp.HtmlEncodedChildContent">ChildContent HTML Encoded Block</option>
@ -52,6 +53,7 @@
<option value="BasicTestApp.InteropOnInitializationComponent">Interop on initialization</option> <option value="BasicTestApp.InteropOnInitializationComponent">Interop on initialization</option>
<option value="BasicTestApp.KeyCasesComponent">Key cases</option> <option value="BasicTestApp.KeyCasesComponent">Key cases</option>
<option value="BasicTestApp.ReorderingFocusComponent">Reordering focus retention</option> <option value="BasicTestApp.ReorderingFocusComponent">Reordering focus retention</option>
<option value="BasicTestApp.RouterTest.UriHelperComponent">UriHelper Test</option>
</select> </select>
@if (SelectedComponentType != null) @if (SelectedComponentType != null)

View File

@ -1 +0,0 @@
<div id="test-info">Oops, that component wasn't found!</div>

View File

@ -27,3 +27,6 @@
</a> </a>
<a href="/" target="_blank">Target (_blank)</a> <a href="/" target="_blank">Target (_blank)</a>
<a href="/subdir/NotAComponent.html">Not a component</a>

View File

@ -1,2 +1,6 @@
@using Microsoft.AspNetCore.Components.Routing @using Microsoft.AspNetCore.Components.Routing
<Router AppAssembly=typeof(BasicTestApp.Program).Assembly FallbackComponent="typeof(Error404)" /> <Router AppAssembly=typeof(BasicTestApp.Program).Assembly>
<NotFoundContent>
<div id="test-info">Oops, that component wasn't found!</div>
</NotFoundContent>
</Router>

View File

@ -0,0 +1,27 @@
@inject IUriHelper UriHelper
@inject Microsoft.JSInterop.IJSRuntime JSRuntime
<button onclick="@Navigate">Navigate</button>
<span id="test-info">@UrlLocation</span>
@functions{
string UrlLocation;
protected override void OnInit()
{
base.OnInit();
UrlLocation = UriHelper.GetAbsoluteUri();
UriHelper.OnLocationChanged += (_, __) =>
{
UrlLocation = UriHelper.GetAbsoluteUri();
StateHasChanged();
};
}
async Task Navigate()
{
await JSRuntime.InvokeAsync<object>("uriHelperNavigate");
}
}

View File

@ -0,0 +1 @@
<div id="test-info">Not a component!</div>

View File

@ -19,6 +19,10 @@
return element.value; return element.value;
} }
function uriHelperNavigate() {
Blazor.navigateTo('/subdir/some-path');
}
(function () { (function () {
// Load either blazor.webassembly.js or blazor.server.js depending // Load either blazor.webassembly.js or blazor.server.js depending
// on the hash part of the URL. This is just to give a way for the // on the hash part of the URL. This is just to give a way for the

View File

@ -37,6 +37,11 @@
<span class="oi oi-list-rich" aria-hidden="true"></span> Error <span class="oi oi-list-rich" aria-hidden="true"></span> Error
</NavLink> </NavLink>
</li> </li>
<li class="nav-item px-3">
<NavLink class="nav-link" href="links">
<span class="oi oi-list-rich" aria-hidden="true"></span> Links
</NavLink>
</li>
</ul> </ul>
</div> </div>

View File

@ -1,4 +1,4 @@
@page @page
@using ComponentsApp.App @using ComponentsApp.App
<!DOCTYPE html> <!DOCTYPE html>
@ -12,8 +12,7 @@
<link href="css/site.css" rel="stylesheet" /> <link href="css/site.css" rel="stylesheet" />
</head> </head>
<body> <body>
<app>@(await Html.RenderComponentAsync<App>(new { Name = "Guest" }))</app>
<app>@(await Html.RenderComponentAsync<App>(new { Name="Guest" }))</app>
<script src="_framework/blazor.server.js" autostart="false"></script> <script src="_framework/blazor.server.js" autostart="false"></script>
<script> <script>

View File

@ -2,6 +2,7 @@ using BasicTestApp;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Components.Server; using Microsoft.AspNetCore.Components.Server;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;

View File

@ -5,6 +5,7 @@ using System;
using System.Buffers; using System.Buffers;
using System.Linq; using System.Linq;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Routing;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.ApplicationParts; using Microsoft.AspNetCore.Mvc.ApplicationParts;
@ -207,6 +208,8 @@ namespace Microsoft.Extensions.DependencyInjection
services.TryAddScoped<StaticComponentRenderer>(); services.TryAddScoped<StaticComponentRenderer>();
services.TryAddScoped<IUriHelper, HttpUriHelper>(); services.TryAddScoped<IUriHelper, HttpUriHelper>();
services.TryAddScoped<IJSRuntime, UnsupportedJavaScriptRuntime>(); services.TryAddScoped<IJSRuntime, UnsupportedJavaScriptRuntime>();
services.TryAddScoped<IComponentContext, UnsupportedComponentContext>();
services.TryAddScoped<INavigationInterception, UnsupportedNavigationInterception>();
services.TryAddTransient<ControllerSaveTempDataPropertyFilter>(); services.TryAddTransient<ControllerSaveTempDataPropertyFilter>();

View File

@ -3,8 +3,6 @@
using System; using System;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
namespace Microsoft.AspNetCore.Mvc.ViewFeatures namespace Microsoft.AspNetCore.Mvc.ViewFeatures
{ {

View File

@ -0,0 +1,12 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Components;
namespace Microsoft.AspNetCore.Mvc.ViewFeatures
{
internal class UnsupportedComponentContext : IComponentContext
{
public bool IsConnected => false;
}
}

View File

@ -0,0 +1,18 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Routing;
namespace Microsoft.AspNetCore.Mvc.ViewFeatures
{
internal sealed class UnsupportedNavigationInterception : INavigationInterception
{
public Task EnableNavigationInterceptionAsync()
{
throw new InvalidOperationException("Navigation interception calls cannot be issued during server-side prerendering, because the page has not yet loaded in the browser. " +
"Prerendered components must wrap any navigation interception calls in conditional logic to ensure those interop calls are not attempted during prerendering.");
}
}
}

View File

@ -37,7 +37,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
var response = await client.GetAsync("http://localhost/components"); var response = await client.GetAsync("http://localhost/components");
// Assert // Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode); await response.AssertStatusCodeAsync(HttpStatusCode.OK);
var content = await response.Content.ReadAsStringAsync(); var content = await response.Content.ReadAsStringAsync();
AssertComponent("\n<p>Hello world!</p>", "Greetings", content); AssertComponent("\n<p>Hello world!</p>", "Greetings", content);
@ -53,7 +53,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
var response = await client.GetAsync("http://localhost/components"); var response = await client.GetAsync("http://localhost/components");
// Assert // Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode); await response.AssertStatusCodeAsync(HttpStatusCode.OK);
var content = await response.Content.ReadAsStringAsync(); var content = await response.Content.ReadAsStringAsync();
AssertComponent("\n<p>Hello world!</p>", "Greetings", content); AssertComponent("\n<p>Hello world!</p>", "Greetings", content);
@ -68,7 +68,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
var response = await client.GetAsync("http://localhost/components/routable"); var response = await client.GetAsync("http://localhost/components/routable");
// Assert // Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode); await response.AssertStatusCodeAsync(HttpStatusCode.OK);
var content = await response.Content.ReadAsStringAsync(); var content = await response.Content.ReadAsStringAsync();
AssertComponent("\nRouter component\n<p>Routed successfully</p>", "Routing", content); AssertComponent("\nRouter component\n<p>Routed successfully</p>", "Routing", content);
@ -84,7 +84,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
var response = await client.GetAsync("http://localhost/components/routable"); var response = await client.GetAsync("http://localhost/components/routable");
// Assert // Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode); await response.AssertStatusCodeAsync(HttpStatusCode.OK);
var content = await response.Content.ReadAsStringAsync(); var content = await response.Content.ReadAsStringAsync();
AssertComponent("\nRouter component\n<p>Routed successfully</p>", "Routing", content); AssertComponent("\nRouter component\n<p>Routed successfully</p>", "Routing", content);
@ -100,7 +100,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
var response = await client.GetAsync("http://localhost/components/false"); var response = await client.GetAsync("http://localhost/components/false");
// Assert // Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode); await response.AssertStatusCodeAsync(HttpStatusCode.OK);
var content = await response.Content.ReadAsStringAsync(); var content = await response.Content.ReadAsStringAsync();
AssertComponent("<p>Hello world!</p>", "Greetings", content, unwrap: true); AssertComponent("<p>Hello world!</p>", "Greetings", content, unwrap: true);
@ -116,7 +116,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
var response = await client.GetAsync("http://localhost/components/routable/false"); var response = await client.GetAsync("http://localhost/components/routable/false");
// Assert // Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode); await response.AssertStatusCodeAsync(HttpStatusCode.OK);
var content = await response.Content.ReadAsStringAsync(); var content = await response.Content.ReadAsStringAsync();
AssertComponent("Router component\n<p>Routed successfully</p>", "Routing", content, unwrap: true); AssertComponent("Router component\n<p>Routed successfully</p>", "Routing", content, unwrap: true);
@ -194,7 +194,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
var response = await client.GetAsync("http://localhost/components"); var response = await client.GetAsync("http://localhost/components");
// Assert // Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode); await response.AssertStatusCodeAsync(HttpStatusCode.OK);
var content = await response.Content.ReadAsStringAsync(); var content = await response.Content.ReadAsStringAsync();
AssertComponent(expectedHtml, "FetchData", content); AssertComponent(expectedHtml, "FetchData", content);

View File

@ -1 +0,0 @@
<p>Route not found</p>

View File

@ -1,6 +1,7 @@
@using Microsoft.AspNetCore.Components.Routing @using Microsoft.AspNetCore.Components.Routing
Router component Router component
<Router <Router AppAssembly="System.Reflection.Assembly.GetAssembly(typeof(RouterContainer))">
AppAssembly="System.Reflection.Assembly.GetAssembly(typeof(RouterContainer))" <NotFoundContent>
FallbackComponent="typeof(Fallback)"> <p>Route not found</p>
</NotFoundContent>
</Router> </Router>