From c62974d39ba5592e82c9d553267c5af3b849b0f3 Mon Sep 17 00:00:00 2001 From: Ajay Bhargav Baaskaran Date: Fri, 20 Mar 2015 15:24:34 -0700 Subject: [PATCH] [Fixes #2179] Validation fix for supporting nested sections in layouts --- src/Microsoft.AspNet.Mvc.Razor/IRazorPage.cs | 9 +- src/Microsoft.AspNet.Mvc.Razor/RazorPage.cs | 24 ++-- src/Microsoft.AspNet.Mvc.Razor/RazorView.cs | 18 ++- .../RazorPageTest.cs | 28 +---- .../RazorViewTest.cs | 114 ++++++++++++++++++ 5 files changed, 149 insertions(+), 44 deletions(-) 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() {