From 340bd7550aa4352e16d5db1d586f2743237e5b38 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Fri, 18 Jul 2014 14:59:29 -0700 Subject: [PATCH] Adding RazorView to Microsoft.AspNet.Mvc.Razor RazorView was part of the previous commit but was separated to make it easier to see the diff in RazorPage that was formerly named RazorView Adding IRazorPage and changes per code review comments --- src/Microsoft.AspNet.Mvc.Razor/IRazorPage.cs | 48 +++++++ .../IRazorPageActivator.cs | 4 +- .../IRazorPageFactory.cs | 8 +- .../Microsoft.AspNet.Mvc.Razor.kproj | 3 +- src/Microsoft.AspNet.Mvc.Razor/RazorPage.cs | 11 +- .../RazorPageActivator.cs | 2 +- src/Microsoft.AspNet.Mvc.Razor/RazorView.cs | 118 ++++++++++++++++++ .../RazorViewEngine.cs | 2 +- ...tory.cs => VirtualPathRazorPageFactory.cs} | 16 +-- src/Microsoft.AspNet.Mvc/MvcServices.cs | 2 +- .../RazorViewTest.cs | 2 +- .../Views/ViewEngine/ViewWithFullPath.cshtml | 2 +- .../Views/ViewEngine/ViewWithLayout.cshtml | 2 +- .../ViewEngine/ViewWithNestedLayout.cshtml | 2 +- 14 files changed, 195 insertions(+), 27 deletions(-) create mode 100644 src/Microsoft.AspNet.Mvc.Razor/IRazorPage.cs create mode 100644 src/Microsoft.AspNet.Mvc.Razor/RazorView.cs rename src/Microsoft.AspNet.Mvc.Razor/{FileBasedRazorPageFactory.cs => VirtualPathRazorPageFactory.cs} (66%) 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