From 364430ed5764429bb875c07a37789a10cbf83198 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Tue, 10 Feb 2015 23:09:08 -0800 Subject: [PATCH] Adding IView.Path and ViewContext.ExecutingPagePath Fixes #1940 --- .../Rendering/ViewEngine/IView.cs | 13 +++++ src/Microsoft.AspNet.Mvc.Core/ViewContext.cs | 45 +++++++++++++++ src/Microsoft.AspNet.Mvc.Razor/RazorView.cs | 25 +++++++-- .../ViewEngineController.ViewWithPaths.txt | 21 +++++++ .../ViewEngineTests.cs | 17 ++++++ .../RazorViewTest.cs | 56 +++++++++++++++++++ .../TestPartialView.cs | 2 + .../CompositeViewEngineWebSite/TestView.cs | 2 + .../Controllers/RoundtripController.cs | 2 + .../Components/ComponentForViewWithPaths.cs | 16 ++++++ .../Controllers/ViewWithPathsController.cs | 16 ++++++ .../ComponentForViewWithPaths/Default.cshtml | 4 ++ .../Views/ViewWithPaths/Index.cshtml | 6 ++ .../Views/ViewWithPaths/_Layout.cshtml | 5 ++ .../Views/ViewWithPaths/_Partial.cshtml | 4 ++ .../Views/ViewWithPaths/_ViewStart.cshtml | 7 +++ 16 files changed, 236 insertions(+), 5 deletions(-) create mode 100644 test/Microsoft.AspNet.Mvc.FunctionalTests/Compiler/Resources/ViewEngineController.ViewWithPaths.txt create mode 100644 test/WebSites/RazorWebSite/Components/ComponentForViewWithPaths.cs create mode 100644 test/WebSites/RazorWebSite/Controllers/ViewWithPathsController.cs create mode 100644 test/WebSites/RazorWebSite/Views/Shared/Components/ComponentForViewWithPaths/Default.cshtml create mode 100644 test/WebSites/RazorWebSite/Views/ViewWithPaths/Index.cshtml create mode 100644 test/WebSites/RazorWebSite/Views/ViewWithPaths/_Layout.cshtml create mode 100644 test/WebSites/RazorWebSite/Views/ViewWithPaths/_Partial.cshtml create mode 100644 test/WebSites/RazorWebSite/Views/ViewWithPaths/_ViewStart.cshtml diff --git a/src/Microsoft.AspNet.Mvc.Core/Rendering/ViewEngine/IView.cs b/src/Microsoft.AspNet.Mvc.Core/Rendering/ViewEngine/IView.cs index baba0de289..d622a447ad 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Rendering/ViewEngine/IView.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Rendering/ViewEngine/IView.cs @@ -5,8 +5,21 @@ namespace Microsoft.AspNet.Mvc.Rendering { + /// + /// Specifies the contract for a view. + /// public interface IView { + /// + /// Gets the path of the view as resolved by the . + /// + string Path { get; } + + /// + /// Asynchronously renders the view using the specified . + /// + /// The . + /// A that on completion renders the view. Task RenderAsync([NotNull] ViewContext context); } } diff --git a/src/Microsoft.AspNet.Mvc.Core/ViewContext.cs b/src/Microsoft.AspNet.Mvc.Core/ViewContext.cs index 031fe6047b..4acded193b 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ViewContext.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ViewContext.cs @@ -6,6 +6,9 @@ namespace Microsoft.AspNet.Mvc { + /// + /// Context for view execution. + /// public class ViewContext : ActionContext { // We need a default FormContext if the user uses html
instead of an MvcForm @@ -14,6 +17,13 @@ public class ViewContext : ActionContext private FormContext _formContext; private DynamicViewData _viewBag; + /// + /// Initializes a new instance of . + /// + /// The . + /// The being rendered. + /// The . + /// The to render output to. public ViewContext( [NotNull] ActionContext actionContext, [NotNull] IView view, @@ -31,6 +41,13 @@ public ViewContext( ValidationMessageElement = "span"; } + /// + /// Initializes a new instance of . + /// + /// The to copy values from. + /// The being rendered. + /// The . + /// The to render output to. public ViewContext( [NotNull] ViewContext viewContext, [NotNull] IView view, @@ -49,6 +66,10 @@ public ViewContext( Writer = writer; } + /// + /// Gets or sets the for the form element being rendered. + /// A default context is returned if no form is currently being rendered. + /// public virtual FormContext FormContext { get @@ -62,6 +83,9 @@ public virtual FormContext FormContext } } + /// + /// Gets or sets a value that indicates whether client-side validation is enabled. + /// public bool ClientValidationEnabled { get; set; } /// @@ -84,6 +108,9 @@ public virtual FormContext FormContext /// public string ValidationMessageElement { get; set; } + /// + /// Gets the dynamic view bag. + /// public dynamic ViewBag { get @@ -97,12 +124,30 @@ public dynamic ViewBag } } + /// + /// Gets or sets the currently being rendered, if any. + /// public IView View { get; set; } + /// + /// Gets or sets the . + /// public ViewDataDictionary ViewData { get; set; } + /// + /// Gets or sets the used to write the output. + /// public TextWriter Writer { get; set; } + /// + /// Gets or sets the path of the view file currently being rendered. + /// + /// + /// The rendering of a view may involve one or more files (e.g. _ViewStart, Layouts etc). + /// This property contains the path of the file currently being rendered. + /// + public string ExecutingFilePath { get; set; } + public FormContext GetFormContextForClientValidation() { return (ClientValidationEnabled) ? FormContext : null; diff --git a/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs b/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs index 746c1e1313..67e28e7516 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs @@ -42,6 +42,9 @@ public RazorView(IRazorViewEngine viewEngine, IsPartial = isPartial; } + /// + public string Path => RazorPage.Path; + /// /// Gets instance that the views executes on. /// @@ -93,7 +96,9 @@ private async Task RenderPageAsync(IRazorPage page, // The writer for the body is passed through the ViewContext, allowing things like HtmlHelpers // and ViewComponents to reference it. var oldWriter = context.Writer; + var oldFilePath = context.ExecutingFilePath; context.Writer = writer; + context.ExecutingFilePath = page.Path; try { @@ -109,6 +114,7 @@ private async Task RenderPageAsync(IRazorPage page, finally { context.Writer = oldWriter; + context.ExecutingFilePath = oldFilePath; writer.Dispose(); } } @@ -131,12 +137,21 @@ private async Task RenderViewStartAsync(ViewContext context) var viewStarts = _viewStartProvider.GetViewStartPages(RazorPage.Path); string layout = null; - foreach (var viewStart in viewStarts) + var oldFilePath = context.ExecutingFilePath; + try + { + foreach (var viewStart in viewStarts) + { + context.ExecutingFilePath = viewStart.Path; + // Copy the layout value from the previous view start (if any) to the current. + viewStart.Layout = layout; + await RenderPageCoreAsync(viewStart, context); + layout = viewStart.Layout; + } + } + finally { - // Copy the layout value from the previous view start (if any) to the current. - viewStart.Layout = layout; - await RenderPageCoreAsync(viewStart, context); - layout = viewStart.Layout; + context.ExecutingFilePath = oldFilePath; } // Copy over interesting properties from the ViewStart page to the entry page. diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/Compiler/Resources/ViewEngineController.ViewWithPaths.txt b/test/Microsoft.AspNet.Mvc.FunctionalTests/Compiler/Resources/ViewEngineController.ViewWithPaths.txt new file mode 100644 index 0000000000..5c2e3458e3 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/Compiler/Resources/ViewEngineController.ViewWithPaths.txt @@ -0,0 +1,21 @@ + + /Views/ViewWithPaths/_Layout.cshtml + /Views/ViewWithPaths/Index.cshtml + + + + Views\ViewWithPaths\_ViewStart.cshtml + /Views/ViewWithPaths/Index.cshtml + + + /Views/ViewWithPaths/Index.cshtml + /Views/ViewWithPaths/Index.cshtml + +/Views/Shared/Components/ComponentForViewWithPaths/Default.cshtml +/Views/Shared/Components/ComponentForViewWithPaths/Default.cshtml + + +/Views/ViewWithPaths/_Partial.cshtml +/Views/ViewWithPaths/_Partial.cshtml + + \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/ViewEngineTests.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/ViewEngineTests.cs index 29a5846e1b..568aea5d07 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/ViewEngineTests.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/ViewEngineTests.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Reflection; using System.Threading.Tasks; using Microsoft.AspNet.Builder; using Microsoft.AspNet.TestHost; @@ -395,5 +396,21 @@ Partial that does not specify Layout // Assert Assert.Equal(expected, body.Trim()); } + + [Fact] + public async Task RazorView_SetsViewPathAndExecutingPagePath() + { + // Arrange + var expected = await GetType().GetTypeInfo().Assembly + .ReadResourceAsStringAsync("compiler/resources/ViewEngineController.ViewWithPaths.txt"); + var server = TestServer.Create(_provider, _app); + var client = server.CreateClient(); + + // Act + var body = await client.GetStringAsync("http://localhost/ViewWithPaths"); + + // 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 44c8864209..e52f17e723 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs @@ -2,6 +2,7 @@ // 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; @@ -83,6 +84,61 @@ public async Task RenderAsync_AsPartial_ActivatesViews_WithThePassedInViewContex Assert.Same(expectedWriter, viewContext.Writer); } + [Fact] + public async Task ViewContext_ExecutingPagePath_ReturnsPathOfRazorPageBeingExecuted() + { + // Arrange + var pagePath = "/my/view"; + var paths = new List(); + var page = new TestableRazorPage(v => + { + paths.Add(v.ViewContext.ExecutingFilePath); + Assert.Equal(pagePath, v.ViewContext.View.Path); + }) + { + Path = pagePath + }; + + var viewStart = new TestableRazorPage(v => + { + v.Layout = LayoutPath; + paths.Add(v.ViewContext.ExecutingFilePath); + Assert.Equal(pagePath, v.ViewContext.View.Path); + }) + { + Path = "_ViewStart" + }; + + var layout = new TestableRazorPage(v => + { + v.RenderBodyPublic(); + paths.Add(v.ViewContext.ExecutingFilePath); + Assert.Equal(pagePath, v.ViewContext.View.Path); + }) + { + Path = LayoutPath + }; + + var activator = Mock.Of(); + var viewEngine = new Mock(); + viewEngine.Setup(v => v.FindPage(It.IsAny(), LayoutPath)) + .Returns(new RazorPageResult(LayoutPath, layout)); + var view = new RazorView(viewEngine.Object, + activator, + CreateViewStartProvider(viewStart), + page, + isPartial: false); + + var viewContext = CreateViewContext(view); + var expectedWriter = viewContext.Writer; + + // Act + await view.RenderAsync(viewContext); + + // Assert + Assert.Equal(new[] { "_ViewStart", pagePath, LayoutPath }, paths); + } + [Fact] public async Task RenderAsync_AsPartial_ActivatesViews() { diff --git a/test/WebSites/CompositeViewEngineWebSite/TestPartialView.cs b/test/WebSites/CompositeViewEngineWebSite/TestPartialView.cs index 264fec5e09..48e3ceae35 100644 --- a/test/WebSites/CompositeViewEngineWebSite/TestPartialView.cs +++ b/test/WebSites/CompositeViewEngineWebSite/TestPartialView.cs @@ -9,6 +9,8 @@ namespace CompositeViewEngineWebSite { public class TestPartialView : IView { + public string Path { get; set; } + public async Task RenderAsync(ViewContext context) { await context.Writer.WriteLineAsync("world"); diff --git a/test/WebSites/CompositeViewEngineWebSite/TestView.cs b/test/WebSites/CompositeViewEngineWebSite/TestView.cs index 907c37aaf2..3f6b8ad077 100644 --- a/test/WebSites/CompositeViewEngineWebSite/TestView.cs +++ b/test/WebSites/CompositeViewEngineWebSite/TestView.cs @@ -9,6 +9,8 @@ namespace CompositeViewEngineWebSite { public class TestView : IView { + public string Path { get; set; } + public async Task RenderAsync(ViewContext context) { await context.Writer.WriteLineAsync("Content from test view"); diff --git a/test/WebSites/ModelBindingWebSite/Controllers/RoundtripController.cs b/test/WebSites/ModelBindingWebSite/Controllers/RoundtripController.cs index 8f30225bcf..a9b7a000d2 100644 --- a/test/WebSites/ModelBindingWebSite/Controllers/RoundtripController.cs +++ b/test/WebSites/ModelBindingWebSite/Controllers/RoundtripController.cs @@ -68,6 +68,8 @@ public Person Person(Person boundPerson) private sealed class TestView : IView { + public string Path { get; set; } + public Task RenderAsync(ViewContext context) { throw new NotImplementedException(); diff --git a/test/WebSites/RazorWebSite/Components/ComponentForViewWithPaths.cs b/test/WebSites/RazorWebSite/Components/ComponentForViewWithPaths.cs new file mode 100644 index 0000000000..2833d62c7e --- /dev/null +++ b/test/WebSites/RazorWebSite/Components/ComponentForViewWithPaths.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 +{ + [ViewComponent(Name = "ComponentForViewWithPaths")] + public class ComponentForViewWithPaths : ViewComponent + { + public IViewComponentResult Invoke() + { + return View(); + } + } +} \ No newline at end of file diff --git a/test/WebSites/RazorWebSite/Controllers/ViewWithPathsController.cs b/test/WebSites/RazorWebSite/Controllers/ViewWithPathsController.cs new file mode 100644 index 0000000000..d4064c3d3e --- /dev/null +++ b/test/WebSites/RazorWebSite/Controllers/ViewWithPathsController.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 RazorWebSite.Controllers +{ + public class ViewWithPathsController : Controller + { + [HttpGet("/ViewWithPaths")] + public IActionResult Index() + { + return View(); + } + } +} \ No newline at end of file diff --git a/test/WebSites/RazorWebSite/Views/Shared/Components/ComponentForViewWithPaths/Default.cshtml b/test/WebSites/RazorWebSite/Views/Shared/Components/ComponentForViewWithPaths/Default.cshtml new file mode 100644 index 0000000000..b70a1f94ab --- /dev/null +++ b/test/WebSites/RazorWebSite/Views/Shared/Components/ComponentForViewWithPaths/Default.cshtml @@ -0,0 +1,4 @@ + +@ViewContext.ExecutingFilePath +@ViewContext.View.Path + \ No newline at end of file diff --git a/test/WebSites/RazorWebSite/Views/ViewWithPaths/Index.cshtml b/test/WebSites/RazorWebSite/Views/ViewWithPaths/Index.cshtml new file mode 100644 index 0000000000..7262ab1db0 --- /dev/null +++ b/test/WebSites/RazorWebSite/Views/ViewWithPaths/Index.cshtml @@ -0,0 +1,6 @@ + + @ViewContext.ExecutingFilePath + @ViewContext.View.Path + @Component.Invoke("ComponentForViewWithPaths") + @Html.Partial("_Partial") + \ No newline at end of file diff --git a/test/WebSites/RazorWebSite/Views/ViewWithPaths/_Layout.cshtml b/test/WebSites/RazorWebSite/Views/ViewWithPaths/_Layout.cshtml new file mode 100644 index 0000000000..f79f56199a --- /dev/null +++ b/test/WebSites/RazorWebSite/Views/ViewWithPaths/_Layout.cshtml @@ -0,0 +1,5 @@ + + @ViewContext.ExecutingFilePath + @ViewContext.View.Path + +@RenderBody() \ No newline at end of file diff --git a/test/WebSites/RazorWebSite/Views/ViewWithPaths/_Partial.cshtml b/test/WebSites/RazorWebSite/Views/ViewWithPaths/_Partial.cshtml new file mode 100644 index 0000000000..c182e1f86a --- /dev/null +++ b/test/WebSites/RazorWebSite/Views/ViewWithPaths/_Partial.cshtml @@ -0,0 +1,4 @@ + +@ViewContext.ExecutingFilePath +@ViewContext.View.Path + \ No newline at end of file diff --git a/test/WebSites/RazorWebSite/Views/ViewWithPaths/_ViewStart.cshtml b/test/WebSites/RazorWebSite/Views/ViewWithPaths/_ViewStart.cshtml new file mode 100644 index 0000000000..a567df84dd --- /dev/null +++ b/test/WebSites/RazorWebSite/Views/ViewWithPaths/_ViewStart.cshtml @@ -0,0 +1,7 @@ +@{ + Layout = "_Layout"; +} + + @ViewContext.ExecutingFilePath + @ViewContext.View.Path +