Adding RenderSectionAsync to RazorPage

Fixes #845
This commit is contained in:
Pranav K 2014-10-03 14:24:37 -07:00
parent 39376617cc
commit 4ec6da1ed3
14 changed files with 284 additions and 148 deletions

View File

@ -45,10 +45,9 @@
<strong>Marketing:</strong> <a href="mailto:Marketing@example.com">Marketing@example.com</a>
</address>
@* Remove the Wait() calls once we add support for async sections *@
@{
FlushAsync().Wait();
Task.Delay(TimeSpan.FromSeconds(1)).Wait();
await FlushAsync();
await Task.Delay(TimeSpan.FromSeconds(1));
}
<div class="row">
<div class="col-md-6">

View File

@ -11,7 +11,7 @@
body { padding-top: 0px; }
}
</style>
@RenderSection("header", required: false)
@await RenderSectionAsync("header", required: false)
</head>
<body>
<div class="navbar navbar-inverse navbar-fixed-top">
@ -37,6 +37,6 @@
<p>&copy; @DateTime.Now.Year - My ASP.NET Application</p>
</footer>
</div>
@RenderSection("footer", required: false)
@await RenderSectionAsync("footer", required: false)
</body>
</html>

View File

@ -3,12 +3,12 @@
using System.Threading.Tasks;
namespace Microsoft.AspNet.Mvc.Internal
namespace Microsoft.AspNet.Mvc
{
/// <summary>
/// Utility methods for dealing with <see cref="Task"/>.
/// </summary>
public static class TaskHelper
internal static class TaskHelper
{
/// <summary>
/// Waits for the task to complete and throws the first faulting exception if the task is faulted.
@ -21,5 +21,18 @@ namespace Microsoft.AspNet.Mvc.Internal
{
task.GetAwaiter().GetResult();
}
/// <summary>
/// Waits for the task to complete and throws the first faulting exception if the task is faulted.
/// It preserves the original stack trace when throwing the exception.
/// </summary>
/// <remarks>
/// Invoking this method is equivalent to calling <see cref="Task{TResult}.Result"/> on the
/// <paramref name="task"/> if it is not completed.
/// </remarks>
public static TVal WaitAndThrowIfFaulted<TVal>(Task<TVal> task)
{
return task.GetAwaiter().GetResult();
}
}
}

View File

@ -22,7 +22,6 @@ namespace Microsoft.AspNet.Mvc.Razor
/// <summary>
/// Gets or sets the action invoked to render the body.
/// </summary>
// TODO: https://github.com/aspnet/Mvc/issues/845 tracks making this async
Action<TextWriter> RenderBodyDelegate { get; set; }
/// <summary>
@ -53,12 +52,12 @@ namespace Microsoft.AspNet.Mvc.Razor
/// <summary>
/// Gets or sets the sections that can be rendered by this page.
/// </summary>
Dictionary<string, HelperResult> PreviousSectionWriters { get; set; }
Dictionary<string, RenderAsyncDelegate> PreviousSectionWriters { get; set; }
/// <summary>
/// Gets the sections that are defined by this page.
/// </summary>
Dictionary<string, HelperResult> SectionWriters { get; }
Dictionary<string, RenderAsyncDelegate> SectionWriters { get; }
/// <summary>
/// Renders the page and writes the output to the <see cref="ViewContext.Writer"/>.

View File

@ -205,17 +205,17 @@ namespace Microsoft.AspNet.Mvc.Razor
/// <summary>
/// {0} can only be called from a layout page.
/// </summary>
internal static string RenderBodyCannotBeCalled
internal static string RazorPage_MethodCannotBeCalled
{
get { return GetString("RenderBodyCannotBeCalled"); }
get { return GetString("RazorPage_MethodCannotBeCalled"); }
}
/// <summary>
/// {0} can only be called from a layout page.
/// </summary>
internal static string FormatRenderBodyCannotBeCalled(object p0)
internal static string FormatRazorPage_MethodCannotBeCalled(object p0)
{
return string.Format(CultureInfo.CurrentCulture, GetString("RenderBodyCannotBeCalled"), p0);
return string.Format(CultureInfo.CurrentCulture, GetString("RazorPage_MethodCannotBeCalled"), p0);
}
/// <summary>
@ -251,7 +251,7 @@ namespace Microsoft.AspNet.Mvc.Razor
}
/// <summary>
/// {0} has already been called for the section named '{1}'.
/// The section named '{0}' has already been rendered.
/// </summary>
internal static string SectionAlreadyRendered
{
@ -259,11 +259,11 @@ namespace Microsoft.AspNet.Mvc.Razor
}
/// <summary>
/// {0} has already been called for the section named '{1}'.
/// The section named '{0}' has already been rendered.
/// </summary>
internal static string FormatSectionAlreadyRendered(object p0, object p1)
internal static string FormatSectionAlreadyRendered(object p0)
{
return string.Format(CultureInfo.CurrentCulture, GetString("SectionAlreadyRendered"), p0, p1);
return string.Format(CultureInfo.CurrentCulture, GetString("SectionAlreadyRendered"), p0);
}
/// <summary>
@ -362,22 +362,6 @@ namespace Microsoft.AspNet.Mvc.Razor
return string.Format(CultureInfo.CurrentCulture, GetString("ViewMustBeContextualized"), p0, p1);
}
/// <summary>
/// The method '{0}' cannot be invoked by this view.
/// </summary>
internal static string View_MethodCannotBeCalled
{
get { return GetString("View_MethodCannotBeCalled"); }
}
/// <summary>
/// The method '{0}' cannot be invoked by this view.
/// </summary>
internal static string FormatView_MethodCannotBeCalled(object p0)
{
return string.Format(CultureInfo.CurrentCulture, GetString("View_MethodCannotBeCalled"), p0);
}
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);

View File

@ -30,7 +30,7 @@ namespace Microsoft.AspNet.Mvc.Razor
public RazorPage()
{
SectionWriters = new Dictionary<string, HelperResult>(StringComparer.OrdinalIgnoreCase);
SectionWriters = new Dictionary<string, RenderAsyncDelegate>(StringComparer.OrdinalIgnoreCase);
_writerScopes = new Stack<TextWriter>();
}
@ -105,10 +105,10 @@ namespace Microsoft.AspNet.Mvc.Razor
public bool IsLayoutBeingRendered { get; set; }
/// <inheritdoc />
public Dictionary<string, HelperResult> PreviousSectionWriters { get; set; }
public Dictionary<string, RenderAsyncDelegate> PreviousSectionWriters { get; set; }
/// <inheritdoc />
public Dictionary<string, HelperResult> SectionWriters { get; private set; }
public Dictionary<string, RenderAsyncDelegate> SectionWriters { get; private set; }
/// <inheritdoc />
public abstract Task ExecuteAsync();
@ -215,7 +215,7 @@ namespace Microsoft.AspNet.Mvc.Razor
/// </remarks>
public virtual void WriteTo(TextWriter writer, object value)
{
if (value != null)
if (value != null && value != HtmlString.Empty)
{
var helperResult = value as HelperResult;
if (helperResult != null)
@ -415,7 +415,8 @@ namespace Microsoft.AspNet.Mvc.Razor
{
if (RenderBodyDelegate == null)
{
throw new InvalidOperationException(Resources.FormatRenderBodyCannotBeCalled("RenderBody"));
var message = Resources.FormatRazorPage_MethodCannotBeCalled(nameof(RenderBody));
throw new InvalidOperationException(message);
}
_renderedBody = true;
@ -424,11 +425,11 @@ namespace Microsoft.AspNet.Mvc.Razor
/// <summary>
/// Creates a named content section in the page that can be invoked in a Layout page using
/// <see cref="RenderSection(string)"/> or <see cref="RenderSection(string, bool)"/>.
/// <see cref="RenderSection(string)"/> or <see cref="RenderSectionAsync(string, bool)"/>.
/// </summary>
/// <param name="name">The name of the section to create.</param>
/// <param name="section">The <see cref="HelperResult"/> to execute when rendering the section.</param>
public void DefineSection(string name, HelperResult section)
/// <param name="section">The <see cref="RenderAsyncDelegate"/> to execute when rendering the section.</param>
public void DefineSection(string name, RenderAsyncDelegate section)
{
if (SectionWriters.ContainsKey(name))
{
@ -439,33 +440,84 @@ namespace Microsoft.AspNet.Mvc.Razor
public bool IsSectionDefined([NotNull] string name)
{
EnsureMethodCanBeInvoked("IsSectionDefined");
EnsureMethodCanBeInvoked(nameof(IsSectionDefined));
return PreviousSectionWriters.ContainsKey(name);
}
public HelperResult RenderSection([NotNull] string name)
/// <summary>
/// In layout pages, renders the content of the section named <paramref name="name"/>.
/// </summary>
/// <param name="name">The name of the section to render.</param>
/// <returns>Returns a HtmlString that contains the rendered HTML.</returns>
public HtmlString RenderSection([NotNull] string name)
{
return RenderSection(name, required: true);
}
public HelperResult RenderSection([NotNull] string name, bool required)
/// <summary>
/// In layout pages, renders the content of the section named <paramref name="name"/>.
/// </summary>
/// <param name="name">The section to render.</param>
/// <param name="required">Indicates if this section must be rendered.</param>
/// <returns>Returns a HtmlString that contains the rendered HTML.</returns>
public HtmlString RenderSection([NotNull] string name, bool required)
{
EnsureMethodCanBeInvoked("RenderSection");
if (_renderedSections.Contains(name))
EnsureMethodCanBeInvoked(nameof(RenderSection));
var task = RenderSectionAsyncCore(name, required);
return TaskHelper.WaitAndThrowIfFaulted(task);
}
/// <summary>
/// In layout pages, asynchronously renders the content of the section named <paramref name="name"/>.
/// </summary>
/// <param name="name">The section to render.</param>
/// <returns>A <see cref="Task{TResult}"/> that on completion returns a <see cref="HtmlString"/> containing
/// the rendered HTML.</returns>
public Task<HtmlString> RenderSectionAsync([NotNull] string name)
{
return RenderSectionAsync(name, required: true);
}
/// <summary>
/// In layout pages, asynchronously renders the content of the section named <paramref name="name"/>.
/// </summary>
/// <param name="name">The section to render.</param>
/// <returns>A <see cref="Task{TResult}"/> that on completion returns a <see cref="HtmlString"/> containing
/// the rendered HTML.</returns>
/// <exception cref="InvalidOperationException">if <paramref name="required"/> is <c>true</c> and the section
/// was not registered using the <c>@section</c> in the Razor page.</exception>
public async Task<HtmlString> RenderSectionAsync([NotNull] string name, bool required)
{
EnsureMethodCanBeInvoked(nameof(RenderSectionAsync));
return await RenderSectionAsyncCore(name, required);
}
private async Task<HtmlString> RenderSectionAsyncCore(string sectionName, bool required)
{
if (_renderedSections.Contains(sectionName))
{
throw new InvalidOperationException(Resources.FormatSectionAlreadyRendered("RenderSection", name));
var message = Resources.FormatSectionAlreadyRendered(sectionName);
throw new InvalidOperationException(message);
}
HelperResult action;
if (PreviousSectionWriters.TryGetValue(name, out action))
RenderAsyncDelegate renderDelegate;
if (PreviousSectionWriters.TryGetValue(sectionName, out renderDelegate))
{
_renderedSections.Add(name);
return action;
_renderedSections.Add(sectionName);
using (var writer = new StringCollectionTextWriter(Output.Encoding))
{
await renderDelegate(writer);
// Returning a disposed StringCollectionTextWriter is safe.
return new HtmlString(writer);
}
}
else if (required)
{
// If the section is not found, and it is not optional, throw an error.
throw new InvalidOperationException(Resources.FormatSectionNotDefined(name));
throw new InvalidOperationException(Resources.FormatSectionNotDefined(sectionName));
}
else
{
@ -518,7 +570,8 @@ namespace Microsoft.AspNet.Mvc.Razor
if (RenderBodyDelegate != null && !_renderedBody)
{
// If a body was defined, then RenderBody should have been called.
throw new InvalidOperationException(Resources.FormatRenderBodyNotCalled("RenderBody"));
var message = Resources.FormatRenderBodyNotCalled(nameof(RenderBody));
throw new InvalidOperationException(message);
}
}
@ -536,7 +589,7 @@ namespace Microsoft.AspNet.Mvc.Razor
{
if (PreviousSectionWriters == null)
{
throw new InvalidOperationException(Resources.FormatView_MethodCannotBeCalled(methodName));
throw new InvalidOperationException(Resources.FormatRazorPage_MethodCannotBeCalled(methodName));
}
}
}

View File

@ -0,0 +1,10 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.IO;
using System.Threading.Tasks;
namespace Microsoft.AspNet.Mvc.Razor
{
public delegate Task RenderAsyncDelegate(TextWriter writer);
}

View File

@ -153,7 +153,7 @@
<data name="RazorPage_NullModelMetadata" xml:space="preserve">
<value>The {0} was unable to provide metadata for expression '{1}'.</value>
</data>
<data name="RenderBodyCannotBeCalled" xml:space="preserve">
<data name="RazorPage_MethodCannotBeCalled" xml:space="preserve">
<value>{0} can only be called from a layout page.</value>
</data>
<data name="RenderBodyNotCalled" xml:space="preserve">
@ -163,7 +163,7 @@
<value>Section '{0}' is already defined.</value>
</data>
<data name="SectionAlreadyRendered" xml:space="preserve">
<value>{0} has already been called for the section named '{1}'.</value>
<value>The section named '{0}' has already been rendered.</value>
</data>
<data name="SectionNotDefined" xml:space="preserve">
<value>Section '{0}' is not defined.</value>
@ -183,7 +183,4 @@
<data name="ViewMustBeContextualized" xml:space="preserve">
<value>The '{0}' method must be called before '{1}' can be invoked.</value>
</data>
<data name="View_MethodCannotBeCalled" xml:space="preserve">
<value>The method '{0}' cannot be invoked by this view.</value>
</data>
</root>

View File

@ -17,6 +17,8 @@ namespace Microsoft.AspNet.Mvc.Razor
{
public class RazorPageTest
{
private readonly RenderAsyncDelegate _nullRenderAsyncDelegate = async writer => { };
[Fact]
public async Task WritingScopesRedirectContentWrittenToViewContextWriter()
{
@ -135,8 +137,8 @@ namespace Microsoft.AspNet.Mvc.Razor
var viewContext = CreateViewContext();
var page = CreatePage(v =>
{
v.DefineSection("qux", new HelperResult(action: null));
v.DefineSection("qux", new HelperResult(action: null));
v.DefineSection("qux", _nullRenderAsyncDelegate);
v.DefineSection("qux", _nullRenderAsyncDelegate);
});
// Act
@ -151,23 +153,22 @@ namespace Microsoft.AspNet.Mvc.Razor
public async Task RenderSection_RendersSectionFromPreviousPage()
{
// Arrange
var expected = new HelperResult(action: null);
var expected = "Hello world";
var viewContext = CreateViewContext();
HelperResult actual = null;
var page = CreatePage(v =>
{
actual = v.RenderSection("bar");
v.Write(v.RenderSection("bar"));
});
page.PreviousSectionWriters = new Dictionary<string, HelperResult>
page.PreviousSectionWriters = new Dictionary<string, RenderAsyncDelegate>
{
{ "bar", expected }
{ "bar", writer => writer.WriteAsync(expected) }
};
// Act
await page.ExecuteAsync();
// Assert
Assert.Same(actual, expected);
Assert.Equal(expected, page.RenderedContent);
}
[Fact]
@ -184,7 +185,7 @@ namespace Microsoft.AspNet.Mvc.Razor
await page.ExecuteAsync();
// Assert
Assert.Equal("The method 'RenderSection' cannot be invoked by this view.",
Assert.Equal("RenderSection can only be called from a layout page.",
ex.Message);
}
@ -192,14 +193,13 @@ namespace Microsoft.AspNet.Mvc.Razor
public async Task RenderSection_ThrowsIfRequiredSectionIsNotFound()
{
// Arrange
var expected = new HelperResult(action: null);
var page = CreatePage(v =>
{
v.RenderSection("bar");
});
page.PreviousSectionWriters = new Dictionary<string, HelperResult>
page.PreviousSectionWriters = new Dictionary<string, RenderAsyncDelegate>
{
{ "baz", expected }
{ "baz", _nullRenderAsyncDelegate }
};
// Act
@ -217,7 +217,7 @@ namespace Microsoft.AspNet.Mvc.Razor
// Act and Assert
ExceptionAssert.Throws<InvalidOperationException>(() => page.IsSectionDefined("foo"),
"The method 'IsSectionDefined' cannot be invoked by this view.");
"IsSectionDefined can only be called from a layout page.");
}
[Fact]
@ -231,9 +231,9 @@ namespace Microsoft.AspNet.Mvc.Razor
v.RenderSection("baz");
v.RenderBodyPublic();
});
page.PreviousSectionWriters = new Dictionary<string, HelperResult>
page.PreviousSectionWriters = new Dictionary<string, RenderAsyncDelegate>
{
{ "baz", new HelperResult(writer => { }) }
{ "baz", _nullRenderAsyncDelegate }
};
page.RenderBodyDelegate = CreateBodyAction("body-content");
@ -255,9 +255,9 @@ namespace Microsoft.AspNet.Mvc.Razor
v.RenderSection("baz");
v.RenderBodyPublic();
});
page.PreviousSectionWriters = new Dictionary<string, HelperResult>
page.PreviousSectionWriters = new Dictionary<string, RenderAsyncDelegate>
{
{ "baz", new HelperResult(writer => { }) }
{ "baz", _nullRenderAsyncDelegate }
};
page.RenderBodyDelegate = CreateBodyAction("body-content");
@ -278,32 +278,92 @@ namespace Microsoft.AspNet.Mvc.Razor
v.RenderSection("header");
v.RenderSection("header");
});
page.PreviousSectionWriters = new Dictionary<string, HelperResult>
page.PreviousSectionWriters = new Dictionary<string, RenderAsyncDelegate>
{
{ "header", new HelperResult(writer => { }) }
{ "header", _nullRenderAsyncDelegate }
};
// Act
var ex = await Assert.ThrowsAsync<InvalidOperationException>(page.ExecuteAsync);
// Assert
Assert.Equal("RenderSection has already been called for the section named 'header'.", ex.Message);
Assert.Equal("The section named 'header' has already been rendered.", ex.Message);
}
[Fact]
public async Task RenderSectionAsync_ThrowsIfSectionIsRenderedMoreThanOnce()
{
// Arrange
var expected = new HelperResult(action: null);
var page = CreatePage(async v =>
{
await v.RenderSectionAsync("header");
await v.RenderSectionAsync("header");
});
page.PreviousSectionWriters = new Dictionary<string, RenderAsyncDelegate>
{
{ "header", _nullRenderAsyncDelegate }
};
// Act
var ex = await Assert.ThrowsAsync<InvalidOperationException>(page.ExecuteAsync);
// Assert
Assert.Equal("The section named 'header' has already been rendered.", ex.Message);
}
[Fact]
public async Task RenderSectionAsync_ThrowsIfSectionIsRenderedMoreThanOnce_WithSyncMethod()
{
// Arrange
var expected = new HelperResult(action: null);
var page = CreatePage(async v =>
{
v.RenderSection("header");
await v.RenderSectionAsync("header");
});
page.PreviousSectionWriters = new Dictionary<string, RenderAsyncDelegate>
{
{ "header", _nullRenderAsyncDelegate }
};
// Act
var ex = await Assert.ThrowsAsync<InvalidOperationException>(page.ExecuteAsync);
// Assert
Assert.Equal("The section named 'header' has already been rendered.", ex.Message);
}
[Fact]
public async Task RenderSectionAsync_ThrowsIfNotInvokedFromLayoutPage()
{
// Arrange
var expected = new HelperResult(action: null);
var page = CreatePage(async v =>
{
await v.RenderSectionAsync("header");
});
// Act
var ex = await Assert.ThrowsAsync<InvalidOperationException>(page.ExecuteAsync);
// Assert
Assert.Equal("RenderSectionAsync can only be called from a layout page.", ex.Message);
}
[Fact]
public async Task EnsureBodyAndSectionsWereRendered_ThrowsIfDefinedSectionIsNotRendered()
{
// Arrange
var expected = new HelperResult(action: null);
var page = CreatePage(v =>
{
v.RenderSection("sectionA");
});
page.PreviousSectionWriters = new Dictionary<string, HelperResult>
page.PreviousSectionWriters = new Dictionary<string, RenderAsyncDelegate>
{
{ "header", expected },
{ "footer", expected },
{ "sectionA", expected },
{ "header", _nullRenderAsyncDelegate },
{ "footer", _nullRenderAsyncDelegate },
{ "sectionA", _nullRenderAsyncDelegate },
};
// Act
@ -337,35 +397,38 @@ namespace Microsoft.AspNet.Mvc.Razor
public async Task ExecuteAsync_RendersSectionsAndBody()
{
// Arrange
var expected = @"Layout start
Header section
body content
Footer section
Layout end
";
var page = CreatePage(v =>
var expected = string.Join(Environment.NewLine,
"Layout start",
"Header section",
"Async Header section",
"body content",
"Async Footer section",
"Footer section",
"Layout end");
var page = CreatePage(async v =>
{
v.WriteLiteral("Layout start" + Environment.NewLine);
v.Write(v.RenderSection("header"));
v.Write(await v.RenderSectionAsync("async-header"));
v.Write(v.RenderBodyPublic());
v.Write(await v.RenderSectionAsync("async-footer"));
v.Write(v.RenderSection("footer"));
v.WriteLiteral("Layout end" + Environment.NewLine);
v.WriteLiteral("Layout end");
});
page.RenderBodyDelegate = CreateBodyAction("body content" + Environment.NewLine);
page.PreviousSectionWriters = new Dictionary<string, HelperResult>
page.PreviousSectionWriters = new Dictionary<string, RenderAsyncDelegate>
{
{
"footer", new HelperResult(writer =>
{
writer.WriteLine("Footer section");
})
"footer", writer => writer.WriteLineAsync("Footer section")
},
{
"header", new HelperResult(writer =>
{
writer.WriteLine("Header section");
})
"header", writer => writer.WriteLineAsync("Header section")
},
{
"async-header", writer => writer.WriteLineAsync("Async Header section")
},
{
"async-footer", writer => writer.WriteLineAsync("Async Footer section")
},
};
@ -373,7 +436,7 @@ Layout end
await page.ExecuteAsync();
// Assert
var actual = ((StringWriter)page.Output).ToString();
var actual = page.RenderedContent;
Assert.Equal(expected, actual);
}
@ -399,7 +462,7 @@ Layout end
await page.ExecuteAsync();
// Assert
var actual = ((StringWriter)page.Output).ToString();
var actual = page.RenderedContent;
Assert.Equal(expected, actual);
helper.Verify();
}
@ -410,9 +473,9 @@ Layout end
// Arrange
var writer = new Mock<TextWriter>();
var context = CreateViewContext(writer.Object);
var page = CreatePage(p =>
var page = CreatePage(async p =>
{
p.FlushAsync().Wait();
await p.FlushAsync();
}, context);
// Act
@ -429,10 +492,10 @@ Layout end
var expected = @"A layout page cannot be rendered after 'FlushAsync' has been invoked.";
var writer = new Mock<TextWriter>();
var context = CreateViewContext(writer.Object);
var page = CreatePage(p =>
var page = CreatePage(async p =>
{
p.Layout = "foo";
p.FlushAsync().Wait();
await p.FlushAsync();
}, context);
// Act and Assert
@ -449,10 +512,10 @@ Layout end
var page = CreatePage(p =>
{
p.Layout = "bar";
p.DefineSection("test-section", new HelperResult(_ =>
p.DefineSection("test-section", async _ =>
{
p.FlushAsync().Wait();
}));
await p.FlushAsync();
});
}, context);
// Act
@ -460,7 +523,8 @@ Layout end
page.IsLayoutBeingRendered = true;
// Assert
Assert.DoesNotThrow(() => page.SectionWriters["test-section"].WriteTo(TextWriter.Null));
var renderAsyncDelegate = page.SectionWriters["test-section"];
await Assert.DoesNotThrowAsync(() => renderAsyncDelegate(TextWriter.Null));
}
[Fact]
@ -553,14 +617,27 @@ Layout end
private static TestableRazorPage CreatePage(Action<TestableRazorPage> executeAction,
ViewContext context = null)
{
return CreatePage(page =>
{
executeAction(page);
return Task.FromResult(0);
}, context);
}
private static TestableRazorPage CreatePage(Func<TestableRazorPage, Task> executeAction,
ViewContext context = null)
{
context = context ?? CreateViewContext();
var view = new Mock<TestableRazorPage> { CallBase = true };
if (executeAction != null)
{
view.Setup(v => v.ExecuteAsync())
.Callback(() => executeAction(view.Object))
.Returns(Task.FromResult(0));
.Returns(() =>
{
return executeAction(view.Object);
});
}
view.Object.ViewContext = context;
@ -585,6 +662,15 @@ Layout end
public abstract class TestableRazorPage : RazorPage
{
public string RenderedContent
{
get
{
var writer = Assert.IsType<StringWriter>(Output);
return writer.ToString();
}
}
public HelperResult RenderBodyPublic()
{
return base.RenderBody();

View File

@ -16,6 +16,7 @@ namespace Microsoft.AspNet.Mvc.Razor
public class RazorViewTest
{
private const string LayoutPath = "~/Shared/_Layout.cshtml";
private readonly RenderAsyncDelegate _nullRenderAsyncDelegate = async writer => { };
[Fact]
public async Task RenderAsync_ThrowsIfContextualizeHasNotBeenInvoked()
@ -264,14 +265,14 @@ foot-content";
{
v.WriteLiteral("body-content");
v.Layout = LayoutPath;
v.DefineSection("head", new HelperResult(writer =>
v.DefineSection("head", async writer =>
{
writer.Write("head-content");
}));
v.DefineSection("foot", new HelperResult(writer =>
await writer.WriteAsync("head-content");
});
v.DefineSection("foot", async writer =>
{
writer.Write("foot-content");
}));
await writer.WriteAsync("foot-content");
});
});
var layout = new TestableRazorPage(v =>
{
@ -312,9 +313,9 @@ foot-content";
// Arrange
var page = new TestableRazorPage(v =>
{
v.DefineSection("head", new HelperResult(writer => { }));
v.DefineSection("head", _nullRenderAsyncDelegate);
v.Layout = LayoutPath;
v.DefineSection("foot", new HelperResult(writer => { }));
v.DefineSection("foot", _nullRenderAsyncDelegate);
});
var layout = new TestableRazorPage(v =>
{
@ -374,10 +375,10 @@ body-content";
var page = new TestableRazorPage(v =>
{
v.DefineSection("foo", new HelperResult(writer =>
v.DefineSection("foo", async writer =>
{
writer.WriteLine("foo-content");
}));
await writer.WriteLineAsync("foo-content");
});
v.Layout = "~/Shared/Layout1.cshtml";
v.WriteLiteral("body-content");
});
@ -385,10 +386,7 @@ body-content";
{
v.Write("layout-1" + Environment.NewLine);
v.Write(v.RenderSection("foo"));
v.DefineSection("bar", new HelperResult(writer =>
{
writer.WriteLine("bar-content");
}));
v.DefineSection("bar", writer => writer.WriteLineAsync("bar-content"));
v.RenderBodyPublic();
v.Layout = "~/Shared/Layout2.cshtml";
});
@ -431,12 +429,12 @@ section-content-2";
{
v.Layout = "layout-1";
v.WriteLiteral("body content" + Environment.NewLine);
v.DefineSection("foo", new HelperResult(_ =>
v.DefineSection("foo", async _ =>
{
v.WriteLiteral("section-content-1" + Environment.NewLine);
v.FlushAsync().Wait();
await v.FlushAsync();
v.WriteLiteral("section-content-2");
}));
});
});
var layout1 = new TestableRazorPage(v =>
@ -475,12 +473,12 @@ section-content-2";
var page = new TestableRazorPage(v =>
{
v.Layout = "layout-1";
v.DefineSection("foo", new HelperResult(_ =>
v.DefineSection("foo", async _ =>
{
v.WriteLiteral("section-content-1" + Environment.NewLine);
v.FlushAsync().Wait();
await v.FlushAsync();
v.WriteLiteral("section-content-2");
}));
});
});
var layout1 = new TestableRazorPage(v =>
@ -538,11 +536,11 @@ section-content-2";
var expected = @"A layout page cannot be rendered after 'FlushAsync' has been invoked.";
var page = new TestableRazorPage(v =>
{
v.DefineSection("foo", new HelperResult(writer =>
v.DefineSection("foo", async writer =>
{
writer.WriteLine("foo-content");
v.FlushAsync().Wait();
}));
await v.FlushAsync();
});
v.Layout = "~/Shared/Layout1.cshtml";
v.WriteLiteral("body-content");
});
@ -550,10 +548,7 @@ section-content-2";
{
v.Write("layout-1" + Environment.NewLine);
v.Write(v.RenderSection("foo"));
v.DefineSection("bar", new HelperResult(writer =>
{
writer.WriteLine("bar-content");
}));
v.DefineSection("bar", writer => writer.WriteLineAsync("bar-content"));
v.RenderBodyPublic();
v.Layout = "~/Shared/Layout2.cshtml";
});

View File

@ -7,7 +7,7 @@ RenderBody content
@section content
{
@{
FlushAsync().Wait();
await FlushAsync();
WaitService.WaitForClient();
}
<span>Content that takes time to produce</span>

View File

@ -7,9 +7,9 @@ RenderBody content
@section content
{
@{
FlushAsync().Wait();
await FlushAsync();
WaitService.WaitForClient();
}
@Component.InvokeAsync("ComponentThatSetsTitle").Result
@await Component.InvokeAsync("ComponentThatSetsTitle")
<span>Content that takes time to produce</span>
}

View File

@ -5,7 +5,7 @@
WaitService.WaitForClient();
}
@RenderBody()
@RenderSection("content")
@await RenderSectionAsync("content")
@{
WaitService.NotifyClient();
}

View File

@ -6,7 +6,7 @@
WaitService.WaitForClient();
}
@await Html.PartialAsync("_PartialThatSetsTitle")
@RenderSection("content")
@await RenderSectionAsync("content")
@{
WaitService.NotifyClient();
}