Update `HelperResult` to take in an async func.

- The corresponding Razor change results in `HelperResult`s being rendered with async lambdas.
- This change enables `TagHelper`s and other async code to exist inside of `HelperResult` blocks.
- Added test to validate Templates (they generate `HelperResult`s) can utilize `TagHelper`s correctly.
- Rename `RazorPage`s `RenderBodyDelegate` to `RenderBodyDelegateAsync`.

aspnet/Razor#494
This commit is contained in:
N. Taylor Mullen 2015-08-24 12:57:38 -07:00
parent 3041dee86d
commit cc5c0d6cbe
7 changed files with 49 additions and 26 deletions

View File

@ -3,6 +3,7 @@
using System; using System;
using System.IO; using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNet.Html.Abstractions; using Microsoft.AspNet.Html.Abstractions;
using Microsoft.Framework.Internal; using Microsoft.Framework.Internal;
using Microsoft.Framework.WebEncoders; using Microsoft.Framework.WebEncoders;
@ -14,24 +15,26 @@ namespace Microsoft.AspNet.Mvc.Razor
/// </summary> /// </summary>
public class HelperResult : IHtmlContent public class HelperResult : IHtmlContent
{ {
private readonly Action<TextWriter> _action; private readonly Func<TextWriter, Task> _asyncAction;
/// <summary> /// <summary>
/// Creates a new instance of <see cref="HelperResult"/>. /// Creates a new instance of <see cref="HelperResult"/>.
/// </summary> /// </summary>
/// <param name="action">The delegate to invoke when /// <param name="asyncAction">The asynchronous delegate to invoke when
/// <see cref="WriteTo(TextWriter, IHtmlEncoder)"/> is called.</param> /// <see cref="WriteTo(TextWriter, IHtmlEncoder)"/> is called.</param>
public HelperResult([NotNull] Action<TextWriter> action) /// <remarks>Calls to <see cref="WriteTo(TextWriter, IHtmlEncoder)"/> result in a blocking invocation of
/// <paramref name="asyncAction"/>.</remarks>
public HelperResult([NotNull] Func<TextWriter, Task> asyncAction)
{ {
_action = action; _asyncAction = asyncAction;
} }
/// <summary> /// <summary>
/// Gets the delegate to invoke when <see cref="WriteTo(TextWriter, IHtmlEncoder)"/> is called. /// Gets the asynchronous delegate to invoke when <see cref="WriteTo(TextWriter, IHtmlEncoder)"/> is called.
/// </summary> /// </summary>
public Action<TextWriter> WriteAction public Func<TextWriter, Task> WriteAction
{ {
get { return _action; } get { return _asyncAction; }
} }
/// <summary> /// <summary>
@ -41,7 +44,7 @@ namespace Microsoft.AspNet.Mvc.Razor
/// <param name="encoder">The <see cref="IHtmlEncoder"/> to encode the content.</param> /// <param name="encoder">The <see cref="IHtmlEncoder"/> to encode the content.</param>
public virtual void WriteTo([NotNull] TextWriter writer, [NotNull] IHtmlEncoder encoder) public virtual void WriteTo([NotNull] TextWriter writer, [NotNull] IHtmlEncoder encoder)
{ {
_action(writer); _asyncAction(writer).GetAwaiter().GetResult();
} }
} }
} }

View File

@ -22,7 +22,7 @@ namespace Microsoft.AspNet.Mvc.Razor
/// <summary> /// <summary>
/// Gets or sets the action invoked to render the body. /// Gets or sets the action invoked to render the body.
/// </summary> /// </summary>
Action<TextWriter> RenderBodyDelegate { get; set; } Func<TextWriter, Task> RenderBodyDelegateAsync { get; set; }
/// <summary> /// <summary>
/// Gets or sets a flag that determines if the layout of this page is being rendered. /// Gets or sets a flag that determines if the layout of this page is being rendered.

View File

@ -126,7 +126,7 @@ namespace Microsoft.AspNet.Mvc.Razor
} }
/// <inheritdoc /> /// <inheritdoc />
public Action<TextWriter> RenderBodyDelegate { get; set; } public Func<TextWriter, Task> RenderBodyDelegateAsync { get; set; }
/// <inheritdoc /> /// <inheritdoc />
public bool IsLayoutBeingRendered { get; set; } public bool IsLayoutBeingRendered { get; set; }
@ -685,14 +685,14 @@ namespace Microsoft.AspNet.Mvc.Razor
protected virtual HelperResult RenderBody() protected virtual HelperResult RenderBody()
{ {
if (RenderBodyDelegate == null) if (RenderBodyDelegateAsync == null)
{ {
var message = Resources.FormatRazorPage_MethodCannotBeCalled(nameof(RenderBody), Path); var message = Resources.FormatRazorPage_MethodCannotBeCalled(nameof(RenderBody), Path);
throw new InvalidOperationException(message); throw new InvalidOperationException(message);
} }
_renderedBody = true; _renderedBody = true;
return new HelperResult(RenderBodyDelegate); return new HelperResult(RenderBodyDelegateAsync);
} }
/// <summary> /// <summary>
@ -860,7 +860,7 @@ namespace Microsoft.AspNet.Mvc.Razor
throw new InvalidOperationException(Resources.FormatSectionsNotRendered(Path, sectionNames)); throw new InvalidOperationException(Resources.FormatSectionsNotRendered(Path, sectionNames));
} }
} }
else if (RenderBodyDelegate != null && !_renderedBody) else if (RenderBodyDelegateAsync != null && !_renderedBody)
{ {
// There are no sections defined, but RenderBody was NOT called. // There are no sections defined, but RenderBody was NOT called.
// If a body was defined, then RenderBody should have been called. // If a body was defined, then RenderBody should have been called.

View File

@ -189,7 +189,7 @@ namespace Microsoft.AspNet.Mvc.Razor
// in the layout. // in the layout.
previousPage.IsLayoutBeingRendered = true; previousPage.IsLayoutBeingRendered = true;
layoutPage.PreviousSectionWriters = previousPage.SectionWriters; layoutPage.PreviousSectionWriters = previousPage.SectionWriters;
layoutPage.RenderBodyDelegate = bodyWriter.CopyTo; layoutPage.RenderBodyDelegateAsync = bodyWriter.CopyToAsync;
bodyWriter = await RenderPageAsync(layoutPage, context, executeViewStart: false); bodyWriter = await RenderPageAsync(layoutPage, context, executeViewStart: false);
renderedLayouts.Add(layoutPage); renderedLayouts.Add(layoutPage);

View File

@ -27,6 +27,8 @@
<div> <div>
<p>This website has <em><strong style="font-size: 1.25em;text-decoration: underline;">not</strong></em> been approved yet. Visit <strong><a target="_blank" href="http://www.contoso.com">www.contoso.com</a></strong> for <strong>more</strong> information.</p> <p>This website has <em><strong style="font-size: 1.25em;text-decoration: underline;">not</strong></em> been approved yet. Visit <strong><a target="_blank" href="http://www.contoso.com">www.contoso.com</a></strong> for <strong>more</strong> information.</p>
</div> </div>
@ -36,6 +38,8 @@
<div>["Lorem","ipsum","dolor","sit","amet","consectetur","adipisicing","elit","sed","do","eiusmod","tempor","incididunt","ut","labore","et","dolore","magna","aliquaUt","enim"]</div> <div>["Lorem","ipsum","dolor","sit","amet","consectetur","adipisicing","elit","sed","do","eiusmod","tempor","incididunt","ut","labore","et","dolore","magna","aliquaUt","enim"]</div>
<h3 style="font-family: cursive;">Current Tag Cloud from ViewComponentHelper:</h3> <h3 style="font-family: cursive;">Current Tag Cloud from ViewComponentHelper:</h3>
<section><b>["Lorem","ipsum","dolor","sit","amet","consectetur","adipisicing","elit","sed","do","eiusmod","tempor","incididunt","ut","labore"]</b></section> <section><b>["Lorem","ipsum","dolor","sit","amet","consectetur","adipisicing","elit","sed","do","eiusmod","tempor","incididunt","ut","labore"]</b></section>
<br /><p><em>Rendering Template:</em></p>
<div><h3 style="font-family: cursive;">Tag Cloud from Template: </h3>["Lorem","ipsum","dolor","sit","amet","consectetur","adipisicing","elit","sed","do","eiusmod","tempor","incididunt","ut","labore","et","dolore","magna","aliquaUt","enim"]</div>
</div> </div>

View File

@ -301,7 +301,7 @@ namespace Microsoft.AspNet.Mvc.Razor
{ {
{ "baz", _nullRenderAsyncDelegate } { "baz", _nullRenderAsyncDelegate }
}; };
page.RenderBodyDelegate = CreateBodyAction("body-content"); page.RenderBodyDelegateAsync = CreateBodyAction("body-content");
// Act // Act
await page.ExecuteAsync(); await page.ExecuteAsync();
@ -325,7 +325,7 @@ namespace Microsoft.AspNet.Mvc.Razor
{ {
{ "baz", _nullRenderAsyncDelegate } { "baz", _nullRenderAsyncDelegate }
}; };
page.RenderBodyDelegate = CreateBodyAction("body-content"); page.RenderBodyDelegateAsync = CreateBodyAction("body-content");
// Act // Act
await page.ExecuteAsync(); await page.ExecuteAsync();
@ -338,7 +338,7 @@ namespace Microsoft.AspNet.Mvc.Razor
public async Task RenderSection_ThrowsIfSectionIsRenderedMoreThanOnce() public async Task RenderSection_ThrowsIfSectionIsRenderedMoreThanOnce()
{ {
// Arrange // Arrange
var expected = new HelperResult(action: null); var expected = new HelperResult(asyncAction: null);
var page = CreatePage(v => var page = CreatePage(v =>
{ {
v.Path = "/Views/TestPath/Test.cshtml"; v.Path = "/Views/TestPath/Test.cshtml";
@ -362,7 +362,7 @@ namespace Microsoft.AspNet.Mvc.Razor
public async Task RenderSectionAsync_ThrowsIfSectionIsRenderedMoreThanOnce() public async Task RenderSectionAsync_ThrowsIfSectionIsRenderedMoreThanOnce()
{ {
// Arrange // Arrange
var expected = new HelperResult(action: null); var expected = new HelperResult(asyncAction: null);
var page = CreatePage(async v => var page = CreatePage(async v =>
{ {
v.Path = "/Views/TestPath/Test.cshtml"; v.Path = "/Views/TestPath/Test.cshtml";
@ -386,7 +386,7 @@ namespace Microsoft.AspNet.Mvc.Razor
public async Task RenderSectionAsync_ThrowsIfSectionIsRenderedMoreThanOnce_WithSyncMethod() public async Task RenderSectionAsync_ThrowsIfSectionIsRenderedMoreThanOnce_WithSyncMethod()
{ {
// Arrange // Arrange
var expected = new HelperResult(action: null); var expected = new HelperResult(asyncAction: null);
var page = CreatePage(async v => var page = CreatePage(async v =>
{ {
v.Path = "/Views/TestPath/Test.cshtml"; v.Path = "/Views/TestPath/Test.cshtml";
@ -410,7 +410,7 @@ namespace Microsoft.AspNet.Mvc.Razor
public async Task RenderSectionAsync_ThrowsIfNotInvokedFromLayoutPage() public async Task RenderSectionAsync_ThrowsIfNotInvokedFromLayoutPage()
{ {
// Arrange // Arrange
var expected = new HelperResult(action: null); var expected = new HelperResult(asyncAction: null);
var page = CreatePage(async v => var page = CreatePage(async v =>
{ {
v.Path = "/Views/TestPath/Test.cshtml"; v.Path = "/Views/TestPath/Test.cshtml";
@ -434,7 +434,7 @@ namespace Microsoft.AspNet.Mvc.Razor
{ {
}); });
page.Path = path; page.Path = path;
page.RenderBodyDelegate = CreateBodyAction("some content"); page.RenderBodyDelegateAsync = CreateBodyAction("some content");
// Act // Act
await page.ExecuteAsync(); await page.ExecuteAsync();
@ -454,7 +454,7 @@ namespace Microsoft.AspNet.Mvc.Razor
{ {
}); });
page.Path = path; page.Path = path;
page.RenderBodyDelegate = CreateBodyAction("some content"); page.RenderBodyDelegateAsync = CreateBodyAction("some content");
page.PreviousSectionWriters = new Dictionary<string, RenderAsyncDelegate> page.PreviousSectionWriters = new Dictionary<string, RenderAsyncDelegate>
{ {
{ sectionName, _nullRenderAsyncDelegate } { sectionName, _nullRenderAsyncDelegate }
@ -480,7 +480,7 @@ namespace Microsoft.AspNet.Mvc.Razor
v.RenderSection(sectionA); v.RenderSection(sectionA);
v.RenderSection(sectionB); v.RenderSection(sectionB);
}); });
page.RenderBodyDelegate = CreateBodyAction("some content"); page.RenderBodyDelegateAsync = CreateBodyAction("some content");
page.PreviousSectionWriters = new Dictionary<string, RenderAsyncDelegate> page.PreviousSectionWriters = new Dictionary<string, RenderAsyncDelegate>
{ {
{ sectionA, _nullRenderAsyncDelegate }, { sectionA, _nullRenderAsyncDelegate },
@ -514,7 +514,7 @@ namespace Microsoft.AspNet.Mvc.Razor
v.Write(v.RenderSection("footer")); v.Write(v.RenderSection("footer"));
v.WriteLiteral("Layout end"); v.WriteLiteral("Layout end");
}); });
page.RenderBodyDelegate = CreateBodyAction("body content" + Environment.NewLine); page.RenderBodyDelegateAsync = CreateBodyAction("body content" + Environment.NewLine);
page.PreviousSectionWriters = new Dictionary<string, RenderAsyncDelegate> page.PreviousSectionWriters = new Dictionary<string, RenderAsyncDelegate>
{ {
{ {
@ -1834,9 +1834,9 @@ namespace Microsoft.AspNet.Mvc.Razor
new HtmlHelperOptions()); new HtmlHelperOptions());
} }
private static Action<TextWriter> CreateBodyAction(string value) private static Func<TextWriter, Task> CreateBodyAction(string value)
{ {
return (writer) => writer.Write(value); return async (writer) => await writer.WriteAsync(value);
} }
public abstract class TestableRazorPage : RazorPage public abstract class TestableRazorPage : RazorPage

View File

@ -1,4 +1,6 @@
@using TagHelpersWebSite.Models @using TagHelpersWebSite.Models
@using Microsoft.AspNet.Mvc.Razor
@model WebsiteContext @model WebsiteContext
@{ @{
@ -16,6 +18,15 @@
</style> </style>
} }
@functions {
public void RenderTemplate(string title, Func<string, HelperResult> template)
{
Output.WriteLine("<br /><p><em>Rendering Template:</em></p>");
var helperResult = template(title);
helperResult.WriteTo(Output, HtmlEncoder);
}
}
<div condition="!Model.Approved"> <div condition="!Model.Approved">
<p>This website has <strong surround="em">not</strong> been approved yet. Visit www.contoso.com for <strong make-pretty="false">more</strong> information.</p> <p>This website has <strong surround="em">not</strong> been approved yet. Visit www.contoso.com for <strong make-pretty="false">more</strong> information.</p>
</div> </div>
@ -25,6 +36,11 @@
<tag-cloud count="Model.TagsToShow" surround="div" /> <tag-cloud count="Model.TagsToShow" surround="div" />
<h3>Current Tag Cloud from ViewComponentHelper:</h3> <h3>Current Tag Cloud from ViewComponentHelper:</h3>
<section bold>@await Component.InvokeAsync("Tags", 15)</section> <section bold>@await Component.InvokeAsync("Tags", 15)</section>
@{
RenderTemplate(
"Tag Cloud from Template: ",
@<div condition="true"><h3>@item</h3><tag-cloud count="Model.TagsToShow"></tag-cloud></div>);
}
</div> </div>
@section footerContent { @section footerContent {