Introduce ComponentTagHelper (#14592)

* Introduce ComponentTagHelper
Fixes https://github.com/aspnet/AspNetCore/issues/13726
This commit is contained in:
Pranav K 2019-10-11 15:59:52 -07:00 committed by GitHub
commit d299ae2491
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 693 additions and 301 deletions

View File

@ -13,9 +13,7 @@
<link href="css/site.css" rel="stylesheet" /> <link href="css/site.css" rel="stylesheet" />
</head> </head>
<body> <body>
<app> <component type="typeof(App)" render-mode="ServerPrerendered" />
@(await Html.RenderComponentAsync<App>(RenderMode.ServerPrerendered))
</app>
<script src="_framework/blazor.server.js"></script> <script src="_framework/blazor.server.js"></script>
</body> </body>

View File

@ -0,0 +1,60 @@
// 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.Linq;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
using Microsoft.AspNetCore.E2ETesting;
using OpenQA.Selenium;
using TestServer;
using Xunit;
using Xunit.Abstractions;
namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
{
public class ComponentWithParametersTest : ServerTestBase<BasicTestAppServerSiteFixture<PrerenderedStartup>>
{
public ComponentWithParametersTest(
BrowserFixture browserFixture,
BasicTestAppServerSiteFixture<PrerenderedStartup> serverFixture,
ITestOutputHelper output)
: base(browserFixture, serverFixture, output)
{
}
[Fact]
public void PassingParametersToComponentsFromThePageWorks()
{
Navigate("/prerendered/componentwithparameters?QueryValue=testQueryValue");
BeginInteractivity();
Browser.Exists(By.CssSelector(".interactive"));
var parameter1 = Browser.FindElement(By.CssSelector(".Param1"));
Assert.Equal(100, parameter1.FindElements(By.CssSelector("li")).Count);
Assert.Equal("99 99", parameter1.FindElement(By.CssSelector("li:last-child")).Text);
// The assigned value is of a more derived type than the declared model type. This check
// verifies we use the actual model type during round tripping.
var parameter2 = Browser.FindElement(By.CssSelector(".Param2"));
Assert.Equal("Value Derived-Value", parameter2.Text);
// This check verifies CaptureUnmatchedValues works
var parameter3 = Browser.FindElements(By.CssSelector(".Param3 li"));
Assert.Collection(
parameter3,
p => Assert.Equal("key1 testQueryValue", p.Text),
p => Assert.Equal("key2 43", p.Text));
}
private void BeginInteractivity()
{
Browser.FindElement(By.Id("load-boot-script")).Click();
}
}
}

View File

@ -0,0 +1,48 @@
<h3 class="interactive">Component With Parameters</h3>
<ul class="Param1">
@foreach (var value in Param1)
{
<li>@value.StringProperty @value.IntProperty</li>
}
</ul>
@* Making sure polymorphism works *@
<div class="Param2">@DerivedParam2.StringProperty @DerivedParam2.DerivedProperty</div>
@* Making sure CaptureUnmatchedValues works *@
<ul class="Param3">
@foreach (var value in Param3.OrderBy(kvp => kvp.Key))
{
<li>@value.Key @value.Value</li>
}
</ul>
@code
{
[Parameter] public List<TestModel> Param1 { get; set; }
[Parameter] public TestModel Param2 { get; set; }
[Parameter(CaptureUnmatchedValues = true)] public IDictionary<string, object> Param3 { get; set; }
private DerivedModel DerivedParam2 => (DerivedModel)Param2;
public static List<TestModel> TestModelValues => Enumerable.Range(0, 100).Select(c => new TestModel { StringProperty = c.ToString(), IntProperty = c }).ToList();
public static DerivedModel DerivedModelValue = new DerivedModel { StringProperty = "Value", DerivedProperty = "Derived-Value" };
public class TestModel
{
public string StringProperty { get; set; }
public int IntProperty { get; set; }
}
public class DerivedModel : TestModel
{
public string DerivedProperty { get; set; }
}
}

View File

@ -12,7 +12,7 @@
<link href="css/site.css" rel="stylesheet" /> <link href="css/site.css" rel="stylesheet" />
</head> </head>
<body> <body>
<app>@(await Html.RenderComponentAsync<App>(RenderMode.Server))</app> <component type="typeof(App)" render-mode="Server" />
<script src="_framework/blazor.server.js" autostart="false"></script> <script src="_framework/blazor.server.js" autostart="false"></script>
<script> <script>
Blazor.start({ Blazor.start({

View File

@ -0,0 +1,37 @@
@page
<component type="typeof(ComponentWithParameters)"
render-mode="ServerPrerendered"
param-Param1="ComponentWithParameters.TestModelValues"
param-Param2="ComponentWithParameters.DerivedModelValue"
param-key1="QueryValue"
param-key2="43" />
@*
So that E2E tests can make assertions about both the prerendered and
interactive states, we only load the .js file when told to.
*@
<hr />
<button id="load-boot-script" onclick="start()">Load boot script</button>
<script src="_framework/blazor.server.js" autostart="false"></script>
<script>
// Used by InteropOnInitializationComponent
function setElementValue(element, newValue) {
element.value = newValue;
return element.value;
}
function start() {
Blazor.start({
logLevel: 1 // LogLevel.Debug
});
}
</script>
@functions
{
[BindProperty(SupportsGet = true)]
public string QueryValue { get; set; }
}

View File

@ -12,24 +12,24 @@
<div id="test-container"> <div id="test-container">
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.ServerPrerendered)) @(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.ServerPrerendered))
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.Server)) @(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.Server))
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.Static, new { Name = "John" })) <component type="typeof(GreeterComponent)" render-mode="Static" param-name='"John"' />
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.Server)) <component type="typeof(GreeterComponent)" render-mode="Server"/>
<div id="container"> <div id="container">
<p>Some content before</p> <p>Some content before</p>
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.Server)) <component type="typeof(GreeterComponent)" render-mode="Server"/>
<p>Some content between</p> <p>Some content between</p>
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.ServerPrerendered)) <component type="typeof(GreeterComponent)" render-mode="ServerPrerendered"/>
<p>Some content after</p> <p>Some content after</p>
<div id="nested-an-extra-level"> <div id="nested-an-extra-level">
<p>Some content before</p> <p>Some content before</p>
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.Server)) <component type="typeof(GreeterComponent)" render-mode="Server"/>
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.ServerPrerendered)) <component type="typeof(GreeterComponent)" render-mode="ServerPrerendered"/>
<p>Some content after</p> <p>Some content after</p>
</div> </div>
</div> </div>
<div id="container"> <div id="container">
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.Server, new { Name = "Albert" })) <component type="typeof(GreeterComponent)" render-mode="Server" param-name='"Albert"' />
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.ServerPrerendered, new { Name = "Abraham" })) <component type="typeof(GreeterComponent)" render-mode="ServerPrerendered" param-name='"Abraham"' />
</div> </div>
</div> </div>

View File

@ -1,5 +1,6 @@
@page @page
@using BasicTestApp.RouterTest @using BasicTestApp.RouterTest
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
@ -7,7 +8,7 @@
<base href="~/" /> <base href="~/" />
</head> </head>
<body> <body>
<app>@(await Html.RenderComponentAsync<TestRouter>(RenderMode.ServerPrerendered))</app> <app><component type="typeof(TestRouter)" render-mode="ServerPrerendered" /></app>
@* @*
So that E2E tests can make assertions about both the prerendered and So that E2E tests can make assertions about both the prerendered and

View File

@ -1,4 +1,5 @@
@page "" @page ""
@addTagHelper "*, Microsoft.AspNetCore.Mvc.TagHelpers"
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
@ -11,7 +12,7 @@
<link href="_content/TestContentPackage/styles.css" rel="stylesheet" /> <link href="_content/TestContentPackage/styles.css" rel="stylesheet" />
</head> </head>
<body> <body>
<root>@(await Html.RenderComponentAsync<BasicTestApp.Index>(RenderMode.Server))</root> <root><component type="typeof(BasicTestApp.Index)" render-mode="Server" /></root>
<!-- Used for testing interop scenarios between JS and .NET --> <!-- Used for testing interop scenarios between JS and .NET -->
<script src="js/jsinteroptests.js"></script> <script src="js/jsinteroptests.js"></script>

View File

@ -0,0 +1,3 @@
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@using BasicTestApp

View File

@ -105,6 +105,22 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
public CacheTagHelperOptions() { } public CacheTagHelperOptions() { }
public long SizeLimit { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } public long SizeLimit { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
} }
[Microsoft.AspNetCore.Razor.TagHelpers.HtmlTargetElementAttribute("component", Attributes="type", TagStructure=Microsoft.AspNetCore.Razor.TagHelpers.TagStructure.WithoutEndTag)]
public sealed partial class ComponentTagHelper : Microsoft.AspNetCore.Razor.TagHelpers.TagHelper
{
public ComponentTagHelper() { }
[Microsoft.AspNetCore.Razor.TagHelpers.HtmlAttributeNameAttribute("type")]
public System.Type ComponentType { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
[Microsoft.AspNetCore.Razor.TagHelpers.HtmlAttributeNameAttribute("params", DictionaryAttributePrefix="param-")]
public System.Collections.Generic.IDictionary<string, object> Parameters { get { throw null; } set { } }
[Microsoft.AspNetCore.Razor.TagHelpers.HtmlAttributeNameAttribute("render-mode")]
public Microsoft.AspNetCore.Mvc.Rendering.RenderMode RenderMode { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
[Microsoft.AspNetCore.Mvc.ViewFeatures.ViewContextAttribute]
[Microsoft.AspNetCore.Razor.TagHelpers.HtmlAttributeNotBoundAttribute]
public Microsoft.AspNetCore.Mvc.Rendering.ViewContext ViewContext { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
[System.Diagnostics.DebuggerStepThroughAttribute]
public override System.Threading.Tasks.Task ProcessAsync(Microsoft.AspNetCore.Razor.TagHelpers.TagHelperContext context, Microsoft.AspNetCore.Razor.TagHelpers.TagHelperOutput output) { throw null; }
}
[Microsoft.AspNetCore.Razor.TagHelpers.HtmlTargetElementAttribute("distributed-cache", Attributes="name")] [Microsoft.AspNetCore.Razor.TagHelpers.HtmlTargetElementAttribute("distributed-cache", Attributes="name")]
public partial class DistributedCacheTagHelper : Microsoft.AspNetCore.Mvc.TagHelpers.CacheTagHelperBase public partial class DistributedCacheTagHelper : Microsoft.AspNetCore.Mvc.TagHelpers.CacheTagHelperBase
{ {

View File

@ -0,0 +1,80 @@
// 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.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.AspNetCore.Mvc.TagHelpers
{
/// <summary>
/// A <see cref="TagHelper"/> that renders a Razor component.
/// </summary>
[HtmlTargetElement("component", Attributes = ComponentTypeName, TagStructure = TagStructure.WithoutEndTag)]
public sealed class ComponentTagHelper : TagHelper
{
private const string ComponentParameterName = "params";
private const string ComponentParameterPrefix = "param-";
private const string ComponentTypeName = "type";
private const string RenderModeName = "render-mode";
private IDictionary<string, object> _parameters;
/// <summary>
/// Gets or sets the <see cref="Rendering.ViewContext"/> for the current request.
/// </summary>
[HtmlAttributeNotBound]
[ViewContext]
public ViewContext ViewContext { get; set; }
/// <summary>
/// Gets or sets values for component parameters.
/// </summary>
[HtmlAttributeName(ComponentParameterName, DictionaryAttributePrefix = ComponentParameterPrefix)]
public IDictionary<string, object> Parameters
{
get
{
_parameters ??= new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
return _parameters;
}
set => _parameters = value;
}
/// <summary>
/// Gets or sets the component type. This value is required.
/// </summary>
[HtmlAttributeName(ComponentTypeName)]
public Type ComponentType { get; set; }
/// <summary>
/// Gets or sets the <see cref="Rendering.RenderMode"/>
/// </summary>
[HtmlAttributeName(RenderModeName)]
public RenderMode RenderMode { get; set; }
/// <inheritdoc />
public async override Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (output == null)
{
throw new ArgumentNullException(nameof(output));
}
var componentRenderer = ViewContext.HttpContext.RequestServices.GetRequiredService<IComponentRenderer>();
var result = await componentRenderer.RenderComponentAsync(ViewContext, ComponentType, RenderMode, _parameters);
// Reset the TagName. We don't want `component` to render.
output.TagName = null;
output.Content.SetHtmlContent(result);
}
}
}

View File

@ -0,0 +1,75 @@
// 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.IO.Pipes;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.Extensions.DependencyInjection;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.TagHelpers
{
public class ComponentTagHelperTest
{
[Fact]
public async Task ProcessAsync_RendersComponent()
{
// Arrange
var tagHelper = new ComponentTagHelper
{
ViewContext = GetViewContext(),
};
var context = GetTagHelperContext();
var output = GetTagHelperOutput();
// Act
await tagHelper.ProcessAsync(context, output);
// Assert
var content = HtmlContentUtilities.HtmlContentToString(output.Content);
Assert.Equal("Hello world", content);
Assert.Null(output.TagName);
}
private static TagHelperContext GetTagHelperContext()
{
return new TagHelperContext(
"component",
new TagHelperAttributeList(),
new Dictionary<object, object>(),
Guid.NewGuid().ToString("N"));
}
private static TagHelperOutput GetTagHelperOutput()
{
return new TagHelperOutput(
"component",
new TagHelperAttributeList(),
(_, __) => Task.FromResult<TagHelperContent>(new DefaultTagHelperContent()));
}
private ViewContext GetViewContext()
{
var htmlContent = new HtmlContentBuilder().AppendHtml("Hello world");
var renderer = Mock.Of<IComponentRenderer>(c =>
c.RenderComponentAsync(It.IsAny<ViewContext>(), It.IsAny<Type>(), It.IsAny<RenderMode>(), It.IsAny<object>()) == Task.FromResult<IHtmlContent>(htmlContent));
var httpContext = new DefaultHttpContext
{
RequestServices = new ServiceCollection().AddSingleton<IComponentRenderer>(renderer).BuildServiceProvider(),
};
return new ViewContext
{
HttpContext = httpContext,
};
}
}
}

View File

@ -324,8 +324,8 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
} }
public static partial class HtmlHelperComponentExtensions public static partial class HtmlHelperComponentExtensions
{ {
public static System.Threading.Tasks.Task<Microsoft.AspNetCore.Html.IHtmlContent> RenderComponentAsync(this Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper htmlHelper, System.Type componentType, Microsoft.AspNetCore.Mvc.Rendering.RenderMode renderMode, object parameters) { throw null; }
public static System.Threading.Tasks.Task<Microsoft.AspNetCore.Html.IHtmlContent> RenderComponentAsync<TComponent>(this Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper htmlHelper, Microsoft.AspNetCore.Mvc.Rendering.RenderMode renderMode) where TComponent : Microsoft.AspNetCore.Components.IComponent { throw null; } public static System.Threading.Tasks.Task<Microsoft.AspNetCore.Html.IHtmlContent> RenderComponentAsync<TComponent>(this Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper htmlHelper, Microsoft.AspNetCore.Mvc.Rendering.RenderMode renderMode) where TComponent : Microsoft.AspNetCore.Components.IComponent { throw null; }
[System.Diagnostics.DebuggerStepThroughAttribute]
public static System.Threading.Tasks.Task<Microsoft.AspNetCore.Html.IHtmlContent> RenderComponentAsync<TComponent>(this Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper htmlHelper, Microsoft.AspNetCore.Mvc.Rendering.RenderMode renderMode, object parameters) where TComponent : Microsoft.AspNetCore.Components.IComponent { throw null; } public static System.Threading.Tasks.Task<Microsoft.AspNetCore.Html.IHtmlContent> RenderComponentAsync<TComponent>(this Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper htmlHelper, Microsoft.AspNetCore.Mvc.Rendering.RenderMode renderMode, object parameters) where TComponent : Microsoft.AspNetCore.Components.IComponent { throw null; }
} }
public static partial class HtmlHelperDisplayExtensions public static partial class HtmlHelperDisplayExtensions

View File

@ -18,7 +18,6 @@ using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Mvc.ViewFeatures.Buffers; using Microsoft.AspNetCore.Mvc.ViewFeatures.Buffers;
using Microsoft.AspNetCore.Mvc.ViewFeatures.Filters; using Microsoft.AspNetCore.Mvc.ViewFeatures.Filters;
using Microsoft.AspNetCore.Mvc.ViewFeatures.Infrastructure; using Microsoft.AspNetCore.Mvc.ViewFeatures.Infrastructure;
using Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponents;
using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Microsoft.JSInterop; using Microsoft.JSInterop;
@ -206,8 +205,9 @@ namespace Microsoft.Extensions.DependencyInjection
services.TryAddSingleton<SaveTempDataFilter>(); services.TryAddSingleton<SaveTempDataFilter>();
// //
// Component prerendering // Component rendering
// //
services.TryAddScoped<IComponentRenderer, ComponentRenderer>();
services.TryAddScoped<StaticComponentRenderer>(); services.TryAddScoped<StaticComponentRenderer>();
services.TryAddScoped<NavigationManager, HttpNavigationManager>(); services.TryAddScoped<NavigationManager, HttpNavigationManager>();
services.TryAddScoped<IJSRuntime, UnsupportedJavaScriptRuntime>(); services.TryAddScoped<IJSRuntime, UnsupportedJavaScriptRuntime>();

View File

@ -1,127 +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.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponents;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.AspNetCore.Mvc.Rendering
{
/// <summary>
/// Extensions for rendering components.
/// </summary>
public static class HtmlHelperComponentExtensions
{
private static readonly object ComponentSequenceKey = new object();
/// <summary>
/// Renders the <typeparamref name="TComponent"/> <see cref="IComponent"/>.
/// </summary>
/// <param name="htmlHelper">The <see cref="IHtmlHelper"/>.</param>
/// <param name="renderMode">The <see cref="RenderMode"/> for the component.</param>
/// <returns>The HTML produced by the rendered <typeparamref name="TComponent"/>.</returns>
public static Task<IHtmlContent> RenderComponentAsync<TComponent>(this IHtmlHelper htmlHelper, RenderMode renderMode) where TComponent : IComponent
{
if (htmlHelper == null)
{
throw new ArgumentNullException(nameof(htmlHelper));
}
return htmlHelper.RenderComponentAsync<TComponent>(renderMode, null);
}
/// <summary>
/// Renders the <typeparamref name="TComponent"/> <see cref="IComponent"/>.
/// </summary>
/// <param name="htmlHelper">The <see cref="IHtmlHelper"/>.</param>
/// <param name="parameters">An <see cref="object"/> containing the parameters to pass
/// to the component.</param>
/// <param name="renderMode">The <see cref="RenderMode"/> for the component.</param>
/// <returns>The HTML produced by the rendered <typeparamref name="TComponent"/>.</returns>
public static async Task<IHtmlContent> RenderComponentAsync<TComponent>(
this IHtmlHelper htmlHelper,
RenderMode renderMode,
object parameters) where TComponent : IComponent
{
if (htmlHelper == null)
{
throw new ArgumentNullException(nameof(htmlHelper));
}
var context = htmlHelper.ViewContext.HttpContext;
return renderMode switch
{
RenderMode.Server => NonPrerenderedServerComponent(context, GetOrCreateInvocationId(htmlHelper.ViewContext), typeof(TComponent), GetParametersCollection(parameters)),
RenderMode.ServerPrerendered => await PrerenderedServerComponentAsync(context, GetOrCreateInvocationId(htmlHelper.ViewContext), typeof(TComponent), GetParametersCollection(parameters)),
RenderMode.Static => await StaticComponentAsync(context, typeof(TComponent), GetParametersCollection(parameters)),
_ => throw new ArgumentException("Invalid render mode", nameof(renderMode)),
};
}
private static ServerComponentInvocationSequence GetOrCreateInvocationId(ViewContext viewContext)
{
if (!viewContext.Items.TryGetValue(ComponentSequenceKey, out var result))
{
result = new ServerComponentInvocationSequence();
viewContext.Items[ComponentSequenceKey] = result;
}
return (ServerComponentInvocationSequence)result;
}
private static ParameterView GetParametersCollection(object parameters) => parameters == null ?
ParameterView.Empty :
ParameterView.FromDictionary(HtmlHelper.ObjectToDictionary(parameters));
private static async Task<IHtmlContent> StaticComponentAsync(HttpContext context, Type type, ParameterView parametersCollection)
{
var serviceProvider = context.RequestServices;
var prerenderer = serviceProvider.GetRequiredService<StaticComponentRenderer>();
var result = await prerenderer.PrerenderComponentAsync(
parametersCollection,
context,
type);
return new ComponentHtmlContent(result);
}
private static async Task<IHtmlContent> PrerenderedServerComponentAsync(HttpContext context, ServerComponentInvocationSequence invocationId, Type type, ParameterView parametersCollection)
{
var serviceProvider = context.RequestServices;
var prerenderer = serviceProvider.GetRequiredService<StaticComponentRenderer>();
var invocationSerializer = serviceProvider.GetRequiredService<ServerComponentSerializer>();
var currentInvocation = invocationSerializer.SerializeInvocation(
invocationId,
type,
parametersCollection,
prerendered: true);
var result = await prerenderer.PrerenderComponentAsync(
parametersCollection,
context,
type);
return new ComponentHtmlContent(
invocationSerializer.GetPreamble(currentInvocation),
result,
invocationSerializer.GetEpilogue(currentInvocation));
}
private static IHtmlContent NonPrerenderedServerComponent(HttpContext context, ServerComponentInvocationSequence invocationId, Type type, ParameterView parametersCollection)
{
var serviceProvider = context.RequestServices;
var invocationSerializer = serviceProvider.GetRequiredService<ServerComponentSerializer>();
var currentInvocation = invocationSerializer.SerializeInvocation(invocationId, type, parametersCollection, prerendered: false);
return new ComponentHtmlContent(invocationSerializer.GetPreamble(currentInvocation));
}
}
}

View File

@ -0,0 +1,110 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace Microsoft.AspNetCore.Mvc.ViewFeatures
{
internal class ComponentRenderer : IComponentRenderer
{
private static readonly object ComponentSequenceKey = new object();
private readonly StaticComponentRenderer _staticComponentRenderer;
private readonly ServerComponentSerializer _serverComponentSerializer;
public ComponentRenderer(
StaticComponentRenderer staticComponentRenderer,
ServerComponentSerializer serverComponentSerializer)
{
_staticComponentRenderer = staticComponentRenderer;
_serverComponentSerializer = serverComponentSerializer;
}
public async Task<IHtmlContent> RenderComponentAsync(
ViewContext viewContext,
Type componentType,
RenderMode renderMode,
object parameters)
{
if (viewContext is null)
{
throw new ArgumentNullException(nameof(viewContext));
}
if (componentType is null)
{
throw new ArgumentNullException(nameof(componentType));
}
if (!typeof(IComponent).IsAssignableFrom(componentType))
{
throw new ArgumentException(Resources.FormatTypeMustDeriveFromType(componentType, typeof(IComponent)));
}
var context = viewContext.HttpContext;
var parameterView = parameters is null ?
ParameterView.Empty :
ParameterView.FromDictionary(HtmlHelper.ObjectToDictionary(parameters));
return renderMode switch
{
RenderMode.Server => NonPrerenderedServerComponent(context, GetOrCreateInvocationId(viewContext), componentType, parameterView),
RenderMode.ServerPrerendered => await PrerenderedServerComponentAsync(context, GetOrCreateInvocationId(viewContext), componentType, parameterView),
RenderMode.Static => await StaticComponentAsync(context, componentType, parameterView),
_ => throw new ArgumentException(Resources.FormatUnsupportedRenderMode(renderMode), nameof(renderMode)),
};
}
private static ServerComponentInvocationSequence GetOrCreateInvocationId(ViewContext viewContext)
{
if (!viewContext.Items.TryGetValue(ComponentSequenceKey, out var result))
{
result = new ServerComponentInvocationSequence();
viewContext.Items[ComponentSequenceKey] = result;
}
return (ServerComponentInvocationSequence)result;
}
private async Task<IHtmlContent> StaticComponentAsync(HttpContext context, Type type, ParameterView parametersCollection)
{
var result = await _staticComponentRenderer.PrerenderComponentAsync(
parametersCollection,
context,
type);
return new ComponentHtmlContent(result);
}
private async Task<IHtmlContent> PrerenderedServerComponentAsync(HttpContext context, ServerComponentInvocationSequence invocationId, Type type, ParameterView parametersCollection)
{
var currentInvocation = _serverComponentSerializer.SerializeInvocation(
invocationId,
type,
parametersCollection,
prerendered: true);
var result = await _staticComponentRenderer.PrerenderComponentAsync(
parametersCollection,
context,
type);
return new ComponentHtmlContent(
_serverComponentSerializer.GetPreamble(currentInvocation),
result,
_serverComponentSerializer.GetEpilogue(currentInvocation));
}
private IHtmlContent NonPrerenderedServerComponent(HttpContext context, ServerComponentInvocationSequence invocationId, Type type, ParameterView parametersCollection)
{
var serviceProvider = context.RequestServices;
var currentInvocation = _serverComponentSerializer.SerializeInvocation(invocationId, type, parametersCollection, prerendered: false);
return new ComponentHtmlContent(_serverComponentSerializer.GetPreamble(currentInvocation));
}
}
}

View File

@ -0,0 +1,19 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace Microsoft.AspNetCore.Mvc.ViewFeatures
{
internal interface IComponentRenderer
{
Task<IHtmlContent> RenderComponentAsync(
ViewContext viewContext,
Type componentType,
RenderMode renderMode,
object parameters);
}
}

View File

@ -11,11 +11,10 @@ using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.Routing; using Microsoft.AspNetCore.Components.Routing;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponents namespace Microsoft.AspNetCore.Mvc.ViewFeatures
{ {
internal class StaticComponentRenderer internal class StaticComponentRenderer
{ {

View File

@ -2,7 +2,6 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections.Generic;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.JSInterop; using Microsoft.JSInterop;

View File

@ -0,0 +1,70 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.AspNetCore.Mvc.Rendering
{
/// <summary>
/// Extensions for rendering components.
/// </summary>
public static class HtmlHelperComponentExtensions
{
/// <summary>
/// Renders the <typeparamref name="TComponent"/>.
/// </summary>
/// <param name="htmlHelper">The <see cref="IHtmlHelper"/>.</param>
/// <param name="renderMode">The <see cref="RenderMode"/> for the component.</param>
/// <returns>The HTML produced by the rendered <typeparamref name="TComponent"/>.</returns>
public static Task<IHtmlContent> RenderComponentAsync<TComponent>(this IHtmlHelper htmlHelper, RenderMode renderMode) where TComponent : IComponent
=> RenderComponentAsync<TComponent>(htmlHelper, renderMode, parameters: null);
/// <summary>
/// Renders the <typeparamref name="TComponent"/>.
/// </summary>
/// <param name="htmlHelper">The <see cref="IHtmlHelper"/>.</param>
/// <param name="parameters">An <see cref="object"/> containing the parameters to pass
/// to the component.</param>
/// <param name="renderMode">The <see cref="RenderMode"/> for the component.</param>
/// <returns>The HTML produced by the rendered <typeparamref name="TComponent"/>.</returns>
public static Task<IHtmlContent> RenderComponentAsync<TComponent>(
this IHtmlHelper htmlHelper,
RenderMode renderMode,
object parameters) where TComponent : IComponent
=> RenderComponentAsync(htmlHelper, typeof(TComponent), renderMode, parameters);
/// <summary>
/// Renders the specified <paramref name="componentType"/>.
/// </summary>
/// <param name="htmlHelper">The <see cref="IHtmlHelper"/>.</param>
/// <param name="componentType">The component type.</param>
/// <param name="parameters">An <see cref="object"/> containing the parameters to pass
/// to the component.</param>
/// <param name="renderMode">The <see cref="RenderMode"/> for the component.</param>
public static Task<IHtmlContent> RenderComponentAsync(
this IHtmlHelper htmlHelper,
Type componentType,
RenderMode renderMode,
object parameters)
{
if (htmlHelper is null)
{
throw new ArgumentNullException(nameof(htmlHelper));
}
if (componentType is null)
{
throw new ArgumentNullException(nameof(componentType));
}
var viewContext = htmlHelper.ViewContext;
var componentRenderer = viewContext.HttpContext.RequestServices.GetRequiredService<IComponentRenderer>();
return componentRenderer.RenderComponentAsync(viewContext, componentType, renderMode, parameters);
}
}
}

View File

@ -295,4 +295,7 @@
<data name="TempData_CannotDeserializeType" xml:space="preserve"> <data name="TempData_CannotDeserializeType" xml:space="preserve">
<value>Unsupported data type '{0}'.</value> <value>Unsupported data type '{0}'.</value>
</data> </data>
<data name="UnsupportedRenderMode" xml:space="preserve">
<value>Unsupported RenderMode '{0}'.</value>
</data>
</root> </root>

View File

@ -13,7 +13,7 @@ using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponents; using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Abstractions;
@ -22,24 +22,26 @@ using Microsoft.Net.Http.Headers;
using Moq; using Moq;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test namespace Microsoft.AspNetCore.Mvc.ViewFeatures
{ {
public class HtmlHelperComponentExtensionsTests public class ComponentRendererTest
{ {
private const string PrerenderedServerComponentPattern = "^<!--Blazor:(?<preamble>.*?)-->(?<content>.+?)<!--Blazor:(?<epilogue>.*?)-->$"; private const string PrerenderedServerComponentPattern = "^<!--Blazor:(?<preamble>.*?)-->(?<content>.+?)<!--Blazor:(?<epilogue>.*?)-->$";
private const string ServerComponentPattern = "^<!--Blazor:(.*?)-->$"; private const string ServerComponentPattern = "^<!--Blazor:(.*?)-->$";
private static readonly IDataProtectionProvider _dataprotectorProvider = new EphemeralDataProtectionProvider(); private static readonly IDataProtectionProvider _dataprotectorProvider = new EphemeralDataProtectionProvider();
private readonly ComponentRenderer renderer = GetComponentRenderer();
[Fact] [Fact]
public async Task CanRender_ParameterlessComponent() public async Task CanRender_ParameterlessComponent()
{ {
// Arrange // Arrange
var helper = CreateHelper(); var viewContext = GetViewContext();
var writer = new StringWriter(); var writer = new StringWriter();
// Act // Act
var result = await helper.RenderComponentAsync<TestComponent>(RenderMode.Static); var result = await renderer.RenderComponentAsync(viewContext, typeof(TestComponent), RenderMode.Static, null);
result.WriteTo(writer, HtmlEncoder.Default); result.WriteTo(writer, HtmlEncoder.Default);
var content = writer.ToString(); var content = writer.ToString();
@ -51,15 +53,13 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
public async Task CanRender_ParameterlessComponent_ServerMode() public async Task CanRender_ParameterlessComponent_ServerMode()
{ {
// Arrange // Arrange
var helper = CreateHelper(); var viewContext = GetViewContext();
var writer = new StringWriter();
var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose) var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
.ToTimeLimitedDataProtector(); .ToTimeLimitedDataProtector();
// Act // Act
var result = await helper.RenderComponentAsync<TestComponent>(RenderMode.Server); var result = await renderer.RenderComponentAsync(viewContext, typeof(TestComponent), RenderMode.Server, null);
result.WriteTo(writer, HtmlEncoder.Default); var content = HtmlContentUtilities.HtmlContentToString(result);
var content = writer.ToString();
var match = Regex.Match(content, ServerComponentPattern); var match = Regex.Match(content, ServerComponentPattern);
// Assert // Assert
@ -82,15 +82,13 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
public async Task CanPrerender_ParameterlessComponent_ServerMode() public async Task CanPrerender_ParameterlessComponent_ServerMode()
{ {
// Arrange // Arrange
var helper = CreateHelper(); var viewContext = GetViewContext();
var writer = new StringWriter();
var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose) var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
.ToTimeLimitedDataProtector(); .ToTimeLimitedDataProtector();
// Act // Act
var result = await helper.RenderComponentAsync<TestComponent>(RenderMode.ServerPrerendered); var result = await renderer.RenderComponentAsync(viewContext, typeof(TestComponent), RenderMode.ServerPrerendered, null);
result.WriteTo(writer, HtmlEncoder.Default); var content = HtmlContentUtilities.HtmlContentToString(result);
var content = writer.ToString();
var match = Regex.Match(content, PrerenderedServerComponentPattern, RegexOptions.Multiline); var match = Regex.Match(content, PrerenderedServerComponentPattern, RegexOptions.Multiline);
// Assert // Assert
@ -125,21 +123,17 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
public async Task CanRenderMultipleServerComponents() public async Task CanRenderMultipleServerComponents()
{ {
// Arrange // Arrange
var helper = CreateHelper(); var viewContext = GetViewContext();
var firstWriter = new StringWriter();
var secondWriter = new StringWriter();
var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose) var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
.ToTimeLimitedDataProtector(); .ToTimeLimitedDataProtector();
// Act // Act
var firstResult = await helper.RenderComponentAsync<TestComponent>(RenderMode.ServerPrerendered); var firstResult = await renderer.RenderComponentAsync(viewContext, typeof(TestComponent), RenderMode.ServerPrerendered, null);
firstResult.WriteTo(firstWriter, HtmlEncoder.Default); var firstComponent = HtmlContentUtilities.HtmlContentToString(firstResult);
var firstComponent = firstWriter.ToString();
var firstMatch = Regex.Match(firstComponent, PrerenderedServerComponentPattern, RegexOptions.Multiline); var firstMatch = Regex.Match(firstComponent, PrerenderedServerComponentPattern, RegexOptions.Multiline);
var secondResult = await helper.RenderComponentAsync<TestComponent>(RenderMode.Server); var secondResult = await renderer.RenderComponentAsync(viewContext, typeof(TestComponent), RenderMode.Server, null);
secondResult.WriteTo(secondWriter, HtmlEncoder.Default); var secondComponent = HtmlContentUtilities.HtmlContentToString(secondResult);
var secondComponent = secondWriter.ToString();
var secondMatch = Regex.Match(secondComponent, ServerComponentPattern); var secondMatch = Regex.Match(secondComponent, ServerComponentPattern);
// Assert // Assert
@ -171,20 +165,13 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
public async Task CanRender_ComponentWithParametersObject() public async Task CanRender_ComponentWithParametersObject()
{ {
// Arrange // Arrange
var helper = CreateHelper(); var viewContext = GetViewContext();
var writer = new StringWriter();
// Act // Act
var result = await helper.RenderComponentAsync<GreetingComponent>( var result = await renderer.RenderComponentAsync(viewContext, typeof(GreetingComponent), RenderMode.Static, new { Name = "Steve" });
RenderMode.Static,
new
{
Name = "Steve"
});
result.WriteTo(writer, HtmlEncoder.Default);
var content = writer.ToString();
// Assert // Assert
var content = HtmlContentUtilities.HtmlContentToString(result);
Assert.Equal("<p>Hello Steve!</p>", content); Assert.Equal("<p>Hello Steve!</p>", content);
} }
@ -192,20 +179,13 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
public async Task CanRender_ComponentWithParameters_ServerMode() public async Task CanRender_ComponentWithParameters_ServerMode()
{ {
// Arrange // Arrange
var helper = CreateHelper(); var viewContext = GetViewContext();
var writer = new StringWriter();
var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose) var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
.ToTimeLimitedDataProtector(); .ToTimeLimitedDataProtector();
// Act // Act
var result = await helper.RenderComponentAsync<GreetingComponent>( var result = await renderer.RenderComponentAsync(viewContext, typeof(GreetingComponent), RenderMode.Server, new { Name = "Daniel" });
RenderMode.Server, var content = HtmlContentUtilities.HtmlContentToString(result);
new
{
Name = "Daniel"
});
result.WriteTo(writer, HtmlEncoder.Default);
var content = writer.ToString();
var match = Regex.Match(content, ServerComponentPattern); var match = Regex.Match(content, ServerComponentPattern);
// Assert // Assert
@ -237,20 +217,14 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
public async Task CanRender_ComponentWithNullParameters_ServerMode() public async Task CanRender_ComponentWithNullParameters_ServerMode()
{ {
// Arrange // Arrange
var helper = CreateHelper(); var viewContext = GetViewContext();
var writer = new StringWriter();
var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose) var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
.ToTimeLimitedDataProtector(); .ToTimeLimitedDataProtector();
// Act // Act
var result = await helper.RenderComponentAsync<GreetingComponent>(
RenderMode.Server, var result = await renderer.RenderComponentAsync(viewContext, typeof(GreetingComponent), RenderMode.Server, new { Name = (string)null });
new var content = HtmlContentUtilities.HtmlContentToString(result);
{
Name = (string)null
});
result.WriteTo(writer, HtmlEncoder.Default);
var content = writer.ToString();
var match = Regex.Match(content, ServerComponentPattern); var match = Regex.Match(content, ServerComponentPattern);
// Assert // Assert
@ -274,28 +248,22 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
Assert.Null(parameterDefinition.TypeName); Assert.Null(parameterDefinition.TypeName);
Assert.Null(parameterDefinition.Assembly); Assert.Null(parameterDefinition.Assembly);
var value = Assert.Single(serverComponent.ParameterValues);; var value = Assert.Single(serverComponent.ParameterValues); ;
Assert.Null(value); Assert.Null(value);
} }
[Fact] [Fact]
public async Task CanPrerender_ComponentWithParameters_ServerMode() public async Task CanPrerender_ComponentWithParameters_ServerPrerenderedMode()
{ {
// Arrange // Arrange
var helper = CreateHelper(); var viewContext = GetViewContext();
var writer = new StringWriter(); var writer = new StringWriter();
var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose) var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
.ToTimeLimitedDataProtector(); .ToTimeLimitedDataProtector();
// Act // Act
var result = await helper.RenderComponentAsync<GreetingComponent>( var result = await renderer.RenderComponentAsync(viewContext, typeof(GreetingComponent), RenderMode.ServerPrerendered, new { Name = "Daniel" });
RenderMode.ServerPrerendered, var content = HtmlContentUtilities.HtmlContentToString(result);
new
{
Name = "Daniel"
});
result.WriteTo(writer, HtmlEncoder.Default);
var content = writer.ToString();
var match = Regex.Match(content, PrerenderedServerComponentPattern, RegexOptions.Multiline); var match = Regex.Match(content, PrerenderedServerComponentPattern, RegexOptions.Multiline);
// Assert // Assert
@ -336,23 +304,17 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
} }
[Fact] [Fact]
public async Task CanPrerender_ComponentWithNullParameters_ServerMode() public async Task CanPrerender_ComponentWithNullParameters_ServerPrerenderedMode()
{ {
// Arrange // Arrange
var helper = CreateHelper(); var viewContext = GetViewContext();
var writer = new StringWriter(); var writer = new StringWriter();
var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose) var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
.ToTimeLimitedDataProtector(); .ToTimeLimitedDataProtector();
// Act // Act
var result = await helper.RenderComponentAsync<GreetingComponent>( var result = await renderer.RenderComponentAsync(viewContext, typeof(GreetingComponent), RenderMode.ServerPrerendered, new { Name = (string)null });
RenderMode.ServerPrerendered, var content = HtmlContentUtilities.HtmlContentToString(result);
new
{
Name = (string)null
});
result.WriteTo(writer, HtmlEncoder.Default);
var content = writer.ToString();
var match = Regex.Match(content, PrerenderedServerComponentPattern, RegexOptions.Multiline); var match = Regex.Match(content, PrerenderedServerComponentPattern, RegexOptions.Multiline);
// Assert // Assert
@ -396,39 +358,28 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
public async Task ComponentWithInvalidRenderMode_Throws() public async Task ComponentWithInvalidRenderMode_Throws()
{ {
// Arrange // Arrange
var helper = CreateHelper(); var viewContext = GetViewContext();
var writer = new StringWriter();
// Act & Assert // Act & Assert
var result = await Assert.ThrowsAsync<ArgumentException>(() => helper.RenderComponentAsync<GreetingComponent>( var ex = await ExceptionAssert.ThrowsArgumentAsync(
default, () => renderer.RenderComponentAsync(viewContext, typeof(GreetingComponent), default, new { Name = "Daniel" }),
new "renderMode",
{ $"Unsupported RenderMode '{(RenderMode)default}'");
Name = "Steve"
}));
Assert.Equal("renderMode", result.ParamName);
} }
[Fact] [Fact]
public async Task RenderComponent_DoesNotInvokeOnAfterRenderInComponent() public async Task RenderComponent_DoesNotInvokeOnAfterRenderInComponent()
{ {
// Arrange // Arrange
var helper = CreateHelper(); var viewContext = GetViewContext();
var writer = new StringWriter();
// Act // Act
var state = new OnAfterRenderState(); var state = new OnAfterRenderState();
var result = await helper.RenderComponentAsync<OnAfterRenderComponent>( var result = await renderer.RenderComponentAsync(viewContext, typeof(OnAfterRenderComponent), RenderMode.Static, new { state });
RenderMode.Static,
new
{
State = state
});
result.WriteTo(writer, HtmlEncoder.Default);
// Assert // Assert
Assert.Equal("<p>Hello</p>", writer.ToString()); var content = HtmlContentUtilities.HtmlContentToString(result);
Assert.Equal("<p>Hello</p>", content);
Assert.False(state.OnAfterRenderRan); Assert.False(state.OnAfterRenderRan);
} }
@ -436,10 +387,12 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
public async Task CanCatch_ComponentWithSynchronousException() public async Task CanCatch_ComponentWithSynchronousException()
{ {
// Arrange // Arrange
var helper = CreateHelper(); var viewContext = GetViewContext();
// Act & Assert // Act & Assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => helper.RenderComponentAsync<ExceptionComponent>( var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => renderer.RenderComponentAsync(
viewContext,
typeof(ExceptionComponent),
RenderMode.Static, RenderMode.Static,
new new
{ {
@ -454,10 +407,12 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
public async Task CanCatch_ComponentWithAsynchronousException() public async Task CanCatch_ComponentWithAsynchronousException()
{ {
// Arrange // Arrange
var helper = CreateHelper(); var viewContext = GetViewContext();
// Act & Assert // Act & Assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => helper.RenderComponentAsync<ExceptionComponent>( var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => renderer.RenderComponentAsync(
viewContext,
typeof(ExceptionComponent),
RenderMode.Static, RenderMode.Static,
new new
{ {
@ -472,10 +427,12 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
public async Task Rendering_ComponentWithJsInteropThrows() public async Task Rendering_ComponentWithJsInteropThrows()
{ {
// Arrange // Arrange
var helper = CreateHelper(); var viewContext = GetViewContext();
// Act & Assert // Act & Assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => helper.RenderComponentAsync<ExceptionComponent>( var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => renderer.RenderComponentAsync(
viewContext,
typeof(ExceptionComponent),
RenderMode.Static, RenderMode.Static,
new new
{ {
@ -503,11 +460,12 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
var responseMock = new Mock<IHttpResponseFeature>(); var responseMock = new Mock<IHttpResponseFeature>();
responseMock.Setup(r => r.HasStarted).Returns(true); responseMock.Setup(r => r.HasStarted).Returns(true);
ctx.Features.Set(responseMock.Object); ctx.Features.Set(responseMock.Object);
var helper = CreateHelper(ctx); var viewContext = GetViewContext(ctx);
var writer = new StringWriter();
// Act // Act
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => helper.RenderComponentAsync<RedirectComponent>( var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => renderer.RenderComponentAsync(
viewContext,
typeof(RedirectComponent),
RenderMode.Static, RenderMode.Static,
new new
{ {
@ -515,8 +473,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
})); }));
Assert.Equal("A navigation command was attempted during prerendering after the server already started sending the response. " + Assert.Equal("A navigation command was attempted during prerendering after the server already started sending the response. " +
"Navigation commands can not be issued during server-side prerendering after the response from the server has started. Applications must buffer the" + "Navigation commands can not be issued during server-side prerendering after the response from the server has started. Applications must buffer the" +
"reponse and avoid using features like FlushAsync() before all components on the page have been rendered to prevent failed navigation commands.", "reponse and avoid using features like FlushAsync() before all components on the page have been rendered to prevent failed navigation commands.",
exception.Message); exception.Message);
} }
@ -530,10 +488,12 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
ctx.Request.PathBase = "/base"; ctx.Request.PathBase = "/base";
ctx.Request.Path = "/path"; ctx.Request.Path = "/path";
ctx.Request.QueryString = new QueryString("?query=value"); ctx.Request.QueryString = new QueryString("?query=value");
var helper = CreateHelper(ctx); var viewContext = GetViewContext(ctx);
// Act // Act
await helper.RenderComponentAsync<RedirectComponent>( await renderer.RenderComponentAsync(
viewContext,
typeof(RedirectComponent),
RenderMode.Static, RenderMode.Static,
new new
{ {
@ -549,8 +509,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
public async Task CanRender_AsyncComponent() public async Task CanRender_AsyncComponent()
{ {
// Arrange // Arrange
var helper = CreateHelper(); var viewContext = GetViewContext();
var writer = new StringWriter();
var expectedContent = @"<table> var expectedContent = @"<table>
<thead> <thead>
<tr> <tr>
@ -595,29 +554,29 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
</table>"; </table>";
// Act // Act
var result = await helper.RenderComponentAsync<AsyncComponent>(RenderMode.Static); var result = await renderer.RenderComponentAsync(viewContext,typeof(AsyncComponent), RenderMode.Static, null);
result.WriteTo(writer, HtmlEncoder.Default); var content = HtmlContentUtilities.HtmlContentToString(result);
var content = writer.ToString();
// Assert // Assert
Assert.Equal(expectedContent.Replace("\r\n", "\n"), content); Assert.Equal(expectedContent.Replace("\r\n", "\n"), content);
} }
private static IHtmlHelper CreateHelper(HttpContext ctx = null, Action<IServiceCollection> configureServices = null) private static ComponentRenderer GetComponentRenderer() =>
new ComponentRenderer(
new StaticComponentRenderer(HtmlEncoder.Default),
new ServerComponentSerializer(_dataprotectorProvider));
private static ViewContext GetViewContext(HttpContext context = null, Action<IServiceCollection> configureServices = null)
{ {
var services = new ServiceCollection(); var services = new ServiceCollection();
services.AddSingleton(HtmlEncoder.Default);
services.AddSingleton<ServerComponentSerializer>();
services.AddSingleton(_dataprotectorProvider); services.AddSingleton(_dataprotectorProvider);
services.AddSingleton<IJSRuntime, UnsupportedJavaScriptRuntime>(); services.AddSingleton<IJSRuntime, UnsupportedJavaScriptRuntime>();
services.AddSingleton<NavigationManager, HttpNavigationManager>(); services.AddSingleton<NavigationManager, HttpNavigationManager>();
services.AddSingleton<StaticComponentRenderer>();
services.AddSingleton<ILoggerFactory, NullLoggerFactory>(); services.AddSingleton<ILoggerFactory, NullLoggerFactory>();
configureServices?.Invoke(services); configureServices?.Invoke(services);
var helper = new Mock<IHtmlHelper>(); context ??= new DefaultHttpContext();
var context = ctx ?? new DefaultHttpContext();
context.RequestServices = services.BuildServiceProvider(); context.RequestServices = services.BuildServiceProvider();
context.Request.Scheme = "http"; context.Request.Scheme = "http";
context.Request.Host = new HostString("localhost"); context.Request.Host = new HostString("localhost");
@ -625,12 +584,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
context.Request.Path = "/path"; context.Request.Path = "/path";
context.Request.QueryString = QueryString.FromUriComponent("?query=value"); context.Request.QueryString = QueryString.FromUriComponent("?query=value");
helper.Setup(h => h.ViewContext) return new ViewContext { HttpContext = context };
.Returns(new ViewContext()
{
HttpContext = context
});
return helper.Object;
} }
private class TestComponent : IComponent private class TestComponent : IComponent

View File

@ -6,13 +6,11 @@ using System.Collections.Generic;
using System.Runtime.ExceptionServices; using System.Runtime.ExceptionServices;
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Abstractions;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Mvc.RazorComponents namespace Microsoft.AspNetCore.Components.Rendering
{ {
public class HtmlRendererTest public class HtmlRendererTest
{ {

View File

@ -0,0 +1,56 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.Extensions.DependencyInjection;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.Rendering
{
public class HtmlHelperComponentExtensionsTest
{
[Fact]
public async Task RenderComponentAsync_Works()
{
// Arrange
var viewContext = GetViewContext();
var htmlHelper = Mock.Of<IHtmlHelper>(h => h.ViewContext == viewContext);
// Act
var result = await HtmlHelperComponentExtensions.RenderComponentAsync<TestComponent>(htmlHelper, RenderMode.Static);
// Assert
Assert.Equal("Hello world", HtmlContentUtilities.HtmlContentToString(result));
}
private static ViewContext GetViewContext()
{
var htmlContent = new HtmlContentBuilder().AppendHtml("Hello world");
var renderer = Mock.Of<IComponentRenderer>(c =>
c.RenderComponentAsync(It.IsAny<ViewContext>(), It.IsAny<Type>(), It.IsAny<RenderMode>(), It.IsAny<object>()) == Task.FromResult<IHtmlContent>(htmlContent));
var httpContext = new DefaultHttpContext
{
RequestServices = new ServiceCollection().AddSingleton<IComponentRenderer>(renderer).BuildServiceProvider(),
};
var viewContext = new ViewContext { HttpContext = httpContext };
return viewContext;
}
private class TestComponent : IComponent
{
public void Attach(RenderHandle renderHandle)
{
}
public Task SetParametersAsync(ParameterView parameters) => null;
}
}
}

View File

@ -1,2 +1,13 @@
@using Microsoft.AspNetCore.Components.Routing @using Microsoft.AspNetCore.Components.Routing
<Router AppAssembly="@typeof(MvcSandbox.Startup).Assembly" FallbackComponent="@typeof(NotFound)" /> @using MvcSandbox.Components.Shared
<Router AppAssembly="@typeof(MvcSandbox.Startup).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="typeof(MainLayout)" />
</Found>
<NotFound>
<LayoutView Layout="@typeof(MainLayout)">
<p>Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>

View File

@ -1,4 +0,0 @@
@using MvcSandbox.Components.Shared
@layout MainLayout
<h1>Not Found</h1>
<h2>Sorry, nothing was found.</h2>

View File

@ -1,5 +1,4 @@
@page @page
@model MvcSandbox.Pages.ComponentsModel
@{ @{
Layout = null; Layout = null;
} }
@ -15,8 +14,7 @@
<link href="css/site.css" rel="stylesheet" /> <link href="css/site.css" rel="stylesheet" />
</head> </head>
<body> <body>
<app>@(await Html.RenderComponentAsync<MvcSandbox.Components.App>(RenderMode.Static))</app> <component type="typeof(MvcSandbox.Components.App)" render-mode="Static" />
<script src="_framework/blazor.server.js"></script>
<script src="_framework/components.server.js"></script>
</body> </body>
</html> </html>

View File

@ -1,16 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace MvcSandbox.Pages
{
public class ComponentsModel : PageModel
{
public void OnGet()
{
}
}
}

View File

@ -19,6 +19,9 @@
<li class="nav-item"> <li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-page="/PagesHome">Pages Home</a> <a class="nav-link text-dark" asp-area="" asp-page="/PagesHome">Pages Home</a>
</li> </li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-page="/Components">Components</a>
</li>
</ul> </ul>
</div> </div>
</div> </div>

View File

@ -14,7 +14,7 @@
</head> </head>
<body> <body>
<app> <app>
@(await Html.RenderComponentAsync<App>(RenderMode.ServerPrerendered)) <component type="typeof(App)" render-mode="ServerPrerendered" />
</app> </app>
<div id="blazor-error-ui"> <div id="blazor-error-ui">