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:
parent
c1112fcaf1
commit
9e535f6897
15
Mvc.sln
15
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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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] == '/';
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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>,
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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-*"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"dependencies": {
|
||||
"Microsoft.AspNet.Mvc": "",
|
||||
"Microsoft.AspNet.Server.IIS": "1.0.0-*",
|
||||
"Microsoft.AspNet.Mvc.TestConfiguration": ""
|
||||
},
|
||||
"frameworks": {
|
||||
"net45": { },
|
||||
"k10": { }
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<layout>
|
||||
@RenderBody()
|
||||
</layout>
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
@model RazorWebSite.Address
|
||||
@ViewData.Model.ZipCode
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
@{
|
||||
Layout = "~/Views/Shared/_Layout.cshtml";
|
||||
}
|
||||
ViewWithFullPath-content
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
@{
|
||||
Layout = "~/Views/Shared/_Layout.cshtml";
|
||||
}
|
||||
ViewWithLayout-Content
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
@{
|
||||
Layout = "~/Views/ViewEngine/_NestedLayout.cshtml";
|
||||
}
|
||||
ViewWithNestedLayout-Content
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
@using RazorWebSite
|
||||
@model Person
|
||||
<partial>@await Html.PartialAsync("_Partial", Model.Address)
|
||||
</partial>
|
||||
@ViewBag.TestKey
|
||||
|
|
@ -0,0 +1 @@
|
|||
ViewWithoutLayout-Content
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
@{
|
||||
Layout = "/Views/Shared/_Layout.cshtml";
|
||||
}
|
||||
<nested-layout>
|
||||
@Url.Action()
|
||||
@RenderBody()
|
||||
</nested-layout>
|
||||
Loading…
Reference in New Issue