Introduce ComponentTagHelper (#14592)
* Introduce ComponentTagHelper Fixes https://github.com/aspnet/AspNetCore/issues/13726
This commit is contained in:
commit
d299ae2491
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@using BasicTestApp
|
||||
|
||||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>();
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -1,4 +0,0 @@
|
|||
@using MvcSandbox.Components.Shared
|
||||
@layout MainLayout
|
||||
<h1>Not Found</h1>
|
||||
<h2>Sorry, nothing was found.</h2>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
Loading…
Reference in New Issue