diff --git a/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs b/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs index 1ed944a9a7..746c1e1313 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs @@ -62,32 +62,10 @@ namespace Microsoft.AspNet.Mvc.Razor { _pageExecutionFeature = context.HttpContext.GetFeature(); - 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 RenderPageAsync(IRazorPage page, diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/ViewEngineTests.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/ViewEngineTests.cs index f87b3899a4..29a5846e1b 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/ViewEngineTests.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/ViewEngineTests.cs @@ -164,25 +164,41 @@ component-content"; Assert.Equal(expected, body.Trim()); } - public static IEnumerable PartialRazorViews_DoNotRenderLayoutData + public static IEnumerable RazorViewEngine_RendersPartialViewsData { get { yield return new[] { - "ViewWithoutLayout", @"ViewWithoutLayout-Content" + "ViewWithoutLayout", "ViewWithoutLayout-Content" }; yield return new[] { - "PartialViewWithNamePassedIn", @"ViewWithLayout-Content" + "PartialViewWithNamePassedIn", +@" + +ViewWithLayout-Content +" }; yield return new[] { - "ViewWithFullPath", "ViewWithFullPath-content" + "ViewWithFullPath", +@" + +ViewWithFullPath-content +" }; yield return new[] { - "ViewWithNestedLayout", "ViewWithNestedLayout-Content" + "ViewWithNestedLayout", +@" + + +/PartialViewEngine/ViewWithNestedLayout + +ViewWithNestedLayout-Content + +" }; 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 = +@"View With Component With Layout + +Page Content +ViewComponent With Title + +Component With Layout"; + 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 = @"ViewComponent With ViewStart"; + 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 = +@" +Partial that specifies Layout +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/PartialsRenderedViaRenderPartial"); + + // Assert + Assert.Equal(expected, body.Trim()); + } + + [Fact] + public async Task PartialsRenderedViaPartialAsync_CanRenderLayouts() + { + // Arrange + var expected = +@" +Partial that specifies Layout + +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/PartialsRenderedViaPartialAsync"); + + // Assert + Assert.Equal(expected, body.Trim()); + } } } diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs index b107db2f8d..44c8864209 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs @@ -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(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(); + pageFactory.Setup(p => p.CreateInstance(LayoutPath)) + .Returns(layout); + var viewEngine = new Mock(); + viewEngine.Setup(v => v.FindPage(It.IsAny(), LayoutPath)) + .Returns(new RazorPageResult(LayoutPath, layout)); + var viewStartProvider = CreateViewStartProvider(); var view = new RazorView(viewEngine.Object, Mock.Of(), @@ -124,10 +143,9 @@ namespace Microsoft.AspNet.Mvc.Razor await view.RenderAsync(viewContext); // Assert - viewEngine.Verify(v => v.FindPage(It.IsAny(), It.IsAny()), - Times.Never()); Mock.Get(viewStartProvider) .Verify(v => v.GetViewStartPages(It.IsAny()), Times.Never()); + Assert.Equal(expected, viewContext.Writer.ToString()); } [Fact] diff --git a/test/WebSites/RazorWebSite/Components/ComponentWithLayout.cs b/test/WebSites/RazorWebSite/Components/ComponentWithLayout.cs new file mode 100644 index 0000000000..91acffc9b8 --- /dev/null +++ b/test/WebSites/RazorWebSite/Components/ComponentWithLayout.cs @@ -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(); + } + } +} \ No newline at end of file diff --git a/test/WebSites/RazorWebSite/Components/ComponentWithViewStart.cs b/test/WebSites/RazorWebSite/Components/ComponentWithViewStart.cs new file mode 100644 index 0000000000..0d34e55671 --- /dev/null +++ b/test/WebSites/RazorWebSite/Components/ComponentWithViewStart.cs @@ -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(); + } + } +} \ No newline at end of file diff --git a/test/WebSites/RazorWebSite/Controllers/PartialsWithLayoutController.cs b/test/WebSites/RazorWebSite/Controllers/PartialsWithLayoutController.cs new file mode 100644 index 0000000000..c918a7482c --- /dev/null +++ b/test/WebSites/RazorWebSite/Controllers/PartialsWithLayoutController.cs @@ -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(); + } + } +} diff --git a/test/WebSites/RazorWebSite/Controllers/ViewEngineController.cs b/test/WebSites/RazorWebSite/Controllers/ViewEngineController.cs index c625844f3a..b18dce3e40 100644 --- a/test/WebSites/RazorWebSite/Controllers/ViewEngineController.cs +++ b/test/WebSites/RazorWebSite/Controllers/ViewEngineController.cs @@ -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(); + } } } \ No newline at end of file diff --git a/test/WebSites/RazorWebSite/Views/PartialsWithLayout/LayoutForViewStartWithLayout.cshtml b/test/WebSites/RazorWebSite/Views/PartialsWithLayout/LayoutForViewStartWithLayout.cshtml new file mode 100644 index 0000000000..f84acc75ef --- /dev/null +++ b/test/WebSites/RazorWebSite/Views/PartialsWithLayout/LayoutForViewStartWithLayout.cshtml @@ -0,0 +1 @@ +@RenderBody() \ No newline at end of file diff --git a/test/WebSites/RazorWebSite/Views/PartialsWithLayout/PartialThatDoesNotSpecifyLayout.cshtml b/test/WebSites/RazorWebSite/Views/PartialsWithLayout/PartialThatDoesNotSpecifyLayout.cshtml new file mode 100644 index 0000000000..7160805707 --- /dev/null +++ b/test/WebSites/RazorWebSite/Views/PartialsWithLayout/PartialThatDoesNotSpecifyLayout.cshtml @@ -0,0 +1 @@ +Partial that does not specify Layout \ No newline at end of file diff --git a/test/WebSites/RazorWebSite/Views/PartialsWithLayout/PartialThatSpecifiesLayout.cshtml b/test/WebSites/RazorWebSite/Views/PartialsWithLayout/PartialThatSpecifiesLayout.cshtml new file mode 100644 index 0000000000..393fcc5be4 --- /dev/null +++ b/test/WebSites/RazorWebSite/Views/PartialsWithLayout/PartialThatSpecifiesLayout.cshtml @@ -0,0 +1,4 @@ +@{ + Layout = "LayoutForViewStartWithLayout"; +} +Partial that specifies Layout diff --git a/test/WebSites/RazorWebSite/Views/PartialsWithLayout/PartialsRenderedViaPartialAsync.cshtml b/test/WebSites/RazorWebSite/Views/PartialsWithLayout/PartialsRenderedViaPartialAsync.cshtml new file mode 100644 index 0000000000..a31e73014b --- /dev/null +++ b/test/WebSites/RazorWebSite/Views/PartialsWithLayout/PartialsRenderedViaPartialAsync.cshtml @@ -0,0 +1,2 @@ +@await Html.PartialAsync("PartialThatSpecifiesLayout") +@await Html.PartialAsync("PartialThatDoesNotSpecifyLayout") diff --git a/test/WebSites/RazorWebSite/Views/PartialsWithLayout/PartialsRenderedViaRenderPartial.cshtml b/test/WebSites/RazorWebSite/Views/PartialsWithLayout/PartialsRenderedViaRenderPartial.cshtml new file mode 100644 index 0000000000..a19efc0fce --- /dev/null +++ b/test/WebSites/RazorWebSite/Views/PartialsWithLayout/PartialsRenderedViaRenderPartial.cshtml @@ -0,0 +1,4 @@ +@{ + await Html.RenderPartialAsync("PartialThatSpecifiesLayout"); + await Html.RenderPartialAsync("PartialThatDoesNotSpecifyLayout"); +} diff --git a/test/WebSites/RazorWebSite/Views/PartialsWithLayout/_ViewStart.cshtml b/test/WebSites/RazorWebSite/Views/PartialsWithLayout/_ViewStart.cshtml new file mode 100644 index 0000000000..4203835c70 --- /dev/null +++ b/test/WebSites/RazorWebSite/Views/PartialsWithLayout/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "LayoutForViewStartWithLayout"; +} \ No newline at end of file diff --git a/test/WebSites/RazorWebSite/Views/Shared/Components/ComponentWithLayout/Default.cshtml b/test/WebSites/RazorWebSite/Views/Shared/Components/ComponentWithLayout/Default.cshtml new file mode 100644 index 0000000000..77202448c3 --- /dev/null +++ b/test/WebSites/RazorWebSite/Views/Shared/Components/ComponentWithLayout/Default.cshtml @@ -0,0 +1,4 @@ +@{ + Layout = "_ComponentLayout"; +} +Component With Layout \ No newline at end of file diff --git a/test/WebSites/RazorWebSite/Views/Shared/Components/ComponentWithViewStart/Default.cshtml b/test/WebSites/RazorWebSite/Views/Shared/Components/ComponentWithViewStart/Default.cshtml new file mode 100644 index 0000000000..e6b8bc36d4 --- /dev/null +++ b/test/WebSites/RazorWebSite/Views/Shared/Components/ComponentWithViewStart/Default.cshtml @@ -0,0 +1 @@ +@ViewBag.Title \ No newline at end of file diff --git a/test/WebSites/RazorWebSite/Views/Shared/Components/ComponentWithViewStart/_ViewStart.cshtml b/test/WebSites/RazorWebSite/Views/Shared/Components/ComponentWithViewStart/_ViewStart.cshtml new file mode 100644 index 0000000000..6da7c4c9a9 --- /dev/null +++ b/test/WebSites/RazorWebSite/Views/Shared/Components/ComponentWithViewStart/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + throw new Exception("This should not be invoked as part of executing the View Component."); +} \ No newline at end of file diff --git a/test/WebSites/RazorWebSite/Views/Shared/_ComponentLayout.cshtml b/test/WebSites/RazorWebSite/Views/Shared/_ComponentLayout.cshtml new file mode 100644 index 0000000000..1767ceda30 --- /dev/null +++ b/test/WebSites/RazorWebSite/Views/Shared/_ComponentLayout.cshtml @@ -0,0 +1,2 @@ +@ViewBag.Title +@RenderBody() \ No newline at end of file diff --git a/test/WebSites/RazorWebSite/Views/ViewEngine/ViewWithComponentThatHasLayout.cshtml b/test/WebSites/RazorWebSite/Views/ViewEngine/ViewWithComponentThatHasLayout.cshtml new file mode 100644 index 0000000000..2008e37128 --- /dev/null +++ b/test/WebSites/RazorWebSite/Views/ViewEngine/ViewWithComponentThatHasLayout.cshtml @@ -0,0 +1,5 @@ +@{ + Layout = "_LayoutWithTitle"; +} +Page Content +@await Component.InvokeAsync("ComponentWithLayout") diff --git a/test/WebSites/RazorWebSite/Views/ViewEngine/ViewWithComponentThatHasViewStart.cshtml b/test/WebSites/RazorWebSite/Views/ViewEngine/ViewWithComponentThatHasViewStart.cshtml new file mode 100644 index 0000000000..c0e538e707 --- /dev/null +++ b/test/WebSites/RazorWebSite/Views/ViewEngine/ViewWithComponentThatHasViewStart.cshtml @@ -0,0 +1 @@ +@await Component.InvokeAsync("ComponentWithViewStart")