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.IO;
using System.Threading.Tasks;
using Microsoft.AspNet.Html.Abstractions;
using Microsoft.Framework.Internal;
using Microsoft.Framework.WebEncoders;
@ -14,24 +15,26 @@ namespace Microsoft.AspNet.Mvc.Razor
/// </summary>
public class HelperResult : IHtmlContent
{
private readonly Action<TextWriter> _action;
private readonly Func<TextWriter, Task> _asyncAction;
/// <summary>
/// Creates a new instance of <see cref="HelperResult"/>.
/// </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>
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>
/// 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>
public Action<TextWriter> WriteAction
public Func<TextWriter, Task> WriteAction
{
get { return _action; }
get { return _asyncAction; }
}
/// <summary>
@ -41,7 +44,7 @@ namespace Microsoft.AspNet.Mvc.Razor
/// <param name="encoder">The <see cref="IHtmlEncoder"/> to encode the content.</param>
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>
/// Gets or sets the action invoked to render the body.
/// </summary>
Action<TextWriter> RenderBodyDelegate { get; set; }
Func<TextWriter, Task> RenderBodyDelegateAsync { get; set; }
/// <summary>
/// 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 />
public Action<TextWriter> RenderBodyDelegate { get; set; }
public Func<TextWriter, Task> RenderBodyDelegateAsync { get; set; }
/// <inheritdoc />
public bool IsLayoutBeingRendered { get; set; }
@ -685,14 +685,14 @@ namespace Microsoft.AspNet.Mvc.Razor
protected virtual HelperResult RenderBody()
{
if (RenderBodyDelegate == null)
if (RenderBodyDelegateAsync == null)
{
var message = Resources.FormatRazorPage_MethodCannotBeCalled(nameof(RenderBody), Path);
throw new InvalidOperationException(message);
}
_renderedBody = true;
return new HelperResult(RenderBodyDelegate);
return new HelperResult(RenderBodyDelegateAsync);
}
/// <summary>
@ -860,7 +860,7 @@ namespace Microsoft.AspNet.Mvc.Razor
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.
// 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.
previousPage.IsLayoutBeingRendered = true;
layoutPage.PreviousSectionWriters = previousPage.SectionWriters;
layoutPage.RenderBodyDelegate = bodyWriter.CopyTo;
layoutPage.RenderBodyDelegateAsync = bodyWriter.CopyToAsync;
bodyWriter = await RenderPageAsync(layoutPage, context, executeViewStart: false);
renderedLayouts.Add(layoutPage);

View File

@ -27,6 +27,8 @@
<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>
</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>
<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>
<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>

View File

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

View File

@ -1,4 +1,6 @@
@using TagHelpersWebSite.Models
@using Microsoft.AspNet.Mvc.Razor
@model WebsiteContext
@{
@ -16,6 +18,15 @@
</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">
<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>
@ -25,6 +36,11 @@
<tag-cloud count="Model.TagsToShow" surround="div" />
<h3>Current Tag Cloud from ViewComponentHelper:</h3>
<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>
@section footerContent {