From 9e535f6897749a401a29978329b426aad1cc4393 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Fri, 18 Jul 2014 14:58:12 -0700 Subject: [PATCH] Separating view execution and Razor behavior * Introducing RazorPage and RazorPageOfT that represent the Razor execution aspect of view execution. Moving view execution hierarchy behavior (Layout, partial views etc) into a separate RazorView type. * Renaming IVirtualPathViewFactory to IRazorPageFactory, IRazorViewActivator to IRazorPageActivator * Renaming VirtualPathViewFactor to FileBasedPageFactory to correctly reflect what it does. Fixes #814 --- Mvc.sln | 15 +- ...actory.cs => FileBasedRazorPageFactory.cs} | 23 +- ...iewActivator.cs => IRazorPageActivator.cs} | 12 +- .../IRazorPageFactory.cs | 18 + .../Microsoft.AspNet.Mvc.Razor.kproj | 13 +- .../Properties/Resources.Designer.cs | 16 + .../{RazorView.cs => RazorPage.cs} | 135 ++--- ...ViewActivator.cs => RazorPageActivator.cs} | 34 +- .../{RazorViewOfT.cs => RazorPageOfT.cs} | 17 +- .../{ViewEngine => }/RazorViewEngine.cs | 36 +- src/Microsoft.AspNet.Mvc.Razor/Resources.resx | 3 + .../ViewEngine/IVirtualPathViewFactory.cs | 12 - src/Microsoft.AspNet.Mvc/MvcServices.cs | 6 +- ...Microsoft.AspNet.Mvc.FunctionalTests.kproj | 1 + .../ViewEngineTests.cs | 89 +++ .../project.json | 1 + .../Microsoft.AspNet.Mvc.Razor.Test.kproj | 3 +- ...vatorTest.cs => RazorPageActivatorTest.cs} | 44 +- .../RazorPageTest.cs | 300 ++++++++++ .../RazorViewEngineTest.cs | 9 +- .../RazorViewTest.cs | 546 ++++++++++-------- .../Controllers/ViewEngineController.cs | 40 ++ test/WebSites/RazorWebSite/Models/Address.cs | 10 + test/WebSites/RazorWebSite/Models/Person.cs | 12 + test/WebSites/RazorWebSite/Project.json | 11 + test/WebSites/RazorWebSite/RazorWebSite.kproj | 43 ++ test/WebSites/RazorWebSite/Startup.cs | 23 + .../RazorWebSite/Views/Shared/_Layout.cshtml | 3 + .../RazorWebSite/Views/Shared/_Partial.cshtml | 2 + .../Views/ViewEngine/ViewWithFullPath.cshtml | 4 + .../Views/ViewEngine/ViewWithLayout.cshtml | 4 + .../ViewEngine/ViewWithNestedLayout.cshtml | 4 + .../Views/ViewEngine/ViewWithPartial.cshtml | 5 + .../Views/ViewEngine/ViewWithoutLayout.cshtml | 1 + .../Views/ViewEngine/_NestedLayout.cshtml | 7 + 35 files changed, 1077 insertions(+), 425 deletions(-) rename src/Microsoft.AspNet.Mvc.Razor/{ViewEngine/VirtualPathViewFactory.cs => FileBasedRazorPageFactory.cs} (53%) rename src/Microsoft.AspNet.Mvc.Razor/{IRazorViewActivator.cs => IRazorPageActivator.cs} (60%) create mode 100644 src/Microsoft.AspNet.Mvc.Razor/IRazorPageFactory.cs rename src/Microsoft.AspNet.Mvc.Razor/{RazorView.cs => RazorPage.cs} (78%) rename src/Microsoft.AspNet.Mvc.Razor/{RazorViewActivator.cs => RazorPageActivator.cs} (79%) rename src/Microsoft.AspNet.Mvc.Razor/{RazorViewOfT.cs => RazorPageOfT.cs} (53%) rename src/Microsoft.AspNet.Mvc.Razor/{ViewEngine => }/RazorViewEngine.cs (74%) delete mode 100644 src/Microsoft.AspNet.Mvc.Razor/ViewEngine/IVirtualPathViewFactory.cs create mode 100644 test/Microsoft.AspNet.Mvc.FunctionalTests/ViewEngineTests.cs rename test/Microsoft.AspNet.Mvc.Razor.Test/{RazorViewActivatorTest.cs => RazorPageActivatorTest.cs} (85%) create mode 100644 test/Microsoft.AspNet.Mvc.Razor.Test/RazorPageTest.cs create mode 100644 test/WebSites/RazorWebSite/Controllers/ViewEngineController.cs create mode 100644 test/WebSites/RazorWebSite/Models/Address.cs create mode 100644 test/WebSites/RazorWebSite/Models/Person.cs create mode 100644 test/WebSites/RazorWebSite/Project.json create mode 100644 test/WebSites/RazorWebSite/RazorWebSite.kproj create mode 100644 test/WebSites/RazorWebSite/Startup.cs create mode 100644 test/WebSites/RazorWebSite/Views/Shared/_Layout.cshtml create mode 100644 test/WebSites/RazorWebSite/Views/Shared/_Partial.cshtml create mode 100644 test/WebSites/RazorWebSite/Views/ViewEngine/ViewWithFullPath.cshtml create mode 100644 test/WebSites/RazorWebSite/Views/ViewEngine/ViewWithLayout.cshtml create mode 100644 test/WebSites/RazorWebSite/Views/ViewEngine/ViewWithNestedLayout.cshtml create mode 100644 test/WebSites/RazorWebSite/Views/ViewEngine/ViewWithPartial.cshtml create mode 100644 test/WebSites/RazorWebSite/Views/ViewEngine/ViewWithoutLayout.cshtml create mode 100644 test/WebSites/RazorWebSite/Views/ViewEngine/_NestedLayout.cshtml diff --git a/Mvc.sln b/Mvc.sln index 14f6eef714..ef7698c239 100644 --- a/Mvc.sln +++ b/Mvc.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 -VisualStudioVersion = 14.0.21813.0 +VisualStudioVersion = 14.0.21901.1 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{DAAE4C74-D06F-4874-A166-33305D2643CE}" EndProject @@ -51,6 +51,8 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Mvc.Test", EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "CompositeViewEngine", "test\WebSites\CompositeViewEngine\CompositeViewEngine.kproj", "{A853B2BA-4449-4908-A416-5A3C027FC22B}" EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "RazorWebSite", "test\WebSites\RazorWebSite\RazorWebSite.kproj", "{B07CAF59-11ED-40E3-A5DB-E1178F84FA78}" +EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "ValueProvidersSite", "test\WebSites\ValueProvidersSite\ValueProvidersSite.kproj", "{14F79E79-AE79-48FA-95DE-D794EF4EABB3}" EndProject Global @@ -263,6 +265,16 @@ Global {A853B2BA-4449-4908-A416-5A3C027FC22B}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {A853B2BA-4449-4908-A416-5A3C027FC22B}.Release|Mixed Platforms.Build.0 = Release|Any CPU {A853B2BA-4449-4908-A416-5A3C027FC22B}.Release|x86.ActiveCfg = Release|Any CPU + {B07CAF59-11ED-40E3-A5DB-E1178F84FA78}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B07CAF59-11ED-40E3-A5DB-E1178F84FA78}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B07CAF59-11ED-40E3-A5DB-E1178F84FA78}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {B07CAF59-11ED-40E3-A5DB-E1178F84FA78}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {B07CAF59-11ED-40E3-A5DB-E1178F84FA78}.Debug|x86.ActiveCfg = Debug|Any CPU + {B07CAF59-11ED-40E3-A5DB-E1178F84FA78}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B07CAF59-11ED-40E3-A5DB-E1178F84FA78}.Release|Any CPU.Build.0 = Release|Any CPU + {B07CAF59-11ED-40E3-A5DB-E1178F84FA78}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {B07CAF59-11ED-40E3-A5DB-E1178F84FA78}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {B07CAF59-11ED-40E3-A5DB-E1178F84FA78}.Release|x86.ActiveCfg = Release|Any CPU {14F79E79-AE79-48FA-95DE-D794EF4EABB3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {14F79E79-AE79-48FA-95DE-D794EF4EABB3}.Debug|Any CPU.Build.0 = Debug|Any CPU {14F79E79-AE79-48FA-95DE-D794EF4EABB3}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU @@ -299,6 +311,7 @@ Global {42CDBF4A-E238-4C0F-A416-44588363EB4C} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C} {5F945B82-FE5F-425C-956C-8BC2F2020254} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1} {A853B2BA-4449-4908-A416-5A3C027FC22B} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C} + {B07CAF59-11ED-40E3-A5DB-E1178F84FA78} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C} {14F79E79-AE79-48FA-95DE-D794EF4EABB3} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C} EndGlobalSection EndGlobal diff --git a/src/Microsoft.AspNet.Mvc.Razor/ViewEngine/VirtualPathViewFactory.cs b/src/Microsoft.AspNet.Mvc.Razor/FileBasedRazorPageFactory.cs similarity index 53% rename from src/Microsoft.AspNet.Mvc.Razor/ViewEngine/VirtualPathViewFactory.cs rename to src/Microsoft.AspNet.Mvc.Razor/FileBasedRazorPageFactory.cs index 3a8884a404..0cdd72eddf 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/ViewEngine/VirtualPathViewFactory.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/FileBasedRazorPageFactory.cs @@ -3,22 +3,25 @@ using System; using Microsoft.AspNet.Mvc.Core; -using Microsoft.AspNet.Mvc.Rendering; using Microsoft.Framework.DependencyInjection; namespace Microsoft.AspNet.Mvc.Razor { - public class VirtualPathViewFactory : IVirtualPathViewFactory + /// + /// Represents a that creates instances + /// from razor files in the file system. + /// + public class FileBasedRazorPageFactory : IRazorPageFactory { private readonly IRazorCompilationService _compilationService; private readonly ITypeActivator _activator; private readonly IServiceProvider _serviceProvider; private readonly IFileInfoCache _fileInfoCache; - public VirtualPathViewFactory(IRazorCompilationService compilationService, - ITypeActivator typeActivator, - IServiceProvider serviceProvider, - IFileInfoCache fileInfoCache) + public FileBasedRazorPageFactory(IRazorCompilationService compilationService, + ITypeActivator typeActivator, + IServiceProvider serviceProvider, + IFileInfoCache fileInfoCache) { _compilationService = compilationService; _activator = typeActivator; @@ -26,14 +29,16 @@ namespace Microsoft.AspNet.Mvc.Razor _fileInfoCache = fileInfoCache; } - public IView CreateInstance([NotNull] string virtualPath) + /// + public RazorPage CreateInstance([NotNull] string viewPath) { - var fileInfo = _fileInfoCache.GetFileInfo(virtualPath); + var fileInfo = _fileInfoCache.GetFileInfo(viewPath.TrimStart('~')); if (fileInfo != null) { var result = _compilationService.Compile(fileInfo); - return (IView)_activator.CreateInstance(_serviceProvider, result.CompiledType); + var page = (RazorPage)_activator.CreateInstance(_serviceProvider, result.CompiledType); + return page; } return null; diff --git a/src/Microsoft.AspNet.Mvc.Razor/IRazorViewActivator.cs b/src/Microsoft.AspNet.Mvc.Razor/IRazorPageActivator.cs similarity index 60% rename from src/Microsoft.AspNet.Mvc.Razor/IRazorViewActivator.cs rename to src/Microsoft.AspNet.Mvc.Razor/IRazorPageActivator.cs index e3b2966170..99eb83c493 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/IRazorViewActivator.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/IRazorPageActivator.cs @@ -4,15 +4,15 @@ namespace Microsoft.AspNet.Mvc.Razor { /// - /// Provides methods to activate properties on a view instance. + /// Provides methods to activate properties on a instance. /// - public interface IRazorViewActivator + public interface IRazorPageActivator { /// - /// When implemented in a type, activates an instantiated view. + /// When implemented in a type, activates an instantiated page. /// - /// The view to activate. - /// The for the view. - void Activate(RazorView view, ViewContext context); + /// The page to activate. + /// The for the executing view. + void Activate(RazorPage 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 new file mode 100644 index 0000000000..1953c83585 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor/IRazorPageFactory.cs @@ -0,0 +1,18 @@ +// 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. + +namespace Microsoft.AspNet.Mvc.Razor +{ + /// + /// Defines methods that are used for creating instances at a given path. + /// + public interface IRazorPageFactory + { + /// + /// Creates a for the specified path. + /// + /// The path to locate the RazorPage. + /// The RazorPage instance if it exists, null otherwise. + RazorPage 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 86c67fcad0..9d991f2d35 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Microsoft.AspNet.Mvc.Razor.kproj +++ b/src/Microsoft.AspNet.Mvc.Razor/Microsoft.AspNet.Mvc.Razor.kproj @@ -29,19 +29,20 @@ + - + + + + - - + + - - - \ 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 0187e832a0..21ff13315a 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/Properties/Resources.Designer.cs @@ -202,6 +202,22 @@ namespace Microsoft.AspNet.Mvc.Razor return string.Format(CultureInfo.CurrentCulture, GetString("ViewCannotBeActivated"), p0, p1); } + /// + /// '{0} must be set to access '{1}'. + /// + internal static string ViewContextMustBeSet + { + get { return GetString("ViewContextMustBeSet"); } + } + + /// + /// '{0} must be set to access '{1}'. + /// + internal static string FormatViewContextMustBeSet(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("ViewContextMustBeSet"), p0, p1); + } + /// /// View '{0}' must have extension '{1}' when the view represents a full path. /// diff --git a/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs b/src/Microsoft.AspNet.Mvc.Razor/RazorPage.cs similarity index 78% rename from src/Microsoft.AspNet.Mvc.Razor/RazorView.cs rename to src/Microsoft.AspNet.Mvc.Razor/RazorPage.cs index 2615dae58f..a869f01f8b 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/RazorPage.cs @@ -7,19 +7,25 @@ using System.IO; using System.Linq; using System.Net; using System.Security.Principal; -using System.Text; using System.Threading.Tasks; using Microsoft.AspNet.Http; using Microsoft.AspNet.Mvc.Rendering; -using Microsoft.Framework.DependencyInjection; namespace Microsoft.AspNet.Mvc.Razor { - public abstract class RazorView : IView + /// + /// Represents properties and methods that are needed in order to render a view that uses Razor syntax. + /// + public abstract class RazorPage { private readonly HashSet _renderedSections = new HashSet(StringComparer.OrdinalIgnoreCase); private bool _renderedBody; + public RazorPage() + { + SectionWriters = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + [Activate] public IUrlHelper Url { get; set; } @@ -40,7 +46,22 @@ namespace Microsoft.AspNet.Mvc.Razor public string Layout { get; set; } - protected TextWriter Output { get; set; } + /// + /// Gets the TextWriter that the page is writing output to. + /// + public virtual TextWriter Output + { + get + { + if (ViewContext == null) + { + var message = Resources.FormatViewContextMustBeSet("ViewContext", "Output"); + throw new InvalidOperationException(message); + } + + return ViewContext.Writer; + } + } public virtual IPrincipal User { @@ -63,66 +84,11 @@ namespace Microsoft.AspNet.Mvc.Razor } } - private string BodyContent { get; set; } + public string BodyContent { get; set; } - private Dictionary SectionWriters { get; set; } + public Dictionary PreviousSectionWriters { get; set; } - private Dictionary PreviousSectionWriters { get; set; } - - public virtual async Task RenderAsync([NotNull] ViewContext context) - { - SectionWriters = new Dictionary(StringComparer.OrdinalIgnoreCase); - ViewContext = context; - - var contentBuilder = new StringBuilder(1024); - using (var bodyWriter = new StringWriter(contentBuilder)) - { - Output = bodyWriter; - - // The writer for the body is passed through the ViewContext, allowing things like HtmlHelpers - // and ViewComponents to reference it. - var oldWriter = context.Writer; - - try - { - context.Writer = bodyWriter; - await ExecuteAsync(); - - // Verify that RenderBody is called, or that RenderSection is called for all sections - VerifyRenderedBodyOrSections(); - } - finally - { - context.Writer = oldWriter; - } - } - - var bodyContent = contentBuilder.ToString(); - if (!string.IsNullOrEmpty(Layout)) - { - await RenderLayoutAsync(context, bodyContent); - } - else - { - await context.Writer.WriteAsync(bodyContent); - } - } - - private async Task RenderLayoutAsync(ViewContext context, string bodyContent) - { - var virtualPathFactory = context.HttpContext.RequestServices.GetService(); - var layoutView = (RazorView)virtualPathFactory.CreateInstance(Layout); - - if (layoutView == null) - { - var message = Resources.FormatLayoutCannotBeLocated(Layout); - throw new InvalidOperationException(message); - } - - layoutView.PreviousSectionWriters = SectionWriters; - layoutView.BodyContent = bodyContent; - await layoutView.RenderAsync(context); - } + public Dictionary SectionWriters { get; private set; } public abstract Task ExecuteAsync(); @@ -326,6 +292,32 @@ 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. + if (PreviousSectionWriters != null) + { + var sectionsNotRendered = PreviousSectionWriters.Keys.Except(_renderedSections, + StringComparer.OrdinalIgnoreCase); + if (sectionsNotRendered.Any()) + { + var sectionNames = string.Join(", ", sectionsNotRendered); + throw new InvalidOperationException(Resources.FormatSectionsNotRendered(sectionNames)); + } + } + + // If BodyContent is set, ensure it was rendered. + if (BodyContent != null && !_renderedBody) + { + // If a body was defined, then RenderBody should have been called. + throw new InvalidOperationException(Resources.FormatRenderBodyNotCalled("RenderBody")); + } + } + private void EnsureMethodCanBeInvoked(string methodName) { if (PreviousSectionWriters == null) @@ -333,24 +325,5 @@ namespace Microsoft.AspNet.Mvc.Razor throw new InvalidOperationException(Resources.FormatView_MethodCannotBeCalled(methodName)); } } - - private void VerifyRenderedBodyOrSections() - { - if (BodyContent != null) - { - var sectionsNotRendered = PreviousSectionWriters.Keys.Except(_renderedSections, - StringComparer.OrdinalIgnoreCase); - if (sectionsNotRendered.Any()) - { - var sectionNames = String.Join(", ", sectionsNotRendered); - throw new InvalidOperationException(Resources.FormatSectionsNotRendered(sectionNames)); - } - else if (!_renderedBody) - { - // If a body was defined, then RenderBody should have been called. - throw new InvalidOperationException(Resources.FormatRenderBodyNotCalled("RenderBody")); - } - } - } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor/RazorViewActivator.cs b/src/Microsoft.AspNet.Mvc.Razor/RazorPageActivator.cs similarity index 79% rename from src/Microsoft.AspNet.Mvc.Razor/RazorViewActivator.cs rename to src/Microsoft.AspNet.Mvc.Razor/RazorPageActivator.cs index 83752dc4f5..ffb424c3e6 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/RazorViewActivator.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/RazorPageActivator.cs @@ -10,30 +10,26 @@ using Microsoft.Framework.DependencyInjection; namespace Microsoft.AspNet.Mvc.Razor { /// - public class RazorViewActivator : IRazorViewActivator + public class RazorPageActivator : IRazorPageActivator { - // Name of the "public TModel Model" property on RazorView + // Name of the "public TModel Model" property on RazorPage private const string ModelPropertyName = "Model"; private readonly ITypeActivator _typeActivator; - private readonly ConcurrentDictionary _activationInfo; + private readonly ConcurrentDictionary _activationInfo; /// - /// Initializes a new instance of the RazorViewActivator class. + /// Initializes a new instance of the class. /// - public RazorViewActivator(ITypeActivator typeActivator) + public RazorPageActivator(ITypeActivator typeActivator) { _typeActivator = typeActivator; - _activationInfo = new ConcurrentDictionary(); + _activationInfo = new ConcurrentDictionary(); } - /// - /// Activates the specified view by using the specified ViewContext. - /// - /// The view to activate. - /// The ViewContext for the executing view. - public void Activate([NotNull] RazorView view, [NotNull] ViewContext context) + /// + public void Activate([NotNull] RazorPage page, [NotNull] ViewContext context) { - var activationInfo = _activationInfo.GetOrAdd(view.GetType(), + var activationInfo = _activationInfo.GetOrAdd(page.GetType(), CreateViewActivationInfo); context.ViewData = CreateViewDataDictionary(context, activationInfo); @@ -41,11 +37,11 @@ namespace Microsoft.AspNet.Mvc.Razor for (var i = 0; i < activationInfo.PropertyActivators.Length; i++) { var activateInfo = activationInfo.PropertyActivators[i]; - activateInfo.Activate(view, context); + activateInfo.Activate(page, context); } } - private ViewDataDictionary CreateViewDataDictionary(ViewContext context, ViewActivationInfo activationInfo) + private ViewDataDictionary CreateViewDataDictionary(ViewContext context, PageActivationInfo activationInfo) { // Create a ViewDataDictionary if the ViewContext.ViewData is not set or the type of // ViewContext.ViewData is an incompatibile type. @@ -66,10 +62,10 @@ namespace Microsoft.AspNet.Mvc.Razor return context.ViewData; } - private ViewActivationInfo CreateViewActivationInfo(Type type) + private PageActivationInfo CreateViewActivationInfo(Type type) { // Look for a property named "Model". If it is non-null, we'll assume this is - // the equivalent of TModel Model property on RazorView + // the equivalent of TModel Model property on RazorPage var modelProperty = type.GetRuntimeProperty(ModelPropertyName); if (modelProperty == null) { @@ -80,7 +76,7 @@ namespace Microsoft.AspNet.Mvc.Razor var modelType = modelProperty.PropertyType; var viewDataType = typeof(ViewDataDictionary<>).MakeGenericType(modelType); - return new ViewActivationInfo + return new PageActivationInfo { ViewDataDictionaryType = viewDataType, PropertyActivators = PropertyActivator.GetPropertiesToActivate(type, @@ -115,7 +111,7 @@ namespace Microsoft.AspNet.Mvc.Razor return new PropertyActivator(property, valueAccessor); } - private class ViewActivationInfo + private class PageActivationInfo { public PropertyActivator[] PropertyActivators { get; set; } diff --git a/src/Microsoft.AspNet.Mvc.Razor/RazorViewOfT.cs b/src/Microsoft.AspNet.Mvc.Razor/RazorPageOfT.cs similarity index 53% rename from src/Microsoft.AspNet.Mvc.Razor/RazorViewOfT.cs rename to src/Microsoft.AspNet.Mvc.Razor/RazorPageOfT.cs index 0523215dc5..f895cb794c 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/RazorViewOfT.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/RazorPageOfT.cs @@ -1,12 +1,13 @@ // 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.Threading.Tasks; -using Microsoft.Framework.DependencyInjection; - namespace Microsoft.AspNet.Mvc.Razor { - public abstract class RazorView : RazorView + /// + /// Represents the properties and methods that are needed in order to render a view that uses Razor syntax. + /// + /// The type of the view data model. + public abstract class RazorPage : RazorPage { public TModel Model { @@ -18,13 +19,5 @@ namespace Microsoft.AspNet.Mvc.Razor [Activate] public ViewDataDictionary ViewData { get; set; } - - public override Task RenderAsync([NotNull] ViewContext context) - { - var viewActivator = context.HttpContext.RequestServices.GetService(); - viewActivator.Activate(this, context); - - return base.RenderAsync(context); - } } } diff --git a/src/Microsoft.AspNet.Mvc.Razor/ViewEngine/RazorViewEngine.cs b/src/Microsoft.AspNet.Mvc.Razor/RazorViewEngine.cs similarity index 74% rename from src/Microsoft.AspNet.Mvc.Razor/ViewEngine/RazorViewEngine.cs rename to src/Microsoft.AspNet.Mvc.Razor/RazorViewEngine.cs index 42f1ca483d..081be65b5c 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/ViewEngine/RazorViewEngine.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/RazorViewEngine.cs @@ -26,11 +26,14 @@ namespace Microsoft.AspNet.Mvc.Razor "/Views/Shared/{0}" + ViewExtension, }; - private readonly IVirtualPathViewFactory _virtualPathFactory; + private readonly IRazorPageFactory _pageFactory; + private readonly IRazorPageActivator _viewActivator; - public RazorViewEngine(IVirtualPathViewFactory virtualPathFactory) + public RazorViewEngine(IRazorPageFactory pageFactory, + IRazorPageActivator viewActivator) { - _virtualPathFactory = virtualPathFactory; + _pageFactory = pageFactory; + _viewActivator = viewActivator; } public IEnumerable ViewLocationFormats @@ -41,18 +44,19 @@ namespace Microsoft.AspNet.Mvc.Razor public ViewEngineResult FindView([NotNull] IDictionary context, [NotNull] string viewName) { - var viewEngineResult = CreateViewEngineResult(context, viewName); + var viewEngineResult = CreateViewEngineResult(context, viewName, partial: false); return viewEngineResult; } public ViewEngineResult FindPartialView([NotNull] IDictionary context, [NotNull] string partialViewName) { - return FindView(context, partialViewName); + return CreateViewEngineResult(context, partialViewName, partial: true); } private ViewEngineResult CreateViewEngineResult([NotNull] IDictionary context, - [NotNull] string viewName) + [NotNull] string viewName, + bool partial) { var nameRepresentsPath = IsSpecificPath(viewName); @@ -64,8 +68,9 @@ namespace Microsoft.AspNet.Mvc.Razor Resources.FormatViewMustEndInExtension(viewName, ViewExtension)); } - var view = _virtualPathFactory.CreateInstance(viewName); - return view != null ? ViewEngineResult.Found(viewName, view) : + var page = _pageFactory.CreateInstance(viewName); + + return page != null ? CreateFoundResult(page, viewName, partial) : ViewEngineResult.NotFound(viewName, new[] { viewName }); } else @@ -76,10 +81,10 @@ namespace Microsoft.AspNet.Mvc.Razor foreach (var path in potentialPaths) { - var view = _virtualPathFactory.CreateInstance(path); - if (view != null) + var page = _pageFactory.CreateInstance(path); + if (page != null) { - return ViewEngineResult.Found(viewName, view); + return CreateFoundResult(page, path, partial); } } @@ -87,6 +92,15 @@ namespace Microsoft.AspNet.Mvc.Razor } } + private ViewEngineResult CreateFoundResult(RazorPage page, string viewName, bool partial) + { + var view = new RazorView(_pageFactory, + _viewActivator, + page, + executeViewHierarchy: !partial); + return ViewEngineResult.Found(viewName, view); + } + private static bool IsSpecificPath(string name) { return name[0] == '~' || name[0] == '/'; diff --git a/src/Microsoft.AspNet.Mvc.Razor/Resources.resx b/src/Microsoft.AspNet.Mvc.Razor/Resources.resx index 208fa3dbcd..767a9ac8df 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Resources.resx +++ b/src/Microsoft.AspNet.Mvc.Razor/Resources.resx @@ -153,6 +153,9 @@ View of type '{0}' cannot be activated by '{1}'. + + '{0} must be set to access '{1}'. + View '{0}' must have extension '{1}' when the view represents a full path. diff --git a/src/Microsoft.AspNet.Mvc.Razor/ViewEngine/IVirtualPathViewFactory.cs b/src/Microsoft.AspNet.Mvc.Razor/ViewEngine/IVirtualPathViewFactory.cs deleted file mode 100644 index 293347dbab..0000000000 --- a/src/Microsoft.AspNet.Mvc.Razor/ViewEngine/IVirtualPathViewFactory.cs +++ /dev/null @@ -1,12 +0,0 @@ -// 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 -{ - public interface IVirtualPathViewFactory - { - IView CreateInstance(string virtualPath); - } -} diff --git a/src/Microsoft.AspNet.Mvc/MvcServices.cs b/src/Microsoft.AspNet.Mvc/MvcServices.cs index 9e9a02e729..f4927cc027 100644 --- a/src/Microsoft.AspNet.Mvc/MvcServices.cs +++ b/src/Microsoft.AspNet.Mvc/MvcServices.cs @@ -36,7 +36,7 @@ namespace Microsoft.AspNet.Mvc yield return describe.Transient(); yield return describe.Transient(); - yield return describe.Instance(new MvcRazorHost(typeof(RazorView).FullName)); + yield return describe.Instance(new MvcRazorHost(typeof(RazorPage).FullName)); yield return describe.Transient(); @@ -44,9 +44,9 @@ namespace Microsoft.AspNet.Mvc 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. - yield return describe.Scoped(); + yield return describe.Scoped(); yield return describe.Singleton(); yield return describe.Transient, diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/Microsoft.AspNet.Mvc.FunctionalTests.kproj b/test/Microsoft.AspNet.Mvc.FunctionalTests/Microsoft.AspNet.Mvc.FunctionalTests.kproj index 76668c07e9..9ff61cc4c7 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/Microsoft.AspNet.Mvc.FunctionalTests.kproj +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/Microsoft.AspNet.Mvc.FunctionalTests.kproj @@ -30,6 +30,7 @@ + diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/ViewEngineTests.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/ViewEngineTests.cs new file mode 100644 index 0000000000..266ac264c2 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/ViewEngineTests.cs @@ -0,0 +1,89 @@ +// 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.Threading.Tasks; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.TestHost; +using RazorWebSite; +using Xunit; + +namespace Microsoft.AspNet.Mvc.FunctionalTests +{ + public class ViewEngineTests + { + private readonly IServiceProvider _provider = TestHelper.CreateServices("RazorWebSite"); + private readonly Action _app = new Startup().Configure; + + public static IEnumerable RazorView_ExecutesPageAndLayoutData + { + get + { + yield return new[] { "ViewWithoutLayout", @"ViewWithoutLayout-Content" }; + yield return new[] + { + "ViewWithLayout", +@" + +ViewWithLayout-Content +" + }; + yield return new[] + { + "ViewWithFullPath", +@" + +ViewWithFullPath-content +" + }; + yield return new[] + { + "ViewWithNestedLayout", +@" + + +/ViewEngine/ViewWithNestedLayout + +ViewWithNestedLayout-Content + +" + }; + } + } + + [Theory] + [MemberData("RazorView_ExecutesPageAndLayoutData")] + public async Task RazorView_ExecutesPageAndLayout(string actionName, string expected) + { + var server = TestServer.Create(_provider, _app); + var client = server.Handler; + + // Act + var result = await client.GetAsync("http://localhost/ViewEngine/" + actionName); + + // Assert + var body = await result.HttpContext.Response.ReadBodyAsStringAsync(); + Assert.Equal(expected, body.Trim()); + } + + [Fact] + public async Task RazorView_ExecutesPartialPagesWithCorrectContext() + { + var expected = +@"98052 + + +test-value"; + var server = TestServer.Create(_provider, _app); + var client = server.Handler; + + // Act + var result = await client.GetAsync("http://localhost/ViewEngine/ViewWithPartial"); + + // Assert + var body = await result.HttpContext.Response.ReadBodyAsStringAsync(); + Assert.Equal(expected, body.Trim()); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json b/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json index 32e567943e..717d9af643 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json @@ -14,6 +14,7 @@ "Microsoft.Framework.DependencyInjection": "1.0.0-*", "Microsoft.Framework.Runtime.Interfaces": "1.0.0-*", "RoutingWebSite": "", + "RazorWebSite": "", "ValueProvidersSite": "", "Xunit.KRunner": "1.0.0-*" }, 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 4bafa70d9c..f4790a60fd 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 @@ -23,8 +23,9 @@ - + + diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewActivatorTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorPageActivatorTest.cs similarity index 85% rename from test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewActivatorTest.cs rename to test/Microsoft.AspNet.Mvc.Razor.Test/RazorPageActivatorTest.cs index 3e9c788fc3..0dd6989741 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewActivatorTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorPageActivatorTest.cs @@ -15,14 +15,14 @@ using Xunit; namespace Microsoft.AspNet.Mvc.Razor { - public class RazorViewActivatorTest + public class RazorPageActivatorTest { [Fact] public void Activate_ActivatesAndContextualizesPropertiesOnViews() { // Arrange - var activator = new RazorViewActivator(Mock.Of()); - var instance = new TestView(); + var activator = new RazorPageActivator(Mock.Of()); + var instance = new TestRazorPage(); var myService = new MyService(); var helper = Mock.Of>(); @@ -37,7 +37,7 @@ namespace Microsoft.AspNet.Mvc.Razor var routeContext = new RouteContext(httpContext.Object); var actionContext = new ActionContext(routeContext, new ActionDescriptor()); var viewContext = new ViewContext(actionContext, - instance, + Mock.Of(), new ViewDataDictionary(Mock.Of()), TextWriter.Null); @@ -55,8 +55,8 @@ namespace Microsoft.AspNet.Mvc.Razor public void Activate_ThrowsIfTheViewDoesNotDeriveFromRazorViewOfT() { // Arrange - var activator = new RazorViewActivator(Mock.Of()); - var instance = new DoesNotDeriveFromRazorViewOfT(); + var activator = new RazorPageActivator(Mock.Of()); + var instance = new DoesNotDeriveFromRazorPageOfT(); var myService = new MyService(); var helper = Mock.Of>(); @@ -67,7 +67,7 @@ namespace Microsoft.AspNet.Mvc.Razor var routeContext = new RouteContext(httpContext.Object); var actionContext = new ActionContext(routeContext, new ActionDescriptor()); var viewContext = new ViewContext(actionContext, - instance, + Mock.Of(), new ViewDataDictionary(Mock.Of()), TextWriter.Null); @@ -76,7 +76,7 @@ namespace Microsoft.AspNet.Mvc.Razor var message = string.Format(CultureInfo.InvariantCulture, "View of type '{0}' cannot be activated by '{1}'.", instance.GetType().FullName, - typeof(RazorViewActivator).FullName); + typeof(RazorPageActivator).FullName); Assert.Equal(message, ex.Message); } @@ -86,8 +86,8 @@ namespace Microsoft.AspNet.Mvc.Razor { // Arrange var typeActivator = new TypeActivator(); - var activator = new RazorViewActivator(typeActivator); - var instance = new TestView(); + var activator = new RazorPageActivator(typeActivator); + var instance = new TestRazorPage(); var myService = new MyService(); var helper = Mock.Of>(); @@ -106,7 +106,7 @@ namespace Microsoft.AspNet.Mvc.Razor Model = new MyModel() }; var viewContext = new ViewContext(actionContext, - instance, + Mock.Of(), viewData, TextWriter.Null); @@ -122,8 +122,8 @@ namespace Microsoft.AspNet.Mvc.Razor { // Arrange var typeActivator = new TypeActivator(); - var activator = new RazorViewActivator(typeActivator); - var instance = new TestView(); + var activator = new RazorPageActivator(typeActivator); + var instance = new TestRazorPage(); var myService = new MyService(); var helper = Mock.Of>(); var serviceProvider = new Mock(); @@ -141,7 +141,7 @@ namespace Microsoft.AspNet.Mvc.Razor Model = new MyModel() }; var viewContext = new ViewContext(actionContext, - instance, + Mock.Of(), viewData, TextWriter.Null); @@ -157,8 +157,8 @@ namespace Microsoft.AspNet.Mvc.Razor { // Arrange var typeActivator = new TypeActivator(); - var activator = new RazorViewActivator(typeActivator); - var instance = new DoesNotDeriveFromRazorViewOfTButHasModelProperty(); + var activator = new RazorPageActivator(typeActivator); + var instance = new DoesNotDeriveFromRazorPageOfTButHasModelProperty(); var myService = new MyService(); var helper = Mock.Of>(); var serviceProvider = new Mock(); @@ -173,7 +173,7 @@ namespace Microsoft.AspNet.Mvc.Razor var actionContext = new ActionContext(routeContext, new ActionDescriptor()); var viewData = new ViewDataDictionary(Mock.Of()); var viewContext = new ViewContext(actionContext, - instance, + Mock.Of(), viewData, TextWriter.Null); @@ -184,7 +184,7 @@ namespace Microsoft.AspNet.Mvc.Razor Assert.IsType>(viewContext.ViewData); } - private abstract class TestViewBase : RazorView + private abstract class TestPageBase : RazorPage { [Activate] public MyService MyService { get; set; } @@ -192,7 +192,7 @@ namespace Microsoft.AspNet.Mvc.Razor public MyService MyService2 { get; set; } } - private class TestView : TestViewBase + private class TestRazorPage : TestPageBase { [Activate] internal IHtmlHelper Html { get; private set; } @@ -203,11 +203,11 @@ namespace Microsoft.AspNet.Mvc.Razor } } - private abstract class DoesNotDeriveFromRazorViewOfTBase : RazorView + private abstract class DoesNotDeriveFromRazorPageOfTBase : RazorPage { } - private class DoesNotDeriveFromRazorViewOfT : DoesNotDeriveFromRazorViewOfTBase + private class DoesNotDeriveFromRazorPageOfT : DoesNotDeriveFromRazorPageOfTBase { public override Task ExecuteAsync() { @@ -215,7 +215,7 @@ namespace Microsoft.AspNet.Mvc.Razor } } - private class DoesNotDeriveFromRazorViewOfTButHasModelProperty : DoesNotDeriveFromRazorViewOfTBase + private class DoesNotDeriveFromRazorPageOfTButHasModelProperty : DoesNotDeriveFromRazorPageOfTBase { public string Model { get; set; } diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorPageTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorPageTest.cs new file mode 100644 index 0000000000..b59cd388b3 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorPageTest.cs @@ -0,0 +1,300 @@ +// 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.Threading.Tasks; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Mvc.Rendering; +using Microsoft.AspNet.Testing; +using Moq; +using Xunit; + +namespace Microsoft.AspNet.Mvc.Razor +{ + public class RazorPageTest + { + [Fact] + public async Task DefineSection_ThrowsIfSectionIsAlreadyDefined() + { + // Arrange + var viewContext = CreateViewContext(); + var page = CreatePage(v => + { + v.DefineSection("qux", new HelperResult(action: null)); + v.DefineSection("qux", new HelperResult(action: null)); + }); + + // Act + var ex = await Assert.ThrowsAsync( + () => page.ExecuteAsync()); + + // Assert + Assert.Equal("Section 'qux' is already defined.", ex.Message); + } + + [Fact] + public async Task RenderSection_RendersSectionFromPreviousPage() + { + // Arrange + var expected = new HelperResult(action: null); + var viewContext = CreateViewContext(); + HelperResult actual = null; + var page = CreatePage(v => + { + actual = v.RenderSection("bar"); + }); + page.PreviousSectionWriters = new Dictionary + { + { "bar", expected } + }; + + // Act + await page.ExecuteAsync(); + + // Assert + Assert.Same(actual, expected); + } + + [Fact] + public async Task RenderSection_ThrowsIfPreviousSectionWritersIsNotSet() + { + // Arrange + Exception ex = null; + var page = CreatePage(v => + { + ex = Assert.Throws(() => v.RenderSection("bar")); + }); + + // Act + await page.ExecuteAsync(); + + // Assert + Assert.Equal("The method 'RenderSection' cannot be invoked by this view.", + ex.Message); + } + + [Fact] + public async Task RenderSection_ThrowsIfRequiredSectionIsNotFound() + { + // Arrange + var expected = new HelperResult(action: null); + var page = CreatePage(v => + { + v.RenderSection("bar"); + }); + page.PreviousSectionWriters = new Dictionary + { + { "baz", expected } + }; + + // Act + var ex = await Assert.ThrowsAsync(() => page.ExecuteAsync()); + + // Assert + Assert.Equal("Section 'bar' is not defined.", ex.Message); + } + + [Fact] + public void IsSectionDefined_ThrowsIfPreviousSectionWritersIsNotRegistered() + { + // Arrange + var page = CreatePage(v => { }); + + // Act and Assert + ExceptionAssert.Throws(() => page.IsSectionDefined("foo"), + "The method 'IsSectionDefined' cannot be invoked by this view."); + } + + [Fact] + public async Task IsSectionDefined_ReturnsFalseIfSectionNotDefined() + { + // Arrange + bool? actual = null; + var page = CreatePage(v => + { + actual = v.IsSectionDefined("foo"); + v.RenderSection("baz"); + v.RenderBodyPublic(); + }); + page.PreviousSectionWriters = new Dictionary + { + { "baz", new HelperResult(writer => { }) } + }; + page.BodyContent = "body-content"; + + // Act + await page.ExecuteAsync(); + + // Assert + Assert.Equal(false, actual); + } + + [Fact] + public async Task IsSectionDefined_ReturnsTrueIfSectionDefined() + { + // Arrange + bool? actual = null; + var page = CreatePage(v => + { + actual = v.IsSectionDefined("baz"); + v.RenderSection("baz"); + v.RenderBodyPublic(); + }); + page.PreviousSectionWriters = new Dictionary + { + { "baz", new HelperResult(writer => { }) } + }; + page.BodyContent = "body-content"; + + // Act + await page.ExecuteAsync(); + + // Assert + Assert.Equal(true, actual); + } + + + [Fact] + public async Task RenderSection_ThrowsIfSectionIsRenderedMoreThanOnce() + { + // Arrange + var expected = new HelperResult(action: null); + var page = CreatePage(v => + { + v.RenderSection("header"); + v.RenderSection("header"); + }); + page.PreviousSectionWriters = new Dictionary + { + { "header", new HelperResult(writer => { }) } + }; + + // Act + var ex = await Assert.ThrowsAsync(page.ExecuteAsync); + + // Assert + Assert.Equal("RenderSection has already been called for the section named 'header'.", ex.Message); + } + + [Fact] + public async Task EnsureBodyAndSectionsWereRendered_ThrowsIfDefinedSectionIsNotRendered() + { + // Arrange + var expected = new HelperResult(action: null); + var page = CreatePage(v => + { + v.RenderSection("sectionA"); + }); + page.PreviousSectionWriters = new Dictionary + { + { "header", expected }, + { "footer", expected }, + { "sectionA", expected }, + }; + + // Act + await page.ExecuteAsync(); + var ex = Assert.Throws(() => page.EnsureBodyAndSectionsWereRendered()); + + // Assert + Assert.Equal("The following sections have been defined but have not been rendered: 'header, footer'.", + ex.Message); + } + + [Fact] + public async Task EnsureBodyAndSectionsWereRendered_ThrowsIfRenderBodyIsNotCalledFromPage() + { + // Arrange + var expected = new HelperResult(action: null); + var page = CreatePage(v => + { + }); + page.BodyContent = "some content"; + + // Act + await page.ExecuteAsync(); + var ex = Assert.Throws(() => page.EnsureBodyAndSectionsWereRendered()); + + // Assert + Assert.Equal("RenderBody must be called from a layout page.", ex.Message); + } + + [Fact] + public async Task ExecuteAsync_RendersSectionsAndBody() + { + // Arrange + var expected = @"Layout start +Header section +body content +Footer section +Layout end +"; + var page = CreatePage(v => + { + v.WriteLiteral("Layout start" + Environment.NewLine); + v.Write(v.RenderSection("header")); + v.Write(v.RenderBodyPublic()); + v.Write(v.RenderSection("footer")); + v.WriteLiteral("Layout end" + Environment.NewLine); + + }); + page.BodyContent = "body content" + Environment.NewLine; + page.PreviousSectionWriters = new Dictionary + { + { + "footer", new HelperResult(writer => + { + writer.WriteLine("Footer section"); + }) + }, + { + "header", new HelperResult(writer => + { + writer.WriteLine("Header section"); + }) + }, + }; + + // Act + await page.ExecuteAsync(); + + // Assert + var actual = ((StringWriter)page.Output).ToString(); + Assert.Equal(expected, actual); + } + + private static TestableRazorPage CreatePage(Action executeAction) + { + var view = new Mock { CallBase = true }; + if (executeAction != null) + { + view.Setup(v => v.ExecuteAsync()) + .Callback(() => executeAction(view.Object)) + .Returns(Task.FromResult(0)); + } + view.Object.ViewContext = CreateViewContext(); + + return view.Object; + } + + private static ViewContext CreateViewContext() + { + var actionContext = new ActionContext(Mock.Of(), routeData: null, actionDescriptor: null); + return new ViewContext( + actionContext, + Mock.Of(), + null, + new StringWriter()); + } + + public abstract class TestableRazorPage : RazorPage + { + public HtmlString RenderBodyPublic() + { + return base.RenderBody(); + } + } + } +} \ 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 22c3155d54..b967c579b2 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewEngineTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewEngineTest.cs @@ -162,10 +162,13 @@ namespace Microsoft.AspNet.Mvc.Razor.Test private IViewEngine CreateSearchLocationViewEngineTester() { - var virtualPathFactory = new Mock(); - virtualPathFactory.Setup(vpf => vpf.CreateInstance(It.IsAny())).Returns(null); + var pageFactory = new Mock(); + pageFactory.Setup(vpf => vpf.CreateInstance(It.IsAny())) + .Returns(null); - var viewEngine = new RazorViewEngine(virtualPathFactory.Object); + var pageActivator = Mock.Of(); + + var viewEngine = new RazorViewEngine(pageFactory.Object, pageActivator); return viewEngine; } diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs index 33b827430e..ee350c61c1 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs @@ -5,317 +5,385 @@ using System; using System.IO; using System.Threading.Tasks; using Microsoft.AspNet.Http; -using Microsoft.AspNet.Mvc.Rendering; -using Microsoft.AspNet.Testing; +using Microsoft.AspNet.Mvc.ModelBinding; using Moq; using Xunit; -namespace Microsoft.AspNet.Mvc.Razor.Test +namespace Microsoft.AspNet.Mvc.Razor { public class RazorViewTest { private const string LayoutPath = "~/Shared/_Layout.cshtml"; [Fact] - public async Task DefineSection_ThrowsIfSectionIsAlreadyDefined() + public async Task RenderAsync_WithoutHierarchy_DoesNotCreateOutputBuffer() { // Arrange - var view = CreateView(v => + TextWriter actual = null; + var page = new TestableRazorPage(v => { - v.DefineSection("qux", new HelperResult(action: null)); - v.DefineSection("qux", new HelperResult(action: null)); + actual = v.Output; + v.Write("Hello world"); }); - var viewContext = CreateViewContext(layoutView: null); + var view = new RazorView(Mock.Of(), + Mock.Of(), + page, + executeViewHierarchy: false); + var viewContext = CreateViewContext(view); + var expected = viewContext.Writer; // Act - var ex = await Assert.ThrowsAsync( - () => view.RenderAsync(viewContext)); + await view.RenderAsync(viewContext); // Assert - Assert.Equal("Section 'qux' is already defined.", ex.Message); + Assert.Same(expected, actual); + Assert.Equal("Hello world", expected.ToString()); } [Fact] - public async Task RenderSection_RendersSectionFromPreviousPage() + public async Task RenderAsync_WithoutHierarchy_ActivatesViews_WithACopyOfViewContext() { // Arrange - var expected = new HelperResult(action: null); - HelperResult actual = null; - var view = CreateView(v => + var viewData = new ViewDataDictionary(Mock.Of()); + var page = new TestableRazorPage(v => + { + // viewData is assigned to ViewContext by the activator + Assert.Same(viewData, v.ViewContext.ViewData); + }); + var activator = new Mock(); + + var view = new RazorView(Mock.Of(), + activator.Object, + page, + executeViewHierarchy: false); + var viewContext = CreateViewContext(view); + var expectedViewData = viewContext.ViewData; + var expectedWriter = viewContext.Writer; + activator.Setup(a => a.Activate(page, It.IsAny())) + .Callback((RazorPage p, ViewContext c) => + { + Assert.NotSame(c, viewContext); + c.ViewData = viewData; + }) + .Verifiable(); + + // Act + await view.RenderAsync(viewContext); + + // Assert + activator.Verify(); + Assert.Same(expectedViewData, viewContext.ViewData); + Assert.Same(expectedWriter, viewContext.Writer); + } + + [Fact] + public async Task RenderAsync_WithoutHierarchy_ActivatesViews() + { + // Arrange + var page = new TestableRazorPage(v => { }); + var activator = new Mock(); + activator.Setup(a => a.Activate(page, It.IsAny())) + .Verifiable(); + var view = new RazorView(Mock.Of(), + activator.Object, + page, + executeViewHierarchy: false); + var viewContext = CreateViewContext(view); + + // Act + await view.RenderAsync(viewContext); + + // Assert + activator.Verify(); + } + + [Fact] + public async Task RenderAsync_WithoutHierarchy_DoesNotExecuteLayoutPages() + { + var page = new TestableRazorPage(v => { - v.DefineSection("bar", expected); v.Layout = LayoutPath; }); - var layoutView = CreateView(v => + var pageFactory = new Mock(); + var view = new RazorView(pageFactory.Object, + Mock.Of(), + page, + executeViewHierarchy: false); + var viewContext = CreateViewContext(view); + + // Act + await view.RenderAsync(viewContext); + + // Assert + pageFactory.Verify(v => v.CreateInstance(It.IsAny()), Times.Never()); + } + + [Fact] + public async Task RenderAsync_WithHierarchy_CreatesOutputBuffer() + { + // Arrange + TextWriter actual = null; + var page = new TestableRazorPage(v => + { + actual = v.Output; + }); + var view = new RazorView(Mock.Of(), + Mock.Of(), + page, + executeViewHierarchy: true); + var viewContext = CreateViewContext(view); + var original = viewContext.Writer; + + // Act + await view.RenderAsync(viewContext); + + // Assert + Assert.IsType(actual); + Assert.NotSame(original, actual); + } + + [Fact] + public async Task RenderAsync_WithHierarchy_CopiesBufferedContentToOutput() + { + // Arrange + var page = new TestableRazorPage(v => + { + v.WriteLiteral("Hello world"); + }); + var view = new RazorView(Mock.Of(), + Mock.Of(), + page, + executeViewHierarchy: true); + var viewContext = CreateViewContext(view); + var original = viewContext.Writer; + + // Act + await view.RenderAsync(viewContext); + + // Assert + Assert.Equal("Hello world", original.ToString()); + } + + [Fact] + public async Task RenderAsync_WithHierarchy_ActivatesPages() + { + // Arrange + var page = new TestableRazorPage(v => + { + v.WriteLiteral("Hello world"); + }); + var activator = new Mock(); + activator.Setup(a => a.Activate(page, It.IsAny())) + .Verifiable(); + var view = new RazorView(Mock.Of(), + activator.Object, + page, + executeViewHierarchy: true); + var viewContext = CreateViewContext(view); + + // Act + await view.RenderAsync(viewContext); + + // Assert + activator.Verify(); + } + + [Fact] + public async Task RenderAsync_WithHierarchy_ExecutesLayoutPages() + { + // Arrange + var expected = +@"layout-content +head-content +body-content +foot-content"; + + var page = new TestableRazorPage(v => + { + v.WriteLiteral("body-content"); + v.Layout = LayoutPath; + v.DefineSection("head", new HelperResult(writer => + { + writer.Write("head-content"); + })); + v.DefineSection("foot", new HelperResult(writer => + { + writer.Write("foot-content"); + })); + }); + var layout = new TestableRazorPage(v => + { + v.Write("layout-content" + Environment.NewLine); + v.Write(v.RenderSection("head")); + v.Write(Environment.NewLine); + v.RenderBodyPublic(); + v.Write(Environment.NewLine); + v.Write(v.RenderSection("foot")); + }); + var activator = new Mock(); + activator.Setup(a => a.Activate(page, It.IsAny())) + .Verifiable(); + activator.Setup(a => a.Activate(layout, It.IsAny())) + .Verifiable(); + var pageFactory = new Mock(); + pageFactory.Setup(p => p.CreateInstance(LayoutPath)) + .Returns(layout); + + var view = new RazorView(pageFactory.Object, + activator.Object, + page, + executeViewHierarchy: true); + var viewContext = CreateViewContext(view); + + // Act + await view.RenderAsync(viewContext); + + // Assert + // Verify the activator was invoked for the primary page and layout page. + activator.Verify(); + Assert.Equal(expected, viewContext.Writer.ToString()); + } + + [Fact] + public async Task RenderAsync_WithHierarchy_ThrowsIfSectionsWereDefinedButNotRendered() + { + // Arrange + var page = new TestableRazorPage(v => + { + v.DefineSection("head", new HelperResult(writer => { })); + v.Layout = LayoutPath; + v.DefineSection("foot", new HelperResult(writer => { })); + }); + var layout = new TestableRazorPage(v => { - actual = v.RenderSection("bar"); v.RenderBodyPublic(); }); - var viewContext = CreateViewContext(layoutView); + var pageFactory = new Mock(); + pageFactory.Setup(p => p.CreateInstance(LayoutPath)) + .Returns(layout); - // Act - await view.RenderAsync(viewContext); - - // Assert - Assert.Same(actual, expected); - } - - [Fact] - public async Task RenderSection_ThrowsIfNoPreviousPage() - { - // Arrange - Exception ex = null; - var view = CreateView(v => - { - ex = Assert.Throws(() => v.RenderSection("bar")); - }); - var viewContext = CreateViewContext(layoutView: null); - - // Act - await view.RenderAsync(viewContext); - - // Assert - Assert.Equal("The method 'RenderSection' cannot be invoked by this view.", - ex.Message); - } - - [Fact] - public async Task RenderSection_ThrowsIfRequiredSectionIsNotFound() - { - // Arrange - var expected = new HelperResult(action: null); - var view = CreateView(v => - { - v.DefineSection("baz", expected); - v.Layout = LayoutPath; - }); - var layoutView = CreateView(v => - { - v.RenderSection("bar"); - }); - var viewContext = CreateViewContext(layoutView); - - // Act - var ex = await Assert.ThrowsAsync(() => view.RenderAsync(viewContext)); - - // Assert - Assert.Equal("Section 'bar' is not defined.", ex.Message); - } - - [Fact] - public void IsSectionDefined_ThrowsIfNoPreviousExecutingPage() - { - // Arrange - var view = CreateView(v => { }); - var viewContext = CreateViewContext(layoutView: null); + var view = new RazorView(pageFactory.Object, + Mock.Of(), + page, + executeViewHierarchy: true); + var viewContext = CreateViewContext(view); // Act and Assert - ExceptionAssert.Throws(() => view.IsSectionDefined("foo"), - "The method 'IsSectionDefined' cannot be invoked by this view."); - } - - [Fact] - public async Task IsSectionDefined_ReturnsFalseIfSectionNotDefined() - { - // Arrange - bool? actual = null; - var view = CreateView(v => - { - v.DefineSection("baz", new HelperResult(writer => { })); - v.Layout = LayoutPath; - }); - var layoutView = CreateView(v => - { - actual = v.IsSectionDefined("foo"); - v.RenderSection("baz"); - v.RenderBodyPublic(); - }); - - // Act - await view.RenderAsync(CreateViewContext(layoutView)); - - // Assert - Assert.Equal(false, actual); - } - - [Fact] - public async Task IsSectionDefined_ReturnsTrueIfSectionDefined() - { - // Arrange - bool? actual = null; - var view = CreateView(v => - { - v.DefineSection("baz", new HelperResult(writer => { })); - v.Layout = LayoutPath; - }); - var layoutView = CreateView(v => - { - actual = v.IsSectionDefined("baz"); - v.RenderSection("baz"); - v.RenderBodyPublic(); - }); - - // Act - await view.RenderAsync(CreateViewContext(layoutView)); - - // Assert - Assert.Equal(true, actual); - } - - - [Fact] - public async Task RenderSection_ThrowsIfSectionIsRenderedMoreThanOnce() - { - // Arrange - var expected = new HelperResult(action: null); - var view = CreateView(v => - { - v.DefineSection("header", expected); - v.Layout = LayoutPath; - }); - var layoutView = CreateView(v => - { - v.RenderSection("header"); - v.RenderSection("header"); - }); - var viewContext = CreateViewContext(layoutView); - - // Act var ex = await Assert.ThrowsAsync(() => view.RenderAsync(viewContext)); - - // Assert - Assert.Equal("RenderSection has already been called for the section named 'header'.", ex.Message); + Assert.Equal("The following sections have been defined but have not been rendered: 'head, foot'.", ex.Message); } [Fact] - public async Task RenderAsync_ThrowsIfDefinedSectionIsNotRendered() + public async Task RenderAsync_WithHierarchy_ThrowsIfBodyWasNotRendered() { // Arrange - var expected = new HelperResult(action: null); - var view = CreateView(v => - { - v.DefineSection("header", expected); - v.DefineSection("footer", expected); - v.DefineSection("sectionA", expected); - v.Layout = LayoutPath; - }); - var layoutView = CreateView(v => - { - v.RenderSection("sectionA"); - v.RenderBodyPublic(); - }); - var viewContext = CreateViewContext(layoutView); - - // Act - var ex = await Assert.ThrowsAsync(() => view.RenderAsync(viewContext)); - - // Assert - Assert.Equal("The following sections have been defined but have not been rendered: 'header, footer'.", ex.Message); - } - - [Fact] - public async Task RenderAsync_ThrowsIfRenderBodyIsNotCalledFromPage() - { - // Arrange - var expected = new HelperResult(action: null); - var view = CreateView(v => + var page = new TestableRazorPage(v => { v.Layout = LayoutPath; }); - var layoutView = CreateView(v => + var layout = new TestableRazorPage(v => { }); - var viewContext = CreateViewContext(layoutView); + var pageFactory = new Mock(); + pageFactory.Setup(p => p.CreateInstance(LayoutPath)) + .Returns(layout); - // Act + var view = new RazorView(pageFactory.Object, + Mock.Of(), + page, + executeViewHierarchy: true); + var viewContext = CreateViewContext(view); + + // Act and Assert var ex = await Assert.ThrowsAsync(() => view.RenderAsync(viewContext)); - - // Assert Assert.Equal("RenderBody must be called from a layout page.", ex.Message); } [Fact] - public async Task RenderAsync_RendersSectionsAndBody() + public async Task RenderAsync_WithHierarchy_ExecutesNestedLayoutPages() { // Arrange - var expected = @"Layout start -Header section -body content -Footer section -Layout end -"; - var view = CreateView(v => + var expected = +@"layout-2 +bar-content +layout-1 +foo-content +body-content"; + + var page = new TestableRazorPage(v => { - v.Layout = LayoutPath; - v.WriteLiteral("body content" + Environment.NewLine); - - v.DefineSection("footer", new HelperResult(writer => + v.DefineSection("foo", new HelperResult(writer => { - writer.WriteLine("Footer section"); - })); - - v.DefineSection("header", new HelperResult(writer => - { - writer.WriteLine("Header section"); + writer.WriteLine("foo-content"); })); + v.Layout = "~/Shared/Layout1.cshtml"; + v.WriteLiteral("body-content"); }); - var layoutView = CreateView(v => + var layout1 = new TestableRazorPage(v => { - v.WriteLiteral("Layout start" + Environment.NewLine); - v.Write(v.RenderSection("header")); - v.Write(v.RenderBodyPublic()); - v.Write(v.RenderSection("footer")); - v.WriteLiteral("Layout end" + Environment.NewLine); - + v.Write("layout-1" + Environment.NewLine); + v.Write(v.RenderSection("foo")); + v.DefineSection("bar", new HelperResult(writer => + { + writer.WriteLine("bar-content"); + })); + v.RenderBodyPublic(); + v.Layout = "~/Shared/Layout2.cshtml"; }); - var viewContext = CreateViewContext(layoutView); + var layout2 = new TestableRazorPage(v => + { + v.Write("layout-2" + Environment.NewLine); + 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 view = new RazorView(pageFactory.Object, + Mock.Of(), + page, + executeViewHierarchy: true); + var viewContext = CreateViewContext(view); // Act await view.RenderAsync(viewContext); // Assert - var actual = ((StringWriter)viewContext.Writer).ToString(); - Assert.Equal(expected, actual); + Assert.Equal(expected, viewContext.Writer.ToString()); } - private static TestableRazorView CreateView(Action executeAction) + private static ViewContext CreateViewContext(RazorView view) { - var view = new Mock { CallBase = true }; - if (executeAction != null) - { - view.Setup(v => v.ExecuteAsync()) - .Callback(() => executeAction(view.Object)) - .Returns(Task.FromResult(0)); - } - - return view.Object; - } - - private static ViewContext CreateViewContext(IView layoutView) - { - var viewFactory = new Mock(); - viewFactory.Setup(v => v.CreateInstance(LayoutPath)) - .Returns(layoutView); - var serviceProvider = new Mock(); - serviceProvider.Setup(f => f.GetService(typeof(IVirtualPathViewFactory))) - .Returns(viewFactory.Object); - var httpContext = new Mock(); - httpContext.SetupGet(c => c.RequestServices).Returns(serviceProvider.Object); - - var actionContext = new ActionContext(httpContext.Object, null, null); + var actionContext = new ActionContext(httpContext.Object, routeData: null, actionDescriptor: null); return new ViewContext( actionContext, - layoutView, - null, + view, + new ViewDataDictionary(Mock.Of()), new StringWriter()); } - public abstract class TestableRazorView : RazorView + private class TestableRazorPage : RazorPage { - public HtmlString RenderBodyPublic() + private readonly Action _executeAction; + + public TestableRazorPage(Action executeAction) { - return base.RenderBody(); + _executeAction = executeAction; + } + + public void RenderBodyPublic() + { + Write(RenderBody()); + } + + public override Task ExecuteAsync() + { + _executeAction(this); + return Task.FromResult(0); } } } diff --git a/test/WebSites/RazorWebSite/Controllers/ViewEngineController.cs b/test/WebSites/RazorWebSite/Controllers/ViewEngineController.cs new file mode 100644 index 0000000000..ffb93d978f --- /dev/null +++ b/test/WebSites/RazorWebSite/Controllers/ViewEngineController.cs @@ -0,0 +1,40 @@ +// 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 ViewEngineController : Controller + { + public IActionResult ViewWithoutLayout() + { + return View(); + } + + public IActionResult ViewWithFullPath() + { + return View(@"/Views/ViewEngine/ViewWithFullPath.cshtml"); + } + + public IActionResult ViewWithLayout() + { + return View(); + } + + public IActionResult ViewWithNestedLayout() + { + return View(); + } + + public IActionResult ViewWithPartial() + { + ViewData["TestKey"] = "test-value"; + var model = new Person + { + Address = new Address { ZipCode = "98052" } + }; + return View(model); + } + } +} \ No newline at end of file diff --git a/test/WebSites/RazorWebSite/Models/Address.cs b/test/WebSites/RazorWebSite/Models/Address.cs new file mode 100644 index 0000000000..ee7332a878 --- /dev/null +++ b/test/WebSites/RazorWebSite/Models/Address.cs @@ -0,0 +1,10 @@ +// 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. + +namespace RazorWebSite +{ + public class Address + { + public string ZipCode { get; set; } + } +} \ No newline at end of file diff --git a/test/WebSites/RazorWebSite/Models/Person.cs b/test/WebSites/RazorWebSite/Models/Person.cs new file mode 100644 index 0000000000..e39f8a45ef --- /dev/null +++ b/test/WebSites/RazorWebSite/Models/Person.cs @@ -0,0 +1,12 @@ +// 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. + +namespace RazorWebSite +{ + public class Person + { + public string Name { get; set; } + + public Address Address { get; set; } + } +} \ No newline at end of file diff --git a/test/WebSites/RazorWebSite/Project.json b/test/WebSites/RazorWebSite/Project.json new file mode 100644 index 0000000000..a6de3a4d44 --- /dev/null +++ b/test/WebSites/RazorWebSite/Project.json @@ -0,0 +1,11 @@ +{ + "dependencies": { + "Microsoft.AspNet.Mvc": "", + "Microsoft.AspNet.Server.IIS": "1.0.0-*", + "Microsoft.AspNet.Mvc.TestConfiguration": "" + }, + "frameworks": { + "net45": { }, + "k10": { } + } +} diff --git a/test/WebSites/RazorWebSite/RazorWebSite.kproj b/test/WebSites/RazorWebSite/RazorWebSite.kproj new file mode 100644 index 0000000000..272be37cd7 --- /dev/null +++ b/test/WebSites/RazorWebSite/RazorWebSite.kproj @@ -0,0 +1,43 @@ + + + + 12.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + b07caf59-11ed-40e3-a5db-e1178f84fa78 + Library + + + ConsoleDebugger + + + WebDebugger + + + + + + + 2.0 + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/WebSites/RazorWebSite/Startup.cs b/test/WebSites/RazorWebSite/Startup.cs new file mode 100644 index 0000000000..015d25f562 --- /dev/null +++ b/test/WebSites/RazorWebSite/Startup.cs @@ -0,0 +1,23 @@ +using Microsoft.AspNet.Builder; +using Microsoft.Framework.DependencyInjection; + +namespace RazorWebSite +{ + public class Startup + { + public void Configure(IBuilder app) + { + var configuration = app.GetTestConfiguration(); + + // Set up application services + app.UseServices(services => + { + // Add MVC services to the services container + services.AddMvc(configuration); + }); + + // Add MVC to the request pipeline + app.UseMvc(); + } + } +} diff --git a/test/WebSites/RazorWebSite/Views/Shared/_Layout.cshtml b/test/WebSites/RazorWebSite/Views/Shared/_Layout.cshtml new file mode 100644 index 0000000000..20b44df8d1 --- /dev/null +++ b/test/WebSites/RazorWebSite/Views/Shared/_Layout.cshtml @@ -0,0 +1,3 @@ + +@RenderBody() + \ No newline at end of file diff --git a/test/WebSites/RazorWebSite/Views/Shared/_Partial.cshtml b/test/WebSites/RazorWebSite/Views/Shared/_Partial.cshtml new file mode 100644 index 0000000000..515b4a9995 --- /dev/null +++ b/test/WebSites/RazorWebSite/Views/Shared/_Partial.cshtml @@ -0,0 +1,2 @@ +@model RazorWebSite.Address +@ViewData.Model.ZipCode diff --git a/test/WebSites/RazorWebSite/Views/ViewEngine/ViewWithFullPath.cshtml b/test/WebSites/RazorWebSite/Views/ViewEngine/ViewWithFullPath.cshtml new file mode 100644 index 0000000000..0c334f0585 --- /dev/null +++ b/test/WebSites/RazorWebSite/Views/ViewEngine/ViewWithFullPath.cshtml @@ -0,0 +1,4 @@ +@{ + 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 new file mode 100644 index 0000000000..4904ee6996 --- /dev/null +++ b/test/WebSites/RazorWebSite/Views/ViewEngine/ViewWithLayout.cshtml @@ -0,0 +1,4 @@ +@{ + 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 new file mode 100644 index 0000000000..93aa57fcfa --- /dev/null +++ b/test/WebSites/RazorWebSite/Views/ViewEngine/ViewWithNestedLayout.cshtml @@ -0,0 +1,4 @@ +@{ + Layout = "~/Views/ViewEngine/_NestedLayout.cshtml"; +} +ViewWithNestedLayout-Content \ No newline at end of file diff --git a/test/WebSites/RazorWebSite/Views/ViewEngine/ViewWithPartial.cshtml b/test/WebSites/RazorWebSite/Views/ViewEngine/ViewWithPartial.cshtml new file mode 100644 index 0000000000..c696e6cb3a --- /dev/null +++ b/test/WebSites/RazorWebSite/Views/ViewEngine/ViewWithPartial.cshtml @@ -0,0 +1,5 @@ +@using RazorWebSite +@model Person +@await Html.PartialAsync("_Partial", Model.Address) + +@ViewBag.TestKey \ No newline at end of file diff --git a/test/WebSites/RazorWebSite/Views/ViewEngine/ViewWithoutLayout.cshtml b/test/WebSites/RazorWebSite/Views/ViewEngine/ViewWithoutLayout.cshtml new file mode 100644 index 0000000000..1838444da1 --- /dev/null +++ b/test/WebSites/RazorWebSite/Views/ViewEngine/ViewWithoutLayout.cshtml @@ -0,0 +1 @@ +ViewWithoutLayout-Content \ No newline at end of file diff --git a/test/WebSites/RazorWebSite/Views/ViewEngine/_NestedLayout.cshtml b/test/WebSites/RazorWebSite/Views/ViewEngine/_NestedLayout.cshtml new file mode 100644 index 0000000000..1d357345f6 --- /dev/null +++ b/test/WebSites/RazorWebSite/Views/ViewEngine/_NestedLayout.cshtml @@ -0,0 +1,7 @@ +@{ + Layout = "/Views/Shared/_Layout.cshtml"; +} + +@Url.Action() +@RenderBody() + \ No newline at end of file