Layouts for partials

Fixes #1621
This commit is contained in:
Pranav K 2014-12-02 10:22:08 -08:00
parent 9299565706
commit 7667eba34e
19 changed files with 245 additions and 38 deletions

View File

@ -62,32 +62,10 @@ namespace Microsoft.AspNet.Mvc.Razor
{
_pageExecutionFeature = context.HttpContext.GetFeature<IPageExecutionListenerFeature>();
if (IsPartial)
{
await RenderPartialAsync(context);
}
else
{
var bodyWriter = await RenderPageAsync(RazorPage, context, executeViewStart: true);
await RenderLayoutAsync(context, bodyWriter);
}
}
private async Task RenderPartialAsync(ViewContext context)
{
if (EnableInstrumentation)
{
// When instrmenting, we need to Decorate the output in an instrumented writer which
// RenderPageAsync does.
var bodyWriter = await RenderPageAsync(RazorPage, context, executeViewStart: false);
await bodyWriter.CopyToAsync(context.Writer);
}
else
{
// For the non-instrumented case, we don't need to buffer contents. For Html.Partial, the writer is
// an in memory writer and for Partial views, we directly write to the Response.
await RenderPageCoreAsync(RazorPage, context);
}
// Partials don't execute _ViewStart pages, but may execute Layout pages if the Layout property
// is explicitly specified in the page.
var bodyWriter = await RenderPageAsync(RazorPage, context, executeViewStart: !IsPartial);
await RenderLayoutAsync(context, bodyWriter);
}
private async Task<IBufferedTextWriter> RenderPageAsync(IRazorPage page,

View File

@ -164,25 +164,41 @@ component-content";
Assert.Equal(expected, body.Trim());
}
public static IEnumerable<object[]> PartialRazorViews_DoNotRenderLayoutData
public static IEnumerable<object[]> RazorViewEngine_RendersPartialViewsData
{
get
{
yield return new[]
{
"ViewWithoutLayout", @"ViewWithoutLayout-Content"
"ViewWithoutLayout", "ViewWithoutLayout-Content"
};
yield return new[]
{
"PartialViewWithNamePassedIn", @"ViewWithLayout-Content"
"PartialViewWithNamePassedIn",
@"<layout>
ViewWithLayout-Content
</layout>"
};
yield return new[]
{
"ViewWithFullPath", "ViewWithFullPath-content"
"ViewWithFullPath",
@"<layout>
ViewWithFullPath-content
</layout>"
};
yield return new[]
{
"ViewWithNestedLayout", "ViewWithNestedLayout-Content"
"ViewWithNestedLayout",
@"<layout>
<nested-layout>
/PartialViewEngine/ViewWithNestedLayout
ViewWithNestedLayout-Content
</nested-layout>
</layout>"
};
yield return new[]
{
@ -199,8 +215,8 @@ component-content";
}
[Theory]
[MemberData(nameof(PartialRazorViews_DoNotRenderLayoutData))]
public async Task PartialRazorViews_DoNotRenderLayout(string actionName, string expected)
[MemberData(nameof(RazorViewEngine_RendersPartialViewsData))]
public async Task RazorViewEngine_RendersPartialViews(string actionName, string expected)
{
// Arrange
var server = TestServer.Create(_provider, _app);
@ -289,5 +305,95 @@ View With Layout
// Assert
Assert.Equal(expected, body.Trim());
}
[Fact]
public async Task ViewComponentsExecuteLayout()
{
// Arrange
var expected =
@"<title>View With Component With Layout</title>
Page Content
<component-title>ViewComponent With Title</component-title>
<component-body>
Component With Layout</component-body>";
var server = TestServer.Create(_provider, _app);
var client = server.CreateClient();
// Act
var body = await client.GetStringAsync("http://localhost/ViewEngine/ViewWithComponentThatHasLayout");
// Assert
Assert.Equal(expected, body.Trim());
}
[Fact]
public async Task ViewComponentsDoNotExecuteViewStarts()
{
// Arrange
var expected = @"<page-content>ViewComponent With ViewStart</page-content>";
var server = TestServer.Create(_provider, _app);
var client = server.CreateClient();
// Act
var body = await client.GetStringAsync("http://localhost/ViewEngine/ViewWithComponentThatHasViewStart");
// Assert
Assert.Equal(expected, body.Trim());
}
[Fact]
public async Task PartialDoNotExecuteViewStarts()
{
// Arrange
var expected = "Partial that does not specify Layout";
var server = TestServer.Create(_provider, _app);
var client = server.CreateClient();
// Act
var body = await client.GetStringAsync("http://localhost/PartialsWithLayout/PartialDoesNotExecuteViewStarts");
// Assert
Assert.Equal(expected, body.Trim());
}
[Fact]
public async Task PartialsRenderedViaRenderPartialAsync_CanRenderLayouts()
{
// Arrange
var expected =
@"<layout-for-viewstart-with-layout><layout-for-viewstart-with-layout>
Partial that specifies Layout
</layout-for-viewstart-with-layout>Partial that does not specify Layout
</layout-for-viewstart-with-layout>";
var server = TestServer.Create(_provider, _app);
var client = server.CreateClient();
// Act
var body = await client.GetStringAsync("http://localhost/PartialsWithLayout/PartialsRenderedViaRenderPartial");
// Assert
Assert.Equal(expected, body.Trim());
}
[Fact]
public async Task PartialsRenderedViaPartialAsync_CanRenderLayouts()
{
// Arrange
var expected =
@"<layout-for-viewstart-with-layout><layout-for-viewstart-with-layout>
Partial that specifies Layout
</layout-for-viewstart-with-layout>
Partial that does not specify Layout
</layout-for-viewstart-with-layout>";
var server = TestServer.Create(_provider, _app);
var client = server.CreateClient();
// Act
var body = await client.GetStringAsync("http://localhost/PartialsWithLayout/PartialsRenderedViaPartialAsync");
// Assert
Assert.Equal(expected, body.Trim());
}
}
}

View File

@ -22,7 +22,7 @@ namespace Microsoft.AspNet.Mvc.Razor
#pragma warning restore 1998
[Fact]
public async Task RenderAsync_AsPartial_DoesNotBufferOutput()
public async Task RenderAsync_AsPartial_BuffersOutput()
{
// Arrange
TextWriter actual = null;
@ -43,7 +43,8 @@ namespace Microsoft.AspNet.Mvc.Razor
await view.RenderAsync(viewContext);
// Assert
Assert.Same(expected, actual);
Assert.NotSame(expected, actual);
Assert.IsAssignableFrom<IBufferedTextWriter>(actual);
Assert.Equal("Hello world", viewContext.Writer.ToString());
}
@ -105,13 +106,31 @@ namespace Microsoft.AspNet.Mvc.Razor
}
[Fact]
public async Task RenderAsync_AsPartial_DoesNotExecuteLayoutOrViewStartPages()
public async Task RenderAsync_AsPartial_ExecutesLayout_ButNotViewStartPages()
{
// Arrange
var expected = string.Join(Environment.NewLine,
"layout-content",
"page-content");
var page = new TestableRazorPage(v =>
{
v.Layout = LayoutPath;
v.Write("page-content");
});
var layout = new TestableRazorPage(v =>
{
v.Write("layout-content" + Environment.NewLine);
v.RenderBodyPublic();
});
var pageFactory = new Mock<IRazorPageFactory>();
pageFactory.Setup(p => p.CreateInstance(LayoutPath))
.Returns(layout);
var viewEngine = new Mock<IRazorViewEngine>();
viewEngine.Setup(v => v.FindPage(It.IsAny<ActionContext>(), LayoutPath))
.Returns(new RazorPageResult(LayoutPath, layout));
var viewStartProvider = CreateViewStartProvider();
var view = new RazorView(viewEngine.Object,
Mock.Of<IRazorPageActivator>(),
@ -124,10 +143,9 @@ namespace Microsoft.AspNet.Mvc.Razor
await view.RenderAsync(viewContext);
// Assert
viewEngine.Verify(v => v.FindPage(It.IsAny<ActionContext>(), It.IsAny<string>()),
Times.Never());
Mock.Get(viewStartProvider)
.Verify(v => v.GetViewStartPages(It.IsAny<string>()), Times.Never());
Assert.Equal(expected, viewContext.Writer.ToString());
}
[Fact]

View File

@ -0,0 +1,16 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNet.Mvc;
namespace MvcSample.Web.Components
{
public class ComponentWithLayout : ViewComponent
{
public IViewComponentResult Invoke()
{
ViewData["Title"] = "ViewComponent With Title";
return View();
}
}
}

View File

@ -0,0 +1,16 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNet.Mvc;
namespace MvcSample.Web.Components
{
public class ComponentWithViewStart : ViewComponent
{
public IViewComponentResult Invoke()
{
ViewData["Title"] = "ViewComponent With ViewStart";
return View();
}
}
}

View File

@ -0,0 +1,31 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNet.Mvc;
namespace RazorWebSite.Controllers
{
public class PartialsWithLayoutController : Controller
{
public IActionResult PartialDoesNotExecuteViewStarts()
{
return PartialView("PartialThatDoesNotSpecifyLayout");
}
// This action demonstrates
// (a) _ViewStart does not get executed when executing a partial via RenderPartial
// (b) Partials rendered via RenderPartial can execute Layout.
public IActionResult PartialsRenderedViaRenderPartial()
{
return View();
}
// This action demonstrates
// (a) _ViewStart does not get executed when executing a partial via PartialAsync
// (b) Partials rendered via PartialAsync can execute Layout.
public IActionResult PartialsRenderedViaPartialAsync()
{
return View();
}
}
}

View File

@ -60,5 +60,16 @@ namespace RazorWebSite.Controllers
ViewData["data-from-controller"] = "hello from controller";
return View("ViewWithDataFromController");
}
public ViewResult ViewWithComponentThatHasLayout()
{
ViewData["Title"] = "View With Component With Layout";
return View();
}
public ViewResult ViewWithComponentThatHasViewStart()
{
return View();
}
}
}

View File

@ -0,0 +1 @@
<layout-for-viewstart-with-layout>@RenderBody()</layout-for-viewstart-with-layout>

View File

@ -0,0 +1 @@
Partial that does not specify Layout

View File

@ -0,0 +1,4 @@
@{
Layout = "LayoutForViewStartWithLayout";
}
Partial that specifies Layout

View File

@ -0,0 +1,2 @@
@await Html.PartialAsync("PartialThatSpecifiesLayout")
@await Html.PartialAsync("PartialThatDoesNotSpecifyLayout")

View File

@ -0,0 +1,4 @@
@{
await Html.RenderPartialAsync("PartialThatSpecifiesLayout");
await Html.RenderPartialAsync("PartialThatDoesNotSpecifyLayout");
}

View File

@ -0,0 +1,3 @@
@{
Layout = "LayoutForViewStartWithLayout";
}

View File

@ -0,0 +1,4 @@
@{
Layout = "_ComponentLayout";
}
Component With Layout

View File

@ -0,0 +1,3 @@
@{
throw new Exception("This should not be invoked as part of executing the View Component.");
}

View File

@ -0,0 +1,2 @@
<component-title>@ViewBag.Title</component-title>
<component-body>@RenderBody()</component-body>

View File

@ -0,0 +1,5 @@
@{
Layout = "_LayoutWithTitle";
}
Page Content
@await Component.InvokeAsync("ComponentWithLayout")

View File

@ -0,0 +1 @@
<page-content>@await Component.InvokeAsync("ComponentWithViewStart")</page-content>