Components router refactoring. Fixes #10493 #10445 (#12800)

This commit is contained in:
Steve Sanderson 2019-08-05 13:52:01 +01:00 committed by GitHub
parent 9f2b534436
commit 2ff6a5c0f8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 1516 additions and 590 deletions

View File

@ -1,5 +1,10 @@
<Router AppAssembly="typeof(Program).Assembly">
<Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
</Found>
<NotFound>
<p>Sorry, there's nothing at this address.</p>
<LayoutView Layout="@typeof(MainLayout)">
<p>Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>

View File

@ -1 +1,8 @@
<Router AppAssembly=typeof(Program).Assembly />
<Router AppAssembly=typeof(Program).Assembly>
<Found Context="routeData">
<RouteView RouteData="@routeData" />
</Found>
<NotFound>
Sorry, there's nothing here.
</NotFound>
</Router>

View File

@ -1,5 +1,11 @@
<!--
Configuring this stuff here is temporary. Later we'll move the app config
into Program.cs, and it won't be necessary to specify AppAssembly.
-->
<Router AppAssembly=typeof(StandaloneApp.Program).Assembly />
<Router AppAssembly=typeof(StandaloneApp.Program).Assembly>
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
</Found>
<NotFound>
<LayoutView Layout="@typeof(MainLayout)">
<h2>Not found</h2>
Sorry, there's nothing at this address.
</LayoutView>
</NotFound>
</Router>

View File

@ -1 +0,0 @@
@layout MainLayout

View File

@ -16,6 +16,15 @@ namespace Microsoft.AspNetCore.Components
public abstract System.Threading.Tasks.Task<Microsoft.AspNetCore.Components.AuthenticationState> GetAuthenticationStateAsync();
protected void NotifyAuthenticationStateChanged(System.Threading.Tasks.Task<Microsoft.AspNetCore.Components.AuthenticationState> task) { }
}
public sealed partial class AuthorizeRouteView : Microsoft.AspNetCore.Components.RouteView
{
public AuthorizeRouteView() { }
[Microsoft.AspNetCore.Components.ParameterAttribute]
public Microsoft.AspNetCore.Components.RenderFragment Authorizing { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
[Microsoft.AspNetCore.Components.ParameterAttribute]
public Microsoft.AspNetCore.Components.RenderFragment<Microsoft.AspNetCore.Components.AuthenticationState> NotAuthorized { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
protected override void Render(Microsoft.AspNetCore.Components.RenderTree.RenderTreeBuilder builder) { }
}
public partial class AuthorizeView : Microsoft.AspNetCore.Components.AuthorizeViewCore
{
public AuthorizeView() { }
@ -278,6 +287,16 @@ namespace Microsoft.AspNetCore.Components
[Microsoft.AspNetCore.Components.ParameterAttribute]
public Microsoft.AspNetCore.Components.RenderFragment Body { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
}
public partial class LayoutView : Microsoft.AspNetCore.Components.IComponent
{
public LayoutView() { }
[Microsoft.AspNetCore.Components.ParameterAttribute]
public Microsoft.AspNetCore.Components.RenderFragment ChildContent { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
[Microsoft.AspNetCore.Components.ParameterAttribute]
public System.Type Layout { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public void Attach(Microsoft.AspNetCore.Components.RenderHandle renderHandle) { }
public System.Threading.Tasks.Task SetParametersAsync(Microsoft.AspNetCore.Components.ParameterView parameters) { throw null; }
}
public sealed partial class LocationChangeException : System.Exception
{
public LocationChangeException(string message, System.Exception innerException) { }
@ -323,20 +342,6 @@ namespace Microsoft.AspNetCore.Components
protected OwningComponentBase() { }
protected TService Service { get { throw null; } }
}
public partial class PageDisplay : Microsoft.AspNetCore.Components.IComponent
{
public PageDisplay() { }
[Microsoft.AspNetCore.Components.ParameterAttribute]
public Microsoft.AspNetCore.Components.RenderFragment Authorizing { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
[Microsoft.AspNetCore.Components.ParameterAttribute]
public Microsoft.AspNetCore.Components.RenderFragment<Microsoft.AspNetCore.Components.AuthenticationState> NotAuthorized { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
[Microsoft.AspNetCore.Components.ParameterAttribute]
public System.Type Page { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
[Microsoft.AspNetCore.Components.ParameterAttribute]
public System.Collections.Generic.IDictionary<string, object> PageParameters { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public void Attach(Microsoft.AspNetCore.Components.RenderHandle renderHandle) { }
public System.Threading.Tasks.Task SetParametersAsync(Microsoft.AspNetCore.Components.ParameterView parameters) { throw null; }
}
[System.AttributeUsageAttribute(System.AttributeTargets.Property, AllowMultiple=false, Inherited=true)]
public sealed partial class ParameterAttribute : System.Attribute
{
@ -391,6 +396,23 @@ namespace Microsoft.AspNetCore.Components
public RouteAttribute(string template) { }
public string Template { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
}
public sealed partial class RouteData
{
public RouteData(System.Type pageType, System.Collections.Generic.IReadOnlyDictionary<string, object> routeValues) { }
public System.Type PageType { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
public System.Collections.Generic.IReadOnlyDictionary<string, object> RouteValues { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
}
public partial class RouteView : Microsoft.AspNetCore.Components.IComponent
{
public RouteView() { }
[Microsoft.AspNetCore.Components.ParameterAttribute]
public System.Type DefaultLayout { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
[Microsoft.AspNetCore.Components.ParameterAttribute]
public Microsoft.AspNetCore.Components.RouteData RouteData { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public void Attach(Microsoft.AspNetCore.Components.RenderHandle renderHandle) { }
protected virtual void Render(Microsoft.AspNetCore.Components.RenderTree.RenderTreeBuilder builder) { }
public System.Threading.Tasks.Task SetParametersAsync(Microsoft.AspNetCore.Components.ParameterView parameters) { throw null; }
}
}
namespace Microsoft.AspNetCore.Components.CompilerServices
{
@ -648,15 +670,12 @@ namespace Microsoft.AspNetCore.Components.Routing
[Microsoft.AspNetCore.Components.ParameterAttribute]
public System.Reflection.Assembly AppAssembly { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
[Microsoft.AspNetCore.Components.ParameterAttribute]
public Microsoft.AspNetCore.Components.RenderFragment Authorizing { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
[Microsoft.AspNetCore.Components.ParameterAttribute]
public Microsoft.AspNetCore.Components.RenderFragment<Microsoft.AspNetCore.Components.AuthenticationState> NotAuthorized { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public Microsoft.AspNetCore.Components.RenderFragment<Microsoft.AspNetCore.Components.RouteData> Found { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
[Microsoft.AspNetCore.Components.ParameterAttribute]
public Microsoft.AspNetCore.Components.RenderFragment NotFound { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public void Attach(Microsoft.AspNetCore.Components.RenderHandle renderHandle) { }
public void Dispose() { }
System.Threading.Tasks.Task Microsoft.AspNetCore.Components.IHandleAfterRender.OnAfterRenderAsync() { throw null; }
protected virtual void Render(Microsoft.AspNetCore.Components.RenderTree.RenderTreeBuilder builder, System.Type handler, System.Collections.Generic.IDictionary<string, object> parameters) { }
public System.Threading.Tasks.Task SetParametersAsync(Microsoft.AspNetCore.Components.ParameterView parameters) { throw null; }
}
}

View File

@ -0,0 +1,118 @@
// 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.Authorization;
using Microsoft.AspNetCore.Components.Auth;
using Microsoft.AspNetCore.Components.RenderTree;
namespace Microsoft.AspNetCore.Components
{
/// <summary>
/// Combines the behaviors of <see cref="AuthorizeView"/> and <see cref="RouteView"/>,
/// so that it displays the page matching the specified route but only if the user
/// is authorized to see it.
///
/// Additionally, this component supplies a cascading parameter of type <see cref="Task{AuthenticationState}"/>,
/// which makes the user's current authentication state available to descendants.
/// </summary>
public sealed class AuthorizeRouteView : RouteView
{
// We expect applications to supply their own authorizing/not-authorized content, but
// it's better to have defaults than to make the parameters mandatory because in some
// cases they will never be used (e.g., "authorizing" in out-of-box server-side Blazor)
private static readonly RenderFragment<AuthenticationState> _defaultNotAuthorizedContent
= state => builder => builder.AddContent(0, "Not authorized");
private static readonly RenderFragment _defaultAuthorizingContent
= builder => builder.AddContent(0, "Authorizing...");
private readonly RenderFragment _renderAuthorizeRouteViewCoreDelegate;
private readonly RenderFragment<AuthenticationState> _renderAuthorizedDelegate;
private readonly RenderFragment<AuthenticationState> _renderNotAuthorizedDelegate;
private readonly RenderFragment _renderAuthorizingDelegate;
public AuthorizeRouteView()
{
// Cache the rendering delegates so that we only construct new closure instances
// when they are actually used (e.g., we never prepare a RenderFragment bound to
// the NotAuthorized content except when you are displaying that particular state)
RenderFragment renderBaseRouteViewDelegate = builder => base.Render(builder);
_renderAuthorizedDelegate = authenticateState => renderBaseRouteViewDelegate;
_renderNotAuthorizedDelegate = authenticationState => builder => RenderNotAuthorizedInDefaultLayout(builder, authenticationState);
_renderAuthorizingDelegate = RenderAuthorizingInDefaultLayout;
_renderAuthorizeRouteViewCoreDelegate = RenderAuthorizeRouteViewCore;
}
/// <summary>
/// The content that will be displayed if the user is not authorized.
/// </summary>
[Parameter]
public RenderFragment<AuthenticationState> NotAuthorized { get; set; }
/// <summary>
/// The content that will be displayed while asynchronous authorization is in progress.
/// </summary>
[Parameter]
public RenderFragment Authorizing { get; set; }
[CascadingParameter]
private Task<AuthenticationState> ExistingCascadedAuthenticationState { get; set; }
/// <inheritdoc />
protected override void Render(RenderTreeBuilder builder)
{
if (ExistingCascadedAuthenticationState != null)
{
// If this component is already wrapped in a <CascadingAuthenticationState> (or another
// compatible provider), then don't interfere with the cascaded authentication state.
_renderAuthorizeRouteViewCoreDelegate(builder);
}
else
{
// Otherwise, implicitly wrap the output in a <CascadingAuthenticationState>
builder.OpenComponent<CascadingAuthenticationState>(0);
builder.AddAttribute(1, nameof(CascadingAuthenticationState.ChildContent), _renderAuthorizeRouteViewCoreDelegate);
builder.CloseComponent();
}
}
private void RenderAuthorizeRouteViewCore(RenderTreeBuilder builder)
{
builder.OpenComponent<AuthorizeRouteViewCore>(0);
builder.AddAttribute(1, nameof(AuthorizeRouteViewCore.RouteData), RouteData);
builder.AddAttribute(2, nameof(AuthorizeRouteViewCore.Authorized), _renderAuthorizedDelegate);
builder.AddAttribute(3, nameof(AuthorizeRouteViewCore.Authorizing), _renderAuthorizingDelegate);
builder.AddAttribute(4, nameof(AuthorizeRouteViewCore.NotAuthorized), _renderNotAuthorizedDelegate);
builder.CloseComponent();
}
private void RenderContentInDefaultLayout(RenderTreeBuilder builder, RenderFragment content)
{
builder.OpenComponent<LayoutView>(0);
builder.AddAttribute(1, nameof(LayoutView.Layout), DefaultLayout);
builder.AddAttribute(2, nameof(LayoutView.ChildContent), content);
builder.CloseComponent();
}
private void RenderNotAuthorizedInDefaultLayout(RenderTreeBuilder builder, AuthenticationState authenticationState)
{
var content = NotAuthorized ?? _defaultNotAuthorizedContent;
RenderContentInDefaultLayout(builder, content(authenticationState));
}
private void RenderAuthorizingInDefaultLayout(RenderTreeBuilder builder)
{
var content = Authorizing ?? _defaultAuthorizingContent;
RenderContentInDefaultLayout(builder, content);
}
private class AuthorizeRouteViewCore : AuthorizeViewCore
{
[Parameter]
public RouteData RouteData { get; set; }
protected override IAuthorizeData[] GetAuthorizeData()
=> AttributeAuthorizeDataCache.GetAuthorizeDataForType(RouteData.PageType);
}
}
}

View File

@ -52,6 +52,8 @@ namespace Microsoft.AspNetCore.Components
/// <inheritdoc />
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
// We're using the same sequence number for each of the content items here
// so that we can update existing instances if they are the same shape
if (currentAuthenticationState == null)
{
builder.AddContent(0, Authorizing);
@ -59,11 +61,11 @@ namespace Microsoft.AspNetCore.Components
else if (isAuthorized)
{
var authorized = Authorized ?? ChildContent;
builder.AddContent(1, authorized?.Invoke(currentAuthenticationState));
builder.AddContent(0, authorized?.Invoke(currentAuthenticationState));
}
else
{
builder.AddContent(2, NotAuthorized?.Invoke(currentAuthenticationState));
builder.AddContent(0, NotAuthorized?.Invoke(currentAuthenticationState));
}
}
@ -102,6 +104,12 @@ namespace Microsoft.AspNetCore.Components
private async Task<bool> IsAuthorizedAsync(ClaimsPrincipal user)
{
var authorizeData = GetAuthorizeData();
if (authorizeData == null)
{
// No authorization applies, so no need to consult the authorization service
return true;
}
EnsureNoAuthenticationSchemeSpecified(authorizeData);
var policy = await AuthorizationPolicy.CombineAsync(

View File

@ -0,0 +1,77 @@
// 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.Reflection;
using System.Threading.Tasks;
namespace Microsoft.AspNetCore.Components
{
/// <summary>
/// Displays the specified content inside the specified layout and any further
/// nested layouts.
/// </summary>
public class LayoutView : IComponent
{
private static readonly RenderFragment EmptyRenderFragment = builder => { };
private RenderHandle _renderHandle;
/// <summary>
/// Gets or sets the content to display.
/// </summary>
[Parameter]
public RenderFragment ChildContent { get; set; }
/// <summary>
/// Gets or sets the type of the layout in which to display the content.
/// The type must implement <see cref="IComponent"/> and accept a parameter named <see cref="LayoutComponentBase.Body"/>.
/// </summary>
[Parameter]
public Type Layout { get; set; }
/// <inheritdoc />
public void Attach(RenderHandle renderHandle)
{
_renderHandle = renderHandle;
}
/// <inheritdoc />
public Task SetParametersAsync(ParameterView parameters)
{
parameters.SetParameterProperties(this);
Render();
return Task.CompletedTask;
}
private void Render()
{
// In the middle goes the supplied content
var fragment = ChildContent ?? EmptyRenderFragment;
// Then repeatedly wrap that in each layer of nested layout until we get
// to a layout that has no parent
var layoutType = Layout;
while (layoutType != null)
{
fragment = WrapInLayout(layoutType, fragment);
layoutType = GetParentLayoutType(layoutType);
}
_renderHandle.Render(fragment);
}
private static RenderFragment WrapInLayout(Type layoutType, RenderFragment bodyParam)
{
return builder =>
{
builder.OpenComponent(0, layoutType);
builder.AddAttribute(1, LayoutComponentBase.BodyPropertyName, bodyParam);
builder.CloseComponent();
};
}
private static Type GetParentLayoutType(Type type)
=> type.GetCustomAttribute<LayoutAttribute>()?.LayoutType;
}
}

View File

@ -1,139 +0,0 @@
// 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.Collections.Generic;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Components.Auth;
using Microsoft.AspNetCore.Components.RenderTree;
namespace Microsoft.AspNetCore.Components
{
/// <summary>
/// Displays the specified page component, rendering it inside its layout
/// and any further nested layouts, plus applying any authorization rules.
/// </summary>
public class PageDisplay : IComponent
{
private RenderHandle _renderHandle;
/// <summary>
/// Gets or sets the type of the page component to display.
/// The type must implement <see cref="IComponent"/>.
/// </summary>
[Parameter]
public Type Page { get; set; }
/// <summary>
/// Gets or sets the parameters to pass to the page.
/// </summary>
[Parameter]
public IDictionary<string, object> PageParameters { get; set; }
/// <summary>
/// The content that will be displayed if the user is not authorized.
/// </summary>
[Parameter]
public RenderFragment<AuthenticationState> NotAuthorized { get; set; }
/// <summary>
/// The content that will be displayed while asynchronous authorization is in progress.
/// </summary>
[Parameter]
public RenderFragment Authorizing { get; set; }
/// <inheritdoc />
public void Attach(RenderHandle renderHandle)
{
_renderHandle = renderHandle;
}
/// <inheritdoc />
public Task SetParametersAsync(ParameterView parameters)
{
parameters.SetParameterProperties(this);
Render();
return Task.CompletedTask;
}
private void Render()
{
// In the middle goes the requested page
var fragment = (RenderFragment)RenderPageWithParameters;
// Around that goes an AuthorizeViewCore
fragment = WrapInAuthorizeViewCore(fragment);
// Then repeatedly wrap that in each layer of nested layout until we get
// to a layout that has no parent
Type layoutType = Page;
while ((layoutType = GetLayoutType(layoutType)) != null)
{
fragment = WrapInLayout(layoutType, fragment);
}
_renderHandle.Render(fragment);
}
private RenderFragment WrapInLayout(Type layoutType, RenderFragment bodyParam) => builder =>
{
builder.OpenComponent(0, layoutType);
builder.AddAttribute(1, LayoutComponentBase.BodyPropertyName, bodyParam);
builder.CloseComponent();
};
private void RenderPageWithParameters(RenderTreeBuilder builder)
{
builder.OpenComponent(0, Page);
if (PageParameters != null)
{
foreach (var kvp in PageParameters)
{
builder.AddAttribute(1, kvp.Key, kvp.Value);
}
}
builder.CloseComponent();
}
private RenderFragment WrapInAuthorizeViewCore(RenderFragment pageFragment)
{
var authorizeData = AttributeAuthorizeDataCache.GetAuthorizeDataForType(Page);
if (authorizeData == null)
{
// No authorization, so no need to wrap the fragment
return pageFragment;
}
// Some authorization data exists, so we do need to wrap the fragment
RenderFragment<AuthenticationState> authorized = context => pageFragment;
return builder =>
{
builder.OpenComponent<AuthorizeViewWithSuppliedData>(0);
builder.AddAttribute(1, nameof(AuthorizeViewWithSuppliedData.AuthorizeDataParam), authorizeData);
builder.AddAttribute(2, nameof(AuthorizeViewWithSuppliedData.Authorized), authorized);
builder.AddAttribute(3, nameof(AuthorizeViewWithSuppliedData.NotAuthorized), NotAuthorized ?? DefaultNotAuthorized);
builder.AddAttribute(4, nameof(AuthorizeViewWithSuppliedData.Authorizing), Authorizing);
builder.CloseComponent();
};
}
private static Type GetLayoutType(Type type)
=> type.GetCustomAttribute<LayoutAttribute>()?.LayoutType;
private class AuthorizeViewWithSuppliedData : AuthorizeViewCore
{
[Parameter] public IAuthorizeData[] AuthorizeDataParam { get; private set; }
protected override IAuthorizeData[] GetAuthorizeData() => AuthorizeDataParam;
}
// There has to be some default content. If we render blank by default, developers
// will find it hard to guess why their UI isn't appearing.
private static RenderFragment DefaultNotAuthorized(AuthenticationState authenticationState)
=> builder => builder.AddContent(0, "Not authorized");
}
}

View File

@ -0,0 +1,90 @@
// 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.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.RenderTree;
namespace Microsoft.AspNetCore.Components
{
/// <summary>
/// Displays the specified page component, rendering it inside its layout
/// and any further nested layouts.
/// </summary>
public class RouteView : IComponent
{
private readonly RenderFragment _renderDelegate;
private readonly RenderFragment _renderPageWithParametersDelegate;
private RenderHandle _renderHandle;
/// <summary>
/// Gets or sets the route data. This determines the page that will be
/// displayed and the parameter values that will be supplied to the page.
/// </summary>
[Parameter]
public RouteData RouteData { get; set; }
/// <summary>
/// Gets or sets the type of a layout to be used if the page does not
/// declare any layout. If specified, the type must implement <see cref="IComponent"/>
/// and accept a parameter named <see cref="LayoutComponentBase.Body"/>.
/// </summary>
[Parameter]
public Type DefaultLayout { get; set; }
public RouteView()
{
// Cache the delegate instances
_renderDelegate = Render;
_renderPageWithParametersDelegate = RenderPageWithParameters;
}
/// <inheritdoc />
public void Attach(RenderHandle renderHandle)
{
_renderHandle = renderHandle;
}
/// <inheritdoc />
public Task SetParametersAsync(ParameterView parameters)
{
parameters.SetParameterProperties(this);
if (RouteData == null)
{
throw new InvalidOperationException($"The {nameof(RouteView)} component requires a non-null value for the parameter {nameof(RouteData)}.");
}
_renderHandle.Render(_renderDelegate);
return Task.CompletedTask;
}
/// <summary>
/// Renders the component.
/// </summary>
/// <param name="builder">The <see cref="RenderTreeBuilder"/>.</param>
protected virtual void Render(RenderTreeBuilder builder)
{
var pageLayoutType = RouteData.PageType.GetCustomAttribute<LayoutAttribute>()?.LayoutType
?? DefaultLayout;
builder.OpenComponent<LayoutView>(0);
builder.AddAttribute(1, nameof(LayoutView.Layout), pageLayoutType);
builder.AddAttribute(2, nameof(LayoutView.ChildContent), _renderPageWithParametersDelegate);
builder.CloseComponent();
}
private void RenderPageWithParameters(RenderTreeBuilder builder)
{
builder.OpenComponent(0, RouteData.PageType);
foreach (var kvp in RouteData.RouteValues)
{
builder.AddAttribute(1, kvp.Key, kvp.Value);
}
builder.CloseComponent();
}
}
}

View File

@ -26,6 +26,6 @@ namespace Microsoft.AspNetCore.Components.Routing
public Type Handler { get; set; }
public IDictionary<string, object> Parameters { get; set; }
public IReadOnlyDictionary<string, object> Parameters { get; set; }
}
}

View File

@ -0,0 +1,46 @@
// 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.Collections.Generic;
namespace Microsoft.AspNetCore.Components
{
/// <summary>
/// Describes information determined during routing that specifies
/// the page to be displayed.
/// </summary>
public sealed class RouteData
{
/// <summary>
/// Constructs an instance of <see cref="RouteData"/>.
/// </summary>
/// <param name="pageType">The type of the page matching the route, which must implement <see cref="IComponent"/>.</param>
/// <param name="routeValues">The route parameter values extracted from the matched route.</param>
public RouteData(Type pageType, IReadOnlyDictionary<string, object> routeValues)
{
if (pageType == null)
{
throw new ArgumentNullException(nameof(pageType));
}
if (!typeof(IComponent).IsAssignableFrom(pageType))
{
throw new ArgumentException($"The value must implement {nameof(IComponent)}.", nameof(pageType));
}
PageType = pageType;
RouteValues = routeValues ?? throw new ArgumentNullException(nameof(routeValues));
}
/// <summary>
/// Gets the type of the page matching the route.
/// </summary>
public Type PageType { get; }
/// <summary>
/// Gets route parameter values extracted from the matched route.
/// </summary>
public IReadOnlyDictionary<string, object> RouteValues { get; }
}
}

View File

@ -3,20 +3,21 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.RenderTree;
using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Components.Routing
{
/// <summary>
/// A component that displays whichever other component corresponds to the
/// current navigation location.
/// A component that supplies route data corresponding to the current navigation state.
/// </summary>
public class Router : IComponent, IHandleAfterRender, IDisposable
{
static readonly char[] _queryOrHashStartChar = new[] { '?', '#' };
static readonly ReadOnlyDictionary<string, object> _emptyParametersDictionary
= new ReadOnlyDictionary<string, object>(new Dictionary<string, object>());
RenderHandle _renderHandle;
string _baseUri;
@ -33,25 +34,19 @@ namespace Microsoft.AspNetCore.Components.Routing
[Inject] private ILoggerFactory LoggerFactory { get; set; }
/// <summary>
/// Gets or sets the assembly that should be searched, along with its referenced
/// assemblies, for components matching the URI.
/// Gets or sets the assembly that should be searched for components matching the URI.
/// </summary>
[Parameter] public Assembly AppAssembly { get; set; }
/// <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 content to display when no match is found for the requested route.
/// </summary>
[Parameter] public RenderFragment NotFound { get; set; }
/// <summary>
/// The content that will be displayed if the user is not authorized.
/// Gets or sets the content to display when a match is found for the requested route.
/// </summary>
[Parameter] public RenderFragment<AuthenticationState> NotAuthorized { get; set; }
/// <summary>
/// The content that will be displayed while asynchronous authorization is in progress.
/// </summary>
[Parameter] public RenderFragment Authorizing { get; set; }
[Parameter] public RenderFragment<RouteData> Found { get; set; }
private RouteTable Routes { get; set; }
@ -69,6 +64,22 @@ namespace Microsoft.AspNetCore.Components.Routing
public Task SetParametersAsync(ParameterView parameters)
{
parameters.SetParameterProperties(this);
// Found content is mandatory, because even though we could use something like <RouteView ...> as a
// reasonable default, if it's not declared explicitly in the template then people will have no way
// to discover how to customize this (e.g., to add authorization).
if (Found == null)
{
throw new InvalidOperationException($"The {nameof(Router)} component requires a value for the parameter {nameof(Found)}.");
}
// NotFound content is mandatory, because even though we could display a default message like "Not found",
// it has to be specified explicitly so that it can also be wrapped in a specific layout
if (NotFound == null)
{
throw new InvalidOperationException($"The {nameof(Router)} component requires a value for the parameter {nameof(NotFound)}.");
}
Routes = RouteTableFactory.Create(AppAssembly);
Refresh(isNavigationIntercepted: false);
return Task.CompletedTask;
@ -80,7 +91,7 @@ namespace Microsoft.AspNetCore.Components.Routing
NavigationManager.LocationChanged -= OnLocationChanged;
}
private string StringUntilAny(string str, char[] chars)
private static string StringUntilAny(string str, char[] chars)
{
var firstIndex = str.IndexOfAny(chars);
return firstIndex < 0
@ -88,17 +99,6 @@ namespace Microsoft.AspNetCore.Components.Routing
: str.Substring(0, firstIndex);
}
/// <inheritdoc />
protected virtual void Render(RenderTreeBuilder builder, Type handler, IDictionary<string, object> parameters)
{
builder.OpenComponent(0, typeof(PageDisplay));
builder.AddAttribute(1, nameof(PageDisplay.Page), handler);
builder.AddAttribute(2, nameof(PageDisplay.PageParameters), parameters);
builder.AddAttribute(3, nameof(PageDisplay.NotAuthorized), NotAuthorized);
builder.AddAttribute(4, nameof(PageDisplay.Authorizing), Authorizing);
builder.CloseComponent();
}
private void Refresh(bool isNavigationIntercepted)
{
var locationPath = NavigationManager.ToBaseRelativePath(_locationAbsolute);
@ -116,16 +116,19 @@ namespace Microsoft.AspNetCore.Components.Routing
Log.NavigatingToComponent(_logger, context.Handler, locationPath, _baseUri);
_renderHandle.Render(builder => Render(builder, context.Handler, context.Parameters));
var routeData = new RouteData(
context.Handler,
context.Parameters ?? _emptyParametersDictionary);
_renderHandle.Render(Found(routeData));
}
else
{
if (!isNavigationIntercepted && NotFound != null)
if (!isNavigationIntercepted)
{
Log.DisplayingNotFound(_logger, locationPath, _baseUri);
// We did not find a Component that matches the route.
// Only show the NotFound if the application developer programatically got us here i.e we did not
// Only show the NotFound content 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(NotFound);
}

View File

@ -0,0 +1,355 @@
// 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.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Components.RenderTree;
using Microsoft.AspNetCore.Components.Test.Helpers;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
namespace Microsoft.AspNetCore.Components
{
public class AuthorizeRouteViewTest
{
private readonly static IReadOnlyDictionary<string, object> EmptyParametersDictionary = new Dictionary<string, object>();
private readonly TestAuthenticationStateProvider _authenticationStateProvider;
private readonly TestRenderer _renderer;
private readonly RouteView _authorizeRouteViewComponent;
private readonly int _authorizeRouteViewComponentId;
private readonly TestAuthorizationService _testAuthorizationService;
public AuthorizeRouteViewTest()
{
_authenticationStateProvider = new TestAuthenticationStateProvider();
_authenticationStateProvider.CurrentAuthStateTask = Task.FromResult(
new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity())));
_testAuthorizationService = new TestAuthorizationService();
var serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton<AuthenticationStateProvider>(_authenticationStateProvider);
serviceCollection.AddSingleton<IAuthorizationPolicyProvider, TestAuthorizationPolicyProvider>();
serviceCollection.AddSingleton<IAuthorizationService>(_testAuthorizationService);
_renderer = new TestRenderer(serviceCollection.BuildServiceProvider());
_authorizeRouteViewComponent = new AuthorizeRouteView();
_authorizeRouteViewComponentId = _renderer.AssignRootComponentId(_authorizeRouteViewComponent);
}
[Fact]
public void WhenAuthorized_RendersPageInsideLayout()
{
// Arrange
var routeData = new RouteData(typeof(TestPageRequiringAuthorization), new Dictionary<string, object>
{
{ nameof(TestPageRequiringAuthorization.Message), "Hello, world!" }
});
_testAuthorizationService.NextResult = AuthorizationResult.Success();
// Act
_renderer.RenderRootComponent(_authorizeRouteViewComponentId, ParameterView.FromDictionary(new Dictionary<string, object>
{
{ nameof(AuthorizeRouteView.RouteData), routeData },
{ nameof(AuthorizeRouteView.DefaultLayout), typeof(TestLayout) },
}));
// Assert: renders layout
var batch = _renderer.Batches.Single();
var layoutDiff = batch.GetComponentDiffs<TestLayout>().Single();
Assert.Collection(layoutDiff.Edits,
edit => AssertPrependText(batch, edit, "Layout starts here"),
edit =>
{
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
AssertFrame.Component<TestPageRequiringAuthorization>(batch.ReferenceFrames[edit.ReferenceFrameIndex]);
},
edit => AssertPrependText(batch, edit, "Layout ends here"));
// Assert: renders page
var pageDiff = batch.GetComponentDiffs<TestPageRequiringAuthorization>().Single();
Assert.Collection(pageDiff.Edits,
edit => AssertPrependText(batch, edit, "Hello from the page with message: Hello, world!"));
}
[Fact]
public void WhenNotAuthorized_RendersDefaultNotAuthorizedContentInsideLayout()
{
// Arrange
var routeData = new RouteData(typeof(TestPageRequiringAuthorization), EmptyParametersDictionary);
_testAuthorizationService.NextResult = AuthorizationResult.Failed();
// Act
_renderer.RenderRootComponent(_authorizeRouteViewComponentId, ParameterView.FromDictionary(new Dictionary<string, object>
{
{ nameof(AuthorizeRouteView.RouteData), routeData },
{ nameof(AuthorizeRouteView.DefaultLayout), typeof(TestLayout) },
}));
// Assert: renders layout containing "not authorized" message
var batch = _renderer.Batches.Single();
var layoutDiff = batch.GetComponentDiffs<TestLayout>().Single();
Assert.Collection(layoutDiff.Edits,
edit => AssertPrependText(batch, edit, "Layout starts here"),
edit => AssertPrependText(batch, edit, "Not authorized"),
edit => AssertPrependText(batch, edit, "Layout ends here"));
}
[Fact]
public void WhenNotAuthorized_RendersCustomNotAuthorizedContentInsideLayout()
{
// Arrange
var routeData = new RouteData(typeof(TestPageRequiringAuthorization), EmptyParametersDictionary);
_testAuthorizationService.NextResult = AuthorizationResult.Failed();
_authenticationStateProvider.CurrentAuthStateTask = Task.FromResult(new AuthenticationState(
new ClaimsPrincipal(new TestIdentity { Name = "Bert" })));
// Act
RenderFragment<AuthenticationState> customNotAuthorized =
state => builder => builder.AddContent(0, $"Go away, {state.User.Identity.Name}");
_renderer.RenderRootComponent(_authorizeRouteViewComponentId, ParameterView.FromDictionary(new Dictionary<string, object>
{
{ nameof(AuthorizeRouteView.RouteData), routeData },
{ nameof(AuthorizeRouteView.DefaultLayout), typeof(TestLayout) },
{ nameof(AuthorizeRouteView.NotAuthorized), customNotAuthorized },
}));
// Assert: renders layout containing "not authorized" message
var batch = _renderer.Batches.Single();
var layoutDiff = batch.GetComponentDiffs<TestLayout>().Single();
Assert.Collection(layoutDiff.Edits,
edit => AssertPrependText(batch, edit, "Layout starts here"),
edit => AssertPrependText(batch, edit, "Go away, Bert"),
edit => AssertPrependText(batch, edit, "Layout ends here"));
}
[Fact]
public async Task WhenAuthorizing_RendersDefaultAuthorizingContentInsideLayout()
{
// Arrange
var routeData = new RouteData(typeof(TestPageRequiringAuthorization), EmptyParametersDictionary);
var authStateTcs = new TaskCompletionSource<AuthenticationState>();
_authenticationStateProvider.CurrentAuthStateTask = authStateTcs.Task;
RenderFragment<AuthenticationState> customNotAuthorized =
state => builder => builder.AddContent(0, $"Go away, {state.User.Identity.Name}");
// Act
var firstRenderTask = _renderer.RenderRootComponentAsync(_authorizeRouteViewComponentId, ParameterView.FromDictionary(new Dictionary<string, object>
{
{ nameof(AuthorizeRouteView.RouteData), routeData },
{ nameof(AuthorizeRouteView.DefaultLayout), typeof(TestLayout) },
{ nameof(AuthorizeRouteView.NotAuthorized), customNotAuthorized },
}));
// Assert: renders layout containing "authorizing" message
Assert.False(firstRenderTask.IsCompleted);
var batch = _renderer.Batches.Single();
var layoutDiff = batch.GetComponentDiffs<TestLayout>().Single();
Assert.Collection(layoutDiff.Edits,
edit => AssertPrependText(batch, edit, "Layout starts here"),
edit => AssertPrependText(batch, edit, "Authorizing..."),
edit => AssertPrependText(batch, edit, "Layout ends here"));
// Act 2: updates when authorization completes
authStateTcs.SetResult(new AuthenticationState(
new ClaimsPrincipal(new TestIdentity { Name = "Bert" })));
await firstRenderTask;
// Assert 2: Only the layout is updated
batch = _renderer.Batches.Skip(1).Single();
var nonEmptyDiff = batch.DiffsInOrder.Where(d => d.Edits.Any()).Single();
Assert.Equal(layoutDiff.ComponentId, nonEmptyDiff.ComponentId);
Assert.Collection(nonEmptyDiff.Edits, edit =>
{
Assert.Equal(RenderTreeEditType.UpdateText, edit.Type);
Assert.Equal(1, edit.SiblingIndex);
AssertFrame.Text(batch.ReferenceFrames[edit.ReferenceFrameIndex], "Go away, Bert");
});
}
[Fact]
public void WhenAuthorizing_RendersCustomAuthorizingContentInsideLayout()
{
// Arrange
var routeData = new RouteData(typeof(TestPageRequiringAuthorization), EmptyParametersDictionary);
var authStateTcs = new TaskCompletionSource<AuthenticationState>();
_authenticationStateProvider.CurrentAuthStateTask = authStateTcs.Task;
RenderFragment customAuthorizing =
builder => builder.AddContent(0, "Hold on, we're checking your papers.");
// Act
var firstRenderTask = _renderer.RenderRootComponentAsync(_authorizeRouteViewComponentId, ParameterView.FromDictionary(new Dictionary<string, object>
{
{ nameof(AuthorizeRouteView.RouteData), routeData },
{ nameof(AuthorizeRouteView.DefaultLayout), typeof(TestLayout) },
{ nameof(AuthorizeRouteView.Authorizing), customAuthorizing },
}));
// Assert: renders layout containing "authorizing" message
Assert.False(firstRenderTask.IsCompleted);
var batch = _renderer.Batches.Single();
var layoutDiff = batch.GetComponentDiffs<TestLayout>().Single();
Assert.Collection(layoutDiff.Edits,
edit => AssertPrependText(batch, edit, "Layout starts here"),
edit => AssertPrependText(batch, edit, "Hold on, we're checking your papers."),
edit => AssertPrependText(batch, edit, "Layout ends here"));
}
[Fact]
public void WithoutCascadedAuthenticationState_WrapsOutputInCascadingAuthenticationState()
{
// Arrange/Act
var routeData = new RouteData(typeof(TestPageWithNoAuthorization), EmptyParametersDictionary);
_renderer.RenderRootComponent(_authorizeRouteViewComponentId, ParameterView.FromDictionary(new Dictionary<string, object>
{
{ nameof(AuthorizeRouteView.RouteData), routeData }
}));
// Assert
var batch = _renderer.Batches.Single();
var componentInstances = batch.ReferenceFrames
.Where(f => f.FrameType == RenderTreeFrameType.Component)
.Select(f => f.Component);
Assert.Collection(componentInstances,
// This is the hierarchy inside the AuthorizeRouteView, which contains its
// own CascadingAuthenticationState
component => Assert.IsType<CascadingAuthenticationState>(component),
component => Assert.IsType<CascadingValue<Task<AuthenticationState>>>(component),
component => Assert.IsAssignableFrom<AuthorizeViewCore>(component),
component => Assert.IsType<LayoutView>(component),
component => Assert.IsType<TestPageWithNoAuthorization>(component));
}
[Fact]
public void WithCascadedAuthenticationState_DoesNotWrapOutputInCascadingAuthenticationState()
{
// Arrange
var routeData = new RouteData(typeof(TestPageWithNoAuthorization), EmptyParametersDictionary);
var rootComponent = new AuthorizeRouteViewWithExistingCascadedAuthenticationState(
_authenticationStateProvider.CurrentAuthStateTask,
routeData);
var rootComponentId = _renderer.AssignRootComponentId(rootComponent);
// Act
_renderer.RenderRootComponent(rootComponentId);
// Assert
var batch = _renderer.Batches.Single();
var componentInstances = batch.ReferenceFrames
.Where(f => f.FrameType == RenderTreeFrameType.Component)
.Select(f => f.Component);
Assert.Collection(componentInstances,
// This is the externally-supplied cascading value
component => Assert.IsType<CascadingValue<Task<AuthenticationState>>>(component),
component => Assert.IsType<AuthorizeRouteView>(component),
// This is the hierarchy inside the AuthorizeRouteView. It doesn't contain a
// further CascadingAuthenticationState
component => Assert.IsAssignableFrom<AuthorizeViewCore>(component),
component => Assert.IsType<LayoutView>(component),
component => Assert.IsType<TestPageWithNoAuthorization>(component));
}
[Fact]
public void UpdatesOutputWhenRouteDataChanges()
{
// Arrange/Act 1: Start on some route
// Not asserting about the initial output, as that is covered by other tests
var routeData = new RouteData(typeof(TestPageWithNoAuthorization), EmptyParametersDictionary);
_renderer.RenderRootComponent(_authorizeRouteViewComponentId, ParameterView.FromDictionary(new Dictionary<string, object>
{
{ nameof(AuthorizeRouteView.RouteData), routeData },
{ nameof(AuthorizeRouteView.DefaultLayout), typeof(TestLayout) },
}));
// Act 2: Move to another route
var routeData2 = new RouteData(typeof(TestPageRequiringAuthorization), EmptyParametersDictionary);
var render2Task = _renderer.Dispatcher.InvokeAsync(() => _authorizeRouteViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary<string, object>
{
{ nameof(AuthorizeRouteView.RouteData), routeData2 },
})));
// Assert: we retain the layout instance, and mutate its contents
Assert.True(render2Task.IsCompletedSuccessfully);
Assert.Equal(2, _renderer.Batches.Count);
var batch2 = _renderer.Batches[1];
var diff = batch2.DiffsInOrder.Where(d => d.Edits.Any()).Single();
Assert.Collection(diff.Edits,
edit =>
{
// Inside the layout, we add the new content
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
Assert.Equal(1, edit.SiblingIndex);
AssertFrame.Text(batch2.ReferenceFrames[edit.ReferenceFrameIndex], "Not authorized");
},
edit =>
{
// ... and remove the old content
Assert.Equal(RenderTreeEditType.RemoveFrame, edit.Type);
Assert.Equal(2, edit.SiblingIndex);
});
}
private static void AssertPrependText(CapturedBatch batch, RenderTreeEdit edit, string text)
{
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
ref var referenceFrame = ref batch.ReferenceFrames[edit.ReferenceFrameIndex];
AssertFrame.Text(referenceFrame, text);
}
class TestPageWithNoAuthorization : ComponentBase { }
[Authorize]
class TestPageRequiringAuthorization : ComponentBase
{
[Parameter] public string Message { get; set; }
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
builder.AddContent(0, $"Hello from the page with message: {Message}");
}
}
class TestLayout : LayoutComponentBase
{
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
builder.AddContent(0, "Layout starts here");
builder.AddContent(1, Body);
builder.AddContent(2, "Layout ends here");
}
}
class AuthorizeRouteViewWithExistingCascadedAuthenticationState : AutoRenderComponent
{
private readonly Task<AuthenticationState> _authenticationState;
private readonly RouteData _routeData;
public AuthorizeRouteViewWithExistingCascadedAuthenticationState(
Task<AuthenticationState> authenticationState,
RouteData routeData)
{
_authenticationState = authenticationState;
_routeData = routeData;
}
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
builder.OpenComponent<CascadingValue<Task<AuthenticationState>>>(0);
builder.AddAttribute(1, nameof(CascadingValue<object>.Value), _authenticationState);
builder.AddAttribute(2, nameof(CascadingValue<object>.ChildContent), (RenderFragment)(builder =>
{
builder.OpenComponent<AuthorizeRouteView>(0);
builder.AddAttribute(1, nameof(AuthorizeRouteView.RouteData), _routeData);
builder.CloseComponent();
}));
builder.CloseComponent();
}
}
}
}

View File

@ -2,7 +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.Generic;
using System.Diagnostics;
using System.Linq;
using System.Security.Claims;
@ -331,15 +330,9 @@ namespace Microsoft.AspNetCore.Components
Assert.Equal(2, renderer.Batches.Count);
var batch2 = renderer.Batches[1];
var diff2 = batch2.DiffsByComponentId[authorizeViewComponentId].Single();
Assert.Collection(diff2.Edits,
edit =>
Assert.Collection(diff2.Edits, edit =>
{
Assert.Equal(RenderTreeEditType.RemoveFrame, edit.Type);
Assert.Equal(0, edit.SiblingIndex);
},
edit =>
{
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
Assert.Equal(RenderTreeEditType.UpdateText, edit.Type);
Assert.Equal(0, edit.SiblingIndex);
AssertFrame.Text(
batch2.ReferenceFrames[edit.ReferenceFrameIndex],
@ -513,15 +506,6 @@ namespace Microsoft.AspNetCore.Components
=> Task.FromResult(new AuthenticationState(
new ClaimsPrincipal(new TestIdentity { Name = username })));
class TestIdentity : IIdentity
{
public string AuthenticationType => "Test";
public bool IsAuthenticated => true;
public string Name { get; set; }
}
public TestRenderer CreateTestRenderer(IAuthorizationService authorizationService)
{
var serviceCollection = new ServiceCollection();
@ -530,52 +514,6 @@ namespace Microsoft.AspNetCore.Components
return new TestRenderer(serviceCollection.BuildServiceProvider());
}
private class TestAuthorizationService : IAuthorizationService
{
public AuthorizationResult NextResult { get; set; }
= AuthorizationResult.Failed();
public List<(ClaimsPrincipal user, object resource, IEnumerable<IAuthorizationRequirement> requirements)> AuthorizeCalls { get; }
= new List<(ClaimsPrincipal user, object resource, IEnumerable<IAuthorizationRequirement> requirements)>();
public Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object resource, IEnumerable<IAuthorizationRequirement> requirements)
{
AuthorizeCalls.Add((user, resource, requirements));
// The TestAuthorizationService doesn't actually apply any authorization requirements
// It just returns the specified NextResult, since we're not trying to test the logic
// in DefaultAuthorizationService or similar here. So it's up to tests to set a desired
// NextResult and assert that the expected criteria were passed by inspecting AuthorizeCalls.
return Task.FromResult(NextResult);
}
public Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object resource, string policyName)
=> throw new NotImplementedException();
}
private class TestAuthorizationPolicyProvider : IAuthorizationPolicyProvider
{
private readonly AuthorizationOptions options = new AuthorizationOptions();
public Task<AuthorizationPolicy> GetDefaultPolicyAsync()
=> Task.FromResult(options.DefaultPolicy);
public Task<AuthorizationPolicy> GetFallbackPolicyAsync()
=> Task.FromResult(options.FallbackPolicy);
public Task<AuthorizationPolicy> GetPolicyAsync(string policyName) => Task.FromResult(
new AuthorizationPolicy(new[]
{
new TestPolicyRequirement { PolicyName = policyName }
},
new[] { $"TestScheme:{policyName}" }));
}
public class TestPolicyRequirement : IAuthorizationRequirement
{
public string PolicyName { get; set; }
}
public class AuthorizeViewCoreWithScheme : AuthorizeViewCore
{
protected override IAuthorizeData[] GetAuthorizeData()

View File

@ -37,7 +37,7 @@ namespace Microsoft.AspNetCore.Components
{
// Arrange: Service
var services = new ServiceCollection();
var authStateProvider = new TestAuthStateProvider()
var authStateProvider = new TestAuthenticationStateProvider()
{
CurrentAuthStateTask = Task.FromResult(CreateAuthenticationState("Bert"))
};
@ -70,7 +70,7 @@ namespace Microsoft.AspNetCore.Components
// Arrange: Service
var services = new ServiceCollection();
var authStateTaskCompletionSource = new TaskCompletionSource<AuthenticationState>();
var authStateProvider = new TestAuthStateProvider()
var authStateProvider = new TestAuthenticationStateProvider()
{
CurrentAuthStateTask = authStateTaskCompletionSource.Task
};
@ -122,7 +122,7 @@ namespace Microsoft.AspNetCore.Components
{
// Arrange: Service
var services = new ServiceCollection();
var authStateProvider = new TestAuthStateProvider()
var authStateProvider = new TestAuthenticationStateProvider()
{
CurrentAuthStateTask = Task.FromResult(CreateAuthenticationState(null))
};
@ -189,21 +189,6 @@ namespace Microsoft.AspNetCore.Components
}
}
class TestAuthStateProvider : AuthenticationStateProvider
{
public Task<AuthenticationState> CurrentAuthStateTask { get; set; }
public override Task<AuthenticationState> GetAuthenticationStateAsync()
{
return CurrentAuthStateTask;
}
internal void TriggerAuthenticationStateChanged(Task<AuthenticationState> authState)
{
NotifyAuthenticationStateChanged(authState);
}
}
public static AuthenticationState CreateAuthenticationState(string username)
=> new AuthenticationState(new ClaimsPrincipal(username == null
? new ClaimsIdentity()

View File

@ -0,0 +1,22 @@
// 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
{
public class TestAuthenticationStateProvider : AuthenticationStateProvider
{
public Task<AuthenticationState> CurrentAuthStateTask { get; set; }
public override Task<AuthenticationState> GetAuthenticationStateAsync()
{
return CurrentAuthStateTask;
}
internal void TriggerAuthenticationStateChanged(Task<AuthenticationState> authState)
{
NotifyAuthenticationStateChanged(authState);
}
}
}

View File

@ -0,0 +1,31 @@
// 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.Authorization;
namespace Microsoft.AspNetCore.Components
{
public class TestAuthorizationPolicyProvider : IAuthorizationPolicyProvider
{
private readonly AuthorizationOptions options = new AuthorizationOptions();
public Task<AuthorizationPolicy> GetDefaultPolicyAsync()
=> Task.FromResult(options.DefaultPolicy);
public Task<AuthorizationPolicy> GetFallbackPolicyAsync()
=> Task.FromResult(options.FallbackPolicy);
public Task<AuthorizationPolicy> GetPolicyAsync(string policyName) => Task.FromResult(
new AuthorizationPolicy(new[]
{
new TestPolicyRequirement { PolicyName = policyName }
},
new[] { $"TestScheme:{policyName}" }));
}
public class TestPolicyRequirement : IAuthorizationRequirement
{
public string PolicyName { get; set; }
}
}

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;
using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
namespace Microsoft.AspNetCore.Components
{
public class TestAuthorizationService : IAuthorizationService
{
public AuthorizationResult NextResult { get; set; }
= AuthorizationResult.Failed();
public List<(ClaimsPrincipal user, object resource, IEnumerable<IAuthorizationRequirement> requirements)> AuthorizeCalls { get; }
= new List<(ClaimsPrincipal user, object resource, IEnumerable<IAuthorizationRequirement> requirements)>();
public Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object resource, IEnumerable<IAuthorizationRequirement> requirements)
{
AuthorizeCalls.Add((user, resource, requirements));
// The TestAuthorizationService doesn't actually apply any authorization requirements
// It just returns the specified NextResult, since we're not trying to test the logic
// in DefaultAuthorizationService or similar here. So it's up to tests to set a desired
// NextResult and assert that the expected criteria were passed by inspecting AuthorizeCalls.
return Task.FromResult(NextResult);
}
public Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object resource, string policyName)
=> throw new NotImplementedException();
}
}

View File

@ -0,0 +1,16 @@
// 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.Security.Principal;
namespace Microsoft.AspNetCore.Components
{
public class TestIdentity : IIdentity
{
public string AuthenticationType => "Test";
public bool IsAuthenticated => true;
public string Name { get; set; }
}
}

View File

@ -0,0 +1,325 @@
// 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.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Components.RenderTree;
using Microsoft.AspNetCore.Components.Test.Helpers;
using Xunit;
namespace Microsoft.AspNetCore.Components.Test
{
public class LayoutViewTest
{
private readonly TestRenderer _renderer;
private readonly LayoutView _layoutViewComponent;
private readonly int _layoutViewComponentId;
public LayoutViewTest()
{
_renderer = new TestRenderer();
_layoutViewComponent = new LayoutView();
_layoutViewComponentId = _renderer.AssignRootComponentId(_layoutViewComponent);
}
[Fact]
public void GivenNoParameters_RendersNothing()
{
// Arrange/Act
var setParametersTask = _renderer.Dispatcher.InvokeAsync(() => _layoutViewComponent.SetParametersAsync(ParameterView.Empty));
Assert.True(setParametersTask.IsCompletedSuccessfully);
var frames = _renderer.GetCurrentRenderTreeFrames(_layoutViewComponentId).AsEnumerable();
// Assert
Assert.Single(_renderer.Batches);
Assert.Empty(frames);
}
[Fact]
public void GivenContentButNoLayout_RendersContent()
{
// Arrange/Act
var setParametersTask = _renderer.Dispatcher.InvokeAsync(() => _layoutViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary<string, object>
{
{ nameof(LayoutView.ChildContent), (RenderFragment)(builder => {
builder.AddContent(123, "Hello");
builder.AddContent(456, "Goodbye");
})}
})));
Assert.True(setParametersTask.IsCompletedSuccessfully);
var frames = _renderer.GetCurrentRenderTreeFrames(_layoutViewComponentId).AsEnumerable();
// Assert
Assert.Single(_renderer.Batches);
Assert.Collection(frames,
frame => AssertFrame.Text(frame, "Hello", 123),
frame => AssertFrame.Text(frame, "Goodbye", 456));
}
[Fact]
public void GivenLayoutButNoContent_RendersLayoutWithEmptyBody()
{
// Arrange/Act
var setParametersTask = _renderer.Dispatcher.InvokeAsync(() => _layoutViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary<string, object>
{
{ nameof(LayoutView.Layout), typeof(RootLayout) }
})));
// Assert
Assert.True(setParametersTask.IsCompletedSuccessfully);
var batch = _renderer.Batches.Single();
var layoutViewFrames = _renderer.GetCurrentRenderTreeFrames(_layoutViewComponentId).AsEnumerable();
Assert.Collection(layoutViewFrames,
frame => AssertFrame.Component<RootLayout>(frame, subtreeLength: 2, sequence: 0),
frame => AssertFrame.Attribute(frame, nameof(LayoutComponentBase.Body), sequence: 1));
var rootLayoutComponentId = batch.GetComponentFrames<RootLayout>().Single().ComponentId;
var rootLayoutFrames = _renderer.GetCurrentRenderTreeFrames(rootLayoutComponentId).AsEnumerable();
Assert.Collection(rootLayoutFrames,
frame => AssertFrame.Text(frame, "RootLayout starts here", sequence: 0),
frame => AssertFrame.Region(frame, subtreeLength: 1), // i.e., empty region
frame => AssertFrame.Text(frame, "RootLayout ends here", sequence: 2));
}
[Fact]
public void RendersContentInsideLayout()
{
// Arrange/Act
var setParametersTask = _renderer.Dispatcher.InvokeAsync(() => _layoutViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary<string, object>
{
{ nameof(LayoutView.Layout), typeof(RootLayout) },
{ nameof(LayoutView.ChildContent), (RenderFragment)(builder => {
builder.AddContent(123, "Hello");
builder.AddContent(456, "Goodbye");
})}
})));
// Assert
Assert.True(setParametersTask.IsCompletedSuccessfully);
var batch = _renderer.Batches.Single();
var layoutViewFrames = _renderer.GetCurrentRenderTreeFrames(_layoutViewComponentId).AsEnumerable();
Assert.Collection(layoutViewFrames,
frame => AssertFrame.Component<RootLayout>(frame, subtreeLength: 2, sequence: 0),
frame => AssertFrame.Attribute(frame, nameof(LayoutComponentBase.Body), sequence: 1));
var rootLayoutComponentId = batch.GetComponentFrames<RootLayout>().Single().ComponentId;
var rootLayoutFrames = _renderer.GetCurrentRenderTreeFrames(rootLayoutComponentId).AsEnumerable();
Assert.Collection(rootLayoutFrames,
frame => AssertFrame.Text(frame, "RootLayout starts here", sequence: 0),
frame => AssertFrame.Region(frame, subtreeLength: 3),
frame => AssertFrame.Text(frame, "Hello", sequence: 123),
frame => AssertFrame.Text(frame, "Goodbye", sequence: 456),
frame => AssertFrame.Text(frame, "RootLayout ends here", sequence: 2));
}
[Fact]
public void RendersContentInsideNestedLayout()
{
// Arrange/Act
var setParametersTask = _renderer.Dispatcher.InvokeAsync(() => _layoutViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary<string, object>
{
{ nameof(LayoutView.Layout), typeof(NestedLayout) },
{ nameof(LayoutView.ChildContent), (RenderFragment)(builder => {
builder.AddContent(123, "Hello");
builder.AddContent(456, "Goodbye");
})}
})));
// Assert
Assert.True(setParametersTask.IsCompletedSuccessfully);
var batch = _renderer.Batches.Single();
var layoutViewFrames = _renderer.GetCurrentRenderTreeFrames(_layoutViewComponentId).AsEnumerable();
Assert.Collection(layoutViewFrames,
frame => AssertFrame.Component<RootLayout>(frame, subtreeLength: 2, sequence: 0),
frame => AssertFrame.Attribute(frame, nameof(LayoutComponentBase.Body), sequence: 1));
var rootLayoutComponentId = batch.GetComponentFrames<RootLayout>().Single().ComponentId;
var rootLayoutFrames = _renderer.GetCurrentRenderTreeFrames(rootLayoutComponentId).AsEnumerable();
Assert.Collection(rootLayoutFrames,
frame => AssertFrame.Text(frame, "RootLayout starts here", sequence: 0),
frame => AssertFrame.Region(frame, subtreeLength: 3, sequence: 1),
frame => AssertFrame.Component<NestedLayout>(frame, subtreeLength: 2, sequence: 0),
frame => AssertFrame.Attribute(frame, nameof(LayoutComponentBase.Body), sequence: 1),
frame => AssertFrame.Text(frame, "RootLayout ends here", sequence: 2));
var nestedLayoutComponentId = batch.GetComponentFrames<NestedLayout>().Single().ComponentId;
var nestedLayoutFrames = _renderer.GetCurrentRenderTreeFrames(nestedLayoutComponentId).AsEnumerable();
Assert.Collection(nestedLayoutFrames,
frame => AssertFrame.Text(frame, "NestedLayout starts here", sequence: 0),
frame => AssertFrame.Region(frame, subtreeLength: 3, sequence: 1),
frame => AssertFrame.Text(frame, "Hello", sequence: 123),
frame => AssertFrame.Text(frame, "Goodbye", sequence: 456),
frame => AssertFrame.Text(frame, "NestedLayout ends here", sequence: 2));
}
[Fact]
public void CanChangeContentWithSameLayout()
{
// Arrange
var setParametersTask = _renderer.Dispatcher.InvokeAsync(() => _layoutViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary<string, object>
{
{ nameof(LayoutView.Layout), typeof(NestedLayout) },
{ nameof(LayoutView.ChildContent), (RenderFragment)(builder => {
builder.AddContent(0, "Initial content");
})}
})));
// Act
Assert.True(setParametersTask.IsCompletedSuccessfully);
_renderer.Dispatcher.InvokeAsync(() => _layoutViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary<string, object>
{
{ nameof(LayoutView.Layout), typeof(NestedLayout) },
{ nameof(LayoutView.ChildContent), (RenderFragment)(builder => {
builder.AddContent(0, "Changed content");
})}
})));
// Assert
Assert.Equal(2, _renderer.Batches.Count);
var batch = _renderer.Batches[1];
Assert.Equal(0, batch.DisposedComponentIDs.Count);
Assert.Collection(batch.DiffsInOrder,
diff => Assert.Empty(diff.Edits), // LayoutView rerendered, but with no changes
diff => Assert.Empty(diff.Edits), // RootLayout rerendered, but with no changes
diff =>
{
// NestedLayout rerendered, patching content in place
Assert.Collection(diff.Edits, edit =>
{
Assert.Equal(RenderTreeEditType.UpdateText, edit.Type);
Assert.Equal(1, edit.SiblingIndex);
AssertFrame.Text(
batch.ReferenceFrames[edit.ReferenceFrameIndex],
"Changed content",
sequence: 0);
});
});
}
[Fact]
public void CanChangeLayout()
{
// Arrange
var setParametersTask1 = _renderer.Dispatcher.InvokeAsync(() => _layoutViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary<string, object>
{
{ nameof(LayoutView.Layout), typeof(NestedLayout) },
{ nameof(LayoutView.ChildContent), (RenderFragment)(builder => {
builder.AddContent(0, "Some content");
})}
})));
Assert.True(setParametersTask1.IsCompletedSuccessfully);
// Act
var setParametersTask2 = _renderer.Dispatcher.InvokeAsync(() => _layoutViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary<string, object>
{
{ nameof(LayoutView.Layout), typeof(OtherNestedLayout) },
})));
// Assert
Assert.True(setParametersTask2.IsCompletedSuccessfully);
Assert.Equal(2, _renderer.Batches.Count);
var batch = _renderer.Batches[1];
Assert.Equal(1, batch.DisposedComponentIDs.Count); // Disposes NestedLayout
Assert.Collection(batch.DiffsInOrder,
diff => Assert.Empty(diff.Edits), // LayoutView rerendered, but with no changes
diff =>
{
// RootLayout rerendered, changing child
Assert.Collection(diff.Edits,
edit =>
{
Assert.Equal(RenderTreeEditType.RemoveFrame, edit.Type);
Assert.Equal(1, edit.SiblingIndex);
},
edit =>
{
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
Assert.Equal(1, edit.SiblingIndex);
AssertFrame.Component<OtherNestedLayout>(
batch.ReferenceFrames[edit.ReferenceFrameIndex],
sequence: 0);
});
},
diff =>
{
// Inserts new OtherNestedLayout
Assert.Collection(diff.Edits,
edit =>
{
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
Assert.Equal(0, edit.SiblingIndex);
AssertFrame.Text(
batch.ReferenceFrames[edit.ReferenceFrameIndex],
"OtherNestedLayout starts here");
},
edit =>
{
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
Assert.Equal(1, edit.SiblingIndex);
AssertFrame.Text(
batch.ReferenceFrames[edit.ReferenceFrameIndex],
"Some content");
},
edit =>
{
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
Assert.Equal(2, edit.SiblingIndex);
AssertFrame.Text(
batch.ReferenceFrames[edit.ReferenceFrameIndex],
"OtherNestedLayout ends here");
});
});
}
private class RootLayout : AutoRenderComponent
{
[Parameter]
public RenderFragment Body { get; set; }
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
if (Body == null)
{
// Prove that we don't expect layouts to tolerate null values for Body
throw new InvalidOperationException("Got a null body when not expecting it");
}
builder.AddContent(0, "RootLayout starts here");
builder.AddContent(1, Body);
builder.AddContent(2, "RootLayout ends here");
}
}
[Layout(typeof(RootLayout))]
private class NestedLayout : AutoRenderComponent
{
[Parameter]
public RenderFragment Body { get; set; }
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
builder.AddContent(0, "NestedLayout starts here");
builder.AddContent(1, Body);
builder.AddContent(2, "NestedLayout ends here");
}
}
[Layout(typeof(RootLayout))]
private class OtherNestedLayout : AutoRenderComponent
{
[Parameter]
public RenderFragment Body { get; set; }
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
builder.AddContent(0, "OtherNestedLayout starts here");
builder.AddContent(1, Body);
builder.AddContent(2, "OtherNestedLayout ends here");
}
}
}
}

View File

@ -1,268 +0,0 @@
// 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 Microsoft.AspNetCore.Components.Test.Helpers;
using System.Collections.Generic;
using System.Linq;
using Xunit;
namespace Microsoft.AspNetCore.Components.Test
{
public class PageDisplayTest
{
private TestRenderer _renderer = new TestRenderer();
private PageDisplay _pageDisplayComponent = new PageDisplay();
private int _pageDisplayComponentId;
public PageDisplayTest()
{
_renderer = new TestRenderer();
_pageDisplayComponent = new PageDisplay();
_pageDisplayComponentId = _renderer.AssignRootComponentId(_pageDisplayComponent);
}
[Fact]
public void DisplaysComponentInsideLayout()
{
// Arrange/Act
_renderer.Dispatcher.InvokeAsync(() => _pageDisplayComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary<string, object>
{
{ nameof(PageDisplay.Page), typeof(ComponentWithLayout) }
})));
// Assert
var batch = _renderer.Batches.Single();
Assert.Collection(batch.DiffsInOrder,
diff =>
{
// First is the LayoutDisplay component, which contains a RootLayout
var singleEdit = diff.Edits.Single();
Assert.Equal(RenderTreeEditType.PrependFrame, singleEdit.Type);
AssertFrame.Component<RootLayout>(
batch.ReferenceFrames[singleEdit.ReferenceFrameIndex]);
},
diff =>
{
// ... then a RootLayout which contains a ComponentWithLayout
// First is the LayoutDisplay component, which contains a RootLayout
Assert.Collection(diff.Edits,
edit =>
{
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
AssertFrame.Text(
batch.ReferenceFrames[edit.ReferenceFrameIndex],
"RootLayout starts here");
},
edit =>
{
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
AssertFrame.Component<ComponentWithLayout>(
batch.ReferenceFrames[edit.ReferenceFrameIndex]);
},
edit =>
{
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
AssertFrame.Text(
batch.ReferenceFrames[edit.ReferenceFrameIndex],
"RootLayout ends here");
});
},
diff =>
{
// ... then the ComponentWithLayout
var singleEdit = diff.Edits.Single();
Assert.Equal(RenderTreeEditType.PrependFrame, singleEdit.Type);
AssertFrame.Text(
batch.ReferenceFrames[singleEdit.ReferenceFrameIndex],
$"{nameof(ComponentWithLayout)} is here.");
});
}
[Fact]
public void DisplaysComponentInsideNestedLayout()
{
// Arrange/Act
_renderer.Dispatcher.InvokeAsync(() => _pageDisplayComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary<string, object>
{
{ nameof(PageDisplay.Page), typeof(ComponentWithNestedLayout) }
})));
// Assert
var batch = _renderer.Batches.Single();
Assert.Collection(batch.DiffsInOrder,
// First, a LayoutDisplay containing a RootLayout
diff => AssertFrame.Component<RootLayout>(
batch.ReferenceFrames[diff.Edits[0].ReferenceFrameIndex]),
// Then a RootLayout containing a NestedLayout
diff => AssertFrame.Component<NestedLayout>(
batch.ReferenceFrames[diff.Edits[1].ReferenceFrameIndex]),
// Then a NestedLayout containing a ComponentWithNestedLayout
diff => AssertFrame.Component<ComponentWithNestedLayout>(
batch.ReferenceFrames[diff.Edits[1].ReferenceFrameIndex]),
// Then the ComponentWithNestedLayout
diff => AssertFrame.Text(
batch.ReferenceFrames[diff.Edits[0].ReferenceFrameIndex],
$"{nameof(ComponentWithNestedLayout)} is here."));
}
[Fact]
public void CanChangeDisplayedPageWithSameLayout()
{
// Arrange
_renderer.Dispatcher.InvokeAsync(() => _pageDisplayComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary<string, object>
{
{ nameof(PageDisplay.Page), typeof(ComponentWithLayout) }
})));
// Act
_renderer.Dispatcher.InvokeAsync(() => _pageDisplayComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary<string, object>
{
{ nameof(PageDisplay.Page), typeof(DifferentComponentWithLayout) }
})));
// Assert
Assert.Equal(2, _renderer.Batches.Count);
var batch = _renderer.Batches[1];
Assert.Equal(1, batch.DisposedComponentIDs.Count); // Disposed only the inner page component
Assert.Collection(batch.DiffsInOrder,
diff => Assert.Empty(diff.Edits), // LayoutDisplay rerendered, but with no changes
diff =>
{
// RootLayout rerendered
Assert.Collection(diff.Edits,
edit =>
{
// Removed old page
Assert.Equal(RenderTreeEditType.RemoveFrame, edit.Type);
Assert.Equal(1, edit.SiblingIndex);
},
edit =>
{
// Inserted new one
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
Assert.Equal(1, edit.SiblingIndex);
AssertFrame.Component<DifferentComponentWithLayout>(
batch.ReferenceFrames[edit.ReferenceFrameIndex]);
});
},
diff =>
{
// New page rendered
var singleEdit = diff.Edits.Single();
Assert.Equal(RenderTreeEditType.PrependFrame, singleEdit.Type);
AssertFrame.Text(
batch.ReferenceFrames[singleEdit.ReferenceFrameIndex],
$"{nameof(DifferentComponentWithLayout)} is here.");
});
}
[Fact]
public void CanChangeDisplayedPageWithDifferentLayout()
{
// Arrange
_renderer.Dispatcher.InvokeAsync(() => _pageDisplayComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary<string, object>
{
{ nameof(PageDisplay.Page), typeof(ComponentWithLayout) }
})));
// Act
_renderer.Dispatcher.InvokeAsync(() => _pageDisplayComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary<string, object>
{
{ nameof(PageDisplay.Page), typeof(ComponentWithNestedLayout) }
})));
// Assert
Assert.Equal(2, _renderer.Batches.Count);
var batch = _renderer.Batches[1];
Assert.Equal(1, batch.DisposedComponentIDs.Count); // Disposed only the inner page component
Assert.Collection(batch.DiffsInOrder,
diff => Assert.Empty(diff.Edits), // LayoutDisplay rerendered, but with no changes
diff =>
{
// RootLayout rerendered
Assert.Collection(diff.Edits,
edit =>
{
// Removed old page
Assert.Equal(RenderTreeEditType.RemoveFrame, edit.Type);
Assert.Equal(1, edit.SiblingIndex);
},
edit =>
{
// Inserted new nested layout
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
Assert.Equal(1, edit.SiblingIndex);
AssertFrame.Component<NestedLayout>(
batch.ReferenceFrames[edit.ReferenceFrameIndex]);
});
},
diff =>
{
// New nested layout rendered
var edit = diff.Edits[1];
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
AssertFrame.Component<ComponentWithNestedLayout>(
batch.ReferenceFrames[edit.ReferenceFrameIndex]);
},
diff =>
{
// New inner page rendered
var singleEdit = diff.Edits.Single();
Assert.Equal(RenderTreeEditType.PrependFrame, singleEdit.Type);
AssertFrame.Text(
batch.ReferenceFrames[singleEdit.ReferenceFrameIndex],
$"{nameof(ComponentWithNestedLayout)} is here.");
});
}
private class RootLayout : AutoRenderComponent
{
[Parameter]
public RenderFragment Body { get; set; }
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
builder.AddContent(0, "RootLayout starts here");
builder.AddContent(1, Body);
builder.AddContent(2, "RootLayout ends here");
}
}
[Layout(typeof(RootLayout))]
private class NestedLayout : AutoRenderComponent
{
[Parameter]
public RenderFragment Body { get; set; }
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
builder.AddContent(0, "NestedLayout starts here");
builder.AddContent(1, Body);
builder.AddContent(2, "NestedLayout ends here");
}
}
[Layout(typeof(RootLayout))]
private class ComponentWithLayout : AutoRenderComponent
{
protected override void BuildRenderTree(RenderTreeBuilder builder)
=> builder.AddContent(0, $"{nameof(ComponentWithLayout)} is here.");
}
[Layout(typeof(RootLayout))]
private class DifferentComponentWithLayout : AutoRenderComponent
{
protected override void BuildRenderTree(RenderTreeBuilder builder)
=> builder.AddContent(0, $"{nameof(DifferentComponentWithLayout)} is here.");
}
[Layout(typeof(NestedLayout))]
private class ComponentWithNestedLayout : AutoRenderComponent
{
protected override void BuildRenderTree(RenderTreeBuilder builder)
=> builder.AddContent(0, $"{nameof(ComponentWithNestedLayout)} is here.");
}
}
}

View File

@ -0,0 +1,208 @@
// 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.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Components.RenderTree;
using Microsoft.AspNetCore.Components.Test.Helpers;
using Xunit;
namespace Microsoft.AspNetCore.Components.Test
{
public class RouteViewTest
{
private readonly TestRenderer _renderer;
private readonly RouteView _routeViewComponent;
private readonly int _routeViewComponentId;
public RouteViewTest()
{
_renderer = new TestRenderer();
_routeViewComponent = new RouteView();
_routeViewComponentId = _renderer.AssignRootComponentId(_routeViewComponent);
}
[Fact]
public void ThrowsIfNoRouteDataSupplied()
{
var ex = Assert.Throws<InvalidOperationException>(() =>
{
// Throws synchronously, so no need to await
_ = _routeViewComponent.SetParametersAsync(ParameterView.Empty);
});
Assert.Equal($"The {nameof(RouteView)} component requires a non-null value for the parameter {nameof(RouteView.RouteData)}.", ex.Message);
}
[Fact]
public void RendersPageInsideLayoutView()
{
// Arrange
var routeParams = new Dictionary<string, object>
{
{ nameof(ComponentWithLayout.Message), "Test message" }
};
var routeData = new RouteData(typeof(ComponentWithLayout), routeParams);
// Act
_renderer.Dispatcher.InvokeAsync(() => _routeViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary<string, object>
{
{ nameof(RouteView.RouteData), routeData },
})));
// Assert: RouteView renders LayoutView
var batch = _renderer.Batches.Single();
var routeViewFrames = _renderer.GetCurrentRenderTreeFrames(_routeViewComponentId).AsEnumerable();
Assert.Collection(routeViewFrames,
frame => AssertFrame.Component<LayoutView>(frame, subtreeLength: 3, sequence: 0),
frame => AssertFrame.Attribute(frame, nameof(LayoutView.Layout), (object)typeof(TestLayout), sequence: 1),
frame => AssertFrame.Attribute(frame, nameof(LayoutView.ChildContent), sequence: 2));
// Assert: LayoutView renders TestLayout
var layoutViewComponentId = batch.GetComponentFrames<LayoutView>().Single().ComponentId;
var layoutViewFrames = _renderer.GetCurrentRenderTreeFrames(layoutViewComponentId).AsEnumerable();
Assert.Collection(layoutViewFrames,
frame => AssertFrame.Component<TestLayout>(frame, subtreeLength: 2, sequence: 0),
frame => AssertFrame.Attribute(frame, nameof(LayoutComponentBase.Body), sequence: 1));
// Assert: TestLayout renders page
var testLayoutComponentId = batch.GetComponentFrames<TestLayout>().Single().ComponentId;
var testLayoutFrames = _renderer.GetCurrentRenderTreeFrames(testLayoutComponentId).AsEnumerable();
Assert.Collection(testLayoutFrames,
frame => AssertFrame.Text(frame, "Layout starts here", sequence: 0),
frame => AssertFrame.Region(frame, subtreeLength: 3),
frame => AssertFrame.Component<ComponentWithLayout>(frame, sequence: 0, subtreeLength: 2),
frame => AssertFrame.Attribute(frame, nameof(ComponentWithLayout.Message), "Test message", sequence: 1),
frame => AssertFrame.Text(frame, "Layout ends here", sequence: 2));
// Assert: page itself is rendered, having received parameters from the original route data
var pageComponentId = batch.GetComponentFrames<ComponentWithLayout>().Single().ComponentId;
var pageFrames = _renderer.GetCurrentRenderTreeFrames(pageComponentId).AsEnumerable();
Assert.Collection(pageFrames,
frame => AssertFrame.Text(frame, "Hello from the page with message 'Test message'", sequence: 0));
// Assert: nothing else was rendered
Assert.Equal(4, batch.DiffsInOrder.Count);
}
[Fact]
public void UsesDefaultLayoutIfNoneSetOnPage()
{
// Arrange
var routeParams = new Dictionary<string, object>();
var routeData = new RouteData(typeof(ComponentWithoutLayout), routeParams);
// Act
_renderer.Dispatcher.InvokeAsync(() => _routeViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary<string, object>
{
{ nameof(RouteView.RouteData), routeData },
{ nameof(RouteView.DefaultLayout), typeof(OtherLayout) },
})));
// Assert: uses default layout
// Not asserting about what else gets rendered as that's covered by other tests
var batch = _renderer.Batches.Single();
var routeViewFrames = _renderer.GetCurrentRenderTreeFrames(_routeViewComponentId).AsEnumerable();
Assert.Collection(routeViewFrames,
frame => AssertFrame.Component<LayoutView>(frame, subtreeLength: 3, sequence: 0),
frame => AssertFrame.Attribute(frame, nameof(LayoutView.Layout), (object)typeof(OtherLayout), sequence: 1),
frame => AssertFrame.Attribute(frame, nameof(LayoutView.ChildContent), sequence: 2));
}
[Fact]
public void UsesNoLayoutIfNoneSetOnPageAndNoDefaultSet()
{
// Arrange
var routeParams = new Dictionary<string, object>();
var routeData = new RouteData(typeof(ComponentWithoutLayout), routeParams);
// Act
_renderer.Dispatcher.InvokeAsync(() => _routeViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary<string, object>
{
{ nameof(RouteView.RouteData), routeData },
})));
// Assert: uses no layout
// Not asserting about what else gets rendered as that's covered by other tests
var batch = _renderer.Batches.Single();
var routeViewFrames = _renderer.GetCurrentRenderTreeFrames(_routeViewComponentId).AsEnumerable();
Assert.Collection(routeViewFrames,
frame => AssertFrame.Component<LayoutView>(frame, subtreeLength: 3, sequence: 0),
frame => AssertFrame.Attribute(frame, nameof(LayoutView.Layout), (object)null, sequence: 1),
frame => AssertFrame.Attribute(frame, nameof(LayoutView.ChildContent), sequence: 2));
}
[Fact]
public void PageLayoutSupersedesDefaultLayout()
{
// Arrange
var routeParams = new Dictionary<string, object>();
var routeData = new RouteData(typeof(ComponentWithLayout), routeParams);
// Act
_renderer.Dispatcher.InvokeAsync(() => _routeViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary<string, object>
{
{ nameof(RouteView.RouteData), routeData },
{ nameof(RouteView.DefaultLayout), typeof(OtherLayout) },
})));
// Assert: uses layout specified by page
// Not asserting about what else gets rendered as that's covered by other tests
var batch = _renderer.Batches.Single();
var routeViewFrames = _renderer.GetCurrentRenderTreeFrames(_routeViewComponentId).AsEnumerable();
Assert.Collection(routeViewFrames,
frame => AssertFrame.Component<LayoutView>(frame, subtreeLength: 3, sequence: 0),
frame => AssertFrame.Attribute(frame, nameof(LayoutView.Layout), (object)typeof(TestLayout), sequence: 1),
frame => AssertFrame.Attribute(frame, nameof(LayoutView.ChildContent), sequence: 2));
}
private class ComponentWithoutLayout : AutoRenderComponent
{
[Parameter] public string Message { get; set; }
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
builder.AddContent(0, $"Hello from the page with message '{Message}'");
}
}
[Layout(typeof(TestLayout))]
private class ComponentWithLayout : AutoRenderComponent
{
[Parameter] public string Message { get; set; }
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
builder.AddContent(0, $"Hello from the page with message '{Message}'");
}
}
private class TestLayout : AutoRenderComponent
{
[Parameter]
public RenderFragment Body { get; set; }
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
builder.AddContent(0, "Layout starts here");
builder.AddContent(1, Body);
builder.AddContent(2, "Layout ends here");
}
}
private class OtherLayout : AutoRenderComponent
{
[Parameter]
public RenderFragment Body { get; set; }
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
builder.AddContent(0, "OtherLayout starts here");
builder.AddContent(1, Body);
builder.AddContent(2, "OtherLayout ends here");
}
}
}
}

View File

@ -7,6 +7,7 @@ using System.Linq;
using System.Runtime.ExceptionServices;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.RenderTree;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;
@ -48,6 +49,9 @@ namespace Microsoft.AspNetCore.Components.Test.Helpers
public new int AssignRootComponentId(IComponent component)
=> base.AssignRootComponentId(component);
public new ArrayRange<RenderTreeFrame> GetCurrentRenderTreeFrames(int componentId)
=> base.GetCurrentRenderTreeFrames(componentId);
public void RenderRootComponent(int componentId, ParameterView? parameters = default)
{
var task = Dispatcher.InvokeAsync(() => base.RenderRootComponentAsync(componentId, parameters ?? ParameterView.Empty));

View File

@ -44,6 +44,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
Browser.Equal("False", () => appElement.FindElement(By.Id("identity-authenticated")).Text);
Browser.Equal(string.Empty, () => appElement.FindElement(By.Id("identity-name")).Text);
Browser.Equal("(none)", () => appElement.FindElement(By.Id("test-claim")).Text);
AssertExpectedLayoutUsed();
}
[Fact]
@ -56,6 +57,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
Browser.Equal("True", () => appElement.FindElement(By.Id("identity-authenticated")).Text);
Browser.Equal("someone cool", () => appElement.FindElement(By.Id("identity-name")).Text);
Browser.Equal("Test claim value", () => appElement.FindElement(By.Id("test-claim")).Text);
AssertExpectedLayoutUsed();
}
[Fact]
@ -66,6 +68,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
WaitUntilExists(By.CssSelector("#no-authorization-rule .not-authorized"));
Browser.Equal("You're not authorized, anonymous", () =>
appElement.FindElement(By.CssSelector("#no-authorization-rule .not-authorized")).Text);
AssertExpectedLayoutUsed();
}
[Fact]
@ -75,6 +78,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
var appElement = MountAndNavigateToAuthTest(AuthorizeViewCases);
Browser.Equal("Welcome, Some User!", () =>
appElement.FindElement(By.CssSelector("#no-authorization-rule .authorized")).Text);
AssertExpectedLayoutUsed();
}
[Fact]
@ -84,6 +88,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
var appElement = MountAndNavigateToAuthTest(AuthorizeViewCases);
Browser.Equal("Welcome, Some User!", () =>
appElement.FindElement(By.CssSelector("#authorize-role .authorized")).Text);
AssertExpectedLayoutUsed();
}
[Fact]
@ -93,6 +98,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
var appElement = MountAndNavigateToAuthTest(AuthorizeViewCases);
Browser.Equal("You're not authorized, Some User", () =>
appElement.FindElement(By.CssSelector("#authorize-role .not-authorized")).Text);
AssertExpectedLayoutUsed();
}
[Fact]
@ -102,6 +108,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
var appElement = MountAndNavigateToAuthTest(AuthorizeViewCases);
Browser.Equal("Welcome, Bert!", () =>
appElement.FindElement(By.CssSelector("#authorize-policy .authorized")).Text);
AssertExpectedLayoutUsed();
}
[Fact]
@ -111,6 +118,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
var appElement = MountAndNavigateToAuthTest(AuthorizeViewCases);
Browser.Equal("You're not authorized, Mallory", () =>
appElement.FindElement(By.CssSelector("#authorize-policy .not-authorized")).Text);
AssertExpectedLayoutUsed();
}
[Fact]
@ -120,6 +128,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
var appElement = MountAndNavigateToAuthTest(PageAllowingAnonymous);
Browser.Equal("Welcome to PageAllowingAnonymous!", () =>
appElement.FindElement(By.CssSelector("#auth-success")).Text);
AssertExpectedLayoutUsed();
}
[Fact]
@ -129,6 +138,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
var appElement = MountAndNavigateToAuthTest(PageAllowingAnonymous);
Browser.Equal("Welcome to PageAllowingAnonymous!", () =>
appElement.FindElement(By.CssSelector("#auth-success")).Text);
AssertExpectedLayoutUsed();
}
[Fact]
@ -138,6 +148,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
var appElement = MountAndNavigateToAuthTest(PageRequiringAuthorization);
Browser.Equal("Welcome to PageRequiringAuthorization!", () =>
appElement.FindElement(By.CssSelector("#auth-success")).Text);
AssertExpectedLayoutUsed();
}
[Fact]
@ -147,6 +158,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
var appElement = MountAndNavigateToAuthTest(PageRequiringAuthorization);
Browser.Equal("Sorry, anonymous, you're not authorized.", () =>
appElement.FindElement(By.CssSelector("#auth-failure")).Text);
AssertExpectedLayoutUsed();
}
[Fact]
@ -156,6 +168,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
var appElement = MountAndNavigateToAuthTest(PageRequiringPolicy);
Browser.Equal("Welcome to PageRequiringPolicy!", () =>
appElement.FindElement(By.CssSelector("#auth-success")).Text);
AssertExpectedLayoutUsed();
}
[Fact]
@ -165,6 +178,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
var appElement = MountAndNavigateToAuthTest(PageRequiringPolicy);
Browser.Equal("Sorry, Mallory, you're not authorized.", () =>
appElement.FindElement(By.CssSelector("#auth-failure")).Text);
AssertExpectedLayoutUsed();
}
[Fact]
@ -174,6 +188,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
var appElement = MountAndNavigateToAuthTest(PageRequiringRole);
Browser.Equal("Welcome to PageRequiringRole!", () =>
appElement.FindElement(By.CssSelector("#auth-success")).Text);
AssertExpectedLayoutUsed();
}
[Fact]
@ -183,6 +198,12 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
var appElement = MountAndNavigateToAuthTest(PageRequiringRole);
Browser.Equal("Sorry, Bert, you're not authorized.", () =>
appElement.FindElement(By.CssSelector("#auth-failure")).Text);
AssertExpectedLayoutUsed();
}
private void AssertExpectedLayoutUsed()
{
WaitUntilExists(By.Id("auth-links"));
}
protected IWebElement MountAndNavigateToAuthTest(string authLinkText)

View File

@ -8,19 +8,21 @@
and @page authorization rules.
*@
<CascadingAuthenticationState>
<Router AppAssembly=typeof(BasicTestApp.Program).Assembly>
<Authorizing>Authorizing...</Authorizing>
<NotAuthorized>
<div id="auth-failure">
Sorry, @(context.User.Identity.Name ?? "anonymous"), you're not authorized.
</div>
</NotAuthorized>
</Router>
</CascadingAuthenticationState>
<hr />
<Links />
<Router AppAssembly="@typeof(BasicTestApp.Program).Assembly">
<Found Context="routeData">
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(AuthRouterLayout)">
<Authorizing>Authorizing...</Authorizing>
<NotAuthorized>
<div id="auth-failure">
Sorry, @(context.User.Identity.Name ?? "anonymous"), you're not authorized.
</div>
</NotAuthorized>
</AuthorizeRouteView>
</Found>
<NotFound>
<p>There's nothing here</p>
</NotFound>
</Router>
@code {
protected override void OnInitialized()

View File

@ -1,3 +1,8 @@
@inherits LayoutComponentBase
@Body
<hr />
<ul id="auth-links">
<li><a href="AuthHome">Home</a></li>
<li><a href="CascadingAuthenticationStateConsumer">Cascading authentication state</a></li>

View File

@ -1,4 +1,3 @@
@page "/"
@page "/Default.html"
<div id="test-info">This is the default page.</div>
<Links />

View File

@ -38,4 +38,3 @@
<a href="/subdir/NotAComponent.html">Not a component</a>
<a href="/subdir/routeablecomponentfrompackage.html">Cannot route to me</a>

View File

@ -9,5 +9,3 @@
<button id="go-to-longpage2" @onclick="@(() => NavigationManager.NavigateTo("LongPage2"))">
Navigate programmatically to long page 2
</button>
<Links />

View File

@ -4,5 +4,3 @@
<div style="border: 2px dashed blue; margin: 1rem; padding: 1rem; height: 1500px;">
Scroll past me to find the links
</div>
<Links />

View File

@ -1,3 +1,2 @@
@page "/Other"
<div id="test-info">This is another page.</div>
<Links />

View File

@ -1,6 +1,11 @@
@using Microsoft.AspNetCore.Components.Routing
<Router AppAssembly=typeof(BasicTestApp.Program).Assembly>
<Router AppAssembly="@typeof(BasicTestApp.Program).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" />
</Found>
<NotFound>
<div id="test-info">Oops, that component wasn't found!</div>
<LayoutView Layout="@typeof(RouterTestLayout)">
<div id="test-info">Oops, that component wasn't found!</div>
</LayoutView>
</NotFound>
</Router>

View File

@ -1,7 +1,6 @@
@page "/WithParameters/Name/{firstName}"
@page "/WithParameters/Name/{firstName}/LastName/{lastName}"
<div id="test-info">Your full name is @FirstName @LastName.</div>
<Links />
@code
{

View File

@ -0,0 +1 @@
@layout RouterTestLayout

View File

@ -0,0 +1,6 @@
@using Microsoft.AspNetCore.Components
@inherits LayoutComponentBase
@Body
<BasicTestApp.RouterTest.Links />

View File

@ -1,10 +1,16 @@
@using Microsoft.AspNetCore.Components;
<!--
Configuring this stuff here is temporary. Later we'll move the app config
into Startup.cs, and it won't be necessary to specify AppAssembly.
-->
<CascadingValue Value="Name" Name="Name" IsFixed=true>
<Router AppAssembly=typeof(ComponentsApp.App.App).Assembly />
<Router AppAssembly="@typeof(ComponentsApp.App.App).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
</Found>
<NotFound>
<LayoutView Layout="@typeof(MainLayout)">
<h2>Not found</h2>
Sorry, there's nothing at this address.
</LayoutView>
</NotFound>
</Router>
</CascadingValue>
@code{

View File

@ -1 +0,0 @@
@layout MainLayout

View File

@ -1,6 +1,7 @@
@using Microsoft.AspNetCore.Components.Routing
Router component
<Router AppAssembly="System.Reflection.Assembly.GetAssembly(typeof(RouterContainer))">
<Found Context="routeData"><RouteView RouteData="@routeData" /></Found>
<NotFound>
<p>Route not found</p>
</NotFound>

View File

@ -1,7 +1,14 @@
<CascadingAuthenticationState>
<Router AppAssembly="typeof(Startup).Assembly">
<NotFound>
<Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData">
@*#if (OrganizationalAuth || IndividualAuth)
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
#else
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
#endif*@
</Found>
<NotFound>
<LayoutView Layout="@typeof(MainLayout)">
<p>Sorry, there's nothing at this address.</p>
</NotFound>
</Router>
</CascadingAuthenticationState>
</LayoutView>
</NotFound>
</Router>

View File

@ -917,7 +917,6 @@
"Pages/FetchData.razor",
"Pages/Index.razor",
"Pages/_Host.cshtml",
"Pages/_Imports.razor",
"Properties/launchSettings.json",
"Shared/LoginDisplay.razor",
"Shared/MainLayout.razor",
@ -954,7 +953,6 @@
"Pages/FetchData.razor",
"Pages/Index.razor",
"Pages/_Host.cshtml",
"Pages/_Imports.razor",
"Properties/launchSettings.json",
"Shared/LoginDisplay.razor",
"Shared/MainLayout.razor",
@ -991,7 +989,6 @@
"Pages/FetchData.razor",
"Pages/Index.razor",
"Pages/_Host.cshtml",
"Pages/_Imports.razor",
"Properties/launchSettings.json",
"Shared/LoginDisplay.razor",
"Shared/MainLayout.razor",
@ -1028,7 +1025,6 @@
"Pages/FetchData.razor",
"Pages/Index.razor",
"Pages/_Host.cshtml",
"Pages/_Imports.razor",
"Properties/launchSettings.json",
"Shared/MainLayout.razor",
"Shared/NavMenu.razor",
@ -1064,7 +1060,6 @@
"Pages/FetchData.razor",
"Pages/Index.razor",
"Pages/_Host.cshtml",
"Pages/_Imports.razor",
"Properties/launchSettings.json",
"Shared/LoginDisplay.razor",
"Shared/MainLayout.razor",
@ -1101,7 +1096,6 @@
"Pages/FetchData.razor",
"Pages/Index.razor",
"Pages/_Host.cshtml",
"Pages/_Imports.razor",
"Properties/launchSettings.json",
"Shared/LoginDisplay.razor",
"Shared/MainLayout.razor",