diff --git a/src/Microsoft.AspNet.Mvc.Razor/IRazorPage.cs b/src/Microsoft.AspNet.Mvc.Razor/IRazorPage.cs
index 35d2ceec3e..1b65fec46e 100644
--- a/src/Microsoft.AspNet.Mvc.Razor/IRazorPage.cs
+++ b/src/Microsoft.AspNet.Mvc.Razor/IRazorPage.cs
@@ -72,9 +72,14 @@ namespace Microsoft.AspNet.Mvc.Razor
Task ExecuteAsync();
///
- /// Verifies that RenderBody is called and that RenderSection is called for all sections for a page that is
+ /// Verifies that RenderBody is called for the page that is
/// part of view execution hierarchy.
///
- void EnsureBodyAndSectionsWereRendered();
+ void EnsureBodyWasRendered();
+
+ ///
+ /// Gets the sections that are rendered in the page.
+ ///
+ IEnumerable RenderedSections { get; }
}
}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.Razor/RazorPage.cs b/src/Microsoft.AspNet.Mvc.Razor/RazorPage.cs
index 59a0810fdd..50a53ec79c 100644
--- a/src/Microsoft.AspNet.Mvc.Razor/RazorPage.cs
+++ b/src/Microsoft.AspNet.Mvc.Razor/RazorPage.cs
@@ -5,7 +5,6 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
-using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
@@ -51,6 +50,15 @@ namespace Microsoft.AspNet.Mvc.Razor
}
}
+ ///
+ public IEnumerable RenderedSections
+ {
+ get
+ {
+ return _renderedSections;
+ }
+ }
+
///
public string Path { get; set; }
@@ -716,20 +724,8 @@ namespace Microsoft.AspNet.Mvc.Razor
}
///
- public void EnsureBodyAndSectionsWereRendered()
+ public void EnsureBodyWasRendered()
{
- // If PreviousSectionWriters is set, ensure all defined sections were rendered.
- if (PreviousSectionWriters != null)
- {
- var sectionsNotRendered = PreviousSectionWriters.Keys.Except(_renderedSections,
- StringComparer.OrdinalIgnoreCase);
- if (sectionsNotRendered.Any())
- {
- var sectionNames = string.Join(", ", sectionsNotRendered);
- throw new InvalidOperationException(Resources.FormatSectionsNotRendered(sectionNames));
- }
- }
-
// If BodyContent is set, ensure it was rendered.
if (RenderBodyDelegate != null && !_renderedBody)
{
diff --git a/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs b/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs
index ebc2a16847..1ff1cfe197 100644
--- a/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs
+++ b/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs
@@ -2,7 +2,9 @@
// 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;
+using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.AspNet.PageExecutionInstrumentation;
@@ -168,6 +170,8 @@ namespace Microsoft.AspNet.Mvc.Razor
// A layout page can specify another layout page. We'll need to continue
// looking for layout pages until they're no longer specified.
var previousPage = RazorPage;
+ var unrenderedSections = new HashSet(StringComparer.OrdinalIgnoreCase);
+
while (!string.IsNullOrEmpty(previousPage.Layout))
{
if (!bodyWriter.IsBuffering)
@@ -190,12 +194,22 @@ namespace Microsoft.AspNet.Mvc.Razor
layoutPage.RenderBodyDelegate = bodyWriter.CopyTo;
bodyWriter = await RenderPageAsync(layoutPage, context, executeViewStart: false);
- // Verify that RenderBody is called, or that RenderSection is called for all sections
- layoutPage.EnsureBodyAndSectionsWereRendered();
+ // Verify that RenderBody is called
+ layoutPage.EnsureBodyWasRendered();
+
+ unrenderedSections.UnionWith(layoutPage.PreviousSectionWriters.Keys);
+ unrenderedSections.ExceptWith(layoutPage.RenderedSections);
previousPage = layoutPage;
}
+ // If not all sections are rendered, throw.
+ if (unrenderedSections.Any())
+ {
+ var sectionNames = string.Join(", ", unrenderedSections);
+ throw new InvalidOperationException(Resources.FormatSectionsNotRendered(sectionNames));
+ }
+
if (bodyWriter.IsBuffering)
{
// Only copy buffered content to the Output if we're currently buffering.
diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorPageTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorPageTest.cs
index 1804fe480a..859029ea65 100644
--- a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorPageTest.cs
+++ b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorPageTest.cs
@@ -403,31 +403,7 @@ namespace Microsoft.AspNet.Mvc.Razor
}
[Fact]
- public async Task EnsureBodyAndSectionsWereRendered_ThrowsIfDefinedSectionIsNotRendered()
- {
- // Arrange
- var page = CreatePage(v =>
- {
- v.RenderSection("sectionA");
- });
- page.PreviousSectionWriters = new Dictionary
- {
- { "header", _nullRenderAsyncDelegate },
- { "footer", _nullRenderAsyncDelegate },
- { "sectionA", _nullRenderAsyncDelegate },
- };
-
- // Act
- await page.ExecuteAsync();
- var ex = Assert.Throws(() => page.EnsureBodyAndSectionsWereRendered());
-
- // Assert
- Assert.Equal("The following sections have been defined but have not been rendered: 'header, footer'.",
- ex.Message);
- }
-
- [Fact]
- public async Task EnsureBodyAndSectionsWereRendered_ThrowsIfRenderBodyIsNotCalledFromPage()
+ public async Task EnsureBodyWasRendered_ThrowsIfRenderBodyIsNotCalledFromPage()
{
// Arrange
var expected = new HelperResult(action: null);
@@ -438,7 +414,7 @@ namespace Microsoft.AspNet.Mvc.Razor
// Act
await page.ExecuteAsync();
- var ex = Assert.Throws(() => page.EnsureBodyAndSectionsWereRendered());
+ var ex = Assert.Throws(() => page.EnsureBodyWasRendered());
// Assert
Assert.Equal("RenderBody must be called from a layout page.", ex.Message);
diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs
index 3f99dc90fc..535e558894 100644
--- a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs
+++ b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs
@@ -455,6 +455,57 @@ namespace Microsoft.AspNet.Mvc.Razor
Assert.Equal("The following sections have been defined but have not been rendered: 'head, foot'.", ex.Message);
}
+ [Fact]
+ public async Task RenderAsync_WithNestedSections_ThrowsIfSectionsWereDefinedButNotRendered()
+ {
+ // Arrange
+ var htmlEncoder = new HtmlEncoder();
+ var page = new TestableRazorPage(v =>
+ {
+ v.HtmlEncoder = htmlEncoder;
+ v.Layout = "~/Shared/Layout1.cshtml";
+ v.WriteLiteral("BodyContent");
+ v.DefineSection("foo", async writer =>
+ {
+ await writer.WriteLineAsync("foo-content");
+ });
+ });
+ var nestedLayout = new TestableRazorPage(v =>
+ {
+ v.HtmlEncoder = htmlEncoder;
+ v.Layout = "~/Shared/Layout2.cshtml";
+ v.Write("NestedLayout" + Environment.NewLine);
+ v.RenderBodyPublic();
+ v.DefineSection("foo", async writer =>
+ {
+ await writer.WriteLineAsync(htmlEncoder.HtmlEncode(v.RenderSection("foo").ToString()));
+ });
+ });
+ var baseLayout = new TestableRazorPage(v =>
+ {
+ v.HtmlEncoder = htmlEncoder;
+ v.Write("BaseLayout" + Environment.NewLine);
+ v.RenderBodyPublic();
+ });
+
+ var viewEngine = new Mock();
+ viewEngine.Setup(p => p.FindPage(It.IsAny(), "~/Shared/Layout1.cshtml"))
+ .Returns(new RazorPageResult("~/Shared/Layout1.cshtml", nestedLayout));
+ viewEngine.Setup(p => p.FindPage(It.IsAny(), "~/Shared/Layout2.cshtml"))
+ .Returns(new RazorPageResult("~/Shared/Layout2.cshtml", baseLayout));
+
+ var view = new RazorView(viewEngine.Object,
+ Mock.Of(),
+ CreateViewStartProvider(),
+ page,
+ isPartial: false);
+ var viewContext = CreateViewContext(view);
+
+ // Act and Assert
+ var ex = await Assert.ThrowsAsync(() => view.RenderAsync(viewContext));
+ Assert.Equal("The following sections have been defined but have not been rendered: 'foo'.", ex.Message);
+ }
+
[Fact]
public async Task RenderAsync_ThrowsIfBodyWasNotRendered()
{
@@ -544,6 +595,69 @@ namespace Microsoft.AspNet.Mvc.Razor
Assert.Equal(expected, viewContext.Writer.ToString());
}
+ [Fact]
+ public async Task RenderAsync_ExecutesNestedLayoutsWithNestedSections()
+ {
+ // Arrange
+ var htmlEncoder = new HtmlEncoder();
+ var htmlEncodedNewLine = htmlEncoder.HtmlEncode(Environment.NewLine);
+ var expected = "BaseLayout" +
+ htmlEncodedNewLine +
+ "NestedLayout" +
+ htmlEncodedNewLine +
+ "BodyContent" +
+ "foo-content" +
+ Environment.NewLine +
+ Environment.NewLine;
+
+ var page = new TestableRazorPage(v =>
+ {
+ v.HtmlEncoder = htmlEncoder;
+ v.Layout = "~/Shared/Layout1.cshtml";
+ v.WriteLiteral("BodyContent");
+ v.DefineSection("foo", async writer =>
+ {
+ await writer.WriteLineAsync("foo-content");
+ });
+ });
+ var nestedLayout = new TestableRazorPage(v =>
+ {
+ v.HtmlEncoder = htmlEncoder;
+ v.Layout = "~/Shared/Layout2.cshtml";
+ v.Write("NestedLayout" + Environment.NewLine);
+ v.RenderBodyPublic();
+ v.DefineSection("foo", async writer =>
+ {
+ await writer.WriteLineAsync(htmlEncoder.HtmlEncode(v.RenderSection("foo").ToString()));
+ });
+ });
+ var baseLayout = new TestableRazorPage(v =>
+ {
+ v.HtmlEncoder = htmlEncoder;
+ v.Write("BaseLayout" + Environment.NewLine);
+ v.RenderBodyPublic();
+ v.Write(v.RenderSection("foo"));
+ });
+ var viewEngine = new Mock();
+ viewEngine.Setup(p => p.FindPage(It.IsAny(), "~/Shared/Layout1.cshtml"))
+ .Returns(new RazorPageResult("~/Shared/Layout1.cshtml", nestedLayout));
+ viewEngine.Setup(p => p.FindPage(It.IsAny(), "~/Shared/Layout2.cshtml"))
+ .Returns(new RazorPageResult("~/Shared/Layout2.cshtml", baseLayout));
+
+ var view = new RazorView(viewEngine.Object,
+ Mock.Of(),
+ CreateViewStartProvider(),
+ page,
+ isPartial: false);
+ var viewContext = CreateViewContext(view);
+
+ // Act
+ await view.RenderAsync(viewContext);
+
+ // Assert
+ Assert.Equal(expected, viewContext.Writer.ToString());
+ }
+
[Fact]
public async Task RenderAsync_DoesNotCopyContentOnceRazorTextWriterIsNoLongerBuffering()
{