Skip to content
This repository has been archived by the owner on Dec 14, 2018. It is now read-only.

Commit

Permalink
[Fixes #2179] Validation fix for supporting nested sections in layouts
Browse files Browse the repository at this point in the history
  • Loading branch information
ajaybhargavb committed Mar 22, 2015
1 parent adeb1ba commit c62974d
Show file tree
Hide file tree
Showing 5 changed files with 149 additions and 44 deletions.
9 changes: 7 additions & 2 deletions src/Microsoft.AspNet.Mvc.Razor/IRazorPage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,14 @@ public interface IRazorPage
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; }
}
}
24 changes: 10 additions & 14 deletions src/Microsoft.AspNet.Mvc.Razor/RazorPage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
Expand Down Expand Up @@ -51,6 +50,15 @@ public HttpContext Context
}
}

/// <inheritdoc />
public IEnumerable<string> RenderedSections
{
get
{
return _renderedSections;
}
}

/// <inheritdoc />
public string Path { get; set; }

Expand Down Expand Up @@ -716,20 +724,8 @@ public async Task<HtmlString> FlushAsync()
}

/// <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)
{
Expand Down
18 changes: 16 additions & 2 deletions src/Microsoft.AspNet.Mvc.Razor/RazorView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -168,6 +170,8 @@ private async Task RenderLayoutAsync(ViewContext context,
// 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)
Expand All @@ -190,12 +194,22 @@ private async Task RenderLayoutAsync(ViewContext context,
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.
Expand Down
28 changes: 2 additions & 26 deletions test/Microsoft.AspNet.Mvc.Razor.Test/RazorPageTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -403,31 +403,7 @@ public async Task RenderSectionAsync_ThrowsIfNotInvokedFromLayoutPage()
}

[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);
Expand All @@ -438,7 +414,7 @@ public async Task EnsureBodyAndSectionsWereRendered_ThrowsIfRenderBodyIsNotCalle

// 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);
Expand Down
114 changes: 114 additions & 0 deletions test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,57 @@ public async Task RenderAsync_ThrowsIfSectionsWereDefinedButNotRendered()
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()
{
Expand Down Expand Up @@ -544,6 +595,69 @@ public async Task RenderAsync_ExecutesNestedLayoutPages()
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()
{
Expand Down

0 comments on commit c62974d

Please sign in to comment.