ViewStarts need to be executed as part of View execution

Fixes #834
This commit is contained in:
Pranav K 2014-07-21 09:27:24 -07:00
parent b8ab5c5063
commit e28adbfb3d
19 changed files with 362 additions and 23 deletions

View File

@ -39,6 +39,7 @@
<Content Include="Views\Shared\HelloWorldPartial.cshtml" />
<Content Include="Views\Shared\MyView.cshtml" />
<Content Include="Views\Shared\_Layout.cshtml" />
<Content Include="Views\_ViewStart.cshtml" />
<Content Include="web.config" />
<Content Include="web.Debug.config" />
<Content Include="web.Release.config" />

View File

@ -1,7 +1,6 @@
@using MvcSample.Web.Models
@model User
@{
Layout = "/Views/Shared/_Layout.cshtml";
ViewBag.Title = "Home Page";
string nullValue = null;

View File

@ -27,12 +27,6 @@
<div class="container body-content">
@RenderBody()
<hr />
<address>
@if (@Model != null)
{
@Model.Address
}
</address>
<footer>
<p>&copy; @DateTime.Now.Year - My ASP.NET Application</p>
</footer>

View File

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

View File

@ -16,6 +16,11 @@ namespace Microsoft.AspNet.Mvc.Razor
/// </summary>
ViewContext ViewContext { get; set; }
/// <summary>
/// Gets the path to the page.
/// </summary>
string Path { get; set; }
string BodyContent { get; set; }
/// <summary>

View File

@ -11,8 +11,8 @@ namespace Microsoft.AspNet.Mvc.Razor
/// <summary>
/// Creates a <see cref="IRazorPage"/> for the specified path.
/// </summary>
/// <param name="viewPath">The path to locate the RazorPage.</param>
/// <param name="path">The path to locate the page.</param>
/// <returns>The IRazorPage instance if it exists, null otherwise.</returns>
IRazorPage CreateInstance(string viewPath);
IRazorPage CreateInstance(string path);
}
}

View File

@ -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
{
/// <summary>
/// Defines methods for locating ViewStart pages that are applicable to a page.
/// </summary>
public interface IViewStartProvider
{
/// <summary>
/// Given a view path, returns a sequence of ViewStart instances
/// that are applicable to the specified view.
/// </summary>
/// <param name="path">The path of the page to locate ViewStart files for.</param>
/// <returns>A sequence of <see cref="IRazorPage"/> that represent ViewStart.</returns>
IEnumerable<IRazorPage> GetViewStartPages(string path);
}
}

View File

@ -30,6 +30,9 @@
<Compile Include="Compilation\RoslynCompilationService.cs" />
<Compile Include="Extensions\DictionaryExtensions.cs" />
<Compile Include="IRazorPage.cs" />
<Compile Include="IViewStartProvider.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="ViewStartProvider.cs" />
<Compile Include="VirtualPathRazorPageFactory.cs" />
<Compile Include="HelperResult.cs" />
<Compile Include="IRazorPageActivator.cs" />

View File

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

View File

@ -42,6 +42,9 @@ namespace Microsoft.AspNet.Mvc.Razor
}
}
/// <inheritdoc />
public string Path { get; set; }
/// <inheritdoc />
public ViewContext ViewContext { get; set; }

View File

@ -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.</param>
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<string> RenderPageAsync(IRazorPage page, ViewContext context)
private async Task<string> 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();

View File

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

View File

@ -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
{
/// <inheritdoc />
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;
}
/// <inheritdoc />
public IEnumerable<IRazorPage> 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<string> GetViewStartLocations(string path)
{
if (string.IsNullOrEmpty(path))
{
return Enumerable.Empty<string>();
}
var viewStartLocations = new List<string>();
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;
}
}
}

View File

@ -30,14 +30,15 @@ namespace Microsoft.AspNet.Mvc.Razor
}
/// <inheritdoc />
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;
}

View File

@ -43,6 +43,7 @@ namespace Microsoft.AspNet.Mvc
yield return describe.Singleton<IViewEngineProvider, DefaultViewEngineProvider>();
yield return describe.Scoped<ICompositeViewEngine, CompositeViewEngine>();
yield return describe.Singleton<IRazorCompilationService, RazorCompilationService>();
yield return describe.Singleton<IViewStartProvider, ViewStartProvider>();
yield return describe.Singleton<IRazorPageActivator, RazorPageActivator>();
// Virtual path view factory needs to stay scoped so views can get get scoped services.

View File

@ -28,6 +28,7 @@
<Compile Include="RazorPageTest.cs" />
<Compile Include="RazorViewTest.cs" />
<Compile Include="SpanFactory.cs" />
<Compile Include="ViewStartProviderTest.cs" />
</ItemGroup>
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>

View File

@ -160,15 +160,35 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
}, result.SearchedLocations);
}
[Fact]
public void FindView_ReturnsRazorView_IfLookupWasSuccessful()
{
// Arrange
var pageFactory = new Mock<IRazorPageFactory>();
pageFactory.Setup(p => p.CreateInstance(It.IsAny<string>()))
.Returns(Mock.Of<IRazorPage>());
var viewEngine = new RazorViewEngine(pageFactory.Object,
Mock.Of<IRazorPageActivator>(),
Mock.Of<IViewStartProvider>());
// Act
var result = viewEngine.FindView(_controllerTestContext, "test-view");
// Assert
Assert.True(result.Success);
Assert.IsType<RazorView>(result.View);
Assert.Equal("/Views/bar/test-view.cshtml", result.ViewName);
}
private IViewEngine CreateSearchLocationViewEngineTester()
{
var pageFactory = new Mock<IRazorPageFactory>();
pageFactory.Setup(vpf => vpf.CreateInstance(It.IsAny<string>()))
.Returns<RazorPage>(null);
var pageActivator = Mock.Of<IRazorPageActivator>();
var viewEngine = new RazorViewEngine(pageFactory.Object, pageActivator);
var viewEngine = new RazorViewEngine(pageFactory.Object,
Mock.Of<IRazorPageActivator>(),
Mock.Of<IViewStartProvider>());
return viewEngine;
}

View File

@ -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<IRazorPageFactory>(),
Mock.Of<IRazorPageActivator>(),
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<IRazorPageActivator>();
var view = new RazorView(Mock.Of<IRazorPageFactory>(),
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<IRazorPageFactory>(),
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<IRazorPageFactory>();
var viewStartProvider = CreateViewStartProvider();
var view = new RazorView(pageFactory.Object,
Mock.Of<IRazorPageActivator>(),
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<string>()), Times.Never());
Mock.Get(viewStartProvider).Verify(v => v.GetViewStartPages(It.IsAny<string>()), Times.Never());
}
[Fact]
@ -129,6 +135,7 @@ namespace Microsoft.AspNet.Mvc.Razor
});
var view = new RazorView(Mock.Of<IRazorPageFactory>(),
Mock.Of<IRazorPageActivator>(),
CreateViewStartProvider(),
page,
executeViewHierarchy: true);
var viewContext = CreateViewContext(view);
@ -152,6 +159,7 @@ namespace Microsoft.AspNet.Mvc.Razor
});
var view = new RazorView(Mock.Of<IRazorPageFactory>(),
Mock.Of<IRazorPageActivator>(),
CreateViewStartProvider(),
page,
executeViewHierarchy: true);
var viewContext = CreateViewContext(view);
@ -177,6 +185,49 @@ namespace Microsoft.AspNet.Mvc.Razor
.Verifiable();
var view = new RazorView(Mock.Of<IRazorPageFactory>(),
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<IRazorPageActivator>();
activator.Setup(a => a.Activate(viewStart1, It.IsAny<ViewContext>()))
.Verifiable();
activator.Setup(a => a.Activate(viewStart2, It.IsAny<ViewContext>()))
.Verifiable();
activator.Setup(a => a.Activate(page, It.IsAny<ViewContext>()))
.Verifiable();
var view = new RazorView(Mock.Of<IRazorPageFactory>(),
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<IRazorPageActivator>(),
CreateViewStartProvider(),
page,
executeViewHierarchy: true);
var viewContext = CreateViewContext(view);
@ -290,6 +343,7 @@ foot-content";
var view = new RazorView(pageFactory.Object,
Mock.Of<IRazorPageActivator>(),
CreateViewStartProvider(),
page,
executeViewHierarchy: true);
var viewContext = CreateViewContext(view);
@ -344,6 +398,7 @@ body-content";
var view = new RazorView(pageFactory.Object,
Mock.Of<IRazorPageActivator>(),
CreateViewStartProvider(),
page,
executeViewHierarchy: true);
var viewContext = CreateViewContext(view);
@ -357,8 +412,8 @@ body-content";
private static ViewContext CreateViewContext(RazorView view)
{
var httpContext = new Mock<HttpContext>();
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<IViewStartProvider>();
viewStartProvider.Setup(v => v.GetViewStartPages(It.IsAny<string>()))
.Returns(viewStartPages);
return viewStartProvider.Object;
}
private class TestableRazorPage : RazorPage
{
private readonly Action<TestableRazorPage> _executeAction;

View File

@ -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<IRazorPageFactory>());
// Act
var result = provider.GetViewStartLocations(viewPath);
// Assert
Assert.Empty(result);
}
public static IEnumerable<object[]> 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<string> expected)
{
// Arrange
var provider = new ViewStartProvider(GetAppEnv(appPath), Mock.Of<IRazorPageFactory>());
// Act
var result = provider.GetViewStartLocations(viewPath);
// Assert
Assert.Equal(expected, result);
}
private static IApplicationEnvironment GetAppEnv(string appPath)
{
var appEnv = new Mock<IApplicationEnvironment>();
appEnv.Setup(p => p.ApplicationBasePath)
.Returns(appPath);
return appEnv.Object;
}
}
}