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" />
|
<link href="css/site.css" rel="stylesheet" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<app>
|
<component type="typeof(App)" render-mode="ServerPrerendered" />
|
||||||
@(await Html.RenderComponentAsync<App>(RenderMode.ServerPrerendered))
|
|
||||||
</app>
|
|
||||||
|
|
||||||
<script src="_framework/blazor.server.js"></script>
|
<script src="_framework/blazor.server.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
|
|
@ -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" />
|
<link href="css/site.css" rel="stylesheet" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<app>@(await Html.RenderComponentAsync<App>(RenderMode.Server))</app>
|
<component type="typeof(App)" render-mode="Server" />
|
||||||
<script src="_framework/blazor.server.js" autostart="false"></script>
|
<script src="_framework/blazor.server.js" autostart="false"></script>
|
||||||
<script>
|
<script>
|
||||||
Blazor.start({
|
Blazor.start({
|
||||||
|
|
|
||||||
|
|
@ -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">
|
<div id="test-container">
|
||||||
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.ServerPrerendered))
|
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.ServerPrerendered))
|
||||||
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.Server))
|
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.Server))
|
||||||
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.Static, new { Name = "John" }))
|
<component type="typeof(GreeterComponent)" render-mode="Static" param-name='"John"' />
|
||||||
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.Server))
|
<component type="typeof(GreeterComponent)" render-mode="Server"/>
|
||||||
<div id="container">
|
<div id="container">
|
||||||
<p>Some content before</p>
|
<p>Some content before</p>
|
||||||
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.Server))
|
<component type="typeof(GreeterComponent)" render-mode="Server"/>
|
||||||
<p>Some content between</p>
|
<p>Some content between</p>
|
||||||
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.ServerPrerendered))
|
<component type="typeof(GreeterComponent)" render-mode="ServerPrerendered"/>
|
||||||
<p>Some content after</p>
|
<p>Some content after</p>
|
||||||
<div id="nested-an-extra-level">
|
<div id="nested-an-extra-level">
|
||||||
<p>Some content before</p>
|
<p>Some content before</p>
|
||||||
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.Server))
|
<component type="typeof(GreeterComponent)" render-mode="Server"/>
|
||||||
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.ServerPrerendered))
|
<component type="typeof(GreeterComponent)" render-mode="ServerPrerendered"/>
|
||||||
<p>Some content after</p>
|
<p>Some content after</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="container">
|
<div id="container">
|
||||||
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.Server, new { Name = "Albert" }))
|
<component type="typeof(GreeterComponent)" render-mode="Server" param-name='"Albert"' />
|
||||||
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.ServerPrerendered, new { Name = "Abraham" }))
|
<component type="typeof(GreeterComponent)" render-mode="ServerPrerendered" param-name='"Abraham"' />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
@page
|
@page
|
||||||
@using BasicTestApp.RouterTest
|
@using BasicTestApp.RouterTest
|
||||||
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
|
@ -7,7 +8,7 @@
|
||||||
<base href="~/" />
|
<base href="~/" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<app>@(await Html.RenderComponentAsync<TestRouter>(RenderMode.ServerPrerendered))</app>
|
<app><component type="typeof(TestRouter)" render-mode="ServerPrerendered" /></app>
|
||||||
|
|
||||||
@*
|
@*
|
||||||
So that E2E tests can make assertions about both the prerendered and
|
So that E2E tests can make assertions about both the prerendered and
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
@page ""
|
@page ""
|
||||||
|
@addTagHelper "*, Microsoft.AspNetCore.Mvc.TagHelpers"
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
|
@ -11,7 +12,7 @@
|
||||||
<link href="_content/TestContentPackage/styles.css" rel="stylesheet" />
|
<link href="_content/TestContentPackage/styles.css" rel="stylesheet" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<root>@(await Html.RenderComponentAsync<BasicTestApp.Index>(RenderMode.Server))</root>
|
<root><component type="typeof(BasicTestApp.Index)" render-mode="Server" /></root>
|
||||||
|
|
||||||
<!-- Used for testing interop scenarios between JS and .NET -->
|
<!-- Used for testing interop scenarios between JS and .NET -->
|
||||||
<script src="js/jsinteroptests.js"></script>
|
<script src="js/jsinteroptests.js"></script>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||||
|
@using BasicTestApp
|
||||||
|
|
||||||
|
|
@ -105,6 +105,22 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
|
||||||
public CacheTagHelperOptions() { }
|
public CacheTagHelperOptions() { }
|
||||||
public long SizeLimit { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
public long SizeLimit { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||||
}
|
}
|
||||||
|
[Microsoft.AspNetCore.Razor.TagHelpers.HtmlTargetElementAttribute("component", Attributes="type", TagStructure=Microsoft.AspNetCore.Razor.TagHelpers.TagStructure.WithoutEndTag)]
|
||||||
|
public sealed partial class ComponentTagHelper : Microsoft.AspNetCore.Razor.TagHelpers.TagHelper
|
||||||
|
{
|
||||||
|
public ComponentTagHelper() { }
|
||||||
|
[Microsoft.AspNetCore.Razor.TagHelpers.HtmlAttributeNameAttribute("type")]
|
||||||
|
public System.Type ComponentType { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||||
|
[Microsoft.AspNetCore.Razor.TagHelpers.HtmlAttributeNameAttribute("params", DictionaryAttributePrefix="param-")]
|
||||||
|
public System.Collections.Generic.IDictionary<string, object> Parameters { get { throw null; } set { } }
|
||||||
|
[Microsoft.AspNetCore.Razor.TagHelpers.HtmlAttributeNameAttribute("render-mode")]
|
||||||
|
public Microsoft.AspNetCore.Mvc.Rendering.RenderMode RenderMode { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||||
|
[Microsoft.AspNetCore.Mvc.ViewFeatures.ViewContextAttribute]
|
||||||
|
[Microsoft.AspNetCore.Razor.TagHelpers.HtmlAttributeNotBoundAttribute]
|
||||||
|
public Microsoft.AspNetCore.Mvc.Rendering.ViewContext ViewContext { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||||
|
[System.Diagnostics.DebuggerStepThroughAttribute]
|
||||||
|
public override System.Threading.Tasks.Task ProcessAsync(Microsoft.AspNetCore.Razor.TagHelpers.TagHelperContext context, Microsoft.AspNetCore.Razor.TagHelpers.TagHelperOutput output) { throw null; }
|
||||||
|
}
|
||||||
[Microsoft.AspNetCore.Razor.TagHelpers.HtmlTargetElementAttribute("distributed-cache", Attributes="name")]
|
[Microsoft.AspNetCore.Razor.TagHelpers.HtmlTargetElementAttribute("distributed-cache", Attributes="name")]
|
||||||
public partial class DistributedCacheTagHelper : Microsoft.AspNetCore.Mvc.TagHelpers.CacheTagHelperBase
|
public partial class DistributedCacheTagHelper : Microsoft.AspNetCore.Mvc.TagHelpers.CacheTagHelperBase
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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 partial class HtmlHelperComponentExtensions
|
||||||
{
|
{
|
||||||
|
public static System.Threading.Tasks.Task<Microsoft.AspNetCore.Html.IHtmlContent> RenderComponentAsync(this Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper htmlHelper, System.Type componentType, Microsoft.AspNetCore.Mvc.Rendering.RenderMode renderMode, object parameters) { throw null; }
|
||||||
public static System.Threading.Tasks.Task<Microsoft.AspNetCore.Html.IHtmlContent> RenderComponentAsync<TComponent>(this Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper htmlHelper, Microsoft.AspNetCore.Mvc.Rendering.RenderMode renderMode) where TComponent : Microsoft.AspNetCore.Components.IComponent { throw null; }
|
public static System.Threading.Tasks.Task<Microsoft.AspNetCore.Html.IHtmlContent> RenderComponentAsync<TComponent>(this Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper htmlHelper, Microsoft.AspNetCore.Mvc.Rendering.RenderMode renderMode) where TComponent : Microsoft.AspNetCore.Components.IComponent { throw null; }
|
||||||
[System.Diagnostics.DebuggerStepThroughAttribute]
|
|
||||||
public static System.Threading.Tasks.Task<Microsoft.AspNetCore.Html.IHtmlContent> RenderComponentAsync<TComponent>(this Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper htmlHelper, Microsoft.AspNetCore.Mvc.Rendering.RenderMode renderMode, object parameters) where TComponent : Microsoft.AspNetCore.Components.IComponent { throw null; }
|
public static System.Threading.Tasks.Task<Microsoft.AspNetCore.Html.IHtmlContent> RenderComponentAsync<TComponent>(this Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper htmlHelper, Microsoft.AspNetCore.Mvc.Rendering.RenderMode renderMode, object parameters) where TComponent : Microsoft.AspNetCore.Components.IComponent { throw null; }
|
||||||
}
|
}
|
||||||
public static partial class HtmlHelperDisplayExtensions
|
public static partial class HtmlHelperDisplayExtensions
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
||||||
using Microsoft.AspNetCore.Mvc.ViewFeatures.Buffers;
|
using Microsoft.AspNetCore.Mvc.ViewFeatures.Buffers;
|
||||||
using Microsoft.AspNetCore.Mvc.ViewFeatures.Filters;
|
using Microsoft.AspNetCore.Mvc.ViewFeatures.Filters;
|
||||||
using Microsoft.AspNetCore.Mvc.ViewFeatures.Infrastructure;
|
using Microsoft.AspNetCore.Mvc.ViewFeatures.Infrastructure;
|
||||||
using Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponents;
|
|
||||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Microsoft.JSInterop;
|
using Microsoft.JSInterop;
|
||||||
|
|
@ -206,8 +205,9 @@ namespace Microsoft.Extensions.DependencyInjection
|
||||||
services.TryAddSingleton<SaveTempDataFilter>();
|
services.TryAddSingleton<SaveTempDataFilter>();
|
||||||
|
|
||||||
//
|
//
|
||||||
// Component prerendering
|
// Component rendering
|
||||||
//
|
//
|
||||||
|
services.TryAddScoped<IComponentRenderer, ComponentRenderer>();
|
||||||
services.TryAddScoped<StaticComponentRenderer>();
|
services.TryAddScoped<StaticComponentRenderer>();
|
||||||
services.TryAddScoped<NavigationManager, HttpNavigationManager>();
|
services.TryAddScoped<NavigationManager, HttpNavigationManager>();
|
||||||
services.TryAddScoped<IJSRuntime, UnsupportedJavaScriptRuntime>();
|
services.TryAddScoped<IJSRuntime, UnsupportedJavaScriptRuntime>();
|
||||||
|
|
|
||||||
|
|
@ -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.Components.Routing;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Http.Extensions;
|
using Microsoft.AspNetCore.Http.Extensions;
|
||||||
using Microsoft.AspNetCore.Http.Features;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponents
|
namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
||||||
{
|
{
|
||||||
internal class StaticComponentRenderer
|
internal class StaticComponentRenderer
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.JSInterop;
|
using Microsoft.JSInterop;
|
||||||
|
|
|
||||||
|
|
@ -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">
|
<data name="TempData_CannotDeserializeType" xml:space="preserve">
|
||||||
<value>Unsupported data type '{0}'.</value>
|
<value>Unsupported data type '{0}'.</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="UnsupportedRenderMode" xml:space="preserve">
|
||||||
|
<value>Unsupported RenderMode '{0}'.</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
|
@ -13,7 +13,7 @@ using Microsoft.AspNetCore.DataProtection;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Http.Features;
|
using Microsoft.AspNetCore.Http.Features;
|
||||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||||
using Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponents;
|
using Microsoft.AspNetCore.Testing;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
|
@ -22,24 +22,26 @@ using Microsoft.Net.Http.Headers;
|
||||||
using Moq;
|
using Moq;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
|
namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
||||||
{
|
{
|
||||||
public class HtmlHelperComponentExtensionsTests
|
public class ComponentRendererTest
|
||||||
{
|
{
|
||||||
private const string PrerenderedServerComponentPattern = "^<!--Blazor:(?<preamble>.*?)-->(?<content>.+?)<!--Blazor:(?<epilogue>.*?)-->$";
|
private const string PrerenderedServerComponentPattern = "^<!--Blazor:(?<preamble>.*?)-->(?<content>.+?)<!--Blazor:(?<epilogue>.*?)-->$";
|
||||||
private const string ServerComponentPattern = "^<!--Blazor:(.*?)-->$";
|
private const string ServerComponentPattern = "^<!--Blazor:(.*?)-->$";
|
||||||
|
|
||||||
private static readonly IDataProtectionProvider _dataprotectorProvider = new EphemeralDataProtectionProvider();
|
private static readonly IDataProtectionProvider _dataprotectorProvider = new EphemeralDataProtectionProvider();
|
||||||
|
|
||||||
|
private readonly ComponentRenderer renderer = GetComponentRenderer();
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task CanRender_ParameterlessComponent()
|
public async Task CanRender_ParameterlessComponent()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var helper = CreateHelper();
|
var viewContext = GetViewContext();
|
||||||
var writer = new StringWriter();
|
var writer = new StringWriter();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await helper.RenderComponentAsync<TestComponent>(RenderMode.Static);
|
var result = await renderer.RenderComponentAsync(viewContext, typeof(TestComponent), RenderMode.Static, null);
|
||||||
result.WriteTo(writer, HtmlEncoder.Default);
|
result.WriteTo(writer, HtmlEncoder.Default);
|
||||||
var content = writer.ToString();
|
var content = writer.ToString();
|
||||||
|
|
||||||
|
|
@ -51,15 +53,13 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
|
||||||
public async Task CanRender_ParameterlessComponent_ServerMode()
|
public async Task CanRender_ParameterlessComponent_ServerMode()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var helper = CreateHelper();
|
var viewContext = GetViewContext();
|
||||||
var writer = new StringWriter();
|
|
||||||
var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
|
var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
|
||||||
.ToTimeLimitedDataProtector();
|
.ToTimeLimitedDataProtector();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await helper.RenderComponentAsync<TestComponent>(RenderMode.Server);
|
var result = await renderer.RenderComponentAsync(viewContext, typeof(TestComponent), RenderMode.Server, null);
|
||||||
result.WriteTo(writer, HtmlEncoder.Default);
|
var content = HtmlContentUtilities.HtmlContentToString(result);
|
||||||
var content = writer.ToString();
|
|
||||||
var match = Regex.Match(content, ServerComponentPattern);
|
var match = Regex.Match(content, ServerComponentPattern);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
|
@ -82,15 +82,13 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
|
||||||
public async Task CanPrerender_ParameterlessComponent_ServerMode()
|
public async Task CanPrerender_ParameterlessComponent_ServerMode()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var helper = CreateHelper();
|
var viewContext = GetViewContext();
|
||||||
var writer = new StringWriter();
|
|
||||||
var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
|
var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
|
||||||
.ToTimeLimitedDataProtector();
|
.ToTimeLimitedDataProtector();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await helper.RenderComponentAsync<TestComponent>(RenderMode.ServerPrerendered);
|
var result = await renderer.RenderComponentAsync(viewContext, typeof(TestComponent), RenderMode.ServerPrerendered, null);
|
||||||
result.WriteTo(writer, HtmlEncoder.Default);
|
var content = HtmlContentUtilities.HtmlContentToString(result);
|
||||||
var content = writer.ToString();
|
|
||||||
var match = Regex.Match(content, PrerenderedServerComponentPattern, RegexOptions.Multiline);
|
var match = Regex.Match(content, PrerenderedServerComponentPattern, RegexOptions.Multiline);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
|
@ -125,21 +123,17 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
|
||||||
public async Task CanRenderMultipleServerComponents()
|
public async Task CanRenderMultipleServerComponents()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var helper = CreateHelper();
|
var viewContext = GetViewContext();
|
||||||
var firstWriter = new StringWriter();
|
|
||||||
var secondWriter = new StringWriter();
|
|
||||||
var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
|
var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
|
||||||
.ToTimeLimitedDataProtector();
|
.ToTimeLimitedDataProtector();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var firstResult = await helper.RenderComponentAsync<TestComponent>(RenderMode.ServerPrerendered);
|
var firstResult = await renderer.RenderComponentAsync(viewContext, typeof(TestComponent), RenderMode.ServerPrerendered, null);
|
||||||
firstResult.WriteTo(firstWriter, HtmlEncoder.Default);
|
var firstComponent = HtmlContentUtilities.HtmlContentToString(firstResult);
|
||||||
var firstComponent = firstWriter.ToString();
|
|
||||||
var firstMatch = Regex.Match(firstComponent, PrerenderedServerComponentPattern, RegexOptions.Multiline);
|
var firstMatch = Regex.Match(firstComponent, PrerenderedServerComponentPattern, RegexOptions.Multiline);
|
||||||
|
|
||||||
var secondResult = await helper.RenderComponentAsync<TestComponent>(RenderMode.Server);
|
var secondResult = await renderer.RenderComponentAsync(viewContext, typeof(TestComponent), RenderMode.Server, null);
|
||||||
secondResult.WriteTo(secondWriter, HtmlEncoder.Default);
|
var secondComponent = HtmlContentUtilities.HtmlContentToString(secondResult);
|
||||||
var secondComponent = secondWriter.ToString();
|
|
||||||
var secondMatch = Regex.Match(secondComponent, ServerComponentPattern);
|
var secondMatch = Regex.Match(secondComponent, ServerComponentPattern);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
|
@ -171,20 +165,13 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
|
||||||
public async Task CanRender_ComponentWithParametersObject()
|
public async Task CanRender_ComponentWithParametersObject()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var helper = CreateHelper();
|
var viewContext = GetViewContext();
|
||||||
var writer = new StringWriter();
|
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await helper.RenderComponentAsync<GreetingComponent>(
|
var result = await renderer.RenderComponentAsync(viewContext, typeof(GreetingComponent), RenderMode.Static, new { Name = "Steve" });
|
||||||
RenderMode.Static,
|
|
||||||
new
|
|
||||||
{
|
|
||||||
Name = "Steve"
|
|
||||||
});
|
|
||||||
result.WriteTo(writer, HtmlEncoder.Default);
|
|
||||||
var content = writer.ToString();
|
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
var content = HtmlContentUtilities.HtmlContentToString(result);
|
||||||
Assert.Equal("<p>Hello Steve!</p>", content);
|
Assert.Equal("<p>Hello Steve!</p>", content);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -192,20 +179,13 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
|
||||||
public async Task CanRender_ComponentWithParameters_ServerMode()
|
public async Task CanRender_ComponentWithParameters_ServerMode()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var helper = CreateHelper();
|
var viewContext = GetViewContext();
|
||||||
var writer = new StringWriter();
|
|
||||||
var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
|
var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
|
||||||
.ToTimeLimitedDataProtector();
|
.ToTimeLimitedDataProtector();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await helper.RenderComponentAsync<GreetingComponent>(
|
var result = await renderer.RenderComponentAsync(viewContext, typeof(GreetingComponent), RenderMode.Server, new { Name = "Daniel" });
|
||||||
RenderMode.Server,
|
var content = HtmlContentUtilities.HtmlContentToString(result);
|
||||||
new
|
|
||||||
{
|
|
||||||
Name = "Daniel"
|
|
||||||
});
|
|
||||||
result.WriteTo(writer, HtmlEncoder.Default);
|
|
||||||
var content = writer.ToString();
|
|
||||||
var match = Regex.Match(content, ServerComponentPattern);
|
var match = Regex.Match(content, ServerComponentPattern);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
|
@ -237,20 +217,14 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
|
||||||
public async Task CanRender_ComponentWithNullParameters_ServerMode()
|
public async Task CanRender_ComponentWithNullParameters_ServerMode()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var helper = CreateHelper();
|
var viewContext = GetViewContext();
|
||||||
var writer = new StringWriter();
|
|
||||||
var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
|
var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
|
||||||
.ToTimeLimitedDataProtector();
|
.ToTimeLimitedDataProtector();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await helper.RenderComponentAsync<GreetingComponent>(
|
|
||||||
RenderMode.Server,
|
var result = await renderer.RenderComponentAsync(viewContext, typeof(GreetingComponent), RenderMode.Server, new { Name = (string)null });
|
||||||
new
|
var content = HtmlContentUtilities.HtmlContentToString(result);
|
||||||
{
|
|
||||||
Name = (string)null
|
|
||||||
});
|
|
||||||
result.WriteTo(writer, HtmlEncoder.Default);
|
|
||||||
var content = writer.ToString();
|
|
||||||
var match = Regex.Match(content, ServerComponentPattern);
|
var match = Regex.Match(content, ServerComponentPattern);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
|
@ -274,28 +248,22 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
|
||||||
Assert.Null(parameterDefinition.TypeName);
|
Assert.Null(parameterDefinition.TypeName);
|
||||||
Assert.Null(parameterDefinition.Assembly);
|
Assert.Null(parameterDefinition.Assembly);
|
||||||
|
|
||||||
var value = Assert.Single(serverComponent.ParameterValues);;
|
var value = Assert.Single(serverComponent.ParameterValues); ;
|
||||||
Assert.Null(value);
|
Assert.Null(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task CanPrerender_ComponentWithParameters_ServerMode()
|
public async Task CanPrerender_ComponentWithParameters_ServerPrerenderedMode()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var helper = CreateHelper();
|
var viewContext = GetViewContext();
|
||||||
var writer = new StringWriter();
|
var writer = new StringWriter();
|
||||||
var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
|
var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
|
||||||
.ToTimeLimitedDataProtector();
|
.ToTimeLimitedDataProtector();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await helper.RenderComponentAsync<GreetingComponent>(
|
var result = await renderer.RenderComponentAsync(viewContext, typeof(GreetingComponent), RenderMode.ServerPrerendered, new { Name = "Daniel" });
|
||||||
RenderMode.ServerPrerendered,
|
var content = HtmlContentUtilities.HtmlContentToString(result);
|
||||||
new
|
|
||||||
{
|
|
||||||
Name = "Daniel"
|
|
||||||
});
|
|
||||||
result.WriteTo(writer, HtmlEncoder.Default);
|
|
||||||
var content = writer.ToString();
|
|
||||||
var match = Regex.Match(content, PrerenderedServerComponentPattern, RegexOptions.Multiline);
|
var match = Regex.Match(content, PrerenderedServerComponentPattern, RegexOptions.Multiline);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
|
@ -336,23 +304,17 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task CanPrerender_ComponentWithNullParameters_ServerMode()
|
public async Task CanPrerender_ComponentWithNullParameters_ServerPrerenderedMode()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var helper = CreateHelper();
|
var viewContext = GetViewContext();
|
||||||
var writer = new StringWriter();
|
var writer = new StringWriter();
|
||||||
var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
|
var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
|
||||||
.ToTimeLimitedDataProtector();
|
.ToTimeLimitedDataProtector();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await helper.RenderComponentAsync<GreetingComponent>(
|
var result = await renderer.RenderComponentAsync(viewContext, typeof(GreetingComponent), RenderMode.ServerPrerendered, new { Name = (string)null });
|
||||||
RenderMode.ServerPrerendered,
|
var content = HtmlContentUtilities.HtmlContentToString(result);
|
||||||
new
|
|
||||||
{
|
|
||||||
Name = (string)null
|
|
||||||
});
|
|
||||||
result.WriteTo(writer, HtmlEncoder.Default);
|
|
||||||
var content = writer.ToString();
|
|
||||||
var match = Regex.Match(content, PrerenderedServerComponentPattern, RegexOptions.Multiline);
|
var match = Regex.Match(content, PrerenderedServerComponentPattern, RegexOptions.Multiline);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
|
@ -396,39 +358,28 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
|
||||||
public async Task ComponentWithInvalidRenderMode_Throws()
|
public async Task ComponentWithInvalidRenderMode_Throws()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var helper = CreateHelper();
|
var viewContext = GetViewContext();
|
||||||
var writer = new StringWriter();
|
|
||||||
|
|
||||||
// Act & Assert
|
// Act & Assert
|
||||||
var result = await Assert.ThrowsAsync<ArgumentException>(() => helper.RenderComponentAsync<GreetingComponent>(
|
var ex = await ExceptionAssert.ThrowsArgumentAsync(
|
||||||
default,
|
() => renderer.RenderComponentAsync(viewContext, typeof(GreetingComponent), default, new { Name = "Daniel" }),
|
||||||
new
|
"renderMode",
|
||||||
{
|
$"Unsupported RenderMode '{(RenderMode)default}'");
|
||||||
Name = "Steve"
|
|
||||||
}));
|
|
||||||
Assert.Equal("renderMode", result.ParamName);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task RenderComponent_DoesNotInvokeOnAfterRenderInComponent()
|
public async Task RenderComponent_DoesNotInvokeOnAfterRenderInComponent()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var helper = CreateHelper();
|
var viewContext = GetViewContext();
|
||||||
var writer = new StringWriter();
|
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var state = new OnAfterRenderState();
|
var state = new OnAfterRenderState();
|
||||||
var result = await helper.RenderComponentAsync<OnAfterRenderComponent>(
|
var result = await renderer.RenderComponentAsync(viewContext, typeof(OnAfterRenderComponent), RenderMode.Static, new { state });
|
||||||
RenderMode.Static,
|
|
||||||
new
|
|
||||||
{
|
|
||||||
State = state
|
|
||||||
});
|
|
||||||
|
|
||||||
result.WriteTo(writer, HtmlEncoder.Default);
|
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal("<p>Hello</p>", writer.ToString());
|
var content = HtmlContentUtilities.HtmlContentToString(result);
|
||||||
|
Assert.Equal("<p>Hello</p>", content);
|
||||||
Assert.False(state.OnAfterRenderRan);
|
Assert.False(state.OnAfterRenderRan);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -436,10 +387,12 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
|
||||||
public async Task CanCatch_ComponentWithSynchronousException()
|
public async Task CanCatch_ComponentWithSynchronousException()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var helper = CreateHelper();
|
var viewContext = GetViewContext();
|
||||||
|
|
||||||
// Act & Assert
|
// Act & Assert
|
||||||
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => helper.RenderComponentAsync<ExceptionComponent>(
|
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => renderer.RenderComponentAsync(
|
||||||
|
viewContext,
|
||||||
|
typeof(ExceptionComponent),
|
||||||
RenderMode.Static,
|
RenderMode.Static,
|
||||||
new
|
new
|
||||||
{
|
{
|
||||||
|
|
@ -454,10 +407,12 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
|
||||||
public async Task CanCatch_ComponentWithAsynchronousException()
|
public async Task CanCatch_ComponentWithAsynchronousException()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var helper = CreateHelper();
|
var viewContext = GetViewContext();
|
||||||
|
|
||||||
// Act & Assert
|
// Act & Assert
|
||||||
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => helper.RenderComponentAsync<ExceptionComponent>(
|
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => renderer.RenderComponentAsync(
|
||||||
|
viewContext,
|
||||||
|
typeof(ExceptionComponent),
|
||||||
RenderMode.Static,
|
RenderMode.Static,
|
||||||
new
|
new
|
||||||
{
|
{
|
||||||
|
|
@ -472,10 +427,12 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
|
||||||
public async Task Rendering_ComponentWithJsInteropThrows()
|
public async Task Rendering_ComponentWithJsInteropThrows()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var helper = CreateHelper();
|
var viewContext = GetViewContext();
|
||||||
|
|
||||||
// Act & Assert
|
// Act & Assert
|
||||||
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => helper.RenderComponentAsync<ExceptionComponent>(
|
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => renderer.RenderComponentAsync(
|
||||||
|
viewContext,
|
||||||
|
typeof(ExceptionComponent),
|
||||||
RenderMode.Static,
|
RenderMode.Static,
|
||||||
new
|
new
|
||||||
{
|
{
|
||||||
|
|
@ -503,11 +460,12 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
|
||||||
var responseMock = new Mock<IHttpResponseFeature>();
|
var responseMock = new Mock<IHttpResponseFeature>();
|
||||||
responseMock.Setup(r => r.HasStarted).Returns(true);
|
responseMock.Setup(r => r.HasStarted).Returns(true);
|
||||||
ctx.Features.Set(responseMock.Object);
|
ctx.Features.Set(responseMock.Object);
|
||||||
var helper = CreateHelper(ctx);
|
var viewContext = GetViewContext(ctx);
|
||||||
var writer = new StringWriter();
|
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => helper.RenderComponentAsync<RedirectComponent>(
|
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => renderer.RenderComponentAsync(
|
||||||
|
viewContext,
|
||||||
|
typeof(RedirectComponent),
|
||||||
RenderMode.Static,
|
RenderMode.Static,
|
||||||
new
|
new
|
||||||
{
|
{
|
||||||
|
|
@ -515,8 +473,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
|
||||||
}));
|
}));
|
||||||
|
|
||||||
Assert.Equal("A navigation command was attempted during prerendering after the server already started sending the response. " +
|
Assert.Equal("A navigation command was attempted during prerendering after the server already started sending the response. " +
|
||||||
"Navigation commands can not be issued during server-side prerendering after the response from the server has started. Applications must buffer the" +
|
"Navigation commands can not be issued during server-side prerendering after the response from the server has started. Applications must buffer the" +
|
||||||
"reponse and avoid using features like FlushAsync() before all components on the page have been rendered to prevent failed navigation commands.",
|
"reponse and avoid using features like FlushAsync() before all components on the page have been rendered to prevent failed navigation commands.",
|
||||||
exception.Message);
|
exception.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -530,10 +488,12 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
|
||||||
ctx.Request.PathBase = "/base";
|
ctx.Request.PathBase = "/base";
|
||||||
ctx.Request.Path = "/path";
|
ctx.Request.Path = "/path";
|
||||||
ctx.Request.QueryString = new QueryString("?query=value");
|
ctx.Request.QueryString = new QueryString("?query=value");
|
||||||
var helper = CreateHelper(ctx);
|
var viewContext = GetViewContext(ctx);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
await helper.RenderComponentAsync<RedirectComponent>(
|
await renderer.RenderComponentAsync(
|
||||||
|
viewContext,
|
||||||
|
typeof(RedirectComponent),
|
||||||
RenderMode.Static,
|
RenderMode.Static,
|
||||||
new
|
new
|
||||||
{
|
{
|
||||||
|
|
@ -549,8 +509,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
|
||||||
public async Task CanRender_AsyncComponent()
|
public async Task CanRender_AsyncComponent()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var helper = CreateHelper();
|
var viewContext = GetViewContext();
|
||||||
var writer = new StringWriter();
|
|
||||||
var expectedContent = @"<table>
|
var expectedContent = @"<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
|
@ -595,29 +554,29 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
|
||||||
</table>";
|
</table>";
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await helper.RenderComponentAsync<AsyncComponent>(RenderMode.Static);
|
var result = await renderer.RenderComponentAsync(viewContext,typeof(AsyncComponent), RenderMode.Static, null);
|
||||||
result.WriteTo(writer, HtmlEncoder.Default);
|
var content = HtmlContentUtilities.HtmlContentToString(result);
|
||||||
var content = writer.ToString();
|
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal(expectedContent.Replace("\r\n", "\n"), content);
|
Assert.Equal(expectedContent.Replace("\r\n", "\n"), content);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IHtmlHelper CreateHelper(HttpContext ctx = null, Action<IServiceCollection> configureServices = null)
|
private static ComponentRenderer GetComponentRenderer() =>
|
||||||
|
new ComponentRenderer(
|
||||||
|
new StaticComponentRenderer(HtmlEncoder.Default),
|
||||||
|
new ServerComponentSerializer(_dataprotectorProvider));
|
||||||
|
|
||||||
|
private static ViewContext GetViewContext(HttpContext context = null, Action<IServiceCollection> configureServices = null)
|
||||||
{
|
{
|
||||||
var services = new ServiceCollection();
|
var services = new ServiceCollection();
|
||||||
services.AddSingleton(HtmlEncoder.Default);
|
|
||||||
services.AddSingleton<ServerComponentSerializer>();
|
|
||||||
services.AddSingleton(_dataprotectorProvider);
|
services.AddSingleton(_dataprotectorProvider);
|
||||||
services.AddSingleton<IJSRuntime, UnsupportedJavaScriptRuntime>();
|
services.AddSingleton<IJSRuntime, UnsupportedJavaScriptRuntime>();
|
||||||
services.AddSingleton<NavigationManager, HttpNavigationManager>();
|
services.AddSingleton<NavigationManager, HttpNavigationManager>();
|
||||||
services.AddSingleton<StaticComponentRenderer>();
|
|
||||||
services.AddSingleton<ILoggerFactory, NullLoggerFactory>();
|
services.AddSingleton<ILoggerFactory, NullLoggerFactory>();
|
||||||
|
|
||||||
configureServices?.Invoke(services);
|
configureServices?.Invoke(services);
|
||||||
|
|
||||||
var helper = new Mock<IHtmlHelper>();
|
context ??= new DefaultHttpContext();
|
||||||
var context = ctx ?? new DefaultHttpContext();
|
|
||||||
context.RequestServices = services.BuildServiceProvider();
|
context.RequestServices = services.BuildServiceProvider();
|
||||||
context.Request.Scheme = "http";
|
context.Request.Scheme = "http";
|
||||||
context.Request.Host = new HostString("localhost");
|
context.Request.Host = new HostString("localhost");
|
||||||
|
|
@ -625,12 +584,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
|
||||||
context.Request.Path = "/path";
|
context.Request.Path = "/path";
|
||||||
context.Request.QueryString = QueryString.FromUriComponent("?query=value");
|
context.Request.QueryString = QueryString.FromUriComponent("?query=value");
|
||||||
|
|
||||||
helper.Setup(h => h.ViewContext)
|
return new ViewContext { HttpContext = context };
|
||||||
.Returns(new ViewContext()
|
|
||||||
{
|
|
||||||
HttpContext = context
|
|
||||||
});
|
|
||||||
return helper.Object;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private class TestComponent : IComponent
|
private class TestComponent : IComponent
|
||||||
|
|
@ -6,13 +6,11 @@ using System.Collections.Generic;
|
||||||
using System.Runtime.ExceptionServices;
|
using System.Runtime.ExceptionServices;
|
||||||
using System.Text.Encodings.Web;
|
using System.Text.Encodings.Web;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Components;
|
|
||||||
using Microsoft.AspNetCore.Components.Rendering;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.RazorComponents
|
namespace Microsoft.AspNetCore.Components.Rendering
|
||||||
{
|
{
|
||||||
public class HtmlRendererTest
|
public class HtmlRendererTest
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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
|
@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
|
@page
|
||||||
@model MvcSandbox.Pages.ComponentsModel
|
|
||||||
@{
|
@{
|
||||||
Layout = null;
|
Layout = null;
|
||||||
}
|
}
|
||||||
|
|
@ -15,8 +14,7 @@
|
||||||
<link href="css/site.css" rel="stylesheet" />
|
<link href="css/site.css" rel="stylesheet" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<app>@(await Html.RenderComponentAsync<MvcSandbox.Components.App>(RenderMode.Static))</app>
|
<component type="typeof(MvcSandbox.Components.App)" render-mode="Static" />
|
||||||
|
<script src="_framework/blazor.server.js"></script>
|
||||||
<script src="_framework/components.server.js"></script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -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">
|
<li class="nav-item">
|
||||||
<a class="nav-link text-dark" asp-area="" asp-page="/PagesHome">Pages Home</a>
|
<a class="nav-link text-dark" asp-area="" asp-page="/PagesHome">Pages Home</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link text-dark" asp-area="" asp-page="/Components">Components</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<app>
|
<app>
|
||||||
@(await Html.RenderComponentAsync<App>(RenderMode.ServerPrerendered))
|
<component type="typeof(App)" render-mode="ServerPrerendered" />
|
||||||
</app>
|
</app>
|
||||||
|
|
||||||
<div id="blazor-error-ui">
|
<div id="blazor-error-ui">
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue