Layout specification and discovery should follow the same behavior as

partials

Fixes #1047
This commit is contained in:
Pranav K 2014-10-28 11:03:41 -07:00
parent 765f113515
commit 933f7eeb22
27 changed files with 706 additions and 186 deletions

View File

@ -0,0 +1,47 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>@ViewBag.Title - Ma Demande ASP.NET</title>
<link rel="stylesheet" href="~/content/bootstrap.min.css" />
<style>
body {
padding-top: 60px;
}
@@media screen and (max-width: 768px) {
body {
padding-top: 0px;
}
}
</style>
@await RenderSectionAsync("header", required: false)
</head>
<body>
<div class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a href="/">Accueil</a></li>
</ul>
</div>
</div>
</div>
<div class="container body-content">
@RenderBody()
<hr />
<footer>
<p>&copy; @DateTime.Now.Year - Mon Application ASP.NET</p>
</footer>
</div>
@await RenderSectionAsync("footer", required: false)
</body>
</html>

View File

@ -1,3 +1,3 @@
@{ @{
Layout = "/Views/Shared/_Layout.cshtml"; Layout = "_Layout";
} }

View File

@ -0,0 +1,22 @@
// 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.Rendering;
namespace Microsoft.AspNet.Mvc.Razor
{
/// <summary>
/// An <see cref="IViewEngine"/> used to render pages that use the Razor syntax.
/// </summary>
public interface IRazorViewEngine : IViewEngine
{
/// <summary>
/// Finds a <see cref="IRazorPage"/> using the same view discovery semantics used in
/// <see cref="IViewEngine.FindPartialView(ActionContext, string)"/>.
/// </summary>
/// <param name="context">The <see cref="ActionContext"/>.</param>
/// <param name="viewName">The name or full path to the view.</param>
/// <returns>A result representing the result of locating the <see cref="IRazorPage"/>.</returns>
RazorPageResult FindPage(ActionContext context, string page);
}
}

View File

@ -13,9 +13,11 @@ namespace Microsoft.AspNet.Mvc.Razor
/// <summary> /// <summary>
/// Creates a <see cref="RazorView"/> providing it with the <see cref="IRazorPage"/> to execute. /// Creates a <see cref="RazorView"/> providing it with the <see cref="IRazorPage"/> to execute.
/// </summary> /// </summary>
/// <param name="viewEngine">The <see cref="IRazorViewEngine"/> that was used to locate Layout pages
/// that will be part of <paramref name="page"/>'s execution.</param>
/// <param name="page">The <see cref="IRazorPage"/> instance to execute.</param> /// <param name="page">The <see cref="IRazorPage"/> instance to execute.</param>
/// <param name="isPartial">Determines if the view is to be executed as a partial.</param> /// <param name="isPartial">Determines if the view is to be executed as a partial.</param>
/// <returns>A <see cref="IView"/> instance that renders the contents of the <paramref name="page"/></returns> /// <returns>A <see cref="IView"/> instance that renders the contents of the <paramref name="page"/></returns>
IView GetView([NotNull] IRazorPage page, bool isPartial); IView GetView([NotNull] IRazorViewEngine viewEngine, [NotNull] IRazorPage page, bool isPartial);
} }
} }

View File

@ -75,7 +75,7 @@ namespace Microsoft.AspNet.Mvc.Razor
} }
/// <summary> /// <summary>
/// The layout view '{0}' could not be located. /// The layout view '{0}' could not be located. The following locations were searched:{1}
/// </summary> /// </summary>
internal static string LayoutCannotBeLocated internal static string LayoutCannotBeLocated
{ {
@ -83,11 +83,11 @@ namespace Microsoft.AspNet.Mvc.Razor
} }
/// <summary> /// <summary>
/// The layout view '{0}' could not be located. /// The layout view '{0}' could not be located. The following locations were searched:{1}
/// </summary> /// </summary>
internal static string FormatLayoutCannotBeLocated(object p0) internal static string FormatLayoutCannotBeLocated(object p0, object p1)
{ {
return string.Format(CultureInfo.CurrentCulture, GetString("LayoutCannotBeLocated"), p0); return string.Format(CultureInfo.CurrentCulture, GetString("LayoutCannotBeLocated"), p0, p1);
} }
/// <summary> /// <summary>

View File

@ -0,0 +1,54 @@
// 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 System.Collections.Generic;
namespace Microsoft.AspNet.Mvc.Razor
{
/// <summary>
/// Represents the results of locating a <see cref="IRazorPage"/>.
/// </summary>
public class RazorPageResult
{
/// <summary>
/// Initializes a new instance of <see cref="RazorPageResult"/> for a successful discovery.
/// </summary>
/// <param name="name">The name of the page that was located.</param>
/// <param name="page">The located <see cref="IRazorPage"/>.</param>
public RazorPageResult([NotNull] string name, [NotNull] IRazorPage page)
{
Name = name;
Page = page;
}
/// <summary>
/// Initializes a new instance of <see cref="RazorPageResult"/> for an unsuccessful discovery.
/// </summary>
/// <param name="name">The name of the page that was located.</param>
/// <param name="page">The locations that were searched.</param>
public RazorPageResult([NotNull] string name, [NotNull] IEnumerable<string> searchedLocations)
{
Name = name;
SearchedLocations = searchedLocations;
}
/// <summary>
/// Gets the name of the page being located.
/// </summary>
/// <remarks>This property maps to the <c>name</c> parameter of
/// <see cref="IRazorViewEngine.FindPage(ActionContext, string)"/>.</remarks>
public string Name { get; }
/// <summary>
/// Gets the <see cref="IRazorPage"/> if found.
/// </summary>
/// <remarks>This property is <c>null</c> if the page was not found.</remarks>
public IRazorPage Page { get; }
/// <summary>
/// Gets the locations that were searched when <see cref="Page"/> could not be located.
/// </summary>
/// <remarks>This property is <c>null</c> if the page was found.</remarks>
public IEnumerable<string> SearchedLocations { get; }
}
}

View File

@ -15,28 +15,28 @@ namespace Microsoft.AspNet.Mvc.Razor
/// </summary> /// </summary>
public class RazorView : IView public class RazorView : IView
{ {
private readonly IRazorPageFactory _pageFactory; private readonly IRazorViewEngine _viewEngine;
private readonly IRazorPageActivator _pageActivator; private readonly IRazorPageActivator _pageActivator;
private readonly IViewStartProvider _viewStartProvider; private readonly IViewStartProvider _viewStartProvider;
private IPageExecutionListenerFeature _pageExecutionFeature; private IPageExecutionListenerFeature _pageExecutionFeature;
/// <summary> /// <summary>
/// Initializes a new instance of RazorView /// Initializes a new instance of <see cref="RazorView"/>
/// </summary> /// </summary>
/// <param name="pageFactory">The page factory used to instantiate layout and _ViewStart pages.</param> /// <param name="viewEngine">The <see cref="IRazorViewEngine"/> used to locate Layout pages.</param>
/// <param name="pageActivator">The <see cref="IRazorPageActivator"/> used to activate pages.</param> /// <param name="pageActivator">The <see cref="IRazorPageActivator"/> used to activate pages.</param>
/// <param name="viewStartProvider">The <see cref="IViewStartProvider"/> used for discovery of _ViewStart /// <param name="viewStartProvider">The <see cref="IViewStartProvider"/> used for discovery of _ViewStart
/// <param name="razorPage">The <see cref="IRazorPage"/> instance to execute.</param> /// <param name="razorPage">The <see cref="IRazorPage"/> instance to execute.</param>
/// <param name="isPartial">Determines if the view is to be executed as a partial.</param> /// <param name="isPartial">Determines if the view is to be executed as a partial.</param>
/// pages</param> /// pages</param>
public RazorView(IRazorPageFactory pageFactory, public RazorView(IRazorViewEngine viewEngine,
IRazorPageActivator pageActivator, IRazorPageActivator pageActivator,
IViewStartProvider viewStartProvider, IViewStartProvider viewStartProvider,
IRazorPage razorPage, IRazorPage razorPage,
bool isPartial bool isPartial
) )
{ {
_pageFactory = pageFactory; _viewEngine = viewEngine;
_pageActivator = pageActivator; _pageActivator = pageActivator;
_viewStartProvider = viewStartProvider; _viewStartProvider = viewStartProvider;
RazorPage = razorPage; RazorPage = razorPage;
@ -186,12 +186,7 @@ namespace Microsoft.AspNet.Mvc.Razor
throw new InvalidOperationException(message); throw new InvalidOperationException(message);
} }
var layoutPage = _pageFactory.CreateInstance(previousPage.Layout); var layoutPage = GetLayoutPage(context, previousPage.Layout);
if (layoutPage == null)
{
var message = Resources.FormatLayoutCannotBeLocated(previousPage.Layout);
throw new InvalidOperationException(message);
}
// Notify the previous page that any writes that are performed on it are part of sections being written // Notify the previous page that any writes that are performed on it are part of sections being written
// in the layout. // in the layout.
@ -212,5 +207,19 @@ namespace Microsoft.AspNet.Mvc.Razor
await bodyWriter.CopyToAsync(context.Writer); await bodyWriter.CopyToAsync(context.Writer);
} }
} }
private IRazorPage GetLayoutPage(ViewContext context, string layoutPath)
{
var layoutPageResult = _viewEngine.FindPage(context, layoutPath);
if (layoutPageResult.Page == null)
{
var locations = Environment.NewLine +
string.Join(Environment.NewLine, layoutPageResult.SearchedLocations);
throw new InvalidOperationException(Resources.FormatLayoutCannotBeLocated(layoutPath, locations));
}
var layoutPage = layoutPageResult.Page;
return layoutPage;
}
} }
} }

View File

@ -11,9 +11,9 @@ using Microsoft.AspNet.Mvc.Rendering;
namespace Microsoft.AspNet.Mvc.Razor namespace Microsoft.AspNet.Mvc.Razor
{ {
/// <summary> /// <summary>
/// Represents a view engine that is used to render a page that uses the Razor syntax. /// Default implementation of <see cref="IRazorViewEngine"/>.
/// </summary> /// </summary>
public class RazorViewEngine : IViewEngine public class RazorViewEngine : IRazorViewEngine
{ {
private const string ViewExtension = ".cshtml"; private const string ViewExtension = ".cshtml";
internal const string ControllerKey = "controller"; internal const string ControllerKey = "controller";
@ -78,7 +78,8 @@ namespace Microsoft.AspNet.Mvc.Razor
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(viewName)); throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(viewName));
} }
return CreateViewEngineResult(context, viewName, partial: false); var pageResult = GetRazorPageResult(context, viewName);
return CreateViewEngineResult(pageResult, _viewFactory, isPartial: false);
} }
/// <inheritdoc /> /// <inheritdoc />
@ -90,36 +91,49 @@ namespace Microsoft.AspNet.Mvc.Razor
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(partialViewName)); throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(partialViewName));
} }
return CreateViewEngineResult(context, partialViewName, partial: true); var pageResult = GetRazorPageResult(context, partialViewName);
return CreateViewEngineResult(pageResult, _viewFactory, isPartial: true);
} }
private ViewEngineResult CreateViewEngineResult(ActionContext context, /// <inheritdoc />
string viewName, public RazorPageResult FindPage([NotNull] ActionContext context,
bool partial) string pageName)
{ {
var nameRepresentsPath = IsSpecificPath(viewName); if (string.IsNullOrEmpty(pageName))
if (nameRepresentsPath)
{ {
if (viewName.EndsWith(ViewExtension, StringComparison.OrdinalIgnoreCase)) throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(pageName));
}
return GetRazorPageResult(context, pageName);
}
private RazorPageResult GetRazorPageResult(ActionContext context,
string pageName)
{
if (IsApplicationRelativePath(pageName))
{
var applicationRelativePath = pageName;
if (!pageName.EndsWith(ViewExtension, StringComparison.OrdinalIgnoreCase))
{ {
var page = _pageFactory.CreateInstance(viewName); applicationRelativePath += ViewExtension;
if (page != null)
{
return CreateFoundResult(context, page, viewName, partial);
}
} }
return ViewEngineResult.NotFound(viewName, new[] { viewName });
var page = _pageFactory.CreateInstance(applicationRelativePath);
if (page != null)
{
return new RazorPageResult(pageName, page);
}
return new RazorPageResult(pageName, new[] { pageName });
} }
else else
{ {
return LocateViewFromViewLocations(context, viewName, partial); return LocatePageFromViewLocations(context, pageName);
} }
} }
private ViewEngineResult LocateViewFromViewLocations(ActionContext context, private RazorPageResult LocatePageFromViewLocations(ActionContext context,
string viewName, string pageName)
bool partial)
{ {
// Initialize the dictionary for the typical case of having controller and action tokens. // Initialize the dictionary for the typical case of having controller and action tokens.
var routeValues = context.RouteData.Values; var routeValues = context.RouteData.Values;
@ -129,7 +143,7 @@ namespace Microsoft.AspNet.Mvc.Razor
var viewLocations = !string.IsNullOrEmpty(areaName) ? AreaViewLocationFormats : var viewLocations = !string.IsNullOrEmpty(areaName) ? AreaViewLocationFormats :
ViewLocationFormats; ViewLocationFormats;
var expanderContext = new ViewLocationExpanderContext(context, viewName); var expanderContext = new ViewLocationExpanderContext(context, pageName);
if (_viewLocationExpanders.Count > 0) if (_viewLocationExpanders.Count > 0)
{ {
expanderContext.Values = new Dictionary<string, string>(StringComparer.Ordinal); expanderContext.Values = new Dictionary<string, string>(StringComparer.Ordinal);
@ -142,15 +156,15 @@ namespace Microsoft.AspNet.Mvc.Razor
} }
// 2. With the values that we've accumumlated so far, check if we have a cached result. // 2. With the values that we've accumumlated so far, check if we have a cached result.
var viewLocation = _viewLocationCache.Get(expanderContext); var pageLocation = _viewLocationCache.Get(expanderContext);
if (!string.IsNullOrEmpty(viewLocation)) if (!string.IsNullOrEmpty(pageLocation))
{ {
var page = _pageFactory.CreateInstance(viewLocation); var page = _pageFactory.CreateInstance(pageLocation);
if (page != null) if (page != null)
{ {
// 2a. We found a IRazorPage at the cached location. // 2a. We found a IRazorPage at the cached location.
return CreateFoundResult(context, page, viewName, partial); return new RazorPageResult(pageName, page);
} }
} }
@ -168,7 +182,7 @@ namespace Microsoft.AspNet.Mvc.Razor
{ {
var transformedPath = string.Format(CultureInfo.InvariantCulture, var transformedPath = string.Format(CultureInfo.InvariantCulture,
path, path,
viewName, pageName,
controllerName, controllerName,
areaName); areaName);
var page = _pageFactory.CreateInstance(transformedPath); var page = _pageFactory.CreateInstance(transformedPath);
@ -176,32 +190,30 @@ namespace Microsoft.AspNet.Mvc.Razor
{ {
// 3a. We found a page. Cache the set of values that produced it and return a found result. // 3a. We found a page. Cache the set of values that produced it and return a found result.
_viewLocationCache.Set(expanderContext, transformedPath); _viewLocationCache.Set(expanderContext, transformedPath);
return CreateFoundResult(context, page, transformedPath, partial); return new RazorPageResult(pageName, page);
} }
searchedLocations.Add(transformedPath); searchedLocations.Add(transformedPath);
} }
// 3b. We did not find a page for any of the paths. // 3b. We did not find a page for any of the paths.
return ViewEngineResult.NotFound(viewName, searchedLocations); return new RazorPageResult(pageName, searchedLocations);
} }
private ViewEngineResult CreateFoundResult(ActionContext actionContext, private ViewEngineResult CreateViewEngineResult(RazorPageResult result,
IRazorPage page, IRazorViewFactory razorViewFactory,
string viewName, bool isPartial)
bool partial)
{ {
// A single request could result in creating multiple IRazorView instances (for partials, view components) if (result.SearchedLocations != null)
// and might store state. We'll use the service container to create new instances as we require. {
return ViewEngineResult.NotFound(result.Name, result.SearchedLocations);
}
var services = actionContext.HttpContext.RequestServices; var view = razorViewFactory.GetView(this, result.Page, isPartial);
return ViewEngineResult.Found(result.Name, view);
var view = _viewFactory.GetView(page, partial);
return ViewEngineResult.Found(viewName, view);
} }
private static bool IsSpecificPath(string name) private static bool IsApplicationRelativePath(string name)
{ {
Debug.Assert(!string.IsNullOrEmpty(name)); Debug.Assert(!string.IsNullOrEmpty(name));
return name[0] == '~' || name[0] == '/'; return name[0] == '~' || name[0] == '/';

View File

@ -12,29 +12,27 @@ namespace Microsoft.AspNet.Mvc.Razor
public class RazorViewFactory : IRazorViewFactory public class RazorViewFactory : IRazorViewFactory
{ {
private readonly IRazorPageActivator _pageActivator; private readonly IRazorPageActivator _pageActivator;
private readonly IRazorPageFactory _pageFactory;
private readonly IViewStartProvider _viewStartProvider; private readonly IViewStartProvider _viewStartProvider;
/// <summary> /// <summary>
/// Initializes a new instance of RazorViewFactory /// Initializes a new instance of RazorViewFactory
/// </summary> /// </summary>
/// <param name="pageFactory">The page factory used to instantiate layout and _ViewStart pages.</param>
/// <param name="pageActivator">The <see cref="IRazorPageActivator"/> used to activate pages.</param> /// <param name="pageActivator">The <see cref="IRazorPageActivator"/> used to activate pages.</param>
/// <param name="viewStartProvider">The <see cref="IViewStartProvider"/> used for discovery of _ViewStart /// <param name="viewStartProvider">The <see cref="IViewStartProvider"/> used for discovery of _ViewStart
/// pages</param> /// pages</param>
public RazorViewFactory(IRazorPageFactory pageFactory, public RazorViewFactory(IRazorPageActivator pageActivator,
IRazorPageActivator pageActivator,
IViewStartProvider viewStartProvider) IViewStartProvider viewStartProvider)
{ {
_pageFactory = pageFactory;
_pageActivator = pageActivator; _pageActivator = pageActivator;
_viewStartProvider = viewStartProvider; _viewStartProvider = viewStartProvider;
} }
/// <inheritdoc /> /// <inheritdoc />
public IView GetView([NotNull] IRazorPage page, bool isPartial) public IView GetView([NotNull] IRazorViewEngine viewEngine,
[NotNull] IRazorPage page,
bool isPartial)
{ {
var razorView = new RazorView(_pageFactory, _pageActivator, _viewStartProvider, page, isPartial); var razorView = new RazorView(viewEngine, _pageActivator, _viewStartProvider, page, isPartial);
return razorView; return razorView;
} }
} }

View File

@ -130,7 +130,7 @@
<value>The {0} returned by '{1}' must be an instance of '{2}'.</value> <value>The {0} returned by '{1}' must be an instance of '{2}'.</value>
</data> </data>
<data name="LayoutCannotBeLocated" xml:space="preserve"> <data name="LayoutCannotBeLocated" xml:space="preserve">
<value>The layout view '{0}' could not be located.</value> <value>The layout view '{0}' could not be located. The following locations were searched:{1}</value>
</data> </data>
<data name="LayoutCannotBeRendered" xml:space="preserve"> <data name="LayoutCannotBeRendered" xml:space="preserve">
<value>A layout page cannot be rendered after '{0}' has been invoked.</value> <value>A layout page cannot be rendered after '{0}' has been invoked.</value>

View File

@ -0,0 +1,102 @@
// 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 System;
using System.Threading.Tasks;
using Microsoft.AspNet.Builder;
using Microsoft.AspNet.TestHost;
using RazorWebSite;
using Xunit;
namespace Microsoft.AspNet.Mvc.FunctionalTests
{
public class RazorViewLocationSpecificationTest
{
private const string BaseUrl = "http://localhost/ViewNameSpecification_Home/";
private readonly IServiceProvider _provider = TestHelper.CreateServices("RazorWebSite");
private readonly Action<IApplicationBuilder> _app = new Startup().Configure;
[Theory]
[InlineData("LayoutSpecifiedWithPartialPathInViewStart")]
[InlineData("LayoutSpecifiedWithPartialPathInViewStart_ForViewSpecifiedWithAppRelativePath")]
[InlineData("LayoutSpecifiedWithPartialPathInViewStart_ForViewSpecifiedWithPartialName")]
[InlineData("LayoutSpecifiedWithPartialPathInViewStart_ForViewSpecifiedWithAppRelativePathWithExtension")]
public async Task PartialLayoutPaths_SpecifiedInViewStarts_GetResolvedByViewEngine(string action)
{
var expected =
@"<layout>
_ViewStart that specifies partial Layout
</layout>";
var server = TestServer.Create(_provider, _app);
var client = server.CreateClient();
// Act
var body = await client.GetStringAsync(BaseUrl + action);
// Assert
Assert.Equal(expected, body.Trim());
}
[Theory]
[InlineData("LayoutSpecifiedWithPartialPathInPage")]
[InlineData("LayoutSpecifiedWithPartialPathInPageWithPartialPath")]
[InlineData("LayoutSpecifiedWithPartialPathInPageWithAppRelativePath")]
[InlineData("LayoutSpecifiedWithPartialPathInPageWithAppRelativePathWithExtension")]
public async Task PartialLayoutPaths_SpecifiedInPage_GetResolvedByViewEngine(string actionName)
{
var expected =
@"<non-shared>
Layout specified in page
</non-shared>";
var server = TestServer.Create(_provider, _app);
var client = server.CreateClient();
// Act
var body = await client.GetStringAsync(BaseUrl + actionName);
// Assert
Assert.Equal(expected, body.Trim());
}
[Theory]
[InlineData("LayoutSpecifiedWithNonPartialPath")]
[InlineData("LayoutSpecifiedWithNonPartialPathWithExtension")]
public async Task NonPartialLayoutPaths_GetResolvedByViewEngine(string actionName)
{
var expected =
@"<non-shared>
Page With Non Partial Layout
</non-shared>";
var server = TestServer.Create(_provider, _app);
var client = server.CreateClient();
// Act
var body = await client.GetStringAsync(BaseUrl + actionName);
// Assert
Assert.Equal(expected, body.Trim());
}
[Theory]
[InlineData("ViewWithPartial_SpecifiedWithPartialName")]
[InlineData("ViewWithPartial_SpecifiedWithAbsoluteName")]
[InlineData("ViewWithPartial_SpecifiedWithAbsoluteNameAndExtension")]
public async Task PartialsCanBeSpecifiedWithPartialPath(string actionName)
{
var expected =
@"<layout>
Non Shared Partial
</layout>";
var server = TestServer.Create(_provider, _app);
var client = server.CreateClient();
// Act
var body = await client.GetStringAsync(BaseUrl + actionName);
// Assert
Assert.Equal(expected, body.Trim());
}
}
}

View File

@ -216,6 +216,43 @@ component-content";
Assert.Equal(expected, body.Trim()); Assert.Equal(expected, body.Trim());
} }
public static IEnumerable<object[]> RazorViewEngine_UsesExpandersForLayoutsData
{
get
{
var expected1 =
@"<language-layout>
View With Layout
</language-layout>";
yield return new[] { "gb", expected1 };
yield return new[] { "na", expected1 };
var expected2 =
@"<fr-language-layout>
View With Layout
</fr-language-layout>";
yield return new[] { "fr", expected2 };
}
}
[Theory]
[MemberData(nameof(RazorViewEngine_UsesExpandersForLayoutsData))]
public async Task RazorViewEngine_UsesExpandersForLayouts(string value, string expected)
{
// Arrange
var server = TestServer.Create(_provider, _app);
var client = server.CreateClient();
// Act
var body = await client.GetStringAsync("http://localhost/TemplateExpander/ViewWithLayout?language-expander-value=" +
value);
// Assert
Assert.Equal(expected, body.Trim());
}
// Inheritance of chunks in _ViewStart is affected by paths being app-relative and not absolute. // Inheritance of chunks in _ViewStart is affected by paths being app-relative and not absolute.
// This change ensures that _ViewStart files correctly inherits directives from parent _ViewStarts // This change ensures that _ViewStart files correctly inherits directives from parent _ViewStarts
// which guarantees that paths flow correctly through MvcRazorHost. // which guarantees that paths flow correctly through MvcRazorHost.

View File

@ -1,7 +1,6 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. // 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. // 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.Collections.Generic;
using System.Linq; using System.Linq;
using Microsoft.AspNet.Mvc.Razor.OptionDescriptors; using Microsoft.AspNet.Mvc.Razor.OptionDescriptors;
@ -38,6 +37,33 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
} }
} }
public static IEnumerable<object[]> ViewLocationExpanderTestData
{
get
{
yield return new object[]
{
_controllerTestContext,
new[]
{
"/Views/{1}/{0}.cshtml",
"/Views/Shared/{0}.cshtml"
}
};
yield return new object[]
{
_areaTestContext,
new[]
{
"/Areas/{2}/Views/{1}/{0}.cshtml",
"/Areas/{2}/Views/Shared/{0}.cshtml",
"/Views/Shared/{0}.cshtml"
}
};
}
}
[Theory] [Theory]
[InlineData(null)] [InlineData(null)]
[InlineData("")] [InlineData("")]
@ -90,11 +116,13 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
var pageFactory = new Mock<IRazorPageFactory>(); var pageFactory = new Mock<IRazorPageFactory>();
var viewFactory = new Mock<IRazorViewFactory>(); var viewFactory = new Mock<IRazorViewFactory>();
var page = Mock.Of<IRazorPage>(); var page = Mock.Of<IRazorPage>();
var view = Mock.Of<IView>();
pageFactory.Setup(p => p.CreateInstance(It.IsAny<string>())) pageFactory.Setup(p => p.CreateInstance(It.IsAny<string>()))
.Returns(Mock.Of<IRazorPage>()); .Returns(Mock.Of<IRazorPage>());
viewFactory.Setup(p => p.GetView(It.IsAny<IRazorPage>(), It.IsAny<bool>())) viewFactory.Setup(p => p.GetView(It.IsAny<IRazorViewEngine>(), It.IsAny<IRazorPage>(), true))
.Returns(Mock.Of<IView>()).Verifiable(); .Returns(view)
.Verifiable();
var viewEngine = CreateViewEngine(pageFactory.Object, viewFactory.Object); var viewEngine = CreateViewEngine(pageFactory.Object, viewFactory.Object);
var context = GetActionContext(_controllerTestContext); var context = GetActionContext(_controllerTestContext);
@ -104,8 +132,8 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
// Assert // Assert
Assert.True(result.Success); Assert.True(result.Success);
Assert.IsAssignableFrom<IView>(result.View); Assert.Same(view, result.View);
Assert.Equal("/Views/bar/test-view.cshtml", result.ViewName); Assert.Equal("test-view", result.ViewName);
viewFactory.Verify(); viewFactory.Verify();
} }
@ -238,11 +266,13 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
var pageFactory = new Mock<IRazorPageFactory>(); var pageFactory = new Mock<IRazorPageFactory>();
var viewFactory = new Mock<IRazorViewFactory>(); var viewFactory = new Mock<IRazorViewFactory>();
var page = Mock.Of<IRazorPage>(); var page = Mock.Of<IRazorPage>();
var view = Mock.Of<IView>();
pageFactory.Setup(p => p.CreateInstance(It.IsAny<string>())) pageFactory.Setup(p => p.CreateInstance(It.IsAny<string>()))
.Returns(Mock.Of<IRazorPage>()); .Returns(Mock.Of<IRazorPage>());
viewFactory.Setup(p => p.GetView(It.IsAny<IRazorPage>(), It.IsAny<bool>())) viewFactory.Setup(p => p.GetView(It.IsAny<IRazorViewEngine>(), It.IsAny<IRazorPage>(), false))
.Returns(Mock.Of<IView>()).Verifiable(); .Returns(view)
.Verifiable();
var viewEngine = CreateViewEngine(pageFactory.Object, viewFactory.Object); var viewEngine = CreateViewEngine(pageFactory.Object, viewFactory.Object);
var context = GetActionContext(_controllerTestContext); var context = GetActionContext(_controllerTestContext);
@ -252,8 +282,8 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
// Assert // Assert
Assert.True(result.Success); Assert.True(result.Success);
Assert.IsAssignableFrom<IView>(result.View); Assert.Same(view, result.View);
Assert.Equal("/Views/bar/test-view.cshtml", result.ViewName); Assert.Equal("test-view", result.ViewName);
viewFactory.Verify(); viewFactory.Verify();
} }
@ -303,35 +333,8 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
pageFactory.Verify(); pageFactory.Verify();
} }
public static IEnumerable<object[]> FindView_UsesViewLocationExpandersToLocateViewsData
{
get
{
yield return new object[]
{
_controllerTestContext,
new[]
{
"/Views/{1}/{0}.cshtml",
"/Views/Shared/{0}.cshtml"
}
};
yield return new object[]
{
_areaTestContext,
new[]
{
"/Areas/{2}/Views/{1}/{0}.cshtml",
"/Areas/{2}/Views/Shared/{0}.cshtml",
"/Views/Shared/{0}.cshtml"
}
};
}
}
[Theory] [Theory]
[MemberData(nameof(FindView_UsesViewLocationExpandersToLocateViewsData))] [MemberData(nameof(ViewLocationExpanderTestData))]
public void FindView_UsesViewLocationExpandersToLocateViews(IDictionary<string, object> routeValues, public void FindView_UsesViewLocationExpandersToLocateViews(IDictionary<string, object> routeValues,
IEnumerable<string> expectedSeeds) IEnumerable<string> expectedSeeds)
{ {
@ -342,7 +345,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
.Verifiable(); .Verifiable();
var viewFactory = new Mock<IRazorViewFactory>(); var viewFactory = new Mock<IRazorViewFactory>();
viewFactory.Setup(p => p.GetView(It.IsAny<IRazorPage>(), It.IsAny<bool>())) viewFactory.Setup(p => p.GetView(It.IsAny<IRazorViewEngine>(), It.IsAny<IRazorPage>(), It.IsAny<bool>()))
.Returns(Mock.Of<IView>()); .Returns(Mock.Of<IView>());
var expander1Result = new[] { "some-seed" }; var expander1Result = new[] { "some-seed" };
@ -376,7 +379,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
var viewEngine = CreateViewEngine(pageFactory.Object, viewFactory.Object, var viewEngine = CreateViewEngine(pageFactory.Object, viewFactory.Object,
new[] { expander1.Object, expander2.Object }); new[] { expander1.Object, expander2.Object });
var context = GetActionContext(routeValues, viewFactory.Object); var context = GetActionContext(routeValues);
// Act // Act
var result = viewEngine.FindView(context, "test-view"); var result = viewEngine.FindView(context, "test-view");
@ -401,17 +404,17 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
.Verifiable(); .Verifiable();
var viewFactory = new Mock<IRazorViewFactory>(); var viewFactory = new Mock<IRazorViewFactory>();
viewFactory.Setup(p => p.GetView(It.IsAny<IRazorPage>(), It.IsAny<bool>())) viewFactory.Setup(p => p.GetView(It.IsAny<IRazorViewEngine>(), It.IsAny<IRazorPage>(), false))
.Returns(Mock.Of<IView>()); .Returns(Mock.Of<IView>());
var cache = GetViewLocationCache(); var cache = GetViewLocationCache();
var cacheMock = Mock.Get<IViewLocationCache>(cache); var cacheMock = Mock.Get(cache);
cacheMock.Setup(c => c.Set(It.IsAny<ViewLocationExpanderContext>(), "/Views/Shared/baz.cshtml")) cacheMock.Setup(c => c.Set(It.IsAny<ViewLocationExpanderContext>(), "/Views/Shared/baz.cshtml"))
.Verifiable(); .Verifiable();
var viewEngine = CreateViewEngine(pageFactory.Object, viewFactory.Object, cache: cache); var viewEngine = CreateViewEngine(pageFactory.Object, viewFactory.Object, cache: cache);
var context = GetActionContext(_controllerTestContext, viewFactory.Object); var context = GetActionContext(_controllerTestContext);
// Act // Act
var result = viewEngine.FindView(context, "baz"); var result = viewEngine.FindView(context, "baz");
@ -432,7 +435,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
.Verifiable(); .Verifiable();
var viewFactory = new Mock<IRazorViewFactory>(); var viewFactory = new Mock<IRazorViewFactory>();
viewFactory.Setup(p => p.GetView(It.IsAny<IRazorPage>(), It.IsAny<bool>())) viewFactory.Setup(p => p.GetView(It.IsAny<IRazorViewEngine>(), It.IsAny<IRazorPage>(), false))
.Returns(Mock.Of<IView>()); .Returns(Mock.Of<IView>());
var expander = new Mock<IViewLocationExpander>(MockBehavior.Strict); var expander = new Mock<IViewLocationExpander>(MockBehavior.Strict);
@ -472,7 +475,8 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
.Verifiable(); .Verifiable();
var viewFactory = new Mock<IRazorViewFactory>(); var viewFactory = new Mock<IRazorViewFactory>();
viewFactory.Setup(p => p.GetView(It.IsAny<IRazorPage>(), It.IsAny<bool>())).Returns(Mock.Of<IView>()); viewFactory.Setup(p => p.GetView(It.IsAny<IRazorViewEngine>(), It.IsAny<IRazorPage>(), false))
.Returns(Mock.Of<IView>());
var cacheMock = new Mock<IViewLocationCache>(); var cacheMock = new Mock<IViewLocationCache>();
cacheMock.Setup(c => c.Get(It.IsAny<ViewLocationExpanderContext>())) cacheMock.Setup(c => c.Get(It.IsAny<ViewLocationExpanderContext>()))
@ -491,7 +495,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
viewFactory.Object, viewFactory.Object,
expanders: new[] { expander.Object }, expanders: new[] { expander.Object },
cache: cacheMock.Object); cache: cacheMock.Object);
var context = GetActionContext(_controllerTestContext, viewFactory.Object); var context = GetActionContext(_controllerTestContext);
// Act // Act
var result = viewEngine.FindView(context, "baz"); var result = viewEngine.FindView(context, "baz");
@ -503,10 +507,97 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
expander.Verify(); expander.Verify();
} }
private IViewEngine CreateViewEngine(IRazorPageFactory pageFactory = null, [Theory]
IRazorViewFactory viewFactory = null, [InlineData(null)]
IEnumerable<IViewLocationExpander> expanders = null, [InlineData("")]
IViewLocationCache cache = null) public void FindPage_ThrowsIfNameIsNullOrEmpty(string pageName)
{
// Arrange
var viewEngine = CreateViewEngine();
var context = GetActionContext(_controllerTestContext);
// Act & Assert
ExceptionAssert.ThrowsArgumentNullOrEmpty(() => viewEngine.FindPage(context, pageName),
"pageName");
}
[Theory]
[MemberData(nameof(ViewLocationExpanderTestData))]
public void FindPage_UsesViewLocationExpander_ToExpandPaths(IDictionary<string, object> routeValues,
IEnumerable<string> expectedSeeds)
{
// Arrange
var page = Mock.Of<IRazorPage>();
var pageFactory = new Mock<IRazorPageFactory>();
pageFactory.Setup(p => p.CreateInstance("expanded-path/bar-layout"))
.Returns(page)
.Verifiable();
var viewFactory = new Mock<IRazorViewFactory>(MockBehavior.Strict);
var expander = new Mock<IViewLocationExpander>();
expander.Setup(e => e.PopulateValues(It.IsAny<ViewLocationExpanderContext>()))
.Callback((ViewLocationExpanderContext c) =>
{
Assert.NotNull(c.ActionContext);
c.Values["expander-key"] = expander.ToString();
})
.Verifiable();
expander.Setup(e => e.ExpandViewLocations(It.IsAny<ViewLocationExpanderContext>(),
It.IsAny<IEnumerable<string>>()))
.Returns((ViewLocationExpanderContext c, IEnumerable<string> seeds) =>
{
Assert.NotNull(c.ActionContext);
Assert.Equal(expectedSeeds, seeds);
Assert.Equal(expander.ToString(), c.Values["expander-key"]);
return new[] { "expanded-path/bar-{0}" };
})
.Verifiable();
var viewEngine = CreateViewEngine(pageFactory.Object, viewFactory.Object,
new[] { expander.Object });
var context = GetActionContext(routeValues);
// Act
var result = viewEngine.FindPage(context, "layout");
// Assert
Assert.Equal("layout", result.Name);
Assert.Same(page, result.Page);
Assert.Null(result.SearchedLocations);
pageFactory.Verify();
expander.Verify();
}
[Fact]
public void FindPage_ReturnsSearchedLocationsIfPageCannotBeFound()
{
// Arrange
var expected = new[]
{
"/Views/bar/layout.cshtml",
"/Views/Shared/layout.cshtml",
};
var page = Mock.Of<IRazorPage>();
var viewEngine = CreateViewEngine();
var context = GetActionContext(_controllerTestContext);
// Act
var result = viewEngine.FindPage(context, "layout");
// Assert
Assert.Equal("layout", result.Name);
Assert.Null(result.Page);
Assert.Equal(expected, result.SearchedLocations);
}
private RazorViewEngine CreateViewEngine(IRazorPageFactory pageFactory = null,
IRazorViewFactory viewFactory = null,
IEnumerable<IViewLocationExpander> expanders = null,
IViewLocationCache cache = null)
{ {
pageFactory = pageFactory ?? Mock.Of<IRazorPageFactory>(); pageFactory = pageFactory ?? Mock.Of<IRazorPageFactory>();
viewFactory = viewFactory ?? Mock.Of<IRazorViewFactory>(); viewFactory = viewFactory ?? Mock.Of<IRazorViewFactory>();
@ -541,14 +632,9 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
return cacheMock.Object; return cacheMock.Object;
} }
private static ActionContext GetActionContext(IDictionary<string, object> routeValues, private static ActionContext GetActionContext(IDictionary<string, object> routeValues)
IRazorViewFactory razorViewFactory = null)
{ {
var httpContext = new DefaultHttpContext(); var httpContext = new DefaultHttpContext();
var serviceProvider = new Mock<IServiceProvider>();
httpContext.RequestServices = serviceProvider.Object;
var routeData = new RouteData(); var routeData = new RouteData();
foreach (var kvp in routeValues) foreach (var kvp in routeValues)
{ {

View File

@ -4,11 +4,10 @@
using Moq; using Moq;
using Xunit; using Xunit;
namespace Microsoft.AspNet.Mvc.Razor.Test namespace Microsoft.AspNet.Mvc.Razor
{ {
public class RazorViewFactoryTest public class RazorViewFactoryTest
{ {
[Theory] [Theory]
[InlineData(false)] [InlineData(false)]
[InlineData(true)] [InlineData(true)]
@ -16,15 +15,17 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
{ {
// Arrange // Arrange
var factory = new RazorViewFactory( var factory = new RazorViewFactory(
Mock.Of<IRazorPageFactory>(),
Mock.Of<IRazorPageActivator>(), Mock.Of<IRazorPageActivator>(),
Mock.Of<IViewStartProvider>()); Mock.Of<IViewStartProvider>());
var page = Mock.Of<IRazorPage>();
var viewEngine = Mock.Of<IRazorViewEngine>();
// Act // Act
var view = factory.GetView(Mock.Of<IRazorPage>(), isPartial); var view = factory.GetView(viewEngine, page, isPartial);
// Assert // Assert
var razorView = Assert.IsType<RazorView>(view); var razorView = Assert.IsType<RazorView>(view);
Assert.Same(page, razorView.RazorPage);
Assert.Equal(razorView.IsPartial, isPartial); Assert.Equal(razorView.IsPartial, isPartial);
} }
@ -33,18 +34,19 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
{ {
// Arrange // Arrange
var factory = new RazorViewFactory( var factory = new RazorViewFactory(
Mock.Of<IRazorPageFactory>(),
Mock.Of<IRazorPageActivator>(), Mock.Of<IRazorPageActivator>(),
Mock.Of<IViewStartProvider>()); Mock.Of<IViewStartProvider>());
var page = Mock.Of<IRazorPage>(); var page = Mock.Of<IRazorPage>();
var viewEngine = Mock.Of<IRazorViewEngine>();
// Act // Act
var view = factory.GetView(page, isPartial: false) as RazorView; var view = factory.GetView(viewEngine, page, isPartial: false);
// Assert // Assert
Assert.NotNull(view); Assert.NotNull(view);
Assert.Same(view.RazorPage, page); var razorView = Assert.IsType<RazorView>(view);
Assert.Same(razorView.RazorPage, page);
} }
} }
} }

View File

@ -28,7 +28,7 @@ namespace Microsoft.AspNet.Mvc.Razor
actual = v.Output; actual = v.Output;
v.Write("Hello world"); v.Write("Hello world");
}); });
var view = new RazorView(Mock.Of<IRazorPageFactory>(), var view = new RazorView(Mock.Of<IRazorViewEngine>(),
Mock.Of<IRazorPageActivator>(), Mock.Of<IRazorPageActivator>(),
CreateViewStartProvider(), CreateViewStartProvider(),
page, page,
@ -55,7 +55,7 @@ namespace Microsoft.AspNet.Mvc.Razor
Assert.Same(viewData, v.ViewContext.ViewData); Assert.Same(viewData, v.ViewContext.ViewData);
}); });
var activator = new Mock<IRazorPageActivator>(); var activator = new Mock<IRazorPageActivator>();
var view = new RazorView(Mock.Of<IRazorPageFactory>(), var view = new RazorView(Mock.Of<IRazorViewEngine>(),
activator.Object, activator.Object,
CreateViewStartProvider(), CreateViewStartProvider(),
page, page,
@ -87,7 +87,7 @@ namespace Microsoft.AspNet.Mvc.Razor
var activator = new Mock<IRazorPageActivator>(); var activator = new Mock<IRazorPageActivator>();
activator.Setup(a => a.Activate(page, It.IsAny<ViewContext>())) activator.Setup(a => a.Activate(page, It.IsAny<ViewContext>()))
.Verifiable(); .Verifiable();
var view = new RazorView(Mock.Of<IRazorPageFactory>(), var view = new RazorView(Mock.Of<IRazorViewEngine>(),
activator.Object, activator.Object,
CreateViewStartProvider(), CreateViewStartProvider(),
page, page,
@ -108,9 +108,9 @@ namespace Microsoft.AspNet.Mvc.Razor
{ {
v.Layout = LayoutPath; v.Layout = LayoutPath;
}); });
var pageFactory = new Mock<IRazorPageFactory>(); var viewEngine = new Mock<IRazorViewEngine>();
var viewStartProvider = CreateViewStartProvider(); var viewStartProvider = CreateViewStartProvider();
var view = new RazorView(pageFactory.Object, var view = new RazorView(viewEngine.Object,
Mock.Of<IRazorPageActivator>(), Mock.Of<IRazorPageActivator>(),
viewStartProvider, viewStartProvider,
page, page,
@ -121,7 +121,8 @@ namespace Microsoft.AspNet.Mvc.Razor
await view.RenderAsync(viewContext); await view.RenderAsync(viewContext);
// Assert // Assert
pageFactory.Verify(v => v.CreateInstance(It.IsAny<string>()), Times.Never()); viewEngine.Verify(v => v.FindPage(It.IsAny<ActionContext>(), It.IsAny<string>()),
Times.Never());
Mock.Get(viewStartProvider) Mock.Get(viewStartProvider)
.Verify(v => v.GetViewStartPages(It.IsAny<string>()), Times.Never()); .Verify(v => v.GetViewStartPages(It.IsAny<string>()), Times.Never());
} }
@ -135,7 +136,7 @@ namespace Microsoft.AspNet.Mvc.Razor
{ {
actual = v.Output; actual = v.Output;
}); });
var view = new RazorView(Mock.Of<IRazorPageFactory>(), var view = new RazorView(Mock.Of<IRazorViewEngine>(),
Mock.Of<IRazorPageActivator>(), Mock.Of<IRazorPageActivator>(),
CreateViewStartProvider(), CreateViewStartProvider(),
page, page,
@ -159,7 +160,7 @@ namespace Microsoft.AspNet.Mvc.Razor
{ {
v.WriteLiteral("Hello world"); v.WriteLiteral("Hello world");
}); });
var view = new RazorView(Mock.Of<IRazorPageFactory>(), var view = new RazorView(Mock.Of<IRazorViewEngine>(),
Mock.Of<IRazorPageActivator>(), Mock.Of<IRazorPageActivator>(),
CreateViewStartProvider(), CreateViewStartProvider(),
page, page,
@ -185,7 +186,7 @@ namespace Microsoft.AspNet.Mvc.Razor
var activator = new Mock<IRazorPageActivator>(); var activator = new Mock<IRazorPageActivator>();
activator.Setup(a => a.Activate(page, It.IsAny<ViewContext>())) activator.Setup(a => a.Activate(page, It.IsAny<ViewContext>()))
.Verifiable(); .Verifiable();
var view = new RazorView(Mock.Of<IRazorPageFactory>(), var view = new RazorView(Mock.Of<IRazorViewEngine>(),
activator.Object, activator.Object,
CreateViewStartProvider(), CreateViewStartProvider(),
page, page,
@ -227,7 +228,7 @@ namespace Microsoft.AspNet.Mvc.Razor
.Verifiable(); .Verifiable();
activator.Setup(a => a.Activate(page, It.IsAny<ViewContext>())) activator.Setup(a => a.Activate(page, It.IsAny<ViewContext>()))
.Verifiable(); .Verifiable();
var view = new RazorView(Mock.Of<IRazorPageFactory>(), var view = new RazorView(Mock.Of<IRazorViewEngine>(),
activator.Object, activator.Object,
CreateViewStartProvider(viewStart1, viewStart2), CreateViewStartProvider(viewStart1, viewStart2),
page, page,
@ -241,6 +242,42 @@ namespace Microsoft.AspNet.Mvc.Razor
activator.Verify(); activator.Verify();
} }
[Fact]
public async Task RenderAsync_ThrowsIfLayoutPageCannotBeFound()
{
// Arrange
var expected = string.Join(Environment.NewLine,
"The layout view 'Does-Not-Exist-Layout' could not be located. " +
"The following locations were searched:",
"path1",
"path2");
var layoutPath = "Does-Not-Exist-Layout";
var page = new TestableRazorPage(v =>
{
v.Layout = layoutPath;
});
var viewEngine = new Mock<IRazorViewEngine>();
var activator = new Mock<IRazorPageActivator>();
var view = new RazorView(viewEngine.Object,
Mock.Of<IRazorPageActivator>(),
Mock.Of<IViewStartProvider>(),
page,
isPartial: false);
var viewContext = CreateViewContext(view);
viewEngine.Setup(v => v.FindPage(viewContext, layoutPath))
.Returns(new RazorPageResult(layoutPath, new[] { "path1", "path2" }))
.Verifiable();
// Act
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => view.RenderAsync(viewContext));
// Assert
Assert.Equal(expected, ex.Message);
viewEngine.Verify();
}
[Fact] [Fact]
public async Task RenderAsync_ExecutesLayoutPages() public async Task RenderAsync_ExecutesLayoutPages()
{ {
@ -278,16 +315,17 @@ foot-content";
.Verifiable(); .Verifiable();
activator.Setup(a => a.Activate(layout, It.IsAny<ViewContext>())) activator.Setup(a => a.Activate(layout, It.IsAny<ViewContext>()))
.Verifiable(); .Verifiable();
var pageFactory = new Mock<IRazorPageFactory>(); var viewEngine = new Mock<IRazorViewEngine>();
pageFactory.Setup(p => p.CreateInstance(LayoutPath))
.Returns(layout);
var view = new RazorView(pageFactory.Object, var view = new RazorView(viewEngine.Object,
activator.Object, activator.Object,
CreateViewStartProvider(), CreateViewStartProvider(),
page, page,
isPartial: false); isPartial: false);
var viewContext = CreateViewContext(view); var viewContext = CreateViewContext(view);
viewEngine.Setup(p => p.FindPage(viewContext, LayoutPath))
.Returns(new RazorPageResult(LayoutPath, layout))
.Verifiable();
// Act // Act
await view.RenderAsync(viewContext); await view.RenderAsync(viewContext);
@ -296,6 +334,7 @@ foot-content";
// Verify the activator was invoked for the primary page and layout page. // Verify the activator was invoked for the primary page and layout page.
activator.Verify(); activator.Verify();
Assert.Equal(expected, viewContext.Writer.ToString()); Assert.Equal(expected, viewContext.Writer.ToString());
viewEngine.Verify();
} }
[Fact] [Fact]
@ -312,11 +351,11 @@ foot-content";
{ {
v.RenderBodyPublic(); v.RenderBodyPublic();
}); });
var pageFactory = new Mock<IRazorPageFactory>(); var viewEngine = new Mock<IRazorViewEngine>();
pageFactory.Setup(p => p.CreateInstance(LayoutPath)) viewEngine.Setup(p => p.FindPage(It.IsAny<ActionContext>(), LayoutPath))
.Returns(layout); .Returns(new RazorPageResult(LayoutPath, layout));
var view = new RazorView(pageFactory.Object, var view = new RazorView(viewEngine.Object,
Mock.Of<IRazorPageActivator>(), Mock.Of<IRazorPageActivator>(),
CreateViewStartProvider(), CreateViewStartProvider(),
page, page,
@ -339,11 +378,11 @@ foot-content";
var layout = new TestableRazorPage(v => var layout = new TestableRazorPage(v =>
{ {
}); });
var pageFactory = new Mock<IRazorPageFactory>(); var viewEngine = new Mock<IRazorViewEngine>();
pageFactory.Setup(p => p.CreateInstance(LayoutPath)) viewEngine.Setup(p => p.FindPage(It.IsAny<ActionContext>(), LayoutPath))
.Returns(layout); .Returns(new RazorPageResult(LayoutPath, layout));
var view = new RazorView(pageFactory.Object, var view = new RazorView(viewEngine.Object,
Mock.Of<IRazorPageActivator>(), Mock.Of<IRazorPageActivator>(),
CreateViewStartProvider(), CreateViewStartProvider(),
page, page,
@ -389,13 +428,13 @@ body-content";
v.Write(v.RenderSection("bar")); v.Write(v.RenderSection("bar"));
v.RenderBodyPublic(); v.RenderBodyPublic();
}); });
var pageFactory = new Mock<IRazorPageFactory>(); var viewEngine = new Mock<IRazorViewEngine>();
pageFactory.Setup(p => p.CreateInstance("~/Shared/Layout1.cshtml")) viewEngine.Setup(p => p.FindPage(It.IsAny<ActionContext>(), "~/Shared/Layout1.cshtml"))
.Returns(layout1); .Returns(new RazorPageResult("~/Shared/Layout1.cshtml", layout1));
pageFactory.Setup(p => p.CreateInstance("~/Shared/Layout2.cshtml")) viewEngine.Setup(p => p.FindPage(It.IsAny<ActionContext>(), "~/Shared/Layout2.cshtml"))
.Returns(layout2); .Returns(new RazorPageResult("~/Shared/Layout2.cshtml", layout2));
var view = new RazorView(pageFactory.Object, var view = new RazorView(viewEngine.Object,
Mock.Of<IRazorPageActivator>(), Mock.Of<IRazorPageActivator>(),
CreateViewStartProvider(), CreateViewStartProvider(),
page, page,
@ -438,11 +477,11 @@ section-content-2";
v.Write(v.RenderSection("foo")); v.Write(v.RenderSection("foo"));
}); });
var pageFactory = new Mock<IRazorPageFactory>(); var viewEngine = new Mock<IRazorViewEngine>();
pageFactory.Setup(p => p.CreateInstance("layout-1")) viewEngine.Setup(p => p.FindPage(It.IsAny<ActionContext>(), "layout-1"))
.Returns(layout1); .Returns(new RazorPageResult("layout-1", layout1));
var view = new RazorView(pageFactory.Object, var view = new RazorView(viewEngine.Object,
Mock.Of<IRazorPageActivator>(), Mock.Of<IRazorPageActivator>(),
CreateViewStartProvider(), CreateViewStartProvider(),
page, page,
@ -483,11 +522,11 @@ section-content-2";
v.Write(v.RenderSection("foo")); v.Write(v.RenderSection("foo"));
}); });
var pageFactory = new Mock<IRazorPageFactory>(); var viewEngine = new Mock<IRazorViewEngine>();
pageFactory.Setup(p => p.CreateInstance("layout-1")) viewEngine.Setup(p => p.FindPage(It.IsAny<ActionContext>(), "layout-1"))
.Returns(layout1); .Returns(new RazorPageResult("layout-1", layout1));
var view = new RazorView(pageFactory.Object, var view = new RazorView(viewEngine.Object,
Mock.Of<IRazorPageActivator>(), Mock.Of<IRazorPageActivator>(),
CreateViewStartProvider(), CreateViewStartProvider(),
page, page,
@ -514,7 +553,7 @@ section-content-2";
v.WriteLiteral("after-flush"); v.WriteLiteral("after-flush");
}); });
var view = new RazorView(Mock.Of<IRazorPageFactory>(), var view = new RazorView(Mock.Of<IRazorViewEngine>(),
Mock.Of<IRazorPageActivator>(), Mock.Of<IRazorPageActivator>(),
CreateViewStartProvider(), CreateViewStartProvider(),
page, page,
@ -541,7 +580,7 @@ section-content-2";
v.Layout = "~/Shared/Layout1.cshtml"; v.Layout = "~/Shared/Layout1.cshtml";
v.WriteLiteral("body-content"); v.WriteLiteral("body-content");
}); });
var layout1 = new TestableRazorPage(v => var layoutPage = new TestableRazorPage(v =>
{ {
v.Write("layout-1" + Environment.NewLine); v.Write("layout-1" + Environment.NewLine);
v.Write(v.RenderSection("foo")); v.Write(v.RenderSection("foo"));
@ -549,11 +588,12 @@ section-content-2";
v.RenderBodyPublic(); v.RenderBodyPublic();
v.Layout = "~/Shared/Layout2.cshtml"; v.Layout = "~/Shared/Layout2.cshtml";
}); });
var pageFactory = new Mock<IRazorPageFactory>(); var viewEngine = new Mock<IRazorViewEngine>();
pageFactory.Setup(p => p.CreateInstance("~/Shared/Layout1.cshtml")) var layoutPath = "~/Shared/Layout1.cshtml";
.Returns(layout1); viewEngine.Setup(p => p.FindPage(It.IsAny<ActionContext>(), layoutPath))
.Returns(new RazorPageResult(layoutPath, layoutPage));
var view = new RazorView(pageFactory.Object, var view = new RazorView(viewEngine.Object,
Mock.Of<IRazorPageActivator>(), Mock.Of<IRazorPageActivator>(),
CreateViewStartProvider(), CreateViewStartProvider(),
page, page,
@ -603,7 +643,7 @@ section-content-2";
var page = new TestableRazorPage(v => var page = new TestableRazorPage(v =>
{ {
v.Layout = "/Layout.cshtml"; v.Layout = "Layout";
Assert.Same(pageWriter, v.Output); Assert.Same(pageWriter, v.Output);
Assert.Same(pageContext, v.PageExecutionContext); Assert.Same(pageContext, v.PageExecutionContext);
}); });
@ -619,14 +659,14 @@ section-content-2";
}); });
layout.Path = "/Layout.cshtml"; layout.Path = "/Layout.cshtml";
var pageFactory = new Mock<IRazorPageFactory>(); var viewEngine = new Mock<IRazorViewEngine>();
pageFactory.Setup(p => p.CreateInstance("/Layout.cshtml")) viewEngine.Setup(p => p.FindPage(It.IsAny<ActionContext>(), "Layout"))
.Returns(layout); .Returns(new RazorPageResult("Layout", layout));
var viewStartProvider = new Mock<IViewStartProvider>(); var viewStartProvider = new Mock<IViewStartProvider>();
viewStartProvider.Setup(v => v.GetViewStartPages(It.IsAny<string>())) viewStartProvider.Setup(v => v.GetViewStartPages(It.IsAny<string>()))
.Returns(Enumerable.Empty<IRazorPage>()) .Returns(Enumerable.Empty<IRazorPage>())
.Verifiable(); .Verifiable();
var view = new RazorView(pageFactory.Object, var view = new RazorView(viewEngine.Object,
Mock.Of<IRazorPageActivator>(), Mock.Of<IRazorPageActivator>(),
viewStartProvider.Object, viewStartProvider.Object,
page, page,
@ -670,7 +710,7 @@ section-content-2";
}); });
page.Path = "/MyPartialPage.cshtml"; page.Path = "/MyPartialPage.cshtml";
var view = new RazorView(Mock.Of<IRazorPageFactory>(), var view = new RazorView(Mock.Of<IRazorViewEngine>(),
Mock.Of<IRazorPageActivator>(), Mock.Of<IRazorPageActivator>(),
Mock.Of<IViewStartProvider>(), Mock.Of<IViewStartProvider>(),
page, page,
@ -701,7 +741,7 @@ section-content-2";
executed = true; executed = true;
}); });
var view = new RazorView(Mock.Of<IRazorPageFactory>(), var view = new RazorView(Mock.Of<IRazorViewEngine>(),
Mock.Of<IRazorPageActivator>(), Mock.Of<IRazorPageActivator>(),
Mock.Of<IViewStartProvider>(), Mock.Of<IViewStartProvider>(),
page, page,
@ -738,9 +778,9 @@ section-content-2";
actualViewStart = v.Layout; actualViewStart = v.Layout;
v.Layout = expectedPage; v.Layout = expectedPage;
}); });
var pageFactory = Mock.Of<IRazorPageFactory>(); var viewEngine = Mock.Of<IRazorViewEngine>();
var view = new RazorView(pageFactory, var view = new RazorView(viewEngine,
Mock.Of<IRazorPageActivator>(), Mock.Of<IRazorPageActivator>(),
CreateViewStartProvider(viewStart1, viewStart2), CreateViewStartProvider(viewStart1, viewStart2),
page, page,
@ -775,9 +815,9 @@ section-content-2";
actual = v.Layout; actual = v.Layout;
v.Layout = null; v.Layout = null;
}); });
var pageFactory = Mock.Of<IRazorPageFactory>(); var viewEngine = Mock.Of<IRazorViewEngine>();
var view = new RazorView(pageFactory, var view = new RazorView(viewEngine,
Mock.Of<IRazorPageActivator>(), Mock.Of<IRazorPageActivator>(),
CreateViewStartProvider(viewStart1, viewStart2), CreateViewStartProvider(viewStart1, viewStart2),
page, page,
@ -813,11 +853,11 @@ section-content-2";
isPartialLayout = v.IsPartial; isPartialLayout = v.IsPartial;
v.RenderBodyPublic(); v.RenderBodyPublic();
}); });
var pageFactory = new Mock<IRazorPageFactory>(); var viewEngine = new Mock<IRazorViewEngine>();
pageFactory.Setup(p => p.CreateInstance("/Layout.cshtml")) viewEngine.Setup(p => p.FindPage(It.IsAny<ActionContext>(), "/Layout.cshtml"))
.Returns(layout); .Returns(new RazorPageResult("Layout", layout));
var view = new RazorView(pageFactory.Object, var view = new RazorView(viewEngine.Object,
Mock.Of<IRazorPageActivator>(), Mock.Of<IRazorPageActivator>(),
CreateViewStartProvider(viewStart), CreateViewStartProvider(viewStart),
page, page,
@ -842,7 +882,7 @@ section-content-2";
{ {
isPartialPage = v.IsPartial; isPartialPage = v.IsPartial;
}); });
var view = new RazorView(Mock.Of<IRazorPageFactory>(), var view = new RazorView(Mock.Of<IRazorViewEngine>(),
Mock.Of<IRazorPageActivator>(), Mock.Of<IRazorPageActivator>(),
CreateViewStartProvider(), CreateViewStartProvider(),
page, page,

View File

@ -11,5 +11,10 @@ namespace RazorWebSite.Controllers
{ {
return View(); return View();
} }
public ViewResult ViewWithLayout()
{
return View();
}
} }
} }

View File

@ -0,0 +1,80 @@
// 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 ViewNameSpecification_HomeController : Controller
{
public IActionResult LayoutSpecifiedWithPartialPathInViewStart()
{
return View();
}
public IActionResult LayoutSpecifiedWithPartialPathInViewStart_ForViewSpecifiedWithPartialName()
{
return View("LayoutSpecifiedWithPartialPathInViewStart");
}
public IActionResult LayoutSpecifiedWithPartialPathInViewStart_ForViewSpecifiedWithAppRelativePath()
{
return View("~/Views/ViewNameSpecification_Home/LayoutSpecifiedWithPartialPathInViewStart");
}
public IActionResult LayoutSpecifiedWithPartialPathInViewStart_ForViewSpecifiedWithAppRelativePathWithExtension()
{
return View("~/Views/ViewNameSpecification_Home/LayoutSpecifiedWithPartialPathInViewStart.cshtml");
}
public IActionResult LayoutSpecifiedWithPartialPathInPage()
{
return View();
}
public IActionResult LayoutSpecifiedWithPartialPathInPageWithPartialPath()
{
return View("LayoutSpecifiedWithPartialPathInPage");
}
public IActionResult LayoutSpecifiedWithPartialPathInPageWithAppRelativePath()
{
return View("~/Views/ViewNameSpecification_Home/LayoutSpecifiedWithPartialPathInPage");
}
public IActionResult LayoutSpecifiedWithPartialPathInPageWithAppRelativePathWithExtension()
{
return View("~/Views/ViewNameSpecification_Home/LayoutSpecifiedWithPartialPathInPage.cshtml");
}
public IActionResult LayoutSpecifiedWithNonPartialPath()
{
ViewData["Layout"] = "~/Views/ViewNameSpecification_Home/_NonSharedLayout";
return View("PageWithNonPartialLayoutPath");
}
public IActionResult LayoutSpecifiedWithNonPartialPathWithExtension()
{
ViewData["Layout"] = "~/Views/ViewNameSpecification_Home/_NonSharedLayout.cshtml";
return View("PageWithNonPartialLayoutPath");
}
public IActionResult ViewWithPartial_SpecifiedWithPartialName()
{
ViewBag.Partial = "NonSharedPartial";
return View("ViewWithPartials");
}
public IActionResult ViewWithPartial_SpecifiedWithAbsoluteName()
{
ViewBag.Partial = "~/Views/ViewNameSpecification_Home/NonSharedPartial";
return View("ViewWithPartials");
}
public IActionResult ViewWithPartial_SpecifiedWithAbsoluteNameAndExtension()
{
ViewBag.Partial = "~/Views/ViewNameSpecification_Home/NonSharedPartial.cshtml";
return View("ViewWithPartials");
}
}
}

View File

@ -0,0 +1,4 @@
@{
Layout = "_LanguageLayout";
}
View With Layout

View File

@ -0,0 +1 @@
<language-layout>@RenderBody()</language-layout>

View File

@ -0,0 +1 @@
<fr-language-layout>@RenderBody()</fr-language-layout>

View File

@ -0,0 +1,4 @@
@{
Layout = "_NonSharedLayout";
}
Layout specified in page

View File

@ -0,0 +1 @@
_ViewStart that specifies partial Layout

View File

@ -0,0 +1 @@
Non Shared Partial

View File

@ -0,0 +1,4 @@
@{
Layout = (string)ViewData["Layout"];
}
Page With Non Partial Layout

View File

@ -0,0 +1,4 @@
@{
var partial = (string)ViewData["Partial"];
}
@Html.Partial(partial)

View File

@ -0,0 +1 @@
<non-shared>@RenderBody()</non-shared>

View File

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