+ Sorry, there's nothing at this address.
+
+
+
diff --git a/src/Components/Blazor/testassets/StandaloneApp/Pages/_Imports.razor b/src/Components/Blazor/testassets/StandaloneApp/Pages/_Imports.razor
deleted file mode 100644
index 5e11c2a20c..0000000000
--- a/src/Components/Blazor/testassets/StandaloneApp/Pages/_Imports.razor
+++ /dev/null
@@ -1 +0,0 @@
-@layout MainLayout
diff --git a/src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.cs b/src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.cs
index c3f77f7714..a0459a4d6f 100644
--- a/src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.cs
+++ b/src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.cs
@@ -16,6 +16,15 @@ namespace Microsoft.AspNetCore.Components
public abstract System.Threading.Tasks.Task GetAuthenticationStateAsync();
protected void NotifyAuthenticationStateChanged(System.Threading.Tasks.Task 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 NotAuthorized { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
+ protected override void Render(Microsoft.AspNetCore.Components.Rendering.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 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 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 routeValues) { }
+ public System.Type PageType { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
+ public System.Collections.Generic.IReadOnlyDictionary 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.Rendering.RenderTreeBuilder builder) { }
+ public System.Threading.Tasks.Task SetParametersAsync(Microsoft.AspNetCore.Components.ParameterView parameters) { throw null; }
+ }
}
namespace Microsoft.AspNetCore.Components.CompilerServices
{
@@ -513,13 +535,13 @@ namespace Microsoft.AspNetCore.Components.Rendering
public Renderer(System.IServiceProvider serviceProvider, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory) { }
public abstract Microsoft.AspNetCore.Components.Dispatcher Dispatcher { get; }
public event System.UnhandledExceptionEventHandler UnhandledSynchronizationException { add { } remove { } }
- protected internal virtual void AddToRenderQueue(int componentId, Microsoft.AspNetCore.Components.RenderFragment renderFragment) { }
protected internal int AssignRootComponentId(Microsoft.AspNetCore.Components.IComponent component) { throw null; }
public virtual System.Threading.Tasks.Task DispatchEventAsync(ulong eventHandlerId, Microsoft.AspNetCore.Components.Rendering.EventFieldInfo fieldInfo, System.EventArgs eventArgs) { throw null; }
public void Dispose() { }
protected virtual void Dispose(bool disposing) { }
protected abstract void HandleException(System.Exception exception);
protected Microsoft.AspNetCore.Components.IComponent InstantiateComponent(System.Type componentType) { throw null; }
+ protected virtual void ProcessPendingRender() { }
protected System.Threading.Tasks.Task RenderRootComponentAsync(int componentId) { throw null; }
[System.Diagnostics.DebuggerStepThroughAttribute]
protected System.Threading.Tasks.Task RenderRootComponentAsync(int componentId, Microsoft.AspNetCore.Components.ParameterView initialParameters) { throw null; }
@@ -648,9 +670,7 @@ 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 NotAuthorized { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
+ public Microsoft.AspNetCore.Components.RenderFragment 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) { }
diff --git a/src/Components/Components/src/Auth/AuthorizeRouteView.cs b/src/Components/Components/src/Auth/AuthorizeRouteView.cs
new file mode 100644
index 0000000000..b0d01ab093
--- /dev/null
+++ b/src/Components/Components/src/Auth/AuthorizeRouteView.cs
@@ -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.Rendering;
+
+namespace Microsoft.AspNetCore.Components
+{
+ ///
+ /// Combines the behaviors of and ,
+ /// 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 ,
+ /// which makes the user's current authentication state available to descendants.
+ ///
+ 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 _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 _renderAuthorizedDelegate;
+ private readonly RenderFragment _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;
+ }
+
+ ///
+ /// The content that will be displayed if the user is not authorized.
+ ///
+ [Parameter]
+ public RenderFragment NotAuthorized { get; set; }
+
+ ///
+ /// The content that will be displayed while asynchronous authorization is in progress.
+ ///
+ [Parameter]
+ public RenderFragment Authorizing { get; set; }
+
+ [CascadingParameter]
+ private Task ExistingCascadedAuthenticationState { get; set; }
+
+ ///
+ protected override void Render(RenderTreeBuilder builder)
+ {
+ if (ExistingCascadedAuthenticationState != null)
+ {
+ // If this component is already wrapped in a (or another
+ // compatible provider), then don't interfere with the cascaded authentication state.
+ _renderAuthorizeRouteViewCoreDelegate(builder);
+ }
+ else
+ {
+ // Otherwise, implicitly wrap the output in a
+ builder.OpenComponent(0);
+ builder.AddAttribute(1, nameof(CascadingAuthenticationState.ChildContent), _renderAuthorizeRouteViewCoreDelegate);
+ builder.CloseComponent();
+ }
+ }
+
+ private void RenderAuthorizeRouteViewCore(RenderTreeBuilder builder)
+ {
+ builder.OpenComponent(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(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);
+ }
+ }
+}
diff --git a/src/Components/Components/src/Auth/AuthorizeViewCore.cs b/src/Components/Components/src/Auth/AuthorizeViewCore.cs
index 05ed8817ed..cdbce6e8d2 100644
--- a/src/Components/Components/src/Auth/AuthorizeViewCore.cs
+++ b/src/Components/Components/src/Auth/AuthorizeViewCore.cs
@@ -52,6 +52,8 @@ namespace Microsoft.AspNetCore.Components
///
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 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(
diff --git a/src/Components/Components/src/ComponentBase.cs b/src/Components/Components/src/ComponentBase.cs
index 7e5fd6507a..018c016975 100644
--- a/src/Components/Components/src/ComponentBase.cs
+++ b/src/Components/Components/src/ComponentBase.cs
@@ -171,10 +171,25 @@ namespace Microsoft.AspNetCore.Components
_renderHandle = renderHandle;
}
+
///
- /// Method invoked to apply initial or updated parameters to the component.
+ /// Sets parameters supplied by the component's parent in the render tree.
///
- /// The parameters to apply.
+ /// The parameters.
+ /// A that completes when the component has finished updating and rendering itself.
+ ///
+ ///
+ /// The method should be passed the entire set of parameter values each
+ /// time is called. It not required that the caller supply a parameter
+ /// value for all parameters that are logically understood by the component.
+ ///
+ ///
+ /// The default implementation of will set the value of each property
+ /// decorated with or that has
+ /// a corresponding value in the . Parameters that do not have a corresponding value
+ /// will be unchanged.
+ ///
+ ///
public virtual Task SetParametersAsync(ParameterView parameters)
{
parameters.SetParameterProperties(this);
diff --git a/src/Components/Components/src/ComponentFactory.cs b/src/Components/Components/src/ComponentFactory.cs
index aebf96bc06..bf5de30d2a 100644
--- a/src/Components/Components/src/ComponentFactory.cs
+++ b/src/Components/Components/src/ComponentFactory.cs
@@ -61,7 +61,7 @@ namespace Microsoft.AspNetCore.Components
(
propertyName: property.Name,
propertyType: property.PropertyType,
- setter: MemberAssignment.CreatePropertySetter(type, property)
+ setter: MemberAssignment.CreatePropertySetter(type, property, cascading: false)
)).ToArray();
return Initialize;
diff --git a/src/Components/Components/src/IComponent.cs b/src/Components/Components/src/IComponent.cs
index ec1059b1e6..936cd37944 100644
--- a/src/Components/Components/src/IComponent.cs
+++ b/src/Components/Components/src/IComponent.cs
@@ -21,6 +21,11 @@ namespace Microsoft.AspNetCore.Components
///
/// The parameters.
/// A that completes when the component has finished updating and rendering itself.
+ ///
+ /// The method should be passed the entire set of parameter values each
+ /// time is called. It not required that the caller supply a parameter
+ /// value for all parameters that are logically understood by the component.
+ ///
Task SetParametersAsync(ParameterView parameters);
}
}
diff --git a/src/Components/Components/src/LayoutView.cs b/src/Components/Components/src/LayoutView.cs
new file mode 100644
index 0000000000..7b88d181f0
--- /dev/null
+++ b/src/Components/Components/src/LayoutView.cs
@@ -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
+{
+ ///
+ /// Displays the specified content inside the specified layout and any further
+ /// nested layouts.
+ ///
+ public class LayoutView : IComponent
+ {
+ private static readonly RenderFragment EmptyRenderFragment = builder => { };
+
+ private RenderHandle _renderHandle;
+
+ ///
+ /// Gets or sets the content to display.
+ ///
+ [Parameter]
+ public RenderFragment ChildContent { get; set; }
+
+ ///
+ /// Gets or sets the type of the layout in which to display the content.
+ /// The type must implement and accept a parameter named .
+ ///
+ [Parameter]
+ public Type Layout { get; set; }
+
+ ///
+ public void Attach(RenderHandle renderHandle)
+ {
+ _renderHandle = renderHandle;
+ }
+
+ ///
+ 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()?.LayoutType;
+ }
+}
diff --git a/src/Components/Components/src/PageDisplay.cs b/src/Components/Components/src/PageDisplay.cs
deleted file mode 100644
index 07d6d4099d..0000000000
--- a/src/Components/Components/src/PageDisplay.cs
+++ /dev/null
@@ -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.Rendering;
-
-namespace Microsoft.AspNetCore.Components
-{
- ///
- /// Displays the specified page component, rendering it inside its layout
- /// and any further nested layouts, plus applying any authorization rules.
- ///
- public class PageDisplay : IComponent
- {
- private RenderHandle _renderHandle;
-
- ///
- /// Gets or sets the type of the page component to display.
- /// The type must implement .
- ///
- [Parameter]
- public Type Page { get; set; }
-
- ///
- /// Gets or sets the parameters to pass to the page.
- ///
- [Parameter]
- public IDictionary PageParameters { get; set; }
-
- ///
- /// The content that will be displayed if the user is not authorized.
- ///
- [Parameter]
- public RenderFragment NotAuthorized { get; set; }
-
- ///
- /// The content that will be displayed while asynchronous authorization is in progress.
- ///
- [Parameter]
- public RenderFragment Authorizing { get; set; }
-
- ///
- public void Attach(RenderHandle renderHandle)
- {
- _renderHandle = renderHandle;
- }
-
- ///
- 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 authorized = context => pageFragment;
- return builder =>
- {
- builder.OpenComponent(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()?.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");
- }
-}
diff --git a/src/Components/Components/src/Reflection/ComponentProperties.cs b/src/Components/Components/src/Reflection/ComponentProperties.cs
index 5caeab20b1..04f2f8d0c1 100644
--- a/src/Components/Components/src/Reflection/ComponentProperties.cs
+++ b/src/Components/Components/src/Reflection/ComponentProperties.cs
@@ -14,6 +14,9 @@ namespace Microsoft.AspNetCore.Components.Reflection
{
private const BindingFlags _bindablePropertyFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.IgnoreCase;
+ // Right now it's not possible for a component to define a Parameter and a Cascading Parameter with
+ // the same name. We don't give you a way to express this in code (would create duplicate properties),
+ // and we don't have the ability to represent it in our data structures.
private readonly static ConcurrentDictionary _cachedWritersByType
= new ConcurrentDictionary();
@@ -44,6 +47,24 @@ namespace Microsoft.AspNetCore.Components.Reflection
ThrowForUnknownIncomingParameterName(targetType, parameterName);
throw null; // Unreachable
}
+ else if (writer.Cascading && !parameter.Cascading)
+ {
+ // We don't allow you to set a cascading parameter with a non-cascading value. Put another way:
+ // cascading parameters are not part of the public API of a component, so it's not reasonable
+ // for someone to set it directly.
+ //
+ // If we find a strong reason for this to work in the future we can reverse our decision since
+ // this throws today.
+ ThrowForSettingCascadingParameterWithNonCascadingValue(targetType, parameterName);
+ throw null; // Unreachable
+ }
+ else if (!writer.Cascading && parameter.Cascading)
+ {
+ // We're giving a more specific error here because trying to set a non-cascading parameter
+ // with a cascading value is likely deliberate (but not supported), or is a bug in our code.
+ ThrowForSettingParameterWithCascadingValue(targetType, parameterName);
+ throw null; // Unreachable
+ }
SetProperty(target, writer, parameterName, parameter.Value);
}
@@ -62,7 +83,24 @@ namespace Microsoft.AspNetCore.Components.Reflection
}
var isUnmatchedValue = !writers.WritersByName.TryGetValue(parameterName, out var writer);
- if (isUnmatchedValue)
+
+ if ((isUnmatchedValue && parameter.Cascading) || (writer != null && !writer.Cascading && parameter.Cascading))
+ {
+ // Don't allow an "extra" cascading value to be collected - or don't allow a non-cascading
+ // parameter to be set with a cascading value.
+ //
+ // This is likely a bug in our infrastructure or an attempt to deliberately do something unsupported.
+ ThrowForSettingParameterWithCascadingValue(targetType, parameterName);
+ throw null; // Unreachable
+
+ }
+ else if (isUnmatchedValue ||
+
+ // Allow unmatched parameters to collide with the names of cascading parameters. This is
+ // valid because cascading parameter names are not part of the public API. There's no
+ // way for the user of a component to know what the names of cascading parameters
+ // are.
+ (writer.Cascading && !parameter.Cascading))
{
unmatched ??= new Dictionary(StringComparer.OrdinalIgnoreCase);
unmatched[parameterName] = parameter.Value;
@@ -138,6 +176,20 @@ namespace Microsoft.AspNetCore.Components.Reflection
}
}
+ private static void ThrowForSettingCascadingParameterWithNonCascadingValue(Type targetType, string parameterName)
+ {
+ throw new InvalidOperationException(
+ $"Object of type '{targetType.FullName}' has a property matching the name '{parameterName}', " +
+ $"but it does not have [{nameof(ParameterAttribute)}] applied.");
+ }
+
+ private static void ThrowForSettingParameterWithCascadingValue(Type targetType, string parameterName)
+ {
+ throw new InvalidOperationException(
+ $"The property '{parameterName}' on component type '{targetType.FullName}' cannot be set " +
+ $"using a cascading value.");
+ }
+
private static void ThrowForCaptureUnmatchedValuesConflict(Type targetType, string parameterName, Dictionary unmatched)
{
throw new InvalidOperationException(
@@ -179,13 +231,14 @@ namespace Microsoft.AspNetCore.Components.Reflection
foreach (var propertyInfo in GetCandidateBindableProperties(targetType))
{
var parameterAttribute = propertyInfo.GetCustomAttribute();
- var isParameter = parameterAttribute != null || propertyInfo.IsDefined(typeof(CascadingParameterAttribute));
+ var cascadingParameterAttribute = propertyInfo.GetCustomAttribute();
+ var isParameter = parameterAttribute != null || cascadingParameterAttribute != null;
if (!isParameter)
{
continue;
}
- var propertySetter = MemberAssignment.CreatePropertySetter(targetType, propertyInfo);
+ var propertySetter = MemberAssignment.CreatePropertySetter(targetType, propertyInfo, cascading: cascadingParameterAttribute != null);
var propertyName = propertyInfo.Name;
if (WritersByName.ContainsKey(propertyName))
@@ -213,7 +266,7 @@ namespace Microsoft.AspNetCore.Components.Reflection
ThrowForInvalidCaptureUnmatchedValuesParameterType(targetType, propertyInfo);
}
- CaptureUnmatchedValuesWriter = MemberAssignment.CreatePropertySetter(targetType, propertyInfo);
+ CaptureUnmatchedValuesWriter = MemberAssignment.CreatePropertySetter(targetType, propertyInfo, cascading: false);
CaptureUnmatchedValuesPropertyName = propertyInfo.Name;
}
}
diff --git a/src/Components/Components/src/Reflection/IPropertySetter.cs b/src/Components/Components/src/Reflection/IPropertySetter.cs
index 575b2e669b..d6a60e2395 100644
--- a/src/Components/Components/src/Reflection/IPropertySetter.cs
+++ b/src/Components/Components/src/Reflection/IPropertySetter.cs
@@ -5,6 +5,8 @@ namespace Microsoft.AspNetCore.Components.Reflection
{
internal interface IPropertySetter
{
+ bool Cascading { get; }
+
void SetValue(object target, object value);
}
}
diff --git a/src/Components/Components/src/Reflection/MemberAssignment.cs b/src/Components/Components/src/Reflection/MemberAssignment.cs
index 0ab288cedc..b59d7d2ed7 100644
--- a/src/Components/Components/src/Reflection/MemberAssignment.cs
+++ b/src/Components/Components/src/Reflection/MemberAssignment.cs
@@ -26,7 +26,7 @@ namespace Microsoft.AspNetCore.Components.Reflection
}
}
- public static IPropertySetter CreatePropertySetter(Type targetType, PropertyInfo property)
+ public static IPropertySetter CreatePropertySetter(Type targetType, PropertyInfo property, bool cascading)
{
if (property.SetMethod == null)
{
@@ -37,19 +37,23 @@ namespace Microsoft.AspNetCore.Components.Reflection
return (IPropertySetter)Activator.CreateInstance(
typeof(PropertySetter<,>).MakeGenericType(targetType, property.PropertyType),
- property.SetMethod);
+ property.SetMethod,
+ cascading);
}
class PropertySetter : IPropertySetter
{
private readonly Action _setterDelegate;
- public PropertySetter(MethodInfo setMethod)
+ public PropertySetter(MethodInfo setMethod, bool cascading)
{
_setterDelegate = (Action)Delegate.CreateDelegate(
typeof(Action), setMethod);
+ Cascading = cascading;
}
+ public bool Cascading { get; }
+
public void SetValue(object target, object value)
{
if (value == null)
diff --git a/src/Components/Components/src/RenderTree/ArrayBuilderSegment.cs b/src/Components/Components/src/RenderTree/ArrayBuilderSegment.cs
index 18317ece48..3377e18163 100644
--- a/src/Components/Components/src/RenderTree/ArrayBuilderSegment.cs
+++ b/src/Components/Components/src/RenderTree/ArrayBuilderSegment.cs
@@ -8,9 +8,12 @@ using System.Collections.Generic;
namespace Microsoft.AspNetCore.Components.RenderTree
{
///
- /// Represents a range of elements within an instance of .
+ /// Types in the Microsoft.AspNetCore.Components.RenderTree are not recommended for use outside
+ /// of the Blazor framework. These types will change in future release.
///
/// The type of the elements in the array
+ //
+ // Represents a range of elements within an instance of .
public readonly struct ArrayBuilderSegment : IEnumerable
{
// The following fields are memory mapped to the WASM client. Do not re-order or use auto-properties.
diff --git a/src/Components/Components/src/RenderTree/ArrayRange.cs b/src/Components/Components/src/RenderTree/ArrayRange.cs
index ec113dbdb8..b27a8d5310 100644
--- a/src/Components/Components/src/RenderTree/ArrayRange.cs
+++ b/src/Components/Components/src/RenderTree/ArrayRange.cs
@@ -4,9 +4,12 @@
namespace Microsoft.AspNetCore.Components.RenderTree
{
///
- /// Represents a range of elements in an array that are in use.
+ /// Types in the Microsoft.AspNetCore.Components.RenderTree are not recommended for use outside
+ /// of the Blazor framework. These types will change in future release.
///
- /// The array item type.
+ ///
+ //
+ // Represents a range of elements in an array that are in use.
public readonly struct ArrayRange
{
///
diff --git a/src/Components/Components/src/RenderTree/RenderTreeDiff.cs b/src/Components/Components/src/RenderTree/RenderTreeDiff.cs
index 9ad92ec31b..da5b3ed3f7 100644
--- a/src/Components/Components/src/RenderTree/RenderTreeDiff.cs
+++ b/src/Components/Components/src/RenderTree/RenderTreeDiff.cs
@@ -1,13 +1,14 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
-using System;
-
namespace Microsoft.AspNetCore.Components.RenderTree
{
///
- /// Describes changes to a component's render tree between successive renders.
+ /// Types in the Microsoft.AspNetCore.Components.RenderTree are not recommended for use outside
+ /// of the Blazor framework. These types will change in future release.
///
+ //
+ // Describes changes to a component's render tree between successive renders.
public readonly struct RenderTreeDiff
{
///
diff --git a/src/Components/Components/src/RenderTree/RenderTreeEdit.cs b/src/Components/Components/src/RenderTree/RenderTreeEdit.cs
index 96f661924b..68437a7471 100644
--- a/src/Components/Components/src/RenderTree/RenderTreeEdit.cs
+++ b/src/Components/Components/src/RenderTree/RenderTreeEdit.cs
@@ -6,8 +6,11 @@ using System.Runtime.InteropServices;
namespace Microsoft.AspNetCore.Components.RenderTree
{
///
- /// Represents a single edit operation on a component's render tree.
+ /// Types in the Microsoft.AspNetCore.Components.RenderTree are not recommended for use outside
+ /// of the Blazor framework. These types will change in future release.
///
+ //
+ // Represents a single edit operation on a component's render tree.
[StructLayout(LayoutKind.Explicit)]
public readonly struct RenderTreeEdit
{
diff --git a/src/Components/Components/src/RenderTree/RenderTreeEditType.cs b/src/Components/Components/src/RenderTree/RenderTreeEditType.cs
index c2f3e4aba6..f508760135 100644
--- a/src/Components/Components/src/RenderTree/RenderTreeEditType.cs
+++ b/src/Components/Components/src/RenderTree/RenderTreeEditType.cs
@@ -4,8 +4,11 @@
namespace Microsoft.AspNetCore.Components.RenderTree
{
///
- /// Describes the type of a render tree edit operation.
+ /// Types in the Microsoft.AspNetCore.Components.RenderTree are not recommended for use outside
+ /// of the Blazor framework. These types will change in future release.
///
+ //
+ //Describes the type of a render tree edit operation.
public enum RenderTreeEditType: int
{
///
diff --git a/src/Components/Components/src/RenderTree/RenderTreeFrame.cs b/src/Components/Components/src/RenderTree/RenderTreeFrame.cs
index f3e003080b..39dd7de74a 100644
--- a/src/Components/Components/src/RenderTree/RenderTreeFrame.cs
+++ b/src/Components/Components/src/RenderTree/RenderTreeFrame.cs
@@ -8,8 +8,11 @@ using Microsoft.AspNetCore.Components.Rendering;
namespace Microsoft.AspNetCore.Components.RenderTree
{
///
- /// Represents an entry in a tree of user interface (UI) items.
+ /// Types in the Microsoft.AspNetCore.Components.RenderTree are not recommended for use outside
+ /// of the Blazor framework. These types will change in future release.
///
+ //
+ // Represents an entry in a tree of user interface (UI) items.
[StructLayout(LayoutKind.Explicit, Pack = 4)]
public readonly struct RenderTreeFrame
{
diff --git a/src/Components/Components/src/RenderTree/RenderTreeFrameType.cs b/src/Components/Components/src/RenderTree/RenderTreeFrameType.cs
index 61d2558305..339a7b6795 100644
--- a/src/Components/Components/src/RenderTree/RenderTreeFrameType.cs
+++ b/src/Components/Components/src/RenderTree/RenderTreeFrameType.cs
@@ -4,8 +4,11 @@
namespace Microsoft.AspNetCore.Components.RenderTree
{
///
- /// Describes the type of a .
+ /// Types in the Microsoft.AspNetCore.Components.RenderTree are not recommended for use outside
+ /// of the Blazor framework. These types will change in future release.
///
+ //
+ // Describes the type of a .
public enum RenderTreeFrameType: short
{
///
diff --git a/src/Components/Components/src/Rendering/Renderer.Log.cs b/src/Components/Components/src/Rendering/Renderer.Log.cs
index 3b70f58973..bd65809632 100644
--- a/src/Components/Components/src/Rendering/Renderer.Log.cs
+++ b/src/Components/Components/src/Rendering/Renderer.Log.cs
@@ -48,7 +48,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
}
}
- internal static void DisposingComponent(ILogger logger, ComponentState componentState)
+ public static void DisposingComponent(ILogger logger, ComponentState componentState)
{
if (logger.IsEnabled(LogLevel.Debug)) // This is almost always false, so skip the evaluations
{
@@ -56,7 +56,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
}
}
- internal static void HandlingEvent(ILogger logger, ulong eventHandlerId, EventArgs eventArgs)
+ public static void HandlingEvent(ILogger logger, ulong eventHandlerId, EventArgs eventArgs)
{
_handlingEvent(logger, eventHandlerId, eventArgs?.GetType().Name ?? "null", null);
}
diff --git a/src/Components/Components/src/Rendering/Renderer.cs b/src/Components/Components/src/Rendering/Renderer.cs
index 6e0ed1633e..ed3d075f0a 100644
--- a/src/Components/Components/src/Rendering/Renderer.cs
+++ b/src/Components/Components/src/Rendering/Renderer.cs
@@ -243,7 +243,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
// Since the task has yielded - process any queued rendering work before we return control
// to the caller.
- ProcessRenderQueue();
+ ProcessPendingRender();
}
// Task completed synchronously or is still running. We already processed all of the rendering
@@ -334,7 +334,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
///
/// The ID of the component to render.
/// A that will supply the updated UI contents.
- protected internal virtual void AddToRenderQueue(int componentId, RenderFragment renderFragment)
+ internal void AddToRenderQueue(int componentId, RenderFragment renderFragment)
{
EnsureSynchronizationContext();
@@ -351,7 +351,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
if (!_isBatchInProgress)
{
- ProcessRenderQueue();
+ ProcessPendingRender();
}
}
@@ -398,13 +398,33 @@ namespace Microsoft.AspNetCore.Components.Rendering
? componentState
: null;
+ ///
+ /// Processses pending renders requests from components if there are any.
+ ///
+ protected virtual void ProcessPendingRender()
+ {
+ ProcessRenderQueue();
+ }
+
private void ProcessRenderQueue()
{
+ EnsureSynchronizationContext();
+
+ if (_isBatchInProgress)
+ {
+ throw new InvalidOperationException("Cannot start a batch when one is already in progress.");
+ }
+
_isBatchInProgress = true;
var updateDisplayTask = Task.CompletedTask;
try
{
+ if (_batchBuilder.ComponentRenderQueue.Count == 0)
+ {
+ return;
+ }
+
// Process render queue until empty
while (_batchBuilder.ComponentRenderQueue.Count > 0)
{
@@ -423,6 +443,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
{
// Ensure we catch errors while running the render functions of the components.
HandleException(e);
+ return;
}
finally
{
diff --git a/src/Components/Components/src/RouteView.cs b/src/Components/Components/src/RouteView.cs
new file mode 100644
index 0000000000..6f77169e06
--- /dev/null
+++ b/src/Components/Components/src/RouteView.cs
@@ -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.Rendering;
+
+namespace Microsoft.AspNetCore.Components
+{
+ ///
+ /// Displays the specified page component, rendering it inside its layout
+ /// and any further nested layouts.
+ ///
+ public class RouteView : IComponent
+ {
+ private readonly RenderFragment _renderDelegate;
+ private readonly RenderFragment _renderPageWithParametersDelegate;
+ private RenderHandle _renderHandle;
+
+ ///
+ /// 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.
+ ///
+ [Parameter]
+ public RouteData RouteData { get; set; }
+
+ ///
+ /// 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
+ /// and accept a parameter named .
+ ///
+ [Parameter]
+ public Type DefaultLayout { get; set; }
+
+ public RouteView()
+ {
+ // Cache the delegate instances
+ _renderDelegate = Render;
+ _renderPageWithParametersDelegate = RenderPageWithParameters;
+ }
+
+ ///
+ public void Attach(RenderHandle renderHandle)
+ {
+ _renderHandle = renderHandle;
+ }
+
+ ///
+ 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;
+ }
+
+ ///
+ /// Renders the component.
+ ///
+ /// The .
+ protected virtual void Render(RenderTreeBuilder builder)
+ {
+ var pageLayoutType = RouteData.PageType.GetCustomAttribute()?.LayoutType
+ ?? DefaultLayout;
+
+ builder.OpenComponent(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();
+ }
+ }
+}
diff --git a/src/Components/Components/src/Routing/RouteContext.cs b/src/Components/Components/src/Routing/RouteContext.cs
index 7de5f3c615..7061e9be41 100644
--- a/src/Components/Components/src/Routing/RouteContext.cs
+++ b/src/Components/Components/src/Routing/RouteContext.cs
@@ -26,6 +26,6 @@ namespace Microsoft.AspNetCore.Components.Routing
public Type Handler { get; set; }
- public IDictionary Parameters { get; set; }
+ public IReadOnlyDictionary Parameters { get; set; }
}
}
diff --git a/src/Components/Components/src/Routing/RouteData.cs b/src/Components/Components/src/Routing/RouteData.cs
new file mode 100644
index 0000000000..e0da00f0c7
--- /dev/null
+++ b/src/Components/Components/src/Routing/RouteData.cs
@@ -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
+{
+ ///
+ /// Describes information determined during routing that specifies
+ /// the page to be displayed.
+ ///
+ public sealed class RouteData
+ {
+ ///
+ /// Constructs an instance of .
+ ///
+ /// The type of the page matching the route, which must implement .
+ /// The route parameter values extracted from the matched route.
+ public RouteData(Type pageType, IReadOnlyDictionary 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));
+ }
+
+ ///
+ /// Gets the type of the page matching the route.
+ ///
+ public Type PageType { get; }
+
+ ///
+ /// Gets route parameter values extracted from the matched route.
+ ///
+ public IReadOnlyDictionary RouteValues { get; }
+ }
+}
diff --git a/src/Components/Components/src/Routing/Router.cs b/src/Components/Components/src/Routing/Router.cs
index 2049929b4c..42161ff828 100644
--- a/src/Components/Components/src/Routing/Router.cs
+++ b/src/Components/Components/src/Routing/Router.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
+using System.Collections.ObjectModel;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Rendering;
@@ -11,12 +12,13 @@ using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Components.Routing
{
///
- /// 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.
///
public class Router : IComponent, IHandleAfterRender, IDisposable
{
static readonly char[] _queryOrHashStartChar = new[] { '?', '#' };
+ static readonly ReadOnlyDictionary _emptyParametersDictionary
+ = new ReadOnlyDictionary(new Dictionary());
RenderHandle _renderHandle;
string _baseUri;
@@ -33,25 +35,19 @@ namespace Microsoft.AspNetCore.Components.Routing
[Inject] private ILoggerFactory LoggerFactory { get; set; }
///
- /// 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.
///
[Parameter] public Assembly AppAssembly { get; set; }
///
- /// 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.
///
[Parameter] public RenderFragment NotFound { get; set; }
///
- /// 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.
///
- [Parameter] public RenderFragment NotAuthorized { get; set; }
-
- ///
- /// The content that will be displayed while asynchronous authorization is in progress.
- ///
- [Parameter] public RenderFragment Authorizing { get; set; }
+ [Parameter] public RenderFragment Found { get; set; }
private RouteTable Routes { get; set; }
@@ -69,6 +65,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 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 +92,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 +100,6 @@ namespace Microsoft.AspNetCore.Components.Routing
: str.Substring(0, firstIndex);
}
- ///
- protected virtual void Render(RenderTreeBuilder builder, Type handler, IDictionary 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 +117,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);
}
diff --git a/src/Components/Components/test/Auth/AuthorizeRouteViewTest.cs b/src/Components/Components/test/Auth/AuthorizeRouteViewTest.cs
new file mode 100644
index 0000000000..792132e0d0
--- /dev/null
+++ b/src/Components/Components/test/Auth/AuthorizeRouteViewTest.cs
@@ -0,0 +1,356 @@
+// 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.Rendering;
+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 EmptyParametersDictionary = new Dictionary();
+ 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);
+ serviceCollection.AddSingleton();
+ serviceCollection.AddSingleton(_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
+ {
+ { nameof(TestPageRequiringAuthorization.Message), "Hello, world!" }
+ });
+ _testAuthorizationService.NextResult = AuthorizationResult.Success();
+
+ // Act
+ _renderer.RenderRootComponent(_authorizeRouteViewComponentId, ParameterView.FromDictionary(new Dictionary
+ {
+ { nameof(AuthorizeRouteView.RouteData), routeData },
+ { nameof(AuthorizeRouteView.DefaultLayout), typeof(TestLayout) },
+ }));
+
+ // Assert: renders layout
+ var batch = _renderer.Batches.Single();
+ var layoutDiff = batch.GetComponentDiffs().Single();
+ Assert.Collection(layoutDiff.Edits,
+ edit => AssertPrependText(batch, edit, "Layout starts here"),
+ edit =>
+ {
+ Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
+ AssertFrame.Component(batch.ReferenceFrames[edit.ReferenceFrameIndex]);
+ },
+ edit => AssertPrependText(batch, edit, "Layout ends here"));
+
+ // Assert: renders page
+ var pageDiff = batch.GetComponentDiffs().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
+ {
+ { 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().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 customNotAuthorized =
+ state => builder => builder.AddContent(0, $"Go away, {state.User.Identity.Name}");
+ _renderer.RenderRootComponent(_authorizeRouteViewComponentId, ParameterView.FromDictionary(new Dictionary
+ {
+ { 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().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();
+ _authenticationStateProvider.CurrentAuthStateTask = authStateTcs.Task;
+ RenderFragment customNotAuthorized =
+ state => builder => builder.AddContent(0, $"Go away, {state.User.Identity.Name}");
+
+ // Act
+ var firstRenderTask = _renderer.RenderRootComponentAsync(_authorizeRouteViewComponentId, ParameterView.FromDictionary(new Dictionary
+ {
+ { 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().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();
+ _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
+ {
+ { 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().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
+ {
+ { 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(component),
+ component => Assert.IsType>>(component),
+ component => Assert.IsAssignableFrom(component),
+ component => Assert.IsType(component),
+ component => Assert.IsType(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>>(component),
+ component => Assert.IsType(component),
+
+ // This is the hierarchy inside the AuthorizeRouteView. It doesn't contain a
+ // further CascadingAuthenticationState
+ component => Assert.IsAssignableFrom(component),
+ component => Assert.IsType(component),
+ component => Assert.IsType(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
+ {
+ { 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
+ {
+ { 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;
+ private readonly RouteData _routeData;
+
+ public AuthorizeRouteViewWithExistingCascadedAuthenticationState(
+ Task authenticationState,
+ RouteData routeData)
+ {
+ _authenticationState = authenticationState;
+ _routeData = routeData;
+ }
+
+ protected override void BuildRenderTree(RenderTreeBuilder builder)
+ {
+ builder.OpenComponent>>(0);
+ builder.AddAttribute(1, nameof(CascadingValue