From 933f7eeb2242bb04d69262de09d9ce1c97847e86 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Tue, 28 Oct 2014 11:03:41 -0700 Subject: [PATCH] Layout specification and discovery should follow the same behavior as partials Fixes #1047 --- .../Views/Shared/fr/_Layout.cshtml | 47 +++++ samples/MvcSample.Web/Views/_ViewStart.cshtml | 2 +- .../IRazorViewEngine.cs | 22 ++ .../IRazorViewFactory.cs | 4 +- .../Properties/Resources.Designer.cs | 8 +- .../RazorPageResult.cs | 54 +++++ src/Microsoft.AspNet.Mvc.Razor/RazorView.cs | 31 ++- .../RazorViewEngine.cs | 94 +++++---- .../RazorViewFactory.cs | 12 +- src/Microsoft.AspNet.Mvc.Razor/Resources.resx | 2 +- .../RazorViewLocationSpecificationTest.cs | 102 +++++++++ .../ViewEngineTests.cs | 37 ++++ .../RazorViewEngineTest.cs | 196 +++++++++++++----- .../RazorViewFactoryTest.cs | 16 +- .../RazorViewTest.cs | 156 ++++++++------ .../Controllers/TemplateExpander.cs | 5 + .../ViewNameSpecification_HomeController.cs | 80 +++++++ .../TemplateExpander/ViewWithLayout.cshtml | 4 + .../TemplateExpander/_LanguageLayout.cshtml | 1 + .../fr/_LanguageLayout.cshtml | 1 + ...ayoutSpecifiedWithPartialPathInPage.cshtml | 4 + ...SpecifiedWithPartialPathInViewStart.cshtml | 1 + .../NonSharedPartial.cshtml | 1 + .../PageWithNonPartialLayoutPath.cshtml | 4 + .../ViewWithPartials.cshtml | 4 + .../_NonSharedLayout.cshtml | 1 + .../_ViewStart.cshtml | 3 + 27 files changed, 706 insertions(+), 186 deletions(-) create mode 100644 samples/MvcSample.Web/Views/Shared/fr/_Layout.cshtml create mode 100644 src/Microsoft.AspNet.Mvc.Razor/IRazorViewEngine.cs create mode 100644 src/Microsoft.AspNet.Mvc.Razor/RazorPageResult.cs create mode 100644 test/Microsoft.AspNet.Mvc.FunctionalTests/RazorViewLocationSpecificationTest.cs create mode 100644 test/WebSites/RazorWebSite/Controllers/ViewNameSpecification_HomeController.cs create mode 100644 test/WebSites/RazorWebSite/Views/TemplateExpander/ViewWithLayout.cshtml create mode 100644 test/WebSites/RazorWebSite/Views/TemplateExpander/_LanguageLayout.cshtml create mode 100644 test/WebSites/RazorWebSite/Views/TemplateExpander/fr/_LanguageLayout.cshtml create mode 100644 test/WebSites/RazorWebSite/Views/ViewNameSpecification_Home/LayoutSpecifiedWithPartialPathInPage.cshtml create mode 100644 test/WebSites/RazorWebSite/Views/ViewNameSpecification_Home/LayoutSpecifiedWithPartialPathInViewStart.cshtml create mode 100644 test/WebSites/RazorWebSite/Views/ViewNameSpecification_Home/NonSharedPartial.cshtml create mode 100644 test/WebSites/RazorWebSite/Views/ViewNameSpecification_Home/PageWithNonPartialLayoutPath.cshtml create mode 100644 test/WebSites/RazorWebSite/Views/ViewNameSpecification_Home/ViewWithPartials.cshtml create mode 100644 test/WebSites/RazorWebSite/Views/ViewNameSpecification_Home/_NonSharedLayout.cshtml create mode 100644 test/WebSites/RazorWebSite/Views/ViewNameSpecification_Home/_ViewStart.cshtml diff --git a/samples/MvcSample.Web/Views/Shared/fr/_Layout.cshtml b/samples/MvcSample.Web/Views/Shared/fr/_Layout.cshtml new file mode 100644 index 0000000000..3eb5e221e2 --- /dev/null +++ b/samples/MvcSample.Web/Views/Shared/fr/_Layout.cshtml @@ -0,0 +1,47 @@ + + + + + + @ViewBag.Title - Ma Demande ASP.NET + + + @await RenderSectionAsync("header", required: false) + + + +
+ @RenderBody() +
+
+

© @DateTime.Now.Year - Mon Application ASP.NET

+
+
+ @await RenderSectionAsync("footer", required: false) + + diff --git a/samples/MvcSample.Web/Views/_ViewStart.cshtml b/samples/MvcSample.Web/Views/_ViewStart.cshtml index ab23e9a239..1af6e49466 100644 --- a/samples/MvcSample.Web/Views/_ViewStart.cshtml +++ b/samples/MvcSample.Web/Views/_ViewStart.cshtml @@ -1,3 +1,3 @@ @{ - Layout = "/Views/Shared/_Layout.cshtml"; + Layout = "_Layout"; } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor/IRazorViewEngine.cs b/src/Microsoft.AspNet.Mvc.Razor/IRazorViewEngine.cs new file mode 100644 index 0000000000..efb1079c6a --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor/IRazorViewEngine.cs @@ -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 +{ + /// + /// An used to render pages that use the Razor syntax. + /// + public interface IRazorViewEngine : IViewEngine + { + /// + /// Finds a using the same view discovery semantics used in + /// . + /// + /// The . + /// The name or full path to the view. + /// A result representing the result of locating the . + RazorPageResult FindPage(ActionContext context, string page); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor/IRazorViewFactory.cs b/src/Microsoft.AspNet.Mvc.Razor/IRazorViewFactory.cs index 1850ffd186..10b0512be7 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/IRazorViewFactory.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/IRazorViewFactory.cs @@ -13,9 +13,11 @@ namespace Microsoft.AspNet.Mvc.Razor /// /// Creates a providing it with the to execute. /// + /// The that was used to locate Layout pages + /// that will be part of 's execution. /// The instance to execute. /// Determines if the view is to be executed as a partial. /// A instance that renders the contents of the - IView GetView([NotNull] IRazorPage page, bool isPartial); + IView GetView([NotNull] IRazorViewEngine viewEngine, [NotNull] IRazorPage page, bool isPartial); } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.Razor/Properties/Resources.Designer.cs index e8dd0ed3ae..cdfe251772 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/Properties/Resources.Designer.cs @@ -75,7 +75,7 @@ namespace Microsoft.AspNet.Mvc.Razor } /// - /// The layout view '{0}' could not be located. + /// The layout view '{0}' could not be located. The following locations were searched:{1} /// internal static string LayoutCannotBeLocated { @@ -83,11 +83,11 @@ namespace Microsoft.AspNet.Mvc.Razor } /// - /// The layout view '{0}' could not be located. + /// The layout view '{0}' could not be located. The following locations were searched:{1} /// - 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); } /// diff --git a/src/Microsoft.AspNet.Mvc.Razor/RazorPageResult.cs b/src/Microsoft.AspNet.Mvc.Razor/RazorPageResult.cs new file mode 100644 index 0000000000..fff00cd911 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor/RazorPageResult.cs @@ -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 +{ + /// + /// Represents the results of locating a . + /// + public class RazorPageResult + { + /// + /// Initializes a new instance of for a successful discovery. + /// + /// The name of the page that was located. + /// The located . + public RazorPageResult([NotNull] string name, [NotNull] IRazorPage page) + { + Name = name; + Page = page; + } + + /// + /// Initializes a new instance of for an unsuccessful discovery. + /// + /// The name of the page that was located. + /// The locations that were searched. + public RazorPageResult([NotNull] string name, [NotNull] IEnumerable searchedLocations) + { + Name = name; + SearchedLocations = searchedLocations; + } + + /// + /// Gets the name of the page being located. + /// + /// This property maps to the name parameter of + /// . + public string Name { get; } + + /// + /// Gets the if found. + /// + /// This property is null if the page was not found. + public IRazorPage Page { get; } + + /// + /// Gets the locations that were searched when could not be located. + /// + /// This property is null if the page was found. + public IEnumerable SearchedLocations { get; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs b/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs index 613e44a619..0133be5f7a 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs @@ -15,28 +15,28 @@ namespace Microsoft.AspNet.Mvc.Razor /// public class RazorView : IView { - private readonly IRazorPageFactory _pageFactory; + private readonly IRazorViewEngine _viewEngine; private readonly IRazorPageActivator _pageActivator; private readonly IViewStartProvider _viewStartProvider; private IPageExecutionListenerFeature _pageExecutionFeature; /// - /// Initializes a new instance of RazorView + /// Initializes a new instance of /// - /// The page factory used to instantiate layout and _ViewStart pages. + /// The used to locate Layout pages. /// The used to activate pages. /// The used for discovery of _ViewStart /// The instance to execute. /// Determines if the view is to be executed as a partial. /// pages - public RazorView(IRazorPageFactory pageFactory, + public RazorView(IRazorViewEngine viewEngine, IRazorPageActivator pageActivator, IViewStartProvider viewStartProvider, IRazorPage razorPage, bool isPartial ) { - _pageFactory = pageFactory; + _viewEngine = viewEngine; _pageActivator = pageActivator; _viewStartProvider = viewStartProvider; RazorPage = razorPage; @@ -186,12 +186,7 @@ namespace Microsoft.AspNet.Mvc.Razor throw new InvalidOperationException(message); } - var layoutPage = _pageFactory.CreateInstance(previousPage.Layout); - if (layoutPage == null) - { - var message = Resources.FormatLayoutCannotBeLocated(previousPage.Layout); - throw new InvalidOperationException(message); - } + var layoutPage = GetLayoutPage(context, previousPage.Layout); // Notify the previous page that any writes that are performed on it are part of sections being written // in the layout. @@ -212,5 +207,19 @@ namespace Microsoft.AspNet.Mvc.Razor 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; + } } } diff --git a/src/Microsoft.AspNet.Mvc.Razor/RazorViewEngine.cs b/src/Microsoft.AspNet.Mvc.Razor/RazorViewEngine.cs index 5f1cb87875..3779f18633 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/RazorViewEngine.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/RazorViewEngine.cs @@ -11,9 +11,9 @@ using Microsoft.AspNet.Mvc.Rendering; namespace Microsoft.AspNet.Mvc.Razor { /// - /// Represents a view engine that is used to render a page that uses the Razor syntax. + /// Default implementation of . /// - public class RazorViewEngine : IViewEngine + public class RazorViewEngine : IRazorViewEngine { private const string ViewExtension = ".cshtml"; internal const string ControllerKey = "controller"; @@ -78,7 +78,8 @@ namespace Microsoft.AspNet.Mvc.Razor throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(viewName)); } - return CreateViewEngineResult(context, viewName, partial: false); + var pageResult = GetRazorPageResult(context, viewName); + return CreateViewEngineResult(pageResult, _viewFactory, isPartial: false); } /// @@ -90,36 +91,49 @@ namespace Microsoft.AspNet.Mvc.Razor 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, - string viewName, - bool partial) + /// + public RazorPageResult FindPage([NotNull] ActionContext context, + string pageName) { - var nameRepresentsPath = IsSpecificPath(viewName); - - if (nameRepresentsPath) + if (string.IsNullOrEmpty(pageName)) { - 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); - if (page != null) - { - return CreateFoundResult(context, page, viewName, partial); - } + applicationRelativePath += ViewExtension; } - 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 { - return LocateViewFromViewLocations(context, viewName, partial); + return LocatePageFromViewLocations(context, pageName); } } - private ViewEngineResult LocateViewFromViewLocations(ActionContext context, - string viewName, - bool partial) + private RazorPageResult LocatePageFromViewLocations(ActionContext context, + string pageName) { // Initialize the dictionary for the typical case of having controller and action tokens. var routeValues = context.RouteData.Values; @@ -129,7 +143,7 @@ namespace Microsoft.AspNet.Mvc.Razor var viewLocations = !string.IsNullOrEmpty(areaName) ? AreaViewLocationFormats : ViewLocationFormats; - var expanderContext = new ViewLocationExpanderContext(context, viewName); + var expanderContext = new ViewLocationExpanderContext(context, pageName); if (_viewLocationExpanders.Count > 0) { expanderContext.Values = new Dictionary(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. - var viewLocation = _viewLocationCache.Get(expanderContext); - if (!string.IsNullOrEmpty(viewLocation)) + var pageLocation = _viewLocationCache.Get(expanderContext); + if (!string.IsNullOrEmpty(pageLocation)) { - var page = _pageFactory.CreateInstance(viewLocation); + var page = _pageFactory.CreateInstance(pageLocation); if (page != null) { // 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, path, - viewName, + pageName, controllerName, areaName); 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. _viewLocationCache.Set(expanderContext, transformedPath); - return CreateFoundResult(context, page, transformedPath, partial); + return new RazorPageResult(pageName, page); } searchedLocations.Add(transformedPath); } // 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, - IRazorPage page, - string viewName, - bool partial) + private ViewEngineResult CreateViewEngineResult(RazorPageResult result, + IRazorViewFactory razorViewFactory, + bool isPartial) { - // A single request could result in creating multiple IRazorView instances (for partials, view components) - // and might store state. We'll use the service container to create new instances as we require. + if (result.SearchedLocations != null) + { + return ViewEngineResult.NotFound(result.Name, result.SearchedLocations); + } - var services = actionContext.HttpContext.RequestServices; - - var view = _viewFactory.GetView(page, partial); - - return ViewEngineResult.Found(viewName, view); + var view = razorViewFactory.GetView(this, result.Page, isPartial); + return ViewEngineResult.Found(result.Name, view); } - private static bool IsSpecificPath(string name) + private static bool IsApplicationRelativePath(string name) { Debug.Assert(!string.IsNullOrEmpty(name)); return name[0] == '~' || name[0] == '/'; diff --git a/src/Microsoft.AspNet.Mvc.Razor/RazorViewFactory.cs b/src/Microsoft.AspNet.Mvc.Razor/RazorViewFactory.cs index 2b9125c647..449b57872e 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/RazorViewFactory.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/RazorViewFactory.cs @@ -12,29 +12,27 @@ namespace Microsoft.AspNet.Mvc.Razor public class RazorViewFactory : IRazorViewFactory { private readonly IRazorPageActivator _pageActivator; - private readonly IRazorPageFactory _pageFactory; private readonly IViewStartProvider _viewStartProvider; /// /// Initializes a new instance of RazorViewFactory /// - /// The page factory used to instantiate layout and _ViewStart pages. /// The used to activate pages. /// The used for discovery of _ViewStart /// pages - public RazorViewFactory(IRazorPageFactory pageFactory, - IRazorPageActivator pageActivator, + public RazorViewFactory(IRazorPageActivator pageActivator, IViewStartProvider viewStartProvider) { - _pageFactory = pageFactory; _pageActivator = pageActivator; _viewStartProvider = viewStartProvider; } /// - 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; } } diff --git a/src/Microsoft.AspNet.Mvc.Razor/Resources.resx b/src/Microsoft.AspNet.Mvc.Razor/Resources.resx index 4cad822f56..0b494963f7 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Resources.resx +++ b/src/Microsoft.AspNet.Mvc.Razor/Resources.resx @@ -130,7 +130,7 @@ The {0} returned by '{1}' must be an instance of '{2}'. - The layout view '{0}' could not be located. + The layout view '{0}' could not be located. The following locations were searched:{1} A layout page cannot be rendered after '{0}' has been invoked. diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/RazorViewLocationSpecificationTest.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/RazorViewLocationSpecificationTest.cs new file mode 100644 index 0000000000..2672907d13 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/RazorViewLocationSpecificationTest.cs @@ -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 _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 = +@" +_ViewStart that specifies partial 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 = +@" +Layout specified in page +"; + 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 = +@" +Page With Non 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()); + } + + [Theory] + [InlineData("ViewWithPartial_SpecifiedWithPartialName")] + [InlineData("ViewWithPartial_SpecifiedWithAbsoluteName")] + [InlineData("ViewWithPartial_SpecifiedWithAbsoluteNameAndExtension")] + public async Task PartialsCanBeSpecifiedWithPartialPath(string actionName) + { + var expected = +@" + +Non Shared Partial + +"; + var server = TestServer.Create(_provider, _app); + var client = server.CreateClient(); + + // Act + var body = await client.GetStringAsync(BaseUrl + actionName); + + // Assert + Assert.Equal(expected, body.Trim()); + } + } +} \ 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 60b1ad8b82..3569843316 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/ViewEngineTests.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/ViewEngineTests.cs @@ -216,6 +216,43 @@ component-content"; Assert.Equal(expected, body.Trim()); } + public static IEnumerable RazorViewEngine_UsesExpandersForLayoutsData + { + get + { + var expected1 = + @" +View With Layout +"; + + yield return new[] { "gb", expected1 }; + yield return new[] { "na", expected1 }; + + var expected2 = + @" +View With 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. // This change ensures that _ViewStart files correctly inherits directives from parent _ViewStarts // which guarantees that paths flow correctly through MvcRazorHost. diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewEngineTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewEngineTest.cs index 53a2552e68..c07bbf87d1 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewEngineTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewEngineTest.cs @@ -1,7 +1,6 @@ // 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.Collections.Generic; using System.Linq; using Microsoft.AspNet.Mvc.Razor.OptionDescriptors; @@ -38,6 +37,33 @@ namespace Microsoft.AspNet.Mvc.Razor.Test } } + public static IEnumerable 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] [InlineData(null)] [InlineData("")] @@ -90,11 +116,13 @@ namespace Microsoft.AspNet.Mvc.Razor.Test var pageFactory = new Mock(); var viewFactory = new Mock(); var page = Mock.Of(); + var view = Mock.Of(); pageFactory.Setup(p => p.CreateInstance(It.IsAny())) .Returns(Mock.Of()); - viewFactory.Setup(p => p.GetView(It.IsAny(), It.IsAny())) - .Returns(Mock.Of()).Verifiable(); + viewFactory.Setup(p => p.GetView(It.IsAny(), It.IsAny(), true)) + .Returns(view) + .Verifiable(); var viewEngine = CreateViewEngine(pageFactory.Object, viewFactory.Object); var context = GetActionContext(_controllerTestContext); @@ -104,8 +132,8 @@ namespace Microsoft.AspNet.Mvc.Razor.Test // Assert Assert.True(result.Success); - Assert.IsAssignableFrom(result.View); - Assert.Equal("/Views/bar/test-view.cshtml", result.ViewName); + Assert.Same(view, result.View); + Assert.Equal("test-view", result.ViewName); viewFactory.Verify(); } @@ -238,11 +266,13 @@ namespace Microsoft.AspNet.Mvc.Razor.Test var pageFactory = new Mock(); var viewFactory = new Mock(); var page = Mock.Of(); + var view = Mock.Of(); pageFactory.Setup(p => p.CreateInstance(It.IsAny())) .Returns(Mock.Of()); - viewFactory.Setup(p => p.GetView(It.IsAny(), It.IsAny())) - .Returns(Mock.Of()).Verifiable(); + viewFactory.Setup(p => p.GetView(It.IsAny(), It.IsAny(), false)) + .Returns(view) + .Verifiable(); var viewEngine = CreateViewEngine(pageFactory.Object, viewFactory.Object); var context = GetActionContext(_controllerTestContext); @@ -252,8 +282,8 @@ namespace Microsoft.AspNet.Mvc.Razor.Test // Assert Assert.True(result.Success); - Assert.IsAssignableFrom(result.View); - Assert.Equal("/Views/bar/test-view.cshtml", result.ViewName); + Assert.Same(view, result.View); + Assert.Equal("test-view", result.ViewName); viewFactory.Verify(); } @@ -303,35 +333,8 @@ namespace Microsoft.AspNet.Mvc.Razor.Test pageFactory.Verify(); } - public static IEnumerable 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] - [MemberData(nameof(FindView_UsesViewLocationExpandersToLocateViewsData))] + [MemberData(nameof(ViewLocationExpanderTestData))] public void FindView_UsesViewLocationExpandersToLocateViews(IDictionary routeValues, IEnumerable expectedSeeds) { @@ -342,7 +345,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Test .Verifiable(); var viewFactory = new Mock(); - viewFactory.Setup(p => p.GetView(It.IsAny(), It.IsAny())) + viewFactory.Setup(p => p.GetView(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(Mock.Of()); var expander1Result = new[] { "some-seed" }; @@ -376,7 +379,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Test var viewEngine = CreateViewEngine(pageFactory.Object, viewFactory.Object, new[] { expander1.Object, expander2.Object }); - var context = GetActionContext(routeValues, viewFactory.Object); + var context = GetActionContext(routeValues); // Act var result = viewEngine.FindView(context, "test-view"); @@ -401,17 +404,17 @@ namespace Microsoft.AspNet.Mvc.Razor.Test .Verifiable(); var viewFactory = new Mock(); - viewFactory.Setup(p => p.GetView(It.IsAny(), It.IsAny())) + viewFactory.Setup(p => p.GetView(It.IsAny(), It.IsAny(), false)) .Returns(Mock.Of()); var cache = GetViewLocationCache(); - var cacheMock = Mock.Get(cache); + var cacheMock = Mock.Get(cache); cacheMock.Setup(c => c.Set(It.IsAny(), "/Views/Shared/baz.cshtml")) .Verifiable(); var viewEngine = CreateViewEngine(pageFactory.Object, viewFactory.Object, cache: cache); - var context = GetActionContext(_controllerTestContext, viewFactory.Object); + var context = GetActionContext(_controllerTestContext); // Act var result = viewEngine.FindView(context, "baz"); @@ -432,7 +435,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Test .Verifiable(); var viewFactory = new Mock(); - viewFactory.Setup(p => p.GetView(It.IsAny(), It.IsAny())) + viewFactory.Setup(p => p.GetView(It.IsAny(), It.IsAny(), false)) .Returns(Mock.Of()); var expander = new Mock(MockBehavior.Strict); @@ -472,7 +475,8 @@ namespace Microsoft.AspNet.Mvc.Razor.Test .Verifiable(); var viewFactory = new Mock(); - viewFactory.Setup(p => p.GetView(It.IsAny(), It.IsAny())).Returns(Mock.Of()); + viewFactory.Setup(p => p.GetView(It.IsAny(), It.IsAny(), false)) + .Returns(Mock.Of()); var cacheMock = new Mock(); cacheMock.Setup(c => c.Get(It.IsAny())) @@ -491,7 +495,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Test viewFactory.Object, expanders: new[] { expander.Object }, cache: cacheMock.Object); - var context = GetActionContext(_controllerTestContext, viewFactory.Object); + var context = GetActionContext(_controllerTestContext); // Act var result = viewEngine.FindView(context, "baz"); @@ -503,10 +507,97 @@ namespace Microsoft.AspNet.Mvc.Razor.Test expander.Verify(); } - private IViewEngine CreateViewEngine(IRazorPageFactory pageFactory = null, - IRazorViewFactory viewFactory = null, - IEnumerable expanders = null, - IViewLocationCache cache = null) + [Theory] + [InlineData(null)] + [InlineData("")] + 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 routeValues, + IEnumerable expectedSeeds) + { + // Arrange + var page = Mock.Of(); + var pageFactory = new Mock(); + pageFactory.Setup(p => p.CreateInstance("expanded-path/bar-layout")) + .Returns(page) + .Verifiable(); + + var viewFactory = new Mock(MockBehavior.Strict); + + var expander = new Mock(); + expander.Setup(e => e.PopulateValues(It.IsAny())) + .Callback((ViewLocationExpanderContext c) => + { + Assert.NotNull(c.ActionContext); + c.Values["expander-key"] = expander.ToString(); + }) + .Verifiable(); + expander.Setup(e => e.ExpandViewLocations(It.IsAny(), + It.IsAny>())) + .Returns((ViewLocationExpanderContext c, IEnumerable 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(); + + 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 expanders = null, + IViewLocationCache cache = null) { pageFactory = pageFactory ?? Mock.Of(); viewFactory = viewFactory ?? Mock.Of(); @@ -541,14 +632,9 @@ namespace Microsoft.AspNet.Mvc.Razor.Test return cacheMock.Object; } - private static ActionContext GetActionContext(IDictionary routeValues, - IRazorViewFactory razorViewFactory = null) + private static ActionContext GetActionContext(IDictionary routeValues) { var httpContext = new DefaultHttpContext(); - var serviceProvider = new Mock(); - - httpContext.RequestServices = serviceProvider.Object; - var routeData = new RouteData(); foreach (var kvp in routeValues) { diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewFactoryTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewFactoryTest.cs index 70db43811d..1fe3ba1383 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewFactoryTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewFactoryTest.cs @@ -4,11 +4,10 @@ using Moq; using Xunit; -namespace Microsoft.AspNet.Mvc.Razor.Test +namespace Microsoft.AspNet.Mvc.Razor { public class RazorViewFactoryTest { - [Theory] [InlineData(false)] [InlineData(true)] @@ -16,15 +15,17 @@ namespace Microsoft.AspNet.Mvc.Razor.Test { // Arrange var factory = new RazorViewFactory( - Mock.Of(), Mock.Of(), Mock.Of()); + var page = Mock.Of(); + var viewEngine = Mock.Of(); // Act - var view = factory.GetView(Mock.Of(), isPartial); + var view = factory.GetView(viewEngine, page, isPartial); // Assert var razorView = Assert.IsType(view); + Assert.Same(page, razorView.RazorPage); Assert.Equal(razorView.IsPartial, isPartial); } @@ -33,18 +34,19 @@ namespace Microsoft.AspNet.Mvc.Razor.Test { // Arrange var factory = new RazorViewFactory( - Mock.Of(), Mock.Of(), Mock.Of()); var page = Mock.Of(); + var viewEngine = Mock.Of(); // Act - var view = factory.GetView(page, isPartial: false) as RazorView; + var view = factory.GetView(viewEngine, page, isPartial: false); // Assert Assert.NotNull(view); - Assert.Same(view.RazorPage, page); + var razorView = Assert.IsType(view); + Assert.Same(razorView.RazorPage, page); } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs index dd18c4993d..e2eb8d08d9 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs @@ -28,7 +28,7 @@ namespace Microsoft.AspNet.Mvc.Razor actual = v.Output; v.Write("Hello world"); }); - var view = new RazorView(Mock.Of(), + var view = new RazorView(Mock.Of(), Mock.Of(), CreateViewStartProvider(), page, @@ -55,7 +55,7 @@ namespace Microsoft.AspNet.Mvc.Razor Assert.Same(viewData, v.ViewContext.ViewData); }); var activator = new Mock(); - var view = new RazorView(Mock.Of(), + var view = new RazorView(Mock.Of(), activator.Object, CreateViewStartProvider(), page, @@ -87,7 +87,7 @@ namespace Microsoft.AspNet.Mvc.Razor var activator = new Mock(); activator.Setup(a => a.Activate(page, It.IsAny())) .Verifiable(); - var view = new RazorView(Mock.Of(), + var view = new RazorView(Mock.Of(), activator.Object, CreateViewStartProvider(), page, @@ -108,9 +108,9 @@ namespace Microsoft.AspNet.Mvc.Razor { v.Layout = LayoutPath; }); - var pageFactory = new Mock(); + var viewEngine = new Mock(); var viewStartProvider = CreateViewStartProvider(); - var view = new RazorView(pageFactory.Object, + var view = new RazorView(viewEngine.Object, Mock.Of(), viewStartProvider, page, @@ -121,7 +121,8 @@ namespace Microsoft.AspNet.Mvc.Razor await view.RenderAsync(viewContext); // Assert - pageFactory.Verify(v => v.CreateInstance(It.IsAny()), Times.Never()); + viewEngine.Verify(v => v.FindPage(It.IsAny(), It.IsAny()), + Times.Never()); Mock.Get(viewStartProvider) .Verify(v => v.GetViewStartPages(It.IsAny()), Times.Never()); } @@ -135,7 +136,7 @@ namespace Microsoft.AspNet.Mvc.Razor { actual = v.Output; }); - var view = new RazorView(Mock.Of(), + var view = new RazorView(Mock.Of(), Mock.Of(), CreateViewStartProvider(), page, @@ -159,7 +160,7 @@ namespace Microsoft.AspNet.Mvc.Razor { v.WriteLiteral("Hello world"); }); - var view = new RazorView(Mock.Of(), + var view = new RazorView(Mock.Of(), Mock.Of(), CreateViewStartProvider(), page, @@ -185,7 +186,7 @@ namespace Microsoft.AspNet.Mvc.Razor var activator = new Mock(); activator.Setup(a => a.Activate(page, It.IsAny())) .Verifiable(); - var view = new RazorView(Mock.Of(), + var view = new RazorView(Mock.Of(), activator.Object, CreateViewStartProvider(), page, @@ -227,7 +228,7 @@ namespace Microsoft.AspNet.Mvc.Razor .Verifiable(); activator.Setup(a => a.Activate(page, It.IsAny())) .Verifiable(); - var view = new RazorView(Mock.Of(), + var view = new RazorView(Mock.Of(), activator.Object, CreateViewStartProvider(viewStart1, viewStart2), page, @@ -241,6 +242,42 @@ namespace Microsoft.AspNet.Mvc.Razor 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(); + var activator = new Mock(); + var view = new RazorView(viewEngine.Object, + Mock.Of(), + Mock.Of(), + 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(() => view.RenderAsync(viewContext)); + + // Assert + Assert.Equal(expected, ex.Message); + viewEngine.Verify(); + } + [Fact] public async Task RenderAsync_ExecutesLayoutPages() { @@ -278,16 +315,17 @@ foot-content"; .Verifiable(); activator.Setup(a => a.Activate(layout, It.IsAny())) .Verifiable(); - var pageFactory = new Mock(); - pageFactory.Setup(p => p.CreateInstance(LayoutPath)) - .Returns(layout); + var viewEngine = new Mock(); - var view = new RazorView(pageFactory.Object, + var view = new RazorView(viewEngine.Object, activator.Object, CreateViewStartProvider(), page, isPartial: false); var viewContext = CreateViewContext(view); + viewEngine.Setup(p => p.FindPage(viewContext, LayoutPath)) + .Returns(new RazorPageResult(LayoutPath, layout)) + .Verifiable(); // Act await view.RenderAsync(viewContext); @@ -296,6 +334,7 @@ foot-content"; // Verify the activator was invoked for the primary page and layout page. activator.Verify(); Assert.Equal(expected, viewContext.Writer.ToString()); + viewEngine.Verify(); } [Fact] @@ -312,11 +351,11 @@ foot-content"; { v.RenderBodyPublic(); }); - var pageFactory = new Mock(); - pageFactory.Setup(p => p.CreateInstance(LayoutPath)) - .Returns(layout); + var viewEngine = new Mock(); + viewEngine.Setup(p => p.FindPage(It.IsAny(), LayoutPath)) + .Returns(new RazorPageResult(LayoutPath, layout)); - var view = new RazorView(pageFactory.Object, + var view = new RazorView(viewEngine.Object, Mock.Of(), CreateViewStartProvider(), page, @@ -339,11 +378,11 @@ foot-content"; var layout = new TestableRazorPage(v => { }); - var pageFactory = new Mock(); - pageFactory.Setup(p => p.CreateInstance(LayoutPath)) - .Returns(layout); + var viewEngine = new Mock(); + viewEngine.Setup(p => p.FindPage(It.IsAny(), LayoutPath)) + .Returns(new RazorPageResult(LayoutPath, layout)); - var view = new RazorView(pageFactory.Object, + var view = new RazorView(viewEngine.Object, Mock.Of(), CreateViewStartProvider(), page, @@ -389,13 +428,13 @@ body-content"; v.Write(v.RenderSection("bar")); v.RenderBodyPublic(); }); - var pageFactory = new Mock(); - pageFactory.Setup(p => p.CreateInstance("~/Shared/Layout1.cshtml")) - .Returns(layout1); - pageFactory.Setup(p => p.CreateInstance("~/Shared/Layout2.cshtml")) - .Returns(layout2); + var viewEngine = new Mock(); + viewEngine.Setup(p => p.FindPage(It.IsAny(), "~/Shared/Layout1.cshtml")) + .Returns(new RazorPageResult("~/Shared/Layout1.cshtml", layout1)); + viewEngine.Setup(p => p.FindPage(It.IsAny(), "~/Shared/Layout2.cshtml")) + .Returns(new RazorPageResult("~/Shared/Layout2.cshtml", layout2)); - var view = new RazorView(pageFactory.Object, + var view = new RazorView(viewEngine.Object, Mock.Of(), CreateViewStartProvider(), page, @@ -438,11 +477,11 @@ section-content-2"; v.Write(v.RenderSection("foo")); }); - var pageFactory = new Mock(); - pageFactory.Setup(p => p.CreateInstance("layout-1")) - .Returns(layout1); + var viewEngine = new Mock(); + viewEngine.Setup(p => p.FindPage(It.IsAny(), "layout-1")) + .Returns(new RazorPageResult("layout-1", layout1)); - var view = new RazorView(pageFactory.Object, + var view = new RazorView(viewEngine.Object, Mock.Of(), CreateViewStartProvider(), page, @@ -483,11 +522,11 @@ section-content-2"; v.Write(v.RenderSection("foo")); }); - var pageFactory = new Mock(); - pageFactory.Setup(p => p.CreateInstance("layout-1")) - .Returns(layout1); + var viewEngine = new Mock(); + viewEngine.Setup(p => p.FindPage(It.IsAny(), "layout-1")) + .Returns(new RazorPageResult("layout-1", layout1)); - var view = new RazorView(pageFactory.Object, + var view = new RazorView(viewEngine.Object, Mock.Of(), CreateViewStartProvider(), page, @@ -514,7 +553,7 @@ section-content-2"; v.WriteLiteral("after-flush"); }); - var view = new RazorView(Mock.Of(), + var view = new RazorView(Mock.Of(), Mock.Of(), CreateViewStartProvider(), page, @@ -541,7 +580,7 @@ section-content-2"; v.Layout = "~/Shared/Layout1.cshtml"; v.WriteLiteral("body-content"); }); - var layout1 = new TestableRazorPage(v => + var layoutPage = new TestableRazorPage(v => { v.Write("layout-1" + Environment.NewLine); v.Write(v.RenderSection("foo")); @@ -549,11 +588,12 @@ section-content-2"; v.RenderBodyPublic(); v.Layout = "~/Shared/Layout2.cshtml"; }); - var pageFactory = new Mock(); - pageFactory.Setup(p => p.CreateInstance("~/Shared/Layout1.cshtml")) - .Returns(layout1); + var viewEngine = new Mock(); + var layoutPath = "~/Shared/Layout1.cshtml"; + viewEngine.Setup(p => p.FindPage(It.IsAny(), layoutPath)) + .Returns(new RazorPageResult(layoutPath, layoutPage)); - var view = new RazorView(pageFactory.Object, + var view = new RazorView(viewEngine.Object, Mock.Of(), CreateViewStartProvider(), page, @@ -603,7 +643,7 @@ section-content-2"; var page = new TestableRazorPage(v => { - v.Layout = "/Layout.cshtml"; + v.Layout = "Layout"; Assert.Same(pageWriter, v.Output); Assert.Same(pageContext, v.PageExecutionContext); }); @@ -619,14 +659,14 @@ section-content-2"; }); layout.Path = "/Layout.cshtml"; - var pageFactory = new Mock(); - pageFactory.Setup(p => p.CreateInstance("/Layout.cshtml")) - .Returns(layout); + var viewEngine = new Mock(); + viewEngine.Setup(p => p.FindPage(It.IsAny(), "Layout")) + .Returns(new RazorPageResult("Layout", layout)); var viewStartProvider = new Mock(); viewStartProvider.Setup(v => v.GetViewStartPages(It.IsAny())) .Returns(Enumerable.Empty()) .Verifiable(); - var view = new RazorView(pageFactory.Object, + var view = new RazorView(viewEngine.Object, Mock.Of(), viewStartProvider.Object, page, @@ -670,7 +710,7 @@ section-content-2"; }); page.Path = "/MyPartialPage.cshtml"; - var view = new RazorView(Mock.Of(), + var view = new RazorView(Mock.Of(), Mock.Of(), Mock.Of(), page, @@ -701,7 +741,7 @@ section-content-2"; executed = true; }); - var view = new RazorView(Mock.Of(), + var view = new RazorView(Mock.Of(), Mock.Of(), Mock.Of(), page, @@ -738,9 +778,9 @@ section-content-2"; actualViewStart = v.Layout; v.Layout = expectedPage; }); - var pageFactory = Mock.Of(); + var viewEngine = Mock.Of(); - var view = new RazorView(pageFactory, + var view = new RazorView(viewEngine, Mock.Of(), CreateViewStartProvider(viewStart1, viewStart2), page, @@ -775,9 +815,9 @@ section-content-2"; actual = v.Layout; v.Layout = null; }); - var pageFactory = Mock.Of(); + var viewEngine = Mock.Of(); - var view = new RazorView(pageFactory, + var view = new RazorView(viewEngine, Mock.Of(), CreateViewStartProvider(viewStart1, viewStart2), page, @@ -813,11 +853,11 @@ section-content-2"; isPartialLayout = v.IsPartial; v.RenderBodyPublic(); }); - var pageFactory = new Mock(); - pageFactory.Setup(p => p.CreateInstance("/Layout.cshtml")) - .Returns(layout); + var viewEngine = new Mock(); + viewEngine.Setup(p => p.FindPage(It.IsAny(), "/Layout.cshtml")) + .Returns(new RazorPageResult("Layout", layout)); - var view = new RazorView(pageFactory.Object, + var view = new RazorView(viewEngine.Object, Mock.Of(), CreateViewStartProvider(viewStart), page, @@ -842,7 +882,7 @@ section-content-2"; { isPartialPage = v.IsPartial; }); - var view = new RazorView(Mock.Of(), + var view = new RazorView(Mock.Of(), Mock.Of(), CreateViewStartProvider(), page, diff --git a/test/WebSites/RazorWebSite/Controllers/TemplateExpander.cs b/test/WebSites/RazorWebSite/Controllers/TemplateExpander.cs index 267e7998b3..c8bcebf7a5 100644 --- a/test/WebSites/RazorWebSite/Controllers/TemplateExpander.cs +++ b/test/WebSites/RazorWebSite/Controllers/TemplateExpander.cs @@ -11,5 +11,10 @@ namespace RazorWebSite.Controllers { return View(); } + + public ViewResult ViewWithLayout() + { + return View(); + } } } \ No newline at end of file diff --git a/test/WebSites/RazorWebSite/Controllers/ViewNameSpecification_HomeController.cs b/test/WebSites/RazorWebSite/Controllers/ViewNameSpecification_HomeController.cs new file mode 100644 index 0000000000..75d4030ff4 --- /dev/null +++ b/test/WebSites/RazorWebSite/Controllers/ViewNameSpecification_HomeController.cs @@ -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"); + } + } +} \ No newline at end of file diff --git a/test/WebSites/RazorWebSite/Views/TemplateExpander/ViewWithLayout.cshtml b/test/WebSites/RazorWebSite/Views/TemplateExpander/ViewWithLayout.cshtml new file mode 100644 index 0000000000..3a876212bd --- /dev/null +++ b/test/WebSites/RazorWebSite/Views/TemplateExpander/ViewWithLayout.cshtml @@ -0,0 +1,4 @@ +@{ + Layout = "_LanguageLayout"; +} +View With Layout diff --git a/test/WebSites/RazorWebSite/Views/TemplateExpander/_LanguageLayout.cshtml b/test/WebSites/RazorWebSite/Views/TemplateExpander/_LanguageLayout.cshtml new file mode 100644 index 0000000000..4ef43dfb2d --- /dev/null +++ b/test/WebSites/RazorWebSite/Views/TemplateExpander/_LanguageLayout.cshtml @@ -0,0 +1 @@ +@RenderBody() \ No newline at end of file diff --git a/test/WebSites/RazorWebSite/Views/TemplateExpander/fr/_LanguageLayout.cshtml b/test/WebSites/RazorWebSite/Views/TemplateExpander/fr/_LanguageLayout.cshtml new file mode 100644 index 0000000000..9341e9bdfc --- /dev/null +++ b/test/WebSites/RazorWebSite/Views/TemplateExpander/fr/_LanguageLayout.cshtml @@ -0,0 +1 @@ +@RenderBody() \ No newline at end of file diff --git a/test/WebSites/RazorWebSite/Views/ViewNameSpecification_Home/LayoutSpecifiedWithPartialPathInPage.cshtml b/test/WebSites/RazorWebSite/Views/ViewNameSpecification_Home/LayoutSpecifiedWithPartialPathInPage.cshtml new file mode 100644 index 0000000000..6b5dc686ea --- /dev/null +++ b/test/WebSites/RazorWebSite/Views/ViewNameSpecification_Home/LayoutSpecifiedWithPartialPathInPage.cshtml @@ -0,0 +1,4 @@ +@{ + Layout = "_NonSharedLayout"; +} +Layout specified in page diff --git a/test/WebSites/RazorWebSite/Views/ViewNameSpecification_Home/LayoutSpecifiedWithPartialPathInViewStart.cshtml b/test/WebSites/RazorWebSite/Views/ViewNameSpecification_Home/LayoutSpecifiedWithPartialPathInViewStart.cshtml new file mode 100644 index 0000000000..f9fb7e6236 --- /dev/null +++ b/test/WebSites/RazorWebSite/Views/ViewNameSpecification_Home/LayoutSpecifiedWithPartialPathInViewStart.cshtml @@ -0,0 +1 @@ +_ViewStart that specifies partial Layout \ No newline at end of file diff --git a/test/WebSites/RazorWebSite/Views/ViewNameSpecification_Home/NonSharedPartial.cshtml b/test/WebSites/RazorWebSite/Views/ViewNameSpecification_Home/NonSharedPartial.cshtml new file mode 100644 index 0000000000..d7db120db3 --- /dev/null +++ b/test/WebSites/RazorWebSite/Views/ViewNameSpecification_Home/NonSharedPartial.cshtml @@ -0,0 +1 @@ +Non Shared Partial \ No newline at end of file diff --git a/test/WebSites/RazorWebSite/Views/ViewNameSpecification_Home/PageWithNonPartialLayoutPath.cshtml b/test/WebSites/RazorWebSite/Views/ViewNameSpecification_Home/PageWithNonPartialLayoutPath.cshtml new file mode 100644 index 0000000000..1de0d2308c --- /dev/null +++ b/test/WebSites/RazorWebSite/Views/ViewNameSpecification_Home/PageWithNonPartialLayoutPath.cshtml @@ -0,0 +1,4 @@ +@{ + Layout = (string)ViewData["Layout"]; +} +Page With Non Partial Layout diff --git a/test/WebSites/RazorWebSite/Views/ViewNameSpecification_Home/ViewWithPartials.cshtml b/test/WebSites/RazorWebSite/Views/ViewNameSpecification_Home/ViewWithPartials.cshtml new file mode 100644 index 0000000000..b4396d7849 --- /dev/null +++ b/test/WebSites/RazorWebSite/Views/ViewNameSpecification_Home/ViewWithPartials.cshtml @@ -0,0 +1,4 @@ +@{ + var partial = (string)ViewData["Partial"]; +} +@Html.Partial(partial) diff --git a/test/WebSites/RazorWebSite/Views/ViewNameSpecification_Home/_NonSharedLayout.cshtml b/test/WebSites/RazorWebSite/Views/ViewNameSpecification_Home/_NonSharedLayout.cshtml new file mode 100644 index 0000000000..7f2a2ecc74 --- /dev/null +++ b/test/WebSites/RazorWebSite/Views/ViewNameSpecification_Home/_NonSharedLayout.cshtml @@ -0,0 +1 @@ +@RenderBody() \ No newline at end of file diff --git a/test/WebSites/RazorWebSite/Views/ViewNameSpecification_Home/_ViewStart.cshtml b/test/WebSites/RazorWebSite/Views/ViewNameSpecification_Home/_ViewStart.cshtml new file mode 100644 index 0000000000..77b65af1c3 --- /dev/null +++ b/test/WebSites/RazorWebSite/Views/ViewNameSpecification_Home/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "_Layout"; +} \ No newline at end of file