diff --git a/src/Microsoft.AspNet.Mvc.Core/Rendering/ViewEngine/IView.cs b/src/Microsoft.AspNet.Mvc.Core/Rendering/ViewEngine/IView.cs index baba0de289..2f13a2e352 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 @@ using System.Threading.Tasks; 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 @@ using Microsoft.AspNet.Mvc.Rendering; 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 @@ namespace Microsoft.AspNet.Mvc 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 @@ namespace Microsoft.AspNet.Mvc 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 @@ namespace Microsoft.AspNet.Mvc 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 @@ namespace Microsoft.AspNet.Mvc } } + /// + /// Gets or sets a value that indicates whether client-side validation is enabled. + /// public bool ClientValidationEnabled { get; set; } /// @@ -84,6 +108,9 @@ namespace Microsoft.AspNet.Mvc /// public string ValidationMessageElement { get; set; } + /// + /// Gets the dynamic view bag. + /// public dynamic ViewBag { get @@ -97,12 +124,30 @@ namespace Microsoft.AspNet.Mvc } } + /// + /// 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..5a68fc8cc8 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs @@ -42,6 +42,12 @@ namespace Microsoft.AspNet.Mvc.Razor IsPartial = isPartial; } + /// + public string Path + { + get { return RazorPage.Path; } + } + /// /// Gets instance that the views executes on. /// @@ -93,7 +99,9 @@ namespace Microsoft.AspNet.Mvc.Razor // 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 +117,7 @@ namespace Microsoft.AspNet.Mvc.Razor finally { context.Writer = oldWriter; + context.ExecutingFilePath = oldFilePath; writer.Dispose(); } } @@ -131,12 +140,21 @@ namespace Microsoft.AspNet.Mvc.Razor var viewStarts = _viewStartProvider.GetViewStartPages(RazorPage.Path); string layout = null; - foreach (var viewStart in viewStarts) + var oldFilePath = context.ExecutingFilePath; + try { - // Copy the layout value from the previous view start (if any) to the current. - viewStart.Layout = layout; - await RenderPageCoreAsync(viewStart, context); - layout = viewStart.Layout; + 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 + { + 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 @@ namespace Microsoft.AspNet.Mvc.Razor 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 7e8bdfcd2c..b47d95050b 100644 --- a/test/WebSites/ModelBindingWebSite/Controllers/RoundtripController.cs +++ b/test/WebSites/ModelBindingWebSite/Controllers/RoundtripController.cs @@ -68,6 +68,8 @@ namespace ModelBindingWebSite.Controllers 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 +