diff --git a/src/Microsoft.AspNet.Mvc.Razor/IRazorPage.cs b/src/Microsoft.AspNet.Mvc.Razor/IRazorPage.cs new file mode 100644 index 0000000000..bfa26714cd --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor/IRazorPage.cs @@ -0,0 +1,48 @@ +// 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.Threading.Tasks; + +namespace Microsoft.AspNet.Mvc.Razor +{ + /// + /// Represents properties and methods that are used by for execution. + /// + public interface IRazorPage + { + /// + /// Gets or sets the view context of the renderign view. + /// + ViewContext ViewContext { get; set; } + + string BodyContent { get; set; } + + /// + /// Gets or sets the path of a layout page. + /// + string Layout { get; set; } + + /// + /// Gets or sets the sections that can be rendered by this page. + /// + Dictionary PreviousSectionWriters { get; set; } + + /// + /// Gets the sections that are defined by this page. + /// + Dictionary SectionWriters { get; } + + /// + /// Renders the page and writes the output to the . + /// + /// A task representing the result of executing the page. + Task ExecuteAsync(); + + /// + /// Verifies that RenderBody is called and that RenderSection is called for all sections for a page that is + /// part of view execution hierarchy. + /// + void EnsureBodyAndSectionsWereRendered(); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor/IRazorPageActivator.cs b/src/Microsoft.AspNet.Mvc.Razor/IRazorPageActivator.cs index 99eb83c493..6faabe5be3 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/IRazorPageActivator.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/IRazorPageActivator.cs @@ -4,7 +4,7 @@ namespace Microsoft.AspNet.Mvc.Razor { /// - /// Provides methods to activate properties on a instance. + /// Provides methods to activate properties on a instance. /// public interface IRazorPageActivator { @@ -13,6 +13,6 @@ namespace Microsoft.AspNet.Mvc.Razor /// /// The page to activate. /// The for the executing view. - void Activate(RazorPage page, ViewContext context); + void Activate(IRazorPage page, ViewContext context); } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor/IRazorPageFactory.cs b/src/Microsoft.AspNet.Mvc.Razor/IRazorPageFactory.cs index 1953c83585..0e4ecbe9ce 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/IRazorPageFactory.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/IRazorPageFactory.cs @@ -4,15 +4,15 @@ namespace Microsoft.AspNet.Mvc.Razor { /// - /// Defines methods that are used for creating instances at a given path. + /// Defines methods that are used for creating instances at a given path. /// public interface IRazorPageFactory { /// - /// Creates a for the specified path. + /// Creates a for the specified path. /// /// The path to locate the RazorPage. - /// The RazorPage instance if it exists, null otherwise. - RazorPage CreateInstance(string viewPath); + /// The IRazorPage instance if it exists, null otherwise. + IRazorPage CreateInstance(string viewPath); } } 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 9d991f2d35..b3d6054eb8 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Microsoft.AspNet.Mvc.Razor.kproj +++ b/src/Microsoft.AspNet.Mvc.Razor/Microsoft.AspNet.Mvc.Razor.kproj @@ -29,7 +29,8 @@ - + + diff --git a/src/Microsoft.AspNet.Mvc.Razor/RazorPage.cs b/src/Microsoft.AspNet.Mvc.Razor/RazorPage.cs index a869f01f8b..fda790a69b 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/RazorPage.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/RazorPage.cs @@ -16,7 +16,7 @@ namespace Microsoft.AspNet.Mvc.Razor /// /// Represents properties and methods that are needed in order to render a view that uses Razor syntax. /// - public abstract class RazorPage + public abstract class RazorPage : IRazorPage { private readonly HashSet _renderedSections = new HashSet(StringComparer.OrdinalIgnoreCase); private bool _renderedBody; @@ -42,6 +42,7 @@ namespace Microsoft.AspNet.Mvc.Razor } } + /// public ViewContext ViewContext { get; set; } public string Layout { get; set; } @@ -86,10 +87,13 @@ namespace Microsoft.AspNet.Mvc.Razor public string BodyContent { get; set; } + /// public Dictionary PreviousSectionWriters { get; set; } + /// public Dictionary SectionWriters { get; private set; } + /// public abstract Task ExecuteAsync(); public virtual void Write(object value) @@ -292,10 +296,7 @@ namespace Microsoft.AspNet.Mvc.Razor } } - /// - /// Verifies that RenderBody is called and that RenderSection is called for all sections for a page that is - /// part of view execution hierarchy. - /// + /// public void EnsureBodyAndSectionsWereRendered() { // If PreviousSectionWriters is set, ensure all defined sections were rendered. diff --git a/src/Microsoft.AspNet.Mvc.Razor/RazorPageActivator.cs b/src/Microsoft.AspNet.Mvc.Razor/RazorPageActivator.cs index ffb424c3e6..9572a19892 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/RazorPageActivator.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/RazorPageActivator.cs @@ -27,7 +27,7 @@ namespace Microsoft.AspNet.Mvc.Razor } /// - public void Activate([NotNull] RazorPage page, [NotNull] ViewContext context) + public void Activate([NotNull] IRazorPage page, [NotNull] ViewContext context) { var activationInfo = _activationInfo.GetOrAdd(page.GetType(), CreateViewActivationInfo); diff --git a/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs b/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs new file mode 100644 index 0000000000..0982918fbc --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs @@ -0,0 +1,118 @@ +// 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.IO; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNet.Mvc.Rendering; + +namespace Microsoft.AspNet.Mvc.Razor +{ + /// + /// Represents a that executes one or more instances as part of + /// view rendering. + /// + public class RazorView : IView + { + private readonly IRazorPageFactory _pageFactory; + private readonly IRazorPageActivator _pageActivator; + private readonly IRazorPage _page; + private readonly bool _executeViewHierarchy; + + /// + /// Initializes a new instance of RazorView + /// + /// The page to execute + /// The view factory used to instantiate additional views. + /// The used to activate pages. + /// A value that indiciates whether the view hierarchy that involves + /// view start and layout pages are executed as part of the executing the page. + public RazorView([NotNull] IRazorPageFactory pageFactory, + [NotNull] IRazorPageActivator pageActivator, + [NotNull] IRazorPage page, + bool executeViewHierarchy) + { + _pageFactory = pageFactory; + _pageActivator = pageActivator; + _page = page; + _executeViewHierarchy = executeViewHierarchy; + } + + /// + public async Task RenderAsync([NotNull] ViewContext context) + { + if (_executeViewHierarchy) + { + var bodyContent = await RenderPageAsync(_page, context); + await RenderLayoutAsync(context, bodyContent); + } + else + { + await RenderPageCoreAsync(_page, context); + } + } + + private async Task RenderPageAsync(IRazorPage page, ViewContext context) + { + var contentBuilder = new StringBuilder(1024); + using (var bodyWriter = new StringWriter(contentBuilder)) + { + // The writer for the body is passed through the ViewContext, allowing things like HtmlHelpers + // and ViewComponents to reference it. + var oldWriter = context.Writer; + context.Writer = bodyWriter; + try + { + await RenderPageCoreAsync(page, context); + } + finally + { + context.Writer = oldWriter; + } + } + + return contentBuilder.ToString(); + } + + private async Task RenderPageCoreAsync(IRazorPage page, ViewContext context) + { + // Activating a page might mutate the ViewContext (for instance ViewContext.ViewData) is mutated by + // RazorPageActivator. We'll instead pass in a copy of the ViewContext. + var pageViewContext = new ViewContext(context, context.View, context.ViewData, context.Writer); + page.ViewContext = pageViewContext; + _pageActivator.Activate(page, pageViewContext); + + await page.ExecuteAsync(); + } + + private async Task RenderLayoutAsync(ViewContext context, + string bodyContent) + { + // A layout page can specify another layout page. We'll need to continue + // looking for layout pages until they're no longer specified. + var previousPage = _page; + while (!string.IsNullOrEmpty(previousPage.Layout)) + { + var layoutPage = _pageFactory.CreateInstance(previousPage.Layout); + if (layoutPage == null) + { + var message = Resources.FormatLayoutCannotBeLocated(previousPage.Layout); + throw new InvalidOperationException(message); + } + + layoutPage.PreviousSectionWriters = previousPage.SectionWriters; + layoutPage.BodyContent = bodyContent; + + bodyContent = await RenderPageAsync(layoutPage, context); + + // Verify that RenderBody is called, or that RenderSection is called for all sections + layoutPage.EnsureBodyAndSectionsWereRendered(); + + previousPage = layoutPage; + } + + await context.Writer.WriteAsync(bodyContent); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor/RazorViewEngine.cs b/src/Microsoft.AspNet.Mvc.Razor/RazorViewEngine.cs index 081be65b5c..4d36cf0bde 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/RazorViewEngine.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/RazorViewEngine.cs @@ -92,7 +92,7 @@ namespace Microsoft.AspNet.Mvc.Razor } } - private ViewEngineResult CreateFoundResult(RazorPage page, string viewName, bool partial) + private ViewEngineResult CreateFoundResult(IRazorPage page, string viewName, bool partial) { var view = new RazorView(_pageFactory, _viewActivator, diff --git a/src/Microsoft.AspNet.Mvc.Razor/FileBasedRazorPageFactory.cs b/src/Microsoft.AspNet.Mvc.Razor/VirtualPathRazorPageFactory.cs similarity index 66% rename from src/Microsoft.AspNet.Mvc.Razor/FileBasedRazorPageFactory.cs rename to src/Microsoft.AspNet.Mvc.Razor/VirtualPathRazorPageFactory.cs index 0cdd72eddf..aaeff501d3 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/FileBasedRazorPageFactory.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/VirtualPathRazorPageFactory.cs @@ -11,17 +11,17 @@ namespace Microsoft.AspNet.Mvc.Razor /// Represents a that creates instances /// from razor files in the file system. /// - public class FileBasedRazorPageFactory : IRazorPageFactory + public class VirtualPathRazorPageFactory : IRazorPageFactory { private readonly IRazorCompilationService _compilationService; private readonly ITypeActivator _activator; private readonly IServiceProvider _serviceProvider; private readonly IFileInfoCache _fileInfoCache; - public FileBasedRazorPageFactory(IRazorCompilationService compilationService, - ITypeActivator typeActivator, - IServiceProvider serviceProvider, - IFileInfoCache fileInfoCache) + public VirtualPathRazorPageFactory(IRazorCompilationService compilationService, + ITypeActivator typeActivator, + IServiceProvider serviceProvider, + IFileInfoCache fileInfoCache) { _compilationService = compilationService; _activator = typeActivator; @@ -30,14 +30,14 @@ namespace Microsoft.AspNet.Mvc.Razor } /// - public RazorPage CreateInstance([NotNull] string viewPath) + public IRazorPage CreateInstance([NotNull] string viewPath) { - var fileInfo = _fileInfoCache.GetFileInfo(viewPath.TrimStart('~')); + var fileInfo = _fileInfoCache.GetFileInfo(viewPath); if (fileInfo != null) { var result = _compilationService.Compile(fileInfo); - var page = (RazorPage)_activator.CreateInstance(_serviceProvider, result.CompiledType); + var page = (IRazorPage)_activator.CreateInstance(_serviceProvider, result.CompiledType); return page; } diff --git a/src/Microsoft.AspNet.Mvc/MvcServices.cs b/src/Microsoft.AspNet.Mvc/MvcServices.cs index f4927cc027..5b356f40a5 100644 --- a/src/Microsoft.AspNet.Mvc/MvcServices.cs +++ b/src/Microsoft.AspNet.Mvc/MvcServices.cs @@ -46,7 +46,7 @@ namespace Microsoft.AspNet.Mvc yield return describe.Singleton(); // Virtual path view factory needs to stay scoped so views can get get scoped services. - yield return describe.Scoped(); + yield return describe.Scoped(); yield return describe.Singleton(); yield return describe.Transient, diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs index ee350c61c1..dc4fb8deba 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs @@ -60,7 +60,7 @@ namespace Microsoft.AspNet.Mvc.Razor var expectedViewData = viewContext.ViewData; var expectedWriter = viewContext.Writer; activator.Setup(a => a.Activate(page, It.IsAny())) - .Callback((RazorPage p, ViewContext c) => + .Callback((IRazorPage p, ViewContext c) => { Assert.NotSame(c, viewContext); c.ViewData = viewData; diff --git a/test/WebSites/RazorWebSite/Views/ViewEngine/ViewWithFullPath.cshtml b/test/WebSites/RazorWebSite/Views/ViewEngine/ViewWithFullPath.cshtml index 0c334f0585..00b84b6ee8 100644 --- a/test/WebSites/RazorWebSite/Views/ViewEngine/ViewWithFullPath.cshtml +++ b/test/WebSites/RazorWebSite/Views/ViewEngine/ViewWithFullPath.cshtml @@ -1,4 +1,4 @@ @{ - Layout = "~/Views/Shared/_Layout.cshtml"; + Layout = "/Views/Shared/_Layout.cshtml"; } ViewWithFullPath-content \ No newline at end of file diff --git a/test/WebSites/RazorWebSite/Views/ViewEngine/ViewWithLayout.cshtml b/test/WebSites/RazorWebSite/Views/ViewEngine/ViewWithLayout.cshtml index 4904ee6996..917dcb1283 100644 --- a/test/WebSites/RazorWebSite/Views/ViewEngine/ViewWithLayout.cshtml +++ b/test/WebSites/RazorWebSite/Views/ViewEngine/ViewWithLayout.cshtml @@ -1,4 +1,4 @@ @{ - Layout = "~/Views/Shared/_Layout.cshtml"; + Layout = "/Views/Shared/_Layout.cshtml"; } ViewWithLayout-Content \ No newline at end of file diff --git a/test/WebSites/RazorWebSite/Views/ViewEngine/ViewWithNestedLayout.cshtml b/test/WebSites/RazorWebSite/Views/ViewEngine/ViewWithNestedLayout.cshtml index 93aa57fcfa..476c051ead 100644 --- a/test/WebSites/RazorWebSite/Views/ViewEngine/ViewWithNestedLayout.cshtml +++ b/test/WebSites/RazorWebSite/Views/ViewEngine/ViewWithNestedLayout.cshtml @@ -1,4 +1,4 @@ @{ - Layout = "~/Views/ViewEngine/_NestedLayout.cshtml"; + Layout = "/Views/ViewEngine/_NestedLayout.cshtml"; } ViewWithNestedLayout-Content \ No newline at end of file