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
This commit is contained in:
Pranav K 2014-07-18 14:58:12 -07:00
parent c1112fcaf1
commit 9e535f6897
35 changed files with 1077 additions and 425 deletions

15
Mvc.sln
View File

@ -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

View File

@ -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
/// <summary>
/// Represents a <see cref="IRazorPageFactory"/> that creates <see cref="RazorPage"/> instances
/// from razor files in the file system.
/// </summary>
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)
/// <inheritdoc />
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;

View File

@ -4,15 +4,15 @@
namespace Microsoft.AspNet.Mvc.Razor
{
/// <summary>
/// Provides methods to activate properties on a view instance.
/// Provides methods to activate properties on a <see cref="RazorPage"/> instance.
/// </summary>
public interface IRazorViewActivator
public interface IRazorPageActivator
{
/// <summary>
/// When implemented in a type, activates an instantiated view.
/// When implemented in a type, activates an instantiated page.
/// </summary>
/// <param name="view">The view to activate.</param>
/// <param name="context">The <see cref="ViewContext"/> for the view.</param>
void Activate(RazorView view, ViewContext context);
/// <param name="page">The page to activate.</param>
/// <param name="context">The <see cref="ViewContext"/> for the executing view.</param>
void Activate(RazorPage page, ViewContext context);
}
}

View File

@ -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
{
/// <summary>
/// Defines methods that are used for creating <see cref="RazorPage"/> instances at a given path.
/// </summary>
public interface IRazorPageFactory
{
/// <summary>
/// Creates a <see cref="RazorPage"/> for the specified path.
/// </summary>
/// <param name="viewPath">The path to locate the RazorPage.</param>
/// <returns>The RazorPage instance if it exists, null otherwise.</returns>
RazorPage CreateInstance(string viewPath);
}
}

View File

@ -29,19 +29,20 @@
<Compile Include="Compilation\ICompilationService.cs" />
<Compile Include="Compilation\RoslynCompilationService.cs" />
<Compile Include="Extensions\DictionaryExtensions.cs" />
<Compile Include="FileBasedRazorPageFactory.cs" />
<Compile Include="HelperResult.cs" />
<Compile Include="IRazorViewActivator.cs" />
<Compile Include="IRazorPageActivator.cs" />
<Compile Include="IRazorPageFactory.cs" />
<Compile Include="PositionTagged.cs" />
<Compile Include="Properties\Resources.Designer.cs" />
<Compile Include="RazorPage.cs" />
<Compile Include="RazorPageOfT.cs" />
<Compile Include="RazorView.cs" />
<Compile Include="RazorViewActivator.cs" />
<Compile Include="RazorViewOfT.cs" />
<Compile Include="RazorPageActivator.cs" />
<Compile Include="RazorViewEngine.cs" />
<Compile Include="Razor\IRazorCompilationService.cs" />
<Compile Include="Razor\RazorCompilationService.cs" />
<Compile Include="Services\IRoslynMetadataReference.cs" />
<Compile Include="ViewEngine\IVirtualPathViewFactory.cs" />
<Compile Include="ViewEngine\RazorViewEngine.cs" />
<Compile Include="ViewEngine\VirtualPathViewFactory.cs" />
</ItemGroup>
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>

View File

@ -202,6 +202,22 @@ namespace Microsoft.AspNet.Mvc.Razor
return string.Format(CultureInfo.CurrentCulture, GetString("ViewCannotBeActivated"), p0, p1);
}
/// <summary>
/// '{0} must be set to access '{1}'.
/// </summary>
internal static string ViewContextMustBeSet
{
get { return GetString("ViewContextMustBeSet"); }
}
/// <summary>
/// '{0} must be set to access '{1}'.
/// </summary>
internal static string FormatViewContextMustBeSet(object p0, object p1)
{
return string.Format(CultureInfo.CurrentCulture, GetString("ViewContextMustBeSet"), p0, p1);
}
/// <summary>
/// View '{0}' must have extension '{1}' when the view represents a full path.
/// </summary>

View File

@ -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
/// <summary>
/// Represents properties and methods that are needed in order to render a view that uses Razor syntax.
/// </summary>
public abstract class RazorPage
{
private readonly HashSet<string> _renderedSections = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
private bool _renderedBody;
public RazorPage()
{
SectionWriters = new Dictionary<string, HelperResult>(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; }
/// <summary>
/// Gets the TextWriter that the page is writing output to.
/// </summary>
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<string, HelperResult> SectionWriters { get; set; }
public Dictionary<string, HelperResult> PreviousSectionWriters { get; set; }
private Dictionary<string, HelperResult> PreviousSectionWriters { get; set; }
public virtual async Task RenderAsync([NotNull] ViewContext context)
{
SectionWriters = new Dictionary<string, HelperResult>(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<IVirtualPathViewFactory>();
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<string, HelperResult> SectionWriters { get; private set; }
public abstract Task ExecuteAsync();
@ -326,6 +292,32 @@ namespace Microsoft.AspNet.Mvc.Razor
}
}
/// <summary>
/// Verifies that RenderBody is called and that RenderSection is called for all sections for a page that is
/// part of view execution hierarchy.
/// </summary>
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"));
}
}
}
}
}

View File

@ -10,30 +10,26 @@ using Microsoft.Framework.DependencyInjection;
namespace Microsoft.AspNet.Mvc.Razor
{
/// <inheritdoc />
public class RazorViewActivator : IRazorViewActivator
public class RazorPageActivator : IRazorPageActivator
{
// Name of the "public TModel Model" property on RazorView<TModel>
// Name of the "public TModel Model" property on RazorPage<TModel>
private const string ModelPropertyName = "Model";
private readonly ITypeActivator _typeActivator;
private readonly ConcurrentDictionary<Type, ViewActivationInfo> _activationInfo;
private readonly ConcurrentDictionary<Type, PageActivationInfo> _activationInfo;
/// <summary>
/// Initializes a new instance of the RazorViewActivator class.
/// Initializes a new instance of the <see cref="RazorPageActivator"/> class.
/// </summary>
public RazorViewActivator(ITypeActivator typeActivator)
public RazorPageActivator(ITypeActivator typeActivator)
{
_typeActivator = typeActivator;
_activationInfo = new ConcurrentDictionary<Type, ViewActivationInfo>();
_activationInfo = new ConcurrentDictionary<Type, PageActivationInfo>();
}
/// <summary>
/// Activates the specified view by using the specified ViewContext.
/// </summary>
/// <param name="view">The view to activate.</param>
/// <param name="context">The ViewContext for the executing view.</param>
public void Activate([NotNull] RazorView view, [NotNull] ViewContext context)
/// <inheritdoc />
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<TModel> 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<TModel>
// the equivalent of TModel Model property on RazorPage<TModel>
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<ViewContext>.GetPropertiesToActivate(type,
@ -115,7 +111,7 @@ namespace Microsoft.AspNet.Mvc.Razor
return new PropertyActivator<ViewContext>(property, valueAccessor);
}
private class ViewActivationInfo
private class PageActivationInfo
{
public PropertyActivator<ViewContext>[] PropertyActivators { get; set; }

View File

@ -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<TModel> : RazorView
/// <summary>
/// Represents the properties and methods that are needed in order to render a view that uses Razor syntax.
/// </summary>
/// <typeparam name="TModel">The type of the view data model.</typeparam>
public abstract class RazorPage<TModel> : RazorPage
{
public TModel Model
{
@ -18,13 +19,5 @@ namespace Microsoft.AspNet.Mvc.Razor
[Activate]
public ViewDataDictionary<TModel> ViewData { get; set; }
public override Task RenderAsync([NotNull] ViewContext context)
{
var viewActivator = context.HttpContext.RequestServices.GetService<IRazorViewActivator>();
viewActivator.Activate(this, context);
return base.RenderAsync(context);
}
}
}

View File

@ -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<string> ViewLocationFormats
@ -41,18 +44,19 @@ namespace Microsoft.AspNet.Mvc.Razor
public ViewEngineResult FindView([NotNull] IDictionary<string, object> context,
[NotNull] string viewName)
{
var viewEngineResult = CreateViewEngineResult(context, viewName);
var viewEngineResult = CreateViewEngineResult(context, viewName, partial: false);
return viewEngineResult;
}
public ViewEngineResult FindPartialView([NotNull] IDictionary<string, object> context,
[NotNull] string partialViewName)
{
return FindView(context, partialViewName);
return CreateViewEngineResult(context, partialViewName, partial: true);
}
private ViewEngineResult CreateViewEngineResult([NotNull] IDictionary<string, object> 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] == '/';

View File

@ -153,6 +153,9 @@
<data name="ViewCannotBeActivated" xml:space="preserve">
<value>View of type '{0}' cannot be activated by '{1}'.</value>
</data>
<data name="ViewContextMustBeSet" xml:space="preserve">
<value>'{0} must be set to access '{1}'.</value>
</data>
<data name="ViewMustEndInExtension" xml:space="preserve">
<value>View '{0}' must have extension '{1}' when the view represents a full path.</value>
</data>

View File

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

View File

@ -36,7 +36,7 @@ namespace Microsoft.AspNet.Mvc
yield return describe.Transient<IControllerAssemblyProvider, DefaultControllerAssemblyProvider>();
yield return describe.Transient<IActionDiscoveryConventions, DefaultActionDiscoveryConventions>();
yield return describe.Instance<IMvcRazorHost>(new MvcRazorHost(typeof(RazorView).FullName));
yield return describe.Instance<IMvcRazorHost>(new MvcRazorHost(typeof(RazorPage).FullName));
yield return describe.Transient<ICompilationService, RoslynCompilationService>();
@ -44,9 +44,9 @@ namespace Microsoft.AspNet.Mvc
yield return describe.Scoped<ICompositeViewEngine, CompositeViewEngine>();
yield return describe.Singleton<IRazorCompilationService, RazorCompilationService>();
yield return describe.Singleton<IRazorViewActivator, RazorViewActivator>();
yield return describe.Singleton<IRazorPageActivator, RazorPageActivator>();
// Virtual path view factory needs to stay scoped so views can get get scoped services.
yield return describe.Scoped<IVirtualPathViewFactory, VirtualPathViewFactory>();
yield return describe.Scoped<IRazorPageFactory, FileBasedRazorPageFactory>();
yield return describe.Singleton<IFileInfoCache, ExpiringFileInfoCache>();
yield return describe.Transient<INestedProvider<ActionDescriptorProviderContext>,

View File

@ -30,6 +30,7 @@
<Content Include="project.json" />
</ItemGroup>
<ItemGroup>
<Compile Include="ViewEngineTests.cs" />
<Compile Include="ActivatorTests.cs" />
<Compile Include="ValueProviderTests.cs" />
<Compile Include="CompositeViewEngineTests.cs" />

View File

@ -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<IBuilder> _app = new Startup().Configure;
public static IEnumerable<object[]> RazorView_ExecutesPageAndLayoutData
{
get
{
yield return new[] { "ViewWithoutLayout", @"ViewWithoutLayout-Content" };
yield return new[]
{
"ViewWithLayout",
@"<layout>
ViewWithLayout-Content
</layout>"
};
yield return new[]
{
"ViewWithFullPath",
@"<layout>
ViewWithFullPath-content
</layout>"
};
yield return new[]
{
"ViewWithNestedLayout",
@"<layout>
<nested-layout>
/ViewEngine/ViewWithNestedLayout
ViewWithNestedLayout-Content
</nested-layout>
</layout>"
};
}
}
[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 =
@"<partial>98052
</partial>
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());
}
}
}

View File

@ -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-*"
},

View File

@ -23,8 +23,9 @@
<ItemGroup>
<Compile Include="MvcRazorCodeParserTest.cs" />
<Compile Include="RazorCompilationServiceTest.cs" />
<Compile Include="RazorViewActivatorTest.cs" />
<Compile Include="RazorPageActivatorTest.cs" />
<Compile Include="RazorViewEngineTest.cs" />
<Compile Include="RazorPageTest.cs" />
<Compile Include="RazorViewTest.cs" />
<Compile Include="SpanFactory.cs" />
</ItemGroup>

View File

@ -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<ITypeActivator>());
var instance = new TestView();
var activator = new RazorPageActivator(Mock.Of<ITypeActivator>());
var instance = new TestRazorPage();
var myService = new MyService();
var helper = Mock.Of<IHtmlHelper<object>>();
@ -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<IView>(),
new ViewDataDictionary(Mock.Of<IModelMetadataProvider>()),
TextWriter.Null);
@ -55,8 +55,8 @@ namespace Microsoft.AspNet.Mvc.Razor
public void Activate_ThrowsIfTheViewDoesNotDeriveFromRazorViewOfT()
{
// Arrange
var activator = new RazorViewActivator(Mock.Of<ITypeActivator>());
var instance = new DoesNotDeriveFromRazorViewOfT();
var activator = new RazorPageActivator(Mock.Of<ITypeActivator>());
var instance = new DoesNotDeriveFromRazorPageOfT();
var myService = new MyService();
var helper = Mock.Of<IHtmlHelper<object>>();
@ -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<IView>(),
new ViewDataDictionary(Mock.Of<IModelMetadataProvider>()),
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<IHtmlHelper<object>>();
@ -106,7 +106,7 @@ namespace Microsoft.AspNet.Mvc.Razor
Model = new MyModel()
};
var viewContext = new ViewContext(actionContext,
instance,
Mock.Of<IView>(),
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<IHtmlHelper<object>>();
var serviceProvider = new Mock<IServiceProvider>();
@ -141,7 +141,7 @@ namespace Microsoft.AspNet.Mvc.Razor
Model = new MyModel()
};
var viewContext = new ViewContext(actionContext,
instance,
Mock.Of<IView>(),
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<IHtmlHelper<object>>();
var serviceProvider = new Mock<IServiceProvider>();
@ -173,7 +173,7 @@ namespace Microsoft.AspNet.Mvc.Razor
var actionContext = new ActionContext(routeContext, new ActionDescriptor());
var viewData = new ViewDataDictionary(Mock.Of<IModelMetadataProvider>());
var viewContext = new ViewContext(actionContext,
instance,
Mock.Of<IView>(),
viewData,
TextWriter.Null);
@ -184,7 +184,7 @@ namespace Microsoft.AspNet.Mvc.Razor
Assert.IsType<ViewDataDictionary<string>>(viewContext.ViewData);
}
private abstract class TestViewBase<TModel> : RazorView<TModel>
private abstract class TestPageBase<TModel> : RazorPage<TModel>
{
[Activate]
public MyService MyService { get; set; }
@ -192,7 +192,7 @@ namespace Microsoft.AspNet.Mvc.Razor
public MyService MyService2 { get; set; }
}
private class TestView : TestViewBase<MyModel>
private class TestRazorPage : TestPageBase<MyModel>
{
[Activate]
internal IHtmlHelper<object> Html { get; private set; }
@ -203,11 +203,11 @@ namespace Microsoft.AspNet.Mvc.Razor
}
}
private abstract class DoesNotDeriveFromRazorViewOfTBase<TModel> : RazorView
private abstract class DoesNotDeriveFromRazorPageOfTBase<TModel> : RazorPage
{
}
private class DoesNotDeriveFromRazorViewOfT : DoesNotDeriveFromRazorViewOfTBase<MyModel>
private class DoesNotDeriveFromRazorPageOfT : DoesNotDeriveFromRazorPageOfTBase<MyModel>
{
public override Task ExecuteAsync()
{
@ -215,7 +215,7 @@ namespace Microsoft.AspNet.Mvc.Razor
}
}
private class DoesNotDeriveFromRazorViewOfTButHasModelProperty : DoesNotDeriveFromRazorViewOfTBase<MyModel>
private class DoesNotDeriveFromRazorPageOfTButHasModelProperty : DoesNotDeriveFromRazorPageOfTBase<MyModel>
{
public string Model { get; set; }

View File

@ -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<InvalidOperationException>(
() => 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<string, HelperResult>
{
{ "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<InvalidOperationException>(() => 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<string, HelperResult>
{
{ "baz", expected }
};
// Act
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => 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<InvalidOperationException>(() => 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<string, HelperResult>
{
{ "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<string, HelperResult>
{
{ "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<string, HelperResult>
{
{ "header", new HelperResult(writer => { }) }
};
// Act
var ex = await Assert.ThrowsAsync<InvalidOperationException>(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<string, HelperResult>
{
{ "header", expected },
{ "footer", expected },
{ "sectionA", expected },
};
// Act
await page.ExecuteAsync();
var ex = Assert.Throws<InvalidOperationException>(() => 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<InvalidOperationException>(() => 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<string, HelperResult>
{
{
"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<TestableRazorPage> executeAction)
{
var view = new Mock<TestableRazorPage> { 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<HttpContext>(), routeData: null, actionDescriptor: null);
return new ViewContext(
actionContext,
Mock.Of<IView>(),
null,
new StringWriter());
}
public abstract class TestableRazorPage : RazorPage
{
public HtmlString RenderBodyPublic()
{
return base.RenderBody();
}
}
}
}

View File

@ -162,10 +162,13 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
private IViewEngine CreateSearchLocationViewEngineTester()
{
var virtualPathFactory = new Mock<IVirtualPathViewFactory>();
virtualPathFactory.Setup(vpf => vpf.CreateInstance(It.IsAny<string>())).Returns<IView>(null);
var pageFactory = new Mock<IRazorPageFactory>();
pageFactory.Setup(vpf => vpf.CreateInstance(It.IsAny<string>()))
.Returns<RazorPage>(null);
var viewEngine = new RazorViewEngine(virtualPathFactory.Object);
var pageActivator = Mock.Of<IRazorPageActivator>();
var viewEngine = new RazorViewEngine(pageFactory.Object, pageActivator);
return viewEngine;
}

View File

@ -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<IRazorPageFactory>(),
Mock.Of<IRazorPageActivator>(),
page,
executeViewHierarchy: false);
var viewContext = CreateViewContext(view);
var expected = viewContext.Writer;
// Act
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
() => 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<IModelMetadataProvider>());
var page = new TestableRazorPage(v =>
{
// viewData is assigned to ViewContext by the activator
Assert.Same(viewData, v.ViewContext.ViewData);
});
var activator = new Mock<IRazorPageActivator>();
var view = new RazorView(Mock.Of<IRazorPageFactory>(),
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<ViewContext>()))
.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<IRazorPageActivator>();
activator.Setup(a => a.Activate(page, It.IsAny<ViewContext>()))
.Verifiable();
var view = new RazorView(Mock.Of<IRazorPageFactory>(),
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<IRazorPageFactory>();
var view = new RazorView(pageFactory.Object,
Mock.Of<IRazorPageActivator>(),
page,
executeViewHierarchy: false);
var viewContext = CreateViewContext(view);
// Act
await view.RenderAsync(viewContext);
// Assert
pageFactory.Verify(v => v.CreateInstance(It.IsAny<string>()), 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<IRazorPageFactory>(),
Mock.Of<IRazorPageActivator>(),
page,
executeViewHierarchy: true);
var viewContext = CreateViewContext(view);
var original = viewContext.Writer;
// Act
await view.RenderAsync(viewContext);
// Assert
Assert.IsType<StringWriter>(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<IRazorPageFactory>(),
Mock.Of<IRazorPageActivator>(),
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<IRazorPageActivator>();
activator.Setup(a => a.Activate(page, It.IsAny<ViewContext>()))
.Verifiable();
var view = new RazorView(Mock.Of<IRazorPageFactory>(),
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<IRazorPageActivator>();
activator.Setup(a => a.Activate(page, It.IsAny<ViewContext>()))
.Verifiable();
activator.Setup(a => a.Activate(layout, It.IsAny<ViewContext>()))
.Verifiable();
var pageFactory = new Mock<IRazorPageFactory>();
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<IRazorPageFactory>();
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<InvalidOperationException>(() => 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<InvalidOperationException>(() => 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<IRazorPageActivator>(),
page,
executeViewHierarchy: true);
var viewContext = CreateViewContext(view);
// Act and Assert
ExceptionAssert.Throws<InvalidOperationException>(() => 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<InvalidOperationException>(() => 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<InvalidOperationException>(() => 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<IRazorPageFactory>();
pageFactory.Setup(p => p.CreateInstance(LayoutPath))
.Returns(layout);
// Act
var view = new RazorView(pageFactory.Object,
Mock.Of<IRazorPageActivator>(),
page,
executeViewHierarchy: true);
var viewContext = CreateViewContext(view);
// Act and Assert
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => 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<IRazorPageFactory>();
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<IRazorPageActivator>(),
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<TestableRazorView> executeAction)
private static ViewContext CreateViewContext(RazorView view)
{
var view = new Mock<TestableRazorView> { 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<IVirtualPathViewFactory>();
viewFactory.Setup(v => v.CreateInstance(LayoutPath))
.Returns(layoutView);
var serviceProvider = new Mock<IServiceProvider>();
serviceProvider.Setup(f => f.GetService(typeof(IVirtualPathViewFactory)))
.Returns(viewFactory.Object);
var httpContext = new Mock<HttpContext>();
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<IModelMetadataProvider>()),
new StringWriter());
}
public abstract class TestableRazorView : RazorView
private class TestableRazorPage : RazorPage
{
public HtmlString RenderBodyPublic()
private readonly Action<TestableRazorPage> _executeAction;
public TestableRazorPage(Action<TestableRazorPage> executeAction)
{
return base.RenderBody();
_executeAction = executeAction;
}
public void RenderBodyPublic()
{
Write(RenderBody());
}
public override Task ExecuteAsync()
{
_executeAction(this);
return Task.FromResult(0);
}
}
}

View File

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

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -0,0 +1,11 @@
{
"dependencies": {
"Microsoft.AspNet.Mvc": "",
"Microsoft.AspNet.Server.IIS": "1.0.0-*",
"Microsoft.AspNet.Mvc.TestConfiguration": ""
},
"frameworks": {
"net45": { },
"k10": { }
}
}

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="__ToolsVersion__" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">12.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
</PropertyGroup>
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.Props" Condition="'$(VSToolsPath)' != ''" />
<PropertyGroup Label="Globals">
<ProjectGuid>b07caf59-11ed-40e3-a5db-e1178f84fa78</ProjectGuid>
<OutputType>Library</OutputType>
</PropertyGroup>
<PropertyGroup Condition="$(OutputType) == 'Console'">
<DebuggerFlavor>ConsoleDebugger</DebuggerFlavor>
</PropertyGroup>
<PropertyGroup Condition="$(OutputType) == 'Web'">
<DebuggerFlavor>WebDebugger</DebuggerFlavor>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x86'" Label="Configuration">
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x86'" Label="Configuration">
</PropertyGroup>
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
</PropertyGroup>
<ItemGroup>
<Content Include="Project.json" />
<Content Include="Views\Shared\_Partial.cshtml" />
<Content Include="Views\ViewEngine\ViewWithNestedLayout.cshtml" />
<Content Include="Views\ViewEngine\ViewWithLayout.cshtml" />
<Content Include="Views\ViewEngine\ViewWithFullPath.cshtml" />
<Content Include="Views\ViewEngine\ViewWithPartial.cshtml" />
<Content Include="Views\ViewEngine\ViewWithoutLayout.cshtml" />
<Content Include="Views\Shared\_Layout.cshtml" />
<Content Include="Views\ViewEngine\_NestedLayout.cshtml" />
</ItemGroup>
<ItemGroup>
<Compile Include="Controllers\ViewEngineController.cs" />
<Compile Include="Models\Person.cs" />
<Compile Include="Models\Address.cs" />
<Compile Include="Startup.cs" />
</ItemGroup>
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>

View File

@ -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();
}
}
}

View File

@ -0,0 +1,3 @@
<layout>
@RenderBody()
</layout>

View File

@ -0,0 +1,2 @@
@model RazorWebSite.Address
@ViewData.Model.ZipCode

View File

@ -0,0 +1,4 @@
@{
Layout = "~/Views/Shared/_Layout.cshtml";
}
ViewWithFullPath-content

View File

@ -0,0 +1,4 @@
@{
Layout = "~/Views/Shared/_Layout.cshtml";
}
ViewWithLayout-Content

View File

@ -0,0 +1,4 @@
@{
Layout = "~/Views/ViewEngine/_NestedLayout.cshtml";
}
ViewWithNestedLayout-Content

View File

@ -0,0 +1,5 @@
@using RazorWebSite
@model Person
<partial>@await Html.PartialAsync("_Partial", Model.Address)
</partial>
@ViewBag.TestKey

View File

@ -0,0 +1 @@
ViewWithoutLayout-Content

View File

@ -0,0 +1,7 @@
@{
Layout = "/Views/Shared/_Layout.cshtml";
}
<nested-layout>
@Url.Action()
@RenderBody()
</nested-layout>