[Fixes #2179] Validation fix for supporting nested sections in layouts

This commit is contained in:
Ajay Bhargav Baaskaran 2015-03-20 15:24:34 -07:00
parent adeb1ba194
commit c62974d39b
5 changed files with 149 additions and 44 deletions

View File

@ -72,9 +72,14 @@ namespace Microsoft.AspNet.Mvc.Razor
Task ExecuteAsync();
/// <summary>
/// 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.
/// </summary>
void EnsureBodyAndSectionsWereRendered();
void EnsureBodyWasRendered();
/// <summary>
/// Gets the sections that are rendered in the page.
/// </summary>
IEnumerable<string> RenderedSections { get; }
}
}

View File

@ -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
}
}
/// <inheritdoc />
public IEnumerable<string> RenderedSections
{
get
{
return _renderedSections;
}
}
/// <inheritdoc />
public string Path { get; set; }
@ -716,20 +724,8 @@ namespace Microsoft.AspNet.Mvc.Razor
}
/// <inheritdoc />
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)
{

View File

@ -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<string>(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.

View File

@ -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<string, RenderAsyncDelegate>
{
{ "header", _nullRenderAsyncDelegate },
{ "footer", _nullRenderAsyncDelegate },
{ "sectionA", _nullRenderAsyncDelegate },
};
// Act
await page.ExecuteAsync();
var ex = Assert.Throws<InvalidOperationException>(() => 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<InvalidOperationException>(() => page.EnsureBodyAndSectionsWereRendered());
var ex = Assert.Throws<InvalidOperationException>(() => page.EnsureBodyWasRendered());
// Assert
Assert.Equal("RenderBody must be called from a layout page.", ex.Message);

View File

@ -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<IRazorViewEngine>();
viewEngine.Setup(p => p.FindPage(It.IsAny<ActionContext>(), "~/Shared/Layout1.cshtml"))
.Returns(new RazorPageResult("~/Shared/Layout1.cshtml", nestedLayout));
viewEngine.Setup(p => p.FindPage(It.IsAny<ActionContext>(), "~/Shared/Layout2.cshtml"))
.Returns(new RazorPageResult("~/Shared/Layout2.cshtml", baseLayout));
var view = new RazorView(viewEngine.Object,
Mock.Of<IRazorPageActivator>(),
CreateViewStartProvider(),
page,
isPartial: false);
var viewContext = CreateViewContext(view);
// Act and Assert
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => 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<IRazorViewEngine>();
viewEngine.Setup(p => p.FindPage(It.IsAny<ActionContext>(), "~/Shared/Layout1.cshtml"))
.Returns(new RazorPageResult("~/Shared/Layout1.cshtml", nestedLayout));
viewEngine.Setup(p => p.FindPage(It.IsAny<ActionContext>(), "~/Shared/Layout2.cshtml"))
.Returns(new RazorPageResult("~/Shared/Layout2.cshtml", baseLayout));
var view = new RazorView(viewEngine.Object,
Mock.Of<IRazorPageActivator>(),
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()
{