From e28adbfb3db5d2c447b8c264b1405980aaea750e Mon Sep 17 00:00:00 2001 From: Pranav K Date: Mon, 21 Jul 2014 09:27:24 -0700 Subject: [PATCH] ViewStarts need to be executed as part of View execution Fixes #834 --- samples/MvcSample.Web/MvcSample.Web.kproj | 1 + .../MvcSample.Web/Views/Shared/MyView.cshtml | 1 - .../MvcSample.Web/Views/Shared/_Layout.cshtml | 6 -- samples/MvcSample.Web/Views/_ViewStart.cshtml | 3 + src/Microsoft.AspNet.Mvc.Razor/IRazorPage.cs | 5 + .../IRazorPageFactory.cs | 4 +- .../IViewStartProvider.cs | 21 ++++ .../Microsoft.AspNet.Mvc.Razor.kproj | 3 + .../Properties/AssemblyInfo.cs | 6 ++ src/Microsoft.AspNet.Mvc.Razor/RazorPage.cs | 3 + src/Microsoft.AspNet.Mvc.Razor/RazorView.cs | 30 +++++- .../RazorViewEngine.cs | 6 +- .../ViewStartProvider.cs | 92 ++++++++++++++++++ .../VirtualPathRazorPageFactory.cs | 5 +- src/Microsoft.AspNet.Mvc/MvcServices.cs | 1 + .../Microsoft.AspNet.Mvc.Razor.Test.kproj | 1 + .../RazorViewEngineTest.cs | 26 ++++- .../RazorViewTest.cs | 75 ++++++++++++++- .../ViewStartProviderTest.cs | 96 +++++++++++++++++++ 19 files changed, 362 insertions(+), 23 deletions(-) create mode 100644 samples/MvcSample.Web/Views/_ViewStart.cshtml create mode 100644 src/Microsoft.AspNet.Mvc.Razor/IViewStartProvider.cs create mode 100644 src/Microsoft.AspNet.Mvc.Razor/Properties/AssemblyInfo.cs create mode 100644 src/Microsoft.AspNet.Mvc.Razor/ViewStartProvider.cs create mode 100644 test/Microsoft.AspNet.Mvc.Razor.Test/ViewStartProviderTest.cs diff --git a/samples/MvcSample.Web/MvcSample.Web.kproj b/samples/MvcSample.Web/MvcSample.Web.kproj index 46bf609d42..bb973e66dd 100644 --- a/samples/MvcSample.Web/MvcSample.Web.kproj +++ b/samples/MvcSample.Web/MvcSample.Web.kproj @@ -39,6 +39,7 @@ + diff --git a/samples/MvcSample.Web/Views/Shared/MyView.cshtml b/samples/MvcSample.Web/Views/Shared/MyView.cshtml index c8d8ebb613..d2ed197537 100644 --- a/samples/MvcSample.Web/Views/Shared/MyView.cshtml +++ b/samples/MvcSample.Web/Views/Shared/MyView.cshtml @@ -1,7 +1,6 @@ @using MvcSample.Web.Models @model User @{ - Layout = "/Views/Shared/_Layout.cshtml"; ViewBag.Title = "Home Page"; string nullValue = null; diff --git a/samples/MvcSample.Web/Views/Shared/_Layout.cshtml b/samples/MvcSample.Web/Views/Shared/_Layout.cshtml index 4879428656..2f5901fdb6 100644 --- a/samples/MvcSample.Web/Views/Shared/_Layout.cshtml +++ b/samples/MvcSample.Web/Views/Shared/_Layout.cshtml @@ -27,12 +27,6 @@
@RenderBody()
-
- @if (@Model != null) - { - @Model.Address - } -

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

diff --git a/samples/MvcSample.Web/Views/_ViewStart.cshtml b/samples/MvcSample.Web/Views/_ViewStart.cshtml new file mode 100644 index 0000000000..ab23e9a239 --- /dev/null +++ b/samples/MvcSample.Web/Views/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "/Views/Shared/_Layout.cshtml"; +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor/IRazorPage.cs b/src/Microsoft.AspNet.Mvc.Razor/IRazorPage.cs index bfa26714cd..1b39030d2a 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/IRazorPage.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/IRazorPage.cs @@ -16,6 +16,11 @@ namespace Microsoft.AspNet.Mvc.Razor /// ViewContext ViewContext { get; set; } + /// + /// Gets the path to the page. + /// + string Path { get; set; } + string BodyContent { get; set; } /// diff --git a/src/Microsoft.AspNet.Mvc.Razor/IRazorPageFactory.cs b/src/Microsoft.AspNet.Mvc.Razor/IRazorPageFactory.cs index 0e4ecbe9ce..af2276d16e 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/IRazorPageFactory.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/IRazorPageFactory.cs @@ -11,8 +11,8 @@ namespace Microsoft.AspNet.Mvc.Razor /// /// Creates a for the specified path. /// - /// The path to locate the RazorPage. + /// The path to locate the page. /// The IRazorPage instance if it exists, null otherwise. - IRazorPage CreateInstance(string viewPath); + IRazorPage CreateInstance(string path); } } diff --git a/src/Microsoft.AspNet.Mvc.Razor/IViewStartProvider.cs b/src/Microsoft.AspNet.Mvc.Razor/IViewStartProvider.cs new file mode 100644 index 0000000000..47a445d04b --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor/IViewStartProvider.cs @@ -0,0 +1,21 @@ +// 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 +{ + /// + /// Defines methods for locating ViewStart pages that are applicable to a page. + /// + public interface IViewStartProvider + { + /// + /// Given a view path, returns a sequence of ViewStart instances + /// that are applicable to the specified view. + /// + /// The path of the page to locate ViewStart files for. + /// A sequence of that represent ViewStart. + IEnumerable GetViewStartPages(string path); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor/Microsoft.AspNet.Mvc.Razor.kproj b/src/Microsoft.AspNet.Mvc.Razor/Microsoft.AspNet.Mvc.Razor.kproj index b3d6054eb8..e895509e69 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Microsoft.AspNet.Mvc.Razor.kproj +++ b/src/Microsoft.AspNet.Mvc.Razor/Microsoft.AspNet.Mvc.Razor.kproj @@ -30,6 +30,9 @@ + + + diff --git a/src/Microsoft.AspNet.Mvc.Razor/Properties/AssemblyInfo.cs b/src/Microsoft.AspNet.Mvc.Razor/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..6d457fde04 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor/Properties/AssemblyInfo.cs @@ -0,0 +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.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Microsoft.AspNet.Mvc.Razor.Test")] \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor/RazorPage.cs b/src/Microsoft.AspNet.Mvc.Razor/RazorPage.cs index fda790a69b..6aab1aca64 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/RazorPage.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/RazorPage.cs @@ -42,6 +42,9 @@ namespace Microsoft.AspNet.Mvc.Razor } } + /// + public string Path { get; set; } + /// public ViewContext ViewContext { get; set; } diff --git a/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs b/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs index 0982918fbc..ff4aefaebe 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs @@ -3,6 +3,7 @@ using System; using System.IO; +using System.Linq; using System.Text; using System.Threading.Tasks; using Microsoft.AspNet.Mvc.Rendering; @@ -17,6 +18,7 @@ namespace Microsoft.AspNet.Mvc.Razor { private readonly IRazorPageFactory _pageFactory; private readonly IRazorPageActivator _pageActivator; + private readonly IViewStartProvider _viewStartProvider; private readonly IRazorPage _page; private readonly bool _executeViewHierarchy; @@ -30,11 +32,13 @@ namespace Microsoft.AspNet.Mvc.Razor /// view start and layout pages are executed as part of the executing the page. public RazorView([NotNull] IRazorPageFactory pageFactory, [NotNull] IRazorPageActivator pageActivator, + [NotNull] IViewStartProvider viewStartProvider, [NotNull] IRazorPage page, bool executeViewHierarchy) { _pageFactory = pageFactory; _pageActivator = pageActivator; + _viewStartProvider = viewStartProvider; _page = page; _executeViewHierarchy = executeViewHierarchy; } @@ -44,7 +48,7 @@ namespace Microsoft.AspNet.Mvc.Razor { if (_executeViewHierarchy) { - var bodyContent = await RenderPageAsync(_page, context); + var bodyContent = await RenderPageAsync(_page, context, executeViewStart: true); await RenderLayoutAsync(context, bodyContent); } else @@ -53,7 +57,9 @@ namespace Microsoft.AspNet.Mvc.Razor } } - private async Task RenderPageAsync(IRazorPage page, ViewContext context) + private async Task RenderPageAsync(IRazorPage page, + ViewContext context, + bool executeViewStart) { var contentBuilder = new StringBuilder(1024); using (var bodyWriter = new StringWriter(contentBuilder)) @@ -64,6 +70,11 @@ namespace Microsoft.AspNet.Mvc.Razor context.Writer = bodyWriter; try { + if (executeViewStart) + { + // Execute view starts using the same context + writer as the page to render. + await RenderViewStartAsync(context); + } await RenderPageCoreAsync(page, context); } finally @@ -86,6 +97,19 @@ namespace Microsoft.AspNet.Mvc.Razor await page.ExecuteAsync(); } + private async Task RenderViewStartAsync(ViewContext context) + { + var viewStarts = _viewStartProvider.GetViewStartPages(_page.Path); + + foreach (var viewStart in viewStarts) + { + await RenderPageCoreAsync(viewStart, context); + + // Copy over interesting properties from the ViewStart page to the entry page. + _page.Layout = viewStart.Layout; + } + } + private async Task RenderLayoutAsync(ViewContext context, string bodyContent) { @@ -104,7 +128,7 @@ namespace Microsoft.AspNet.Mvc.Razor layoutPage.PreviousSectionWriters = previousPage.SectionWriters; layoutPage.BodyContent = bodyContent; - bodyContent = await RenderPageAsync(layoutPage, context); + bodyContent = await RenderPageAsync(layoutPage, context, executeViewStart: false); // Verify that RenderBody is called, or that RenderSection is called for all sections layoutPage.EnsureBodyAndSectionsWereRendered(); diff --git a/src/Microsoft.AspNet.Mvc.Razor/RazorViewEngine.cs b/src/Microsoft.AspNet.Mvc.Razor/RazorViewEngine.cs index 4d36cf0bde..033ef985ac 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/RazorViewEngine.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/RazorViewEngine.cs @@ -28,12 +28,15 @@ namespace Microsoft.AspNet.Mvc.Razor private readonly IRazorPageFactory _pageFactory; private readonly IRazorPageActivator _viewActivator; + private readonly IViewStartProvider _viewStartProvider; public RazorViewEngine(IRazorPageFactory pageFactory, - IRazorPageActivator viewActivator) + IRazorPageActivator viewActivator, + IViewStartProvider viewStartProvider) { _pageFactory = pageFactory; _viewActivator = viewActivator; + _viewStartProvider = viewStartProvider; } public IEnumerable ViewLocationFormats @@ -96,6 +99,7 @@ namespace Microsoft.AspNet.Mvc.Razor { var view = new RazorView(_pageFactory, _viewActivator, + _viewStartProvider, page, executeViewHierarchy: !partial); return ViewEngineResult.Found(viewName, view); diff --git a/src/Microsoft.AspNet.Mvc.Razor/ViewStartProvider.cs b/src/Microsoft.AspNet.Mvc.Razor/ViewStartProvider.cs new file mode 100644 index 0000000000..2f43fd10e3 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor/ViewStartProvider.cs @@ -0,0 +1,92 @@ +// 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.IO; +using System.Linq; +using Microsoft.Framework.Runtime; + +namespace Microsoft.AspNet.Mvc.Razor +{ + /// + public class ViewStartProvider : IViewStartProvider + { + private const string ViewStartFileName = "_ViewStart.cshtml"; + private readonly string _appRoot; + private readonly IRazorPageFactory _pageFactory; + + public ViewStartProvider(IApplicationEnvironment appEnv, + IRazorPageFactory pageFactory) + { + _appRoot = TrimTrailingSlash(appEnv.ApplicationBasePath); + _pageFactory = pageFactory; + } + + /// + public IEnumerable GetViewStartPages([NotNull] string path) + { + var viewStartLocations = GetViewStartLocations(path); + var viewStarts = viewStartLocations.Select(_pageFactory.CreateInstance) + .Where(p => p != null) + .ToArray(); + + // GetViewStartLocations return ViewStarts inside-out that is the _ViewStart closest to the page + // is the first: e.g. [ /Views/Home/_ViewStart, /Views/_ViewStart, /_ViewStart ] + // However they need to be executed outside in, so we'll reverse the sequence. + Array.Reverse(viewStarts); + + return viewStarts; + } + + internal IEnumerable GetViewStartLocations(string path) + { + if (string.IsNullOrEmpty(path)) + { + return Enumerable.Empty(); + } + + var viewStartLocations = new List(); + var currentDir = GetViewDirectory(_appRoot, path); + while (IsSubDirectory(_appRoot, currentDir)) + { + viewStartLocations.Add(Path.Combine(currentDir, ViewStartFileName)); + currentDir = Path.GetDirectoryName(currentDir); + } + + return viewStartLocations; + } + + private static bool IsSubDirectory(string appRoot, string currentDir) + { + return currentDir.StartsWith(appRoot, StringComparison.OrdinalIgnoreCase); + } + + private static string GetViewDirectory(string appRoot, string viewPath) + { + if (viewPath.StartsWith("~/")) + { + viewPath = viewPath.Substring(2); + } + else if (viewPath[0] == Path.DirectorySeparatorChar || + viewPath[0] == Path.AltDirectorySeparatorChar) + { + viewPath = viewPath.Substring(1); + } + + var viewDir = Path.GetDirectoryName(viewPath); + return Path.GetFullPath(Path.Combine(appRoot, viewDir)); + } + + private static string TrimTrailingSlash(string path) + { + if (path.Length > 0 && + path[path.Length - 1] == Path.DirectorySeparatorChar) + { + return path.Substring(0, path.Length - 1); + } + + return path; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor/VirtualPathRazorPageFactory.cs b/src/Microsoft.AspNet.Mvc.Razor/VirtualPathRazorPageFactory.cs index aaeff501d3..b55ede7a85 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/VirtualPathRazorPageFactory.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/VirtualPathRazorPageFactory.cs @@ -30,14 +30,15 @@ namespace Microsoft.AspNet.Mvc.Razor } /// - public IRazorPage CreateInstance([NotNull] string viewPath) + public IRazorPage CreateInstance([NotNull] string path) { - var fileInfo = _fileInfoCache.GetFileInfo(viewPath); + var fileInfo = _fileInfoCache.GetFileInfo(path); if (fileInfo != null) { var result = _compilationService.Compile(fileInfo); var page = (IRazorPage)_activator.CreateInstance(_serviceProvider, result.CompiledType); + page.Path = path; return page; } diff --git a/src/Microsoft.AspNet.Mvc/MvcServices.cs b/src/Microsoft.AspNet.Mvc/MvcServices.cs index 5b356f40a5..40088f4cf6 100644 --- a/src/Microsoft.AspNet.Mvc/MvcServices.cs +++ b/src/Microsoft.AspNet.Mvc/MvcServices.cs @@ -43,6 +43,7 @@ namespace Microsoft.AspNet.Mvc yield return describe.Singleton(); yield return describe.Scoped(); yield return describe.Singleton(); + yield return describe.Singleton(); yield return describe.Singleton(); // Virtual path view factory needs to stay scoped so views can get get scoped services. diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/Microsoft.AspNet.Mvc.Razor.Test.kproj b/test/Microsoft.AspNet.Mvc.Razor.Test/Microsoft.AspNet.Mvc.Razor.Test.kproj index f4790a60fd..75641c822d 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/Microsoft.AspNet.Mvc.Razor.Test.kproj +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/Microsoft.AspNet.Mvc.Razor.Test.kproj @@ -28,6 +28,7 @@ + \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewEngineTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewEngineTest.cs index b967c579b2..f8c6567354 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewEngineTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewEngineTest.cs @@ -160,15 +160,35 @@ namespace Microsoft.AspNet.Mvc.Razor.Test }, result.SearchedLocations); } + [Fact] + public void FindView_ReturnsRazorView_IfLookupWasSuccessful() + { + // Arrange + var pageFactory = new Mock(); + pageFactory.Setup(p => p.CreateInstance(It.IsAny())) + .Returns(Mock.Of()); + var viewEngine = new RazorViewEngine(pageFactory.Object, + Mock.Of(), + Mock.Of()); + + // Act + var result = viewEngine.FindView(_controllerTestContext, "test-view"); + + // Assert + Assert.True(result.Success); + Assert.IsType(result.View); + Assert.Equal("/Views/bar/test-view.cshtml", result.ViewName); + } + private IViewEngine CreateSearchLocationViewEngineTester() { var pageFactory = new Mock(); pageFactory.Setup(vpf => vpf.CreateInstance(It.IsAny())) .Returns(null); - var pageActivator = Mock.Of(); - - var viewEngine = new RazorViewEngine(pageFactory.Object, pageActivator); + var viewEngine = new RazorViewEngine(pageFactory.Object, + Mock.Of(), + Mock.Of()); return viewEngine; } diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs index dc4fb8deba..e2ec6a95a6 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs @@ -4,8 +4,8 @@ using System; using System.IO; using System.Threading.Tasks; -using Microsoft.AspNet.Http; using Microsoft.AspNet.Mvc.ModelBinding; +using Microsoft.AspNet.PipelineCore; using Moq; using Xunit; @@ -27,6 +27,7 @@ namespace Microsoft.AspNet.Mvc.Razor }); var view = new RazorView(Mock.Of(), Mock.Of(), + CreateViewStartProvider(), page, executeViewHierarchy: false); var viewContext = CreateViewContext(view); @@ -51,9 +52,10 @@ namespace Microsoft.AspNet.Mvc.Razor Assert.Same(viewData, v.ViewContext.ViewData); }); var activator = new Mock(); - + var view = new RazorView(Mock.Of(), activator.Object, + CreateViewStartProvider(), page, executeViewHierarchy: false); var viewContext = CreateViewContext(view); @@ -86,6 +88,7 @@ namespace Microsoft.AspNet.Mvc.Razor .Verifiable(); var view = new RazorView(Mock.Of(), activator.Object, + CreateViewStartProvider(), page, executeViewHierarchy: false); var viewContext = CreateViewContext(view); @@ -98,15 +101,17 @@ namespace Microsoft.AspNet.Mvc.Razor } [Fact] - public async Task RenderAsync_WithoutHierarchy_DoesNotExecuteLayoutPages() + public async Task RenderAsync_WithoutHierarchy_DoesNotExecuteLayoutOrViewStartPages() { var page = new TestableRazorPage(v => { v.Layout = LayoutPath; }); var pageFactory = new Mock(); + var viewStartProvider = CreateViewStartProvider(); var view = new RazorView(pageFactory.Object, Mock.Of(), + viewStartProvider, page, executeViewHierarchy: false); var viewContext = CreateViewContext(view); @@ -116,6 +121,7 @@ namespace Microsoft.AspNet.Mvc.Razor // Assert pageFactory.Verify(v => v.CreateInstance(It.IsAny()), Times.Never()); + Mock.Get(viewStartProvider).Verify(v => v.GetViewStartPages(It.IsAny()), Times.Never()); } [Fact] @@ -129,6 +135,7 @@ namespace Microsoft.AspNet.Mvc.Razor }); var view = new RazorView(Mock.Of(), Mock.Of(), + CreateViewStartProvider(), page, executeViewHierarchy: true); var viewContext = CreateViewContext(view); @@ -152,6 +159,7 @@ namespace Microsoft.AspNet.Mvc.Razor }); var view = new RazorView(Mock.Of(), Mock.Of(), + CreateViewStartProvider(), page, executeViewHierarchy: true); var viewContext = CreateViewContext(view); @@ -177,6 +185,49 @@ namespace Microsoft.AspNet.Mvc.Razor .Verifiable(); var view = new RazorView(Mock.Of(), activator.Object, + CreateViewStartProvider(), + page, + executeViewHierarchy: true); + var viewContext = CreateViewContext(view); + + // Act + await view.RenderAsync(viewContext); + + // Assert + activator.Verify(); + } + + [Fact] + public async Task RenderAsync_WithHierarchy_ExecutesViewStart() + { + // Arrange + var actualLayoutPath = ""; + var layoutPath = "/Views/_Shared/_Layout.cshtml"; + var viewStart1 = new TestableRazorPage(v => + { + v.Layout = "/fake-layout-path"; + }); + var viewStart2 = new TestableRazorPage(v => + { + v.Layout = layoutPath; + }); + var page = new TestableRazorPage(v => + { + // This path must have been set as a consequence of running viewStart + actualLayoutPath = v.Layout; + // Clear out layout so we don't render it + v.Layout = null; + }); + var activator = new Mock(); + activator.Setup(a => a.Activate(viewStart1, It.IsAny())) + .Verifiable(); + activator.Setup(a => a.Activate(viewStart2, It.IsAny())) + .Verifiable(); + activator.Setup(a => a.Activate(page, It.IsAny())) + .Verifiable(); + var view = new RazorView(Mock.Of(), + activator.Object, + CreateViewStartProvider(viewStart1, viewStart2), page, executeViewHierarchy: true); var viewContext = CreateViewContext(view); @@ -231,6 +282,7 @@ foot-content"; var view = new RazorView(pageFactory.Object, activator.Object, + CreateViewStartProvider(), page, executeViewHierarchy: true); var viewContext = CreateViewContext(view); @@ -264,6 +316,7 @@ foot-content"; var view = new RazorView(pageFactory.Object, Mock.Of(), + CreateViewStartProvider(), page, executeViewHierarchy: true); var viewContext = CreateViewContext(view); @@ -290,6 +343,7 @@ foot-content"; var view = new RazorView(pageFactory.Object, Mock.Of(), + CreateViewStartProvider(), page, executeViewHierarchy: true); var viewContext = CreateViewContext(view); @@ -344,6 +398,7 @@ body-content"; var view = new RazorView(pageFactory.Object, Mock.Of(), + CreateViewStartProvider(), page, executeViewHierarchy: true); var viewContext = CreateViewContext(view); @@ -357,8 +412,8 @@ body-content"; private static ViewContext CreateViewContext(RazorView view) { - var httpContext = new Mock(); - var actionContext = new ActionContext(httpContext.Object, routeData: null, actionDescriptor: null); + var httpContext = new DefaultHttpContext(); + var actionContext = new ActionContext(httpContext, routeData: null, actionDescriptor: null); return new ViewContext( actionContext, view, @@ -366,6 +421,16 @@ body-content"; new StringWriter()); } + private static IViewStartProvider CreateViewStartProvider(params IRazorPage[] viewStartPages) + { + viewStartPages = viewStartPages ?? new IRazorPage[0]; + var viewStartProvider = new Mock(); + viewStartProvider.Setup(v => v.GetViewStartPages(It.IsAny())) + .Returns(viewStartPages); + + return viewStartProvider.Object; + } + private class TestableRazorPage : RazorPage { private readonly Action _executeAction; diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/ViewStartProviderTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/ViewStartProviderTest.cs new file mode 100644 index 0000000000..8ccfbdfec9 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/ViewStartProviderTest.cs @@ -0,0 +1,96 @@ +// 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; +using System.Diagnostics; +using Microsoft.Framework.Runtime; +using Moq; +using Xunit; + +namespace Microsoft.AspNet.Mvc.Razor.Test +{ + public class ViewStartProviderTest + { + [Theory] + [InlineData(null)] + [InlineData("")] + public void GetViewStartLocations_ReturnsEmptySequenceIfViewPathIsEmpty(string viewPath) + { + // Arrange + var appPath = @"x:\test"; + var provider = new ViewStartProvider(GetAppEnv(appPath), Mock.Of()); + + // Act + var result = provider.GetViewStartLocations(viewPath); + + // Assert + Assert.Empty(result); + } + + public static IEnumerable GetViewStartLocations_ReturnsPotentialViewStartLocationsData + { + get + { + yield return new object[] + { + @"x:\test\myapp", + "/Views/Home/View.cshtml", + new[] + { + @"x:\test\myapp\Views\Home\_ViewStart.cshtml", + @"x:\test\myapp\Views\_ViewStart.cshtml", + @"x:\test\myapp\_ViewStart.cshtml", + } + }; + + yield return new object[] + { + @"x:\test\myapp", + "Views/Home/View.cshtml", + new[] + { + @"x:\test\myapp\Views\Home\_ViewStart.cshtml", + @"x:\test\myapp\Views\_ViewStart.cshtml", + @"x:\test\myapp\_ViewStart.cshtml", + } + }; + + yield return new object[] + { + @"x:\test\myapp\", + "Views/Home/View.cshtml", + new[] + { + @"x:\test\myapp\Views\Home\_ViewStart.cshtml", + @"x:\test\myapp\Views\_ViewStart.cshtml", + @"x:\test\myapp\_ViewStart.cshtml", + } + }; + } + } + + [Theory] + [MemberData("GetViewStartLocations_ReturnsPotentialViewStartLocationsData")] + public void GetViewStartLocations_ReturnsPotentialViewStartLocations(string appPath, + string viewPath, + IEnumerable expected) + { + // Arrange + var provider = new ViewStartProvider(GetAppEnv(appPath), Mock.Of()); + + // Act + var result = provider.GetViewStartLocations(viewPath); + + // Assert + Assert.Equal(expected, result); + } + + private static IApplicationEnvironment GetAppEnv(string appPath) + { + var appEnv = new Mock(); + appEnv.Setup(p => p.ApplicationBasePath) + .Returns(appPath); + return appEnv.Object; + } + } +} \ No newline at end of file