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 NavigateToCore(string uri, bool forceLoad) { }
[Microsoft.JSInterop.JSInvokableAttribute("NotifyLocationChanged")]
public static void NotifyLocationChanged(string newAbsoluteUri) { }
public static void NotifyLocationChanged(string newAbsoluteUri, bool isInterceptedLink) { }
}
}
namespace Microsoft.AspNetCore.Components.Builder

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Net.Http;
using Microsoft.AspNetCore.Blazor.Services;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.JSInterop;
@ -90,6 +91,7 @@ namespace Microsoft.AspNetCore.Blazor.Hosting
services.AddSingleton<IJSRuntime>(WebAssemblyJSRuntime.Instance);
services.AddSingleton<IComponentContext, WebAssemblyComponentContext>();
services.AddSingleton<IUriHelper>(WebAssemblyUriHelper.Instance);
services.AddSingleton<INavigationInterception>(WebAssemblyNavigationInterception.Instance);
services.AddSingleton<HttpClient>(s =>
{
// 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()
{
WebAssemblyJSRuntime.Instance.Invoke<object>(
Interop.EnableNavigationInterception,
Interop.ListenForNavigationEvents,
typeof(WebAssemblyUriHelper).Assembly.GetName().Name,
nameof(NotifyLocationChanged));
@ -54,10 +54,10 @@ namespace Microsoft.AspNetCore.Blazor.Services
/// For framework use only.
/// </summary>
[JSInvokable(nameof(NotifyLocationChanged))]
public static void NotifyLocationChanged(string newAbsoluteUri)
public static void NotifyLocationChanged(string newAbsoluteUri, bool isInterceptedLink)
{
Instance.SetAbsoluteUri(newAbsoluteUri);
Instance.TriggerOnLocationChanged();
Instance.TriggerOnLocationChanged(isInterceptedLink);
}
/// <summary>

View File

@ -14885,62 +14885,76 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
};
Object.defineProperty(exports, "__esModule", { value: true });
__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
var notifyLocationChangedCallback = null;
// These are the functions we're making available for invocation from .NET
exports.internalFunctions = {
listenForNavigationEvents: listenForNavigationEvents,
enableNavigationInterception: enableNavigationInterception,
navigateTo: navigateTo,
getBaseURI: function () { return document.baseURI; },
getLocationHref: function () { return location.href; },
};
function enableNavigationInterception(assemblyName, functionName) {
if (hasRegisteredEventListeners || assemblyName === undefined || functionName === undefined) {
function listenForNavigationEvents(assemblyName, functionName) {
if (hasRegisteredNavigationEventListeners) {
return;
}
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) {
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
// 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 hrefAttributeName = 'href';
if (anchorTarget && anchorTarget.hasAttribute(hrefAttributeName) && event.button === 0) {
var href = anchorTarget.getAttribute(hrefAttributeName);
var absoluteHref = toAbsoluteUri(href);
if (anchorTarget && anchorTarget.hasAttribute(hrefAttributeName)) {
var targetAttributeValue = anchorTarget.getAttribute('target');
var opensInSameFrame = !targetAttributeValue || targetAttributeValue === '_self';
// Don't stop ctrl/meta-click (etc) from opening links in new tabs/windows
if (isWithinBaseUriSpace(absoluteHref) && !eventHasSpecialKey(event) && opensInSameFrame) {
if (!opensInSameFrame) {
return;
}
var href = anchorTarget.getAttribute(hrefAttributeName);
var absoluteHref = toAbsoluteUri(href);
if (isWithinBaseUriSpace(absoluteHref)) {
event.preventDefault();
performInternalNavigation(absoluteHref);
performInternalNavigation(absoluteHref, true);
}
}
});
window.addEventListener('popstate', handleInternalNavigation);
}
function navigateTo(uri, forceLoad) {
var absoluteUri = toAbsoluteUri(uri);
if (!forceLoad && isWithinBaseUriSpace(absoluteUri)) {
performInternalNavigation(absoluteUri);
performInternalNavigation(absoluteUri, false);
}
else {
location.href = uri;
}
}
exports.navigateTo = navigateTo;
function performInternalNavigation(absoluteInternalHref) {
function performInternalNavigation(absoluteInternalHref, interceptedLink) {
history.pushState(null, /* ignored title */ '', absoluteInternalHref);
handleInternalNavigation();
notifyLocationChanged(interceptedLink);
}
function handleInternalNavigation() {
function notifyLocationChanged(interceptedLink) {
return __awaiter(this, void 0, void 0, function () {
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
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:
_a.sent();
_a.label = 2;

View File

@ -2450,62 +2450,76 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
};
Object.defineProperty(exports, "__esModule", { value: true });
__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
var notifyLocationChangedCallback = null;
// These are the functions we're making available for invocation from .NET
exports.internalFunctions = {
listenForNavigationEvents: listenForNavigationEvents,
enableNavigationInterception: enableNavigationInterception,
navigateTo: navigateTo,
getBaseURI: function () { return document.baseURI; },
getLocationHref: function () { return location.href; },
};
function enableNavigationInterception(assemblyName, functionName) {
if (hasRegisteredEventListeners || assemblyName === undefined || functionName === undefined) {
function listenForNavigationEvents(assemblyName, functionName) {
if (hasRegisteredNavigationEventListeners) {
return;
}
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) {
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
// 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 hrefAttributeName = 'href';
if (anchorTarget && anchorTarget.hasAttribute(hrefAttributeName) && event.button === 0) {
var href = anchorTarget.getAttribute(hrefAttributeName);
var absoluteHref = toAbsoluteUri(href);
if (anchorTarget && anchorTarget.hasAttribute(hrefAttributeName)) {
var targetAttributeValue = anchorTarget.getAttribute('target');
var opensInSameFrame = !targetAttributeValue || targetAttributeValue === '_self';
// Don't stop ctrl/meta-click (etc) from opening links in new tabs/windows
if (isWithinBaseUriSpace(absoluteHref) && !eventHasSpecialKey(event) && opensInSameFrame) {
if (!opensInSameFrame) {
return;
}
var href = anchorTarget.getAttribute(hrefAttributeName);
var absoluteHref = toAbsoluteUri(href);
if (isWithinBaseUriSpace(absoluteHref)) {
event.preventDefault();
performInternalNavigation(absoluteHref);
performInternalNavigation(absoluteHref, true);
}
}
});
window.addEventListener('popstate', handleInternalNavigation);
}
function navigateTo(uri, forceLoad) {
var absoluteUri = toAbsoluteUri(uri);
if (!forceLoad && isWithinBaseUriSpace(absoluteUri)) {
performInternalNavigation(absoluteUri);
performInternalNavigation(absoluteUri, false);
}
else {
location.href = uri;
}
}
exports.navigateTo = navigateTo;
function performInternalNavigation(absoluteInternalHref) {
function performInternalNavigation(absoluteInternalHref, interceptedLink) {
history.pushState(null, /* ignored title */ '', absoluteInternalHref);
handleInternalNavigation();
notifyLocationChanged(interceptedLink);
}
function handleInternalNavigation() {
function notifyLocationChanged(interceptedLink) {
return __awaiter(this, void 0, void 0, function () {
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
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:
_a.sent();
_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": "",
"main": "index.js",
"scripts": {
"build": "yarn run build:debug && yarn run build:production",
"build:debug": "cd src && webpack --mode development --config ./webpack.config.js",
"build:production": "cd src && webpack --mode production --config ./webpack.config.js",
"test": "jest"

View File

@ -1,69 +1,88 @@
import '@dotnet/jsinterop';
let hasRegisteredEventListeners = false;
let hasRegisteredNavigationInterception = false;
let hasRegisteredNavigationEventListeners = false;
// Will be initialized once someone registers
let notifyLocationChangedCallback: { assemblyName: string; functionName: string } | null = null;
// These are the functions we're making available for invocation from .NET
export const internalFunctions = {
listenForNavigationEvents,
enableNavigationInterception,
navigateTo,
getBaseURI: () => document.baseURI,
getLocationHref: () => location.href,
};
function enableNavigationInterception(assemblyName: string, functionName: string) {
if (hasRegisteredEventListeners || assemblyName === undefined || functionName === undefined) {
function listenForNavigationEvents(assemblyName: string, functionName: string) {
if (hasRegisteredNavigationEventListeners) {
return;
}
notifyLocationChangedCallback = { assemblyName, functionName };
hasRegisteredEventListeners = true;
hasRegisteredNavigationEventListeners = true;
window.addEventListener('popstate', () => notifyLocationChanged(false));
}
function enableNavigationInterception() {
if (hasRegisteredNavigationInterception) {
return;
}
hasRegisteredNavigationInterception = true;
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
// 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 hrefAttributeName = 'href';
if (anchorTarget && anchorTarget.hasAttribute(hrefAttributeName) && event.button === 0) {
const href = anchorTarget.getAttribute(hrefAttributeName)!;
const absoluteHref = toAbsoluteUri(href);
if (anchorTarget && anchorTarget.hasAttribute(hrefAttributeName)) {
const targetAttributeValue = anchorTarget.getAttribute('target');
const opensInSameFrame = !targetAttributeValue || targetAttributeValue === '_self';
if (!opensInSameFrame) {
return;
}
// Don't stop ctrl/meta-click (etc) from opening links in new tabs/windows
if (isWithinBaseUriSpace(absoluteHref) && !eventHasSpecialKey(event) && opensInSameFrame) {
const href = anchorTarget.getAttribute(hrefAttributeName)!;
const absoluteHref = toAbsoluteUri(href);
if (isWithinBaseUriSpace(absoluteHref)) {
event.preventDefault();
performInternalNavigation(absoluteHref);
performInternalNavigation(absoluteHref, true);
}
}
});
window.addEventListener('popstate', handleInternalNavigation);
}
export function navigateTo(uri: string, forceLoad: boolean) {
const absoluteUri = toAbsoluteUri(uri);
if (!forceLoad && isWithinBaseUriSpace(absoluteUri)) {
performInternalNavigation(absoluteUri);
performInternalNavigation(absoluteUri, false);
} else {
location.href = uri;
}
}
function performInternalNavigation(absoluteInternalHref: string) {
function performInternalNavigation(absoluteInternalHref: string, interceptedLink: boolean) {
history.pushState(null, /* ignored title */ '', absoluteInternalHref);
handleInternalNavigation();
notifyLocationChanged(interceptedLink);
}
async function handleInternalNavigation() {
async function notifyLocationChanged(interceptedLink: boolean) {
if (notifyLocationChangedCallback) {
await DotNet.invokeMethodAsync(
notifyLocationChangedCallback.assemblyName,
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.";
public static readonly string ListenForNavigationEvents = Prefix + "listenForNavigationEvents";
public static readonly string EnableNavigationInterception = Prefix + "enableNavigationInterception";
public static readonly string GetLocationHref = Prefix + "getLocationHref";

View File

@ -367,7 +367,7 @@ namespace Microsoft.AspNetCore.Components
}
public partial interface IUriHelper
{
event System.EventHandler<string> OnLocationChanged;
event System.EventHandler<Microsoft.AspNetCore.Components.Routing.LocationChangedEventArgs> OnLocationChanged;
string GetAbsoluteUri();
string GetBaseUri();
void NavigateTo(string uri);
@ -599,7 +599,7 @@ namespace Microsoft.AspNetCore.Components
public abstract partial class UriHelperBase : Microsoft.AspNetCore.Components.IUriHelper
{
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() { }
public string GetAbsoluteUri() { throw null; }
public virtual string GetBaseUri() { throw null; }
@ -611,7 +611,7 @@ namespace Microsoft.AspNetCore.Components
protected void SetAbsoluteUri(string uri) { }
public System.Uri ToAbsoluteUri(string href) { throw null; }
public string ToBaseRelativePath(string baseUri, string locationAbsolute) { throw null; }
protected void TriggerOnLocationChanged() { }
protected void TriggerOnLocationChanged(bool isinterceptedLink) { }
}
}
namespace Microsoft.AspNetCore.Components.Forms
@ -849,6 +849,19 @@ namespace Microsoft.AspNetCore.Components.RenderTree
}
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
{
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.
using System;
using Microsoft.AspNetCore.Components.Routing;
namespace Microsoft.AspNetCore.Components
{
@ -19,7 +20,7 @@ namespace Microsoft.AspNetCore.Components
/// <summary>
/// An event that fires when the navigation location has changed.
/// </summary>
event EventHandler<string> OnLocationChanged;
event EventHandler<LocationChangedEventArgs> OnLocationChanged;
/// <summary>
/// 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>
<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.
// 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.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.RenderTree;
namespace Microsoft.AspNetCore.Components.Routing
{
@ -83,11 +82,11 @@ namespace Microsoft.AspNetCore.Components.Routing
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
// only relevant state change is to the _isActive property.
var shouldBeActiveNow = ShouldMatch(newUriAbsolute);
var shouldBeActiveNow = ShouldMatch(args.Location);
if (shouldBeActiveNow != _isActive)
{
_isActive = shouldBeActiveNow;

View File

@ -14,16 +14,21 @@ namespace Microsoft.AspNetCore.Components.Routing
/// A component that displays whichever other component corresponds to the
/// current navigation location.
/// </summary>
public class Router : IComponent, IDisposable
public class Router : IComponent, IHandleAfterRender, IDisposable
{
static readonly char[] _queryOrHashStartChar = new[] { '?', '#' };
RenderHandle _renderHandle;
string _baseUri;
string _locationAbsolute;
bool _navigationInterceptionEnabled;
[Inject] private IUriHelper UriHelper { get; set; }
[Inject] private INavigationInterception NavigationInterception { get; set; }
[Inject] private IComponentContext ComponentContext { get; set; }
/// <summary>
/// Gets or sets the assembly that should be searched, along with its referenced
/// assemblies, for components matching the URI.
@ -33,7 +38,7 @@ namespace Microsoft.AspNetCore.Components.Routing
/// <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.
/// </summary>
[Parameter] public Type FallbackComponent { get; private set; }
[Parameter] public RenderFragment NotFoundContent { get; private set; }
private RouteTable Routes { get; set; }
@ -52,7 +57,7 @@ namespace Microsoft.AspNetCore.Components.Routing
parameters.SetParameterProperties(this);
var types = ComponentResolver.ResolveComponents(AppAssembly);
Routes = RouteTable.Create(types);
Refresh();
Refresh(isNavigationIntercepted: false);
return Task.CompletedTask;
}
@ -79,41 +84,57 @@ namespace Microsoft.AspNetCore.Components.Routing
builder.CloseComponent();
}
private void Refresh()
private void Refresh(bool isNavigationIntercepted)
{
var locationPath = UriHelper.ToBaseRelativePath(_baseUri, _locationAbsolute);
locationPath = StringUntilAny(locationPath, _queryOrHashStartChar);
var context = new RouteContext(locationPath);
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
{
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;
if (_renderHandle.IsInitialized)
_locationAbsolute = args.Location;
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.
using System;
using Microsoft.AspNetCore.Components.Routing;
namespace Microsoft.AspNetCore.Components
{
@ -10,12 +11,12 @@ namespace Microsoft.AspNetCore.Components
/// </summary>
public abstract class UriHelperBase : IUriHelper
{
private EventHandler<string> _onLocationChanged;
private EventHandler<LocationChangedEventArgs> _onLocationChanged;
/// <summary>
/// An event that fires when the navigation location has changed.
/// </summary>
public event EventHandler<string> OnLocationChanged
public event EventHandler<LocationChangedEventArgs> OnLocationChanged
{
add
{
@ -205,9 +206,9 @@ namespace Microsoft.AspNetCore.Components
/// <summary>
/// Triggers the <see cref="OnLocationChanged"/> event with the current URI value.
/// </summary>
protected void TriggerOnLocationChanged()
protected void TriggerOnLocationChanged(bool isinterceptedLink)
{
_onLocationChanged?.Invoke(this, _uri);
_onLocationChanged?.Invoke(this, new LocationChangedEventArgs(_uri, isinterceptedLink));
}
private void AssertInitialized()

View File

@ -29,7 +29,7 @@
"test\\testassets\\ComponentsApp.App\\ComponentsApp.App.csproj",
"test\\testassets\\ComponentsApp.Server\\ComponentsApp.Server.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) { }
protected override void NavigateToCore(string uri, bool forceLoad) { }
[Microsoft.JSInterop.JSInvokableAttribute("NotifyLocationChanged")]
public static void NotifyLocationChanged(string uriAbsolute) { }
public static void NotifyLocationChanged(string uriAbsolute, bool isInterceptedLink) { }
}
}
namespace Microsoft.Extensions.DependencyInjection

View File

@ -8,6 +8,7 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Browser;
using Microsoft.AspNetCore.Components.Browser.Rendering;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.JSInterop;
@ -120,6 +121,12 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
{
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.Rendering;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.Routing;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.DependencyInjection;
@ -49,12 +50,15 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
(authenticationStateProvider as FixedAuthenticationStateProvider)?.Initialize(httpContext.User);
var uriHelper = (RemoteUriHelper)scope.ServiceProvider.GetRequiredService<IUriHelper>();
var navigationInterception = (RemoteNavigationInterception)scope.ServiceProvider.GetRequiredService<INavigationInterception>();
if (client.Connected)
{
uriHelper.AttachJsRuntime(jsRuntime);
uriHelper.InitializeState(
uriAbsolute,
baseUriAbsolute);
navigationInterception.AttachJSRuntime(jsRuntime);
}
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>
public class RemoteUriHelper : UriHelperBase
{
private IJSRuntime _jsRuntime;
private readonly ILogger<RemoteUriHelper> _logger;
private IJSRuntime _jsRuntime;
/// <summary>
/// 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)
{
base.InitializeState(uriAbsolute, baseUriAbsolute);
TriggerOnLocationChanged();
TriggerOnLocationChanged(isinterceptedLink: false);
}
/// <summary>
@ -52,11 +52,13 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
{
throw new InvalidOperationException("JavaScript runtime already initialized.");
}
_jsRuntime = jsRuntime;
_jsRuntime.InvokeAsync<object>(
Interop.EnableNavigationInterception,
typeof(RemoteUriHelper).Assembly.GetName().Name,
nameof(NotifyLocationChanged));
Interop.ListenForNavigationEvents,
typeof(RemoteUriHelper).Assembly.GetName().Name,
nameof(NotifyLocationChanged));
_logger.LogDebug($"{nameof(RemoteUriHelper)} initialized.");
}
@ -65,7 +67,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
/// For framework use only.
/// </summary>
[JSInvokable(nameof(NotifyLocationChanged))]
public static void NotifyLocationChanged(string uriAbsolute)
public static void NotifyLocationChanged(string uriAbsolute, bool isInterceptedLink)
{
var circuit = CircuitHost.Current;
if (circuit == null)
@ -73,19 +75,18 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
var message = $"{nameof(NotifyLocationChanged)} called without a circuit.";
throw new InvalidOperationException(message);
}
var uriHelper = (RemoteUriHelper)circuit.Services.GetRequiredService<IUriHelper>();
uriHelper.SetAbsoluteUri(uriAbsolute);
uriHelper._logger.LogDebug($"Location changed to '{uriAbsolute}'.");
uriHelper.TriggerOnLocationChanged();
uriHelper.TriggerOnLocationChanged(isInterceptedLink);
}
/// <inheritdoc />
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)
{

View File

@ -3,6 +3,7 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Routing;
using Microsoft.AspNetCore.Components.Server;
using Microsoft.AspNetCore.Components.Server.BlazorPack;
using Microsoft.AspNetCore.Components.Server.Circuits;
@ -66,6 +67,7 @@ namespace Microsoft.Extensions.DependencyInjection
// These intentionally replace the non-interactive versions included in MVC.
services.AddScoped<IUriHelper, RemoteUriHelper>();
services.AddScoped<IJSRuntime, RemoteJSRuntime>();
services.AddScoped<INavigationInterception, RemoteNavigationInterception>();
services.AddScoped<IComponentContext, RemoteComponentContext>();
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.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
@ -206,6 +204,5 @@ window.Blazor._internal.forceCloseConnection();");
Browser.True(() => Browser.Manage().Logs.GetLog(LogType.Browser)
.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");
}
[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]
public void CanNavigateProgrammatically()
{
@ -339,12 +349,30 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
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 jsExecutor = (IJavaScriptExecutor)Browser;
var absoluteUri = new Uri(_serverFixture.RootUri, $"{pathBaseWithoutHash}{relativeUri}");
jsExecutor.ExecuteScript($"Blazor.navigateTo('{absoluteUri.ToString().Replace("'", "\\'")}')");
return absoluteUri.AbsoluteUri;
}
private void AssertHighlightedLinks(params string[] linkTexts)

View File

@ -37,6 +37,7 @@
<option value="BasicTestApp.EventBubblingComponent">Event bubbling</option>
<option value="BasicTestApp.EventPreventDefaultComponent">Event preventDefault</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.HtmlMixedChildContent">ChildContent Mixed 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.KeyCasesComponent">Key cases</option>
<option value="BasicTestApp.ReorderingFocusComponent">Reordering focus retention</option>
<option value="BasicTestApp.RouterTest.UriHelperComponent">UriHelper Test</option>
</select>
@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 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
<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;
}
function uriHelperNavigate() {
Blazor.navigateTo('/subdir/some-path');
}
(function () {
// 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

View File

@ -37,6 +37,11 @@
<span class="oi oi-list-rich" aria-hidden="true"></span> Error
</NavLink>
</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>
</div>

View File

@ -1,4 +1,4 @@
@page
@page
@using ComponentsApp.App
<!DOCTYPE html>
@ -12,8 +12,7 @@
<link href="css/site.css" rel="stylesheet" />
</head>
<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>

View File

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

View File

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

View File

@ -3,8 +3,6 @@
using System;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
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");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
await response.AssertStatusCodeAsync(HttpStatusCode.OK);
var content = await response.Content.ReadAsStringAsync();
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");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
await response.AssertStatusCodeAsync(HttpStatusCode.OK);
var content = await response.Content.ReadAsStringAsync();
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");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
await response.AssertStatusCodeAsync(HttpStatusCode.OK);
var content = await response.Content.ReadAsStringAsync();
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");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
await response.AssertStatusCodeAsync(HttpStatusCode.OK);
var content = await response.Content.ReadAsStringAsync();
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");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
await response.AssertStatusCodeAsync(HttpStatusCode.OK);
var content = await response.Content.ReadAsStringAsync();
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");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
await response.AssertStatusCodeAsync(HttpStatusCode.OK);
var content = await response.Content.ReadAsStringAsync();
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");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
await response.AssertStatusCodeAsync(HttpStatusCode.OK);
var content = await response.Content.ReadAsStringAsync();
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
Router component
<Router
AppAssembly="System.Reflection.Assembly.GetAssembly(typeof(RouterContainer))"
FallbackComponent="typeof(Fallback)">
<Router AppAssembly="System.Reflection.Assembly.GetAssembly(typeof(RouterContainer))">
<NotFoundContent>
<p>Route not found</p>
</NotFoundContent>
</Router>