[Fixes #2179] Validation fix for supporting nested sections in layouts
This commit is contained in:
parent
adeb1ba194
commit
c62974d39b
|
|
@ -72,9 +72,14 @@ namespace Microsoft.AspNet.Mvc.Razor
|
||||||
Task ExecuteAsync();
|
Task ExecuteAsync();
|
||||||
|
|
||||||
/// <summary>
|
/// <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.
|
/// part of view execution hierarchy.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
void EnsureBodyAndSectionsWereRendered();
|
void EnsureBodyWasRendered();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the sections that are rendered in the page.
|
||||||
|
/// </summary>
|
||||||
|
IEnumerable<string> RenderedSections { get; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -5,7 +5,6 @@ using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
@ -51,6 +50,15 @@ namespace Microsoft.AspNet.Mvc.Razor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IEnumerable<string> RenderedSections
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _renderedSections;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public string Path { get; set; }
|
public string Path { get; set; }
|
||||||
|
|
||||||
|
|
@ -716,20 +724,8 @@ namespace Microsoft.AspNet.Mvc.Razor
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <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 BodyContent is set, ensure it was rendered.
|
||||||
if (RenderBodyDelegate != null && !_renderedBody)
|
if (RenderBodyDelegate != null && !_renderedBody)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,9 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNet.Mvc.Rendering;
|
using Microsoft.AspNet.Mvc.Rendering;
|
||||||
using Microsoft.AspNet.PageExecutionInstrumentation;
|
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
|
// A layout page can specify another layout page. We'll need to continue
|
||||||
// looking for layout pages until they're no longer specified.
|
// looking for layout pages until they're no longer specified.
|
||||||
var previousPage = RazorPage;
|
var previousPage = RazorPage;
|
||||||
|
var unrenderedSections = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
while (!string.IsNullOrEmpty(previousPage.Layout))
|
while (!string.IsNullOrEmpty(previousPage.Layout))
|
||||||
{
|
{
|
||||||
if (!bodyWriter.IsBuffering)
|
if (!bodyWriter.IsBuffering)
|
||||||
|
|
@ -190,12 +194,22 @@ namespace Microsoft.AspNet.Mvc.Razor
|
||||||
layoutPage.RenderBodyDelegate = bodyWriter.CopyTo;
|
layoutPage.RenderBodyDelegate = bodyWriter.CopyTo;
|
||||||
bodyWriter = await RenderPageAsync(layoutPage, context, executeViewStart: false);
|
bodyWriter = await RenderPageAsync(layoutPage, context, executeViewStart: false);
|
||||||
|
|
||||||
// Verify that RenderBody is called, or that RenderSection is called for all sections
|
// Verify that RenderBody is called
|
||||||
layoutPage.EnsureBodyAndSectionsWereRendered();
|
layoutPage.EnsureBodyWasRendered();
|
||||||
|
|
||||||
|
unrenderedSections.UnionWith(layoutPage.PreviousSectionWriters.Keys);
|
||||||
|
unrenderedSections.ExceptWith(layoutPage.RenderedSections);
|
||||||
|
|
||||||
previousPage = layoutPage;
|
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)
|
if (bodyWriter.IsBuffering)
|
||||||
{
|
{
|
||||||
// Only copy buffered content to the Output if we're currently buffering.
|
// Only copy buffered content to the Output if we're currently buffering.
|
||||||
|
|
|
||||||
|
|
@ -403,31 +403,7 @@ namespace Microsoft.AspNet.Mvc.Razor
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task EnsureBodyAndSectionsWereRendered_ThrowsIfDefinedSectionIsNotRendered()
|
public async Task EnsureBodyWasRendered_ThrowsIfRenderBodyIsNotCalledFromPage()
|
||||||
{
|
|
||||||
// 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()
|
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var expected = new HelperResult(action: null);
|
var expected = new HelperResult(action: null);
|
||||||
|
|
@ -438,7 +414,7 @@ namespace Microsoft.AspNet.Mvc.Razor
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
await page.ExecuteAsync();
|
await page.ExecuteAsync();
|
||||||
var ex = Assert.Throws<InvalidOperationException>(() => page.EnsureBodyAndSectionsWereRendered());
|
var ex = Assert.Throws<InvalidOperationException>(() => page.EnsureBodyWasRendered());
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal("RenderBody must be called from a layout page.", ex.Message);
|
Assert.Equal("RenderBody must be called from a layout page.", ex.Message);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
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]
|
[Fact]
|
||||||
public async Task RenderAsync_ThrowsIfBodyWasNotRendered()
|
public async Task RenderAsync_ThrowsIfBodyWasNotRendered()
|
||||||
{
|
{
|
||||||
|
|
@ -544,6 +595,69 @@ namespace Microsoft.AspNet.Mvc.Razor
|
||||||
Assert.Equal(expected, viewContext.Writer.ToString());
|
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]
|
[Fact]
|
||||||
public async Task RenderAsync_DoesNotCopyContentOnceRazorTextWriterIsNoLongerBuffering()
|
public async Task RenderAsync_DoesNotCopyContentOnceRazorTextWriterIsNoLongerBuffering()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue