Adding IView.Path and ViewContext.ExecutingPagePath

Fixes #1940
This commit is contained in:
Pranav K 2015-02-10 23:09:08 -08:00
parent 29c346ebf8
commit a33e83f363
16 changed files with 239 additions and 5 deletions

View File

@ -5,8 +5,21 @@ using System.Threading.Tasks;
namespace Microsoft.AspNet.Mvc.Rendering
{
/// <summary>
/// Specifies the contract for a view.
/// </summary>
public interface IView
{
/// <summary>
/// Gets the path of the view as resolved by the <see cref="IViewEngine"/>.
/// </summary>
string Path { get; }
/// <summary>
/// Asynchronously renders the view using the specified <paramref name="context"/>.
/// </summary>
/// <param name="context">The <see cref="ViewContext"/>.</param>
/// <returns>A <see cref="Task"/> that on completion renders the view.</returns>
Task RenderAsync([NotNull] ViewContext context);
}
}

View File

@ -6,6 +6,9 @@ using Microsoft.AspNet.Mvc.Rendering;
namespace Microsoft.AspNet.Mvc
{
/// <summary>
/// Context for view execution.
/// </summary>
public class ViewContext : ActionContext
{
// We need a default FormContext if the user uses html <form> instead of an MvcForm
@ -14,6 +17,13 @@ namespace Microsoft.AspNet.Mvc
private FormContext _formContext;
private DynamicViewData _viewBag;
/// <summary>
/// Initializes a new instance of <see cref="ViewContext"/>.
/// </summary>
/// <param name="actionContext">The <see cref="ActionContext"/>.</param>
/// <param name="view">The <see cref="IView"/> being rendered.</param>
/// <param name="viewData">The <see cref="ViewDataDictionary"/>.</param>
/// <param name="writer">The <see cref="TextWriter"/> to render output to.</param>
public ViewContext(
[NotNull] ActionContext actionContext,
[NotNull] IView view,
@ -31,6 +41,13 @@ namespace Microsoft.AspNet.Mvc
ValidationMessageElement = "span";
}
/// <summary>
/// Initializes a new instance of <see cref="ViewContext"/>.
/// </summary>
/// <param name="viewContext">The <see cref="ViewContext"/> to copy values from.</param>
/// <param name="view">The <see cref="IView"/> being rendered.</param>
/// <param name="viewData">The <see cref="ViewDataDictionary"/>.</param>
/// <param name="writer">The <see cref="TextWriter"/> to render output to.</param>
public ViewContext(
[NotNull] ViewContext viewContext,
[NotNull] IView view,
@ -49,6 +66,10 @@ namespace Microsoft.AspNet.Mvc
Writer = writer;
}
/// <summary>
/// Gets or sets the <see cref="Mvc.FormContext"/> for the form element being rendered.
/// A default context is returned if no form is currently being rendered.
/// </summary>
public virtual FormContext FormContext
{
get
@ -62,6 +83,9 @@ namespace Microsoft.AspNet.Mvc
}
}
/// <summary>
/// Gets or sets a value that indicates whether client-side validation is enabled.
/// </summary>
public bool ClientValidationEnabled { get; set; }
/// <summary>
@ -84,6 +108,9 @@ namespace Microsoft.AspNet.Mvc
/// </summary>
public string ValidationMessageElement { get; set; }
/// <summary>
/// Gets the dynamic view bag.
/// </summary>
public dynamic ViewBag
{
get
@ -97,12 +124,30 @@ namespace Microsoft.AspNet.Mvc
}
}
/// <summary>
/// Gets or sets the <see cref="IView"/> currently being rendered, if any.
/// </summary>
public IView View { get; set; }
/// <summary>
/// Gets or sets the <see cref="ViewDataDictionary"/>.
/// </summary>
public ViewDataDictionary ViewData { get; set; }
/// <summary>
/// Gets or sets the <see cref="TextWriter"/> used to write the output.
/// </summary>
public TextWriter Writer { get; set; }
/// <summary>
/// Gets or sets the path of the view file currently being rendered.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public string ExecutingFilePath { get; set; }
public FormContext GetFormContextForClientValidation()
{
return (ClientValidationEnabled) ? FormContext : null;

View File

@ -42,6 +42,12 @@ namespace Microsoft.AspNet.Mvc.Razor
IsPartial = isPartial;
}
/// <inheritdoc />
public string Path
{
get { return RazorPage.Path; }
}
/// <summary>
/// Gets <see cref="IRazorPage"/> instance that the views executes on.
/// </summary>
@ -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.

View File

@ -0,0 +1,21 @@
<Layout>
/Views/ViewWithPaths/_Layout.cshtml
/Views/ViewWithPaths/Index.cshtml
</Layout>
<ViewStart>
Views\ViewWithPaths\_ViewStart.cshtml
/Views/ViewWithPaths/Index.cshtml
</ViewStart>
<Index>
/Views/ViewWithPaths/Index.cshtml
/Views/ViewWithPaths/Index.cshtml
<component>
/Views/Shared/Components/ComponentForViewWithPaths/Default.cshtml
/Views/Shared/Components/ComponentForViewWithPaths/Default.cshtml
</component>
<Partial>
/Views/ViewWithPaths/_Partial.cshtml
/Views/ViewWithPaths/_Partial.cshtml
</Partial>
</Index>

View File

@ -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());
}
}
}

View File

@ -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<string>();
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<IRazorPageActivator>();
var viewEngine = new Mock<IRazorViewEngine>();
viewEngine.Setup(v => v.FindPage(It.IsAny<ActionContext>(), 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()
{

View File

@ -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");

View File

@ -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");

View File

@ -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();

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
{
[ViewComponent(Name = "ComponentForViewWithPaths")]
public class ComponentForViewWithPaths : ViewComponent
{
public IViewComponentResult Invoke()
{
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 RazorWebSite.Controllers
{
public class ViewWithPathsController : Controller
{
[HttpGet("/ViewWithPaths")]
public IActionResult Index()
{
return View();
}
}
}

View File

@ -0,0 +1,4 @@
<component>
@ViewContext.ExecutingFilePath
@ViewContext.View.Path
</component>

View File

@ -0,0 +1,6 @@
<Index>
@ViewContext.ExecutingFilePath
@ViewContext.View.Path
@Component.Invoke("ComponentForViewWithPaths")
@Html.Partial("_Partial")
</Index>

View File

@ -0,0 +1,5 @@
<Layout>
@ViewContext.ExecutingFilePath
@ViewContext.View.Path
</Layout>
@RenderBody()

View File

@ -0,0 +1,4 @@
<Partial>
@ViewContext.ExecutingFilePath
@ViewContext.View.Path
</Partial>

View File

@ -0,0 +1,7 @@
@{
Layout = "_Layout";
}
<ViewStart>
@ViewContext.ExecutingFilePath
@ViewContext.View.Path
</ViewStart>