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

Fix for supporting nested sections in layouts #2226

Merged
merged 1 commit into from
Mar 22, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might not address sections that are redefined but not rendered in the outer definition.

// NestedLayout
@{ Layout = "OuterLayout"; }
@section scripts {
   // stuff
}

// OuterLayout
@{ Layout = "BaseLayout"; }
@section scripts { 
  // Don't render inner scripts
}

// BaseLayout
@RenderSection("scripts");

In this scenario, the scripts section defined by NestedLayout is never rendered but the view would happily succeed. Perhaps we shouldn't be flattening the results in unrenderedSections?

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