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" />
</head>
<body>
<app>
@(await Html.RenderComponentAsync<App>(RenderMode.ServerPrerendered))
</app>
<component type="typeof(App)" render-mode="ServerPrerendered" />
<script src="_framework/blazor.server.js"></script>
</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" />
</head>
<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>
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">
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.ServerPrerendered))
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.Server))
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.Static, new { Name = "John" }))
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.Server))
<component type="typeof(GreeterComponent)" render-mode="Static" param-name='"John"' />
<component type="typeof(GreeterComponent)" render-mode="Server"/>
<div id="container">
<p>Some content before</p>
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.Server))
<component type="typeof(GreeterComponent)" render-mode="Server"/>
<p>Some content between</p>
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.ServerPrerendered))
<component type="typeof(GreeterComponent)" render-mode="ServerPrerendered"/>
<p>Some content after</p>
<div id="nested-an-extra-level">
<p>Some content before</p>
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.Server))
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.ServerPrerendered))
<component type="typeof(GreeterComponent)" render-mode="Server"/>
<component type="typeof(GreeterComponent)" render-mode="ServerPrerendered"/>
<p>Some content after</p>
</div>
</div>
<div id="container">
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.Server, new { Name = "Albert" }))
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.ServerPrerendered, new { Name = "Abraham" }))
<component type="typeof(GreeterComponent)" render-mode="Server" param-name='"Albert"' />
<component type="typeof(GreeterComponent)" render-mode="ServerPrerendered" param-name='"Abraham"' />
</div>
</div>

View File

@ -1,5 +1,6 @@
@page
@using BasicTestApp.RouterTest
<!DOCTYPE html>
<html>
<head>
@ -7,7 +8,7 @@
<base href="~/" />
</head>
<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

View File

@ -1,4 +1,5 @@
@page ""
@addTagHelper "*, Microsoft.AspNetCore.Mvc.TagHelpers"
<!DOCTYPE html>
<html>
<head>
@ -11,7 +12,7 @@
<link href="_content/TestContentPackage/styles.css" rel="stylesheet" />
</head>
<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 -->
<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 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")]
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 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; }
[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 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.Filters;
using Microsoft.AspNetCore.Mvc.ViewFeatures.Infrastructure;
using Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponents;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using Microsoft.JSInterop;
@ -206,8 +205,9 @@ namespace Microsoft.Extensions.DependencyInjection
services.TryAddSingleton<SaveTempDataFilter>();
//
// Component prerendering
// Component rendering
//
services.TryAddScoped<IComponentRenderer, ComponentRenderer>();
services.TryAddScoped<StaticComponentRenderer>();
services.TryAddScoped<NavigationManager, HttpNavigationManager>();
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.Http;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponents
namespace Microsoft.AspNetCore.Mvc.ViewFeatures
{
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.
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
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">
<value>Unsupported data type '{0}'.</value>
</data>
<data name="UnsupportedRenderMode" xml:space="preserve">
<value>Unsupported RenderMode '{0}'.</value>
</data>
</root>

View File

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

View File

@ -6,13 +6,11 @@ using System.Collections.Generic;
using System.Runtime.ExceptionServices;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.RazorComponents
namespace Microsoft.AspNetCore.Components.Rendering
{
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
<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
@model MvcSandbox.Pages.ComponentsModel
@{
Layout = null;
}
@ -15,8 +14,7 @@
<link href="css/site.css" rel="stylesheet" />
</head>
<body>
<app>@(await Html.RenderComponentAsync<MvcSandbox.Components.App>(RenderMode.Static))</app>
<script src="_framework/components.server.js"></script>
<component type="typeof(MvcSandbox.Components.App)" render-mode="Static" />
<script src="_framework/blazor.server.js"></script>
</body>
</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">
<a class="nav-link text-dark" asp-area="" asp-page="/PagesHome">Pages Home</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-page="/Components">Components</a>
</li>
</ul>
</div>
</div>

View File

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