Adding support for ViewLocationExpanders to allow modifying view locations
without changing the view engine. Fixes #1039
This commit is contained in:
parent
a4fff7a2e0
commit
ad8ab4b8fd
|
|
@ -97,6 +97,11 @@ namespace MvcSample.Web
|
|||
Context.Response.WriteAsync("Hello World raw");
|
||||
}
|
||||
|
||||
public ActionResult Language()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
|
||||
[Produces("application/json", "application/xml", "application/custom", "text/json", Type = typeof(User))]
|
||||
public object ReturnUser()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,66 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.AspNet.Mvc;
|
||||
using Microsoft.AspNet.Mvc.Razor;
|
||||
|
||||
namespace MvcSample.Web
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see cref="IViewLocationExpander"/> that replaces adds the language as an extension prefix to view names.
|
||||
/// </summary>
|
||||
/// <example>
|
||||
/// For the default case with no areas, views are generated with the following patterns (assuming controller is
|
||||
/// "Home", action is "Index" and language is "en")
|
||||
/// Views/Home/en/Action
|
||||
/// Views/Home/Action
|
||||
/// Views/Shared/en/Action
|
||||
/// Views/Shared/Action
|
||||
/// </example>
|
||||
public class LanguageViewLocationExpander : IViewLocationExpander
|
||||
{
|
||||
private const string ValueKey = "language";
|
||||
private readonly Func<ActionContext, string> _valueFactory;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of <see cref="LanguageViewLocationExpander"/>.
|
||||
/// </summary>
|
||||
/// <param name="valueFactory">A factory that provides tbe language to use for expansion.</param>
|
||||
public LanguageViewLocationExpander(Func<ActionContext, string> valueFactory)
|
||||
{
|
||||
_valueFactory = valueFactory;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void PopulateValues(ViewLocationExpanderContext context)
|
||||
{
|
||||
var value = _valueFactory(context.ActionContext);
|
||||
if (!string.IsNullOrEmpty(value))
|
||||
{
|
||||
context.Values[ValueKey] = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context,
|
||||
IEnumerable<string> viewLocations)
|
||||
{
|
||||
string value;
|
||||
if (context.Values.TryGetValue(ValueKey, out value))
|
||||
{
|
||||
return ExpandViewLocationsCore(viewLocations, value);
|
||||
}
|
||||
|
||||
return viewLocations;
|
||||
}
|
||||
|
||||
private IEnumerable<string> ExpandViewLocationsCore(IEnumerable<string> viewLocations,
|
||||
string value)
|
||||
{
|
||||
foreach (var location in viewLocations)
|
||||
{
|
||||
yield return location.Replace("{0}", value + "/{0}");
|
||||
yield return location;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
using System;
|
||||
using Microsoft.AspNet.Builder;
|
||||
using Microsoft.AspNet.Mvc;
|
||||
using Microsoft.AspNet.Mvc.Razor;
|
||||
using Microsoft.AspNet.Routing;
|
||||
using Microsoft.Framework.ConfigurationModel;
|
||||
using Microsoft.Framework.DependencyInjection;
|
||||
using Microsoft.Framework.OptionsModel;
|
||||
using MvcSample.Web.Filters;
|
||||
using MvcSample.Web.Services;
|
||||
|
||||
|
|
@ -46,6 +46,12 @@ namespace MvcSample.Web
|
|||
{
|
||||
options.Filters.Add(typeof(PassThroughAttribute), order: 17);
|
||||
});
|
||||
services.SetupOptions<RazorViewEngineOptions>(options =>
|
||||
{
|
||||
var expander = new LanguageViewLocationExpander(
|
||||
context => context.HttpContext.Request.Query["language"]);
|
||||
options.ViewLocationExpanders.Insert(0, expander);
|
||||
});
|
||||
|
||||
// Create the autofac container
|
||||
ContainerBuilder builder = new ContainerBuilder();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
<ul>
|
||||
<li>COLOR</li>
|
||||
<li>HUMOR</li>
|
||||
<li>ITEMIZE</li>
|
||||
</ul>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<ul>
|
||||
<li>COLOUR</li>
|
||||
<li>HUMOUR</li>
|
||||
<li>ITEMISE</li>
|
||||
</ul>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<ul>
|
||||
<li>COULEUR</li>
|
||||
<li>HUMOUR</li>
|
||||
<li>DÉTAILLER</li>
|
||||
</ul>
|
||||
|
|
@ -5,6 +5,12 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>@ViewBag.Title - My ASP.NET Application</title>
|
||||
<link rel="stylesheet" href="~/content/bootstrap.min.css" />
|
||||
<style>
|
||||
body { padding-top: 60px; }
|
||||
@@media screen and (max-width: 768px) {
|
||||
body { padding-top: 0px; }
|
||||
}
|
||||
</style>
|
||||
@RenderSection("header", required: false)
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using Microsoft.AspNet.Mvc.Core;
|
||||
using Microsoft.AspNet.Mvc.OptionDescriptors;
|
||||
using Microsoft.AspNet.Mvc.ReflectedModelBuilder;
|
||||
|
|
@ -16,7 +15,6 @@ namespace Microsoft.AspNet.Mvc
|
|||
public class MvcOptions
|
||||
{
|
||||
private AntiForgeryOptions _antiForgeryOptions = new AntiForgeryOptions();
|
||||
private RazorViewEngineOptions _viewEngineOptions = new RazorViewEngineOptions();
|
||||
private int _maxModelStateErrors = 200;
|
||||
|
||||
public MvcOptions()
|
||||
|
|
@ -71,29 +69,6 @@ namespace Microsoft.AspNet.Mvc
|
|||
/// </summary>
|
||||
public List<InputFormatterDescriptor> InputFormatters { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Provides programmatic configuration for the default <see cref="Rendering.IViewEngine" />.
|
||||
/// </summary>
|
||||
public RazorViewEngineOptions ViewEngineOptions
|
||||
{
|
||||
get
|
||||
{
|
||||
return _viewEngineOptions;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
throw new ArgumentNullException("value",
|
||||
Resources.FormatPropertyOfTypeCannotBeNull("ViewEngineOptions",
|
||||
typeof(MvcOptions)));
|
||||
}
|
||||
|
||||
_viewEngineOptions = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum number of validation errors that are allowed by this application before further
|
||||
/// errors are ignored.
|
||||
|
|
@ -123,8 +98,8 @@ namespace Microsoft.AspNet.Mvc
|
|||
/// Gets a list of the <see cref="ModelValidatorProviderDescriptor" />s used by
|
||||
/// <see cref="ModelBinding.CompositeModelValidatorProvider"/>.
|
||||
/// </summary>
|
||||
public List<ModelValidatorProviderDescriptor> ModelValidatorProviders { get; } =
|
||||
new List<ModelValidatorProviderDescriptor>();
|
||||
public List<ModelValidatorProviderDescriptor> ModelValidatorProviders { get; }
|
||||
= new List<ModelValidatorProviderDescriptor>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets a list of descriptors that represent <see cref="Rendering.IViewEngine"/> used
|
||||
|
|
|
|||
|
|
@ -1354,6 +1354,54 @@ namespace Microsoft.AspNet.Mvc.Core
|
|||
return string.Format(CultureInfo.CurrentCulture, GetString("AttributeRoute_AggregateErrorMessage_ErrorNumber"), p0, p1, p2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Could not find a replacement for view expansion token '{0}'.
|
||||
/// </summary>
|
||||
internal static string TemplatedViewLocationExpander_NoReplacementToken
|
||||
{
|
||||
get { return GetString("TemplatedViewLocationExpander_NoReplacementToken"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Could not find a replacement for view expansion token '{0}'.
|
||||
/// </summary>
|
||||
internal static string FormatTemplatedViewLocationExpander_NoReplacementToken(object p0)
|
||||
{
|
||||
return string.Format(CultureInfo.CurrentCulture, GetString("TemplatedViewLocationExpander_NoReplacementToken"), p0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// {0} must be executed before {1} can be invoked.
|
||||
/// </summary>
|
||||
internal static string TemplatedExpander_PopulateValuesMustBeInvokedFirst
|
||||
{
|
||||
get { return GetString("TemplatedExpander_PopulateValuesMustBeInvokedFirst"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// {0} must be executed before {1} can be invoked.
|
||||
/// </summary>
|
||||
internal static string FormatTemplatedExpander_PopulateValuesMustBeInvokedFirst(object p0, object p1)
|
||||
{
|
||||
return string.Format(CultureInfo.CurrentCulture, GetString("TemplatedExpander_PopulateValuesMustBeInvokedFirst"), p0, p1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The result of value factory cannot be null.
|
||||
/// </summary>
|
||||
internal static string TemplatedExpander_ValueFactoryCannotReturnNull
|
||||
{
|
||||
get { return GetString("TemplatedExpander_ValueFactoryCannotReturnNull"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The result of value factory cannot be null.
|
||||
/// </summary>
|
||||
internal static string FormatTemplatedExpander_ValueFactoryCannotReturnNull()
|
||||
{
|
||||
return GetString("TemplatedExpander_ValueFactoryCannotReturnNull");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A method '{0}' that defines attribute routed actions must not have attributes that implement '{1}' and do not implement '{2}':{3}{4}
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -375,6 +375,15 @@
|
|||
<value>Error {0}:{1}{2}</value>
|
||||
<comment>{0} is the error number, {1} is Environment.NewLine {2} is the error message</comment>
|
||||
</data>
|
||||
<data name="TemplatedViewLocationExpander_NoReplacementToken" xml:space="preserve">
|
||||
<value>Could not find a replacement for view expansion token '{0}'.</value>
|
||||
</data>
|
||||
<data name="TemplatedExpander_PopulateValuesMustBeInvokedFirst" xml:space="preserve">
|
||||
<value>{0} must be executed before {1} can be invoked.</value>
|
||||
</data>
|
||||
<data name="TemplatedExpander_ValueFactoryCannotReturnNull" xml:space="preserve">
|
||||
<value>The result of value factory cannot be null.</value>
|
||||
</data>
|
||||
<data name="AttributeRoute_InvalidHttpConstraints" xml:space="preserve">
|
||||
<value>A method '{0}' that defines attribute routed actions must not have attributes that implement '{1}' and do not implement '{2}':{3}{4}</value>
|
||||
<comment>{0} is the MethodInfo.FullName, {1} is typeof(IActionHttpMethodProvider).FullName, {2} is typeof(IRouteTemplateProvider).FullName, {3} is Environment.NewLine, {4} is the list of actions and their respective invalid IActionHttpMethodProvider attributes formatted using AttributeRoute_InvalidHttpMethodConstraints_Item</comment>
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
"Microsoft.AspNet.Mvc.ModelBinding": "",
|
||||
"Microsoft.AspNet.Routing": "1.0.0-*",
|
||||
"Microsoft.AspNet.Security": "1.0.0-*",
|
||||
"Microsoft.AspNet.Security.DataProtection" : "1.0.0-*",
|
||||
"Microsoft.AspNet.Security.DataProtection": "1.0.0-*",
|
||||
"Microsoft.Framework.DependencyInjection": "1.0.0-*",
|
||||
"Microsoft.Framework.Runtime.Interfaces": "1.0.0-*",
|
||||
"Microsoft.Framework.Logging": "1.0.0-*",
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ using Microsoft.AspNet.FileSystems;
|
|||
using Microsoft.Framework.OptionsModel;
|
||||
using Microsoft.Framework.Runtime;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.Core
|
||||
namespace Microsoft.AspNet.Mvc.Razor
|
||||
{
|
||||
/// <summary>
|
||||
/// A default implementation for the <see cref="IFileInfoCache"/> interface.
|
||||
|
|
@ -37,11 +37,11 @@ namespace Microsoft.AspNet.Mvc.Core
|
|||
}
|
||||
|
||||
public ExpiringFileInfoCache(IApplicationEnvironment env,
|
||||
IOptionsAccessor<MvcOptions> optionsAccessor)
|
||||
IOptionsAccessor<RazorViewEngineOptions> optionsAccessor)
|
||||
{
|
||||
// TODO: Inject the IFileSystem but only when we get it from the host
|
||||
_fileSystem = new PhysicalFileSystem(env.ApplicationBasePath);
|
||||
_offset = optionsAccessor.Options.ViewEngineOptions.ExpirationBeforeCheckingFilesOnDisk;
|
||||
_offset = optionsAccessor.Options.ExpirationBeforeCheckingFilesOnDisk;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
using Microsoft.AspNet.FileSystems;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.Core
|
||||
namespace Microsoft.AspNet.Mvc.Razor
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides cached access to file infos.
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
// 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.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.Razor
|
||||
{
|
||||
/// <summary>
|
||||
/// Default implementation of <see cref="IViewLocationCache"/>.
|
||||
/// </summary>
|
||||
public class DefaultViewLocationCache : IViewLocationCache
|
||||
{
|
||||
private const char CacheKeySeparator = ':';
|
||||
|
||||
// A mapping of keys generated from ViewLocationExpanderContext to view locations.
|
||||
private readonly ConcurrentDictionary<string, string> _cache;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of <see cref="DefaultViewLocationCache"/>.
|
||||
/// </summary>
|
||||
public DefaultViewLocationCache()
|
||||
{
|
||||
_cache = new ConcurrentDictionary<string, string>(StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Get([NotNull] ViewLocationExpanderContext context)
|
||||
{
|
||||
var cacheKey = GenerateKey(context);
|
||||
string result;
|
||||
_cache.TryGetValue(cacheKey, out result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Set([NotNull] ViewLocationExpanderContext context,
|
||||
[NotNull] string value)
|
||||
{
|
||||
var cacheKey = GenerateKey(context);
|
||||
_cache.TryAdd(cacheKey, value);
|
||||
}
|
||||
|
||||
internal static string GenerateKey(ViewLocationExpanderContext context)
|
||||
{
|
||||
var keyBuilder = new StringBuilder();
|
||||
var routeValues = context.ActionContext.RouteData.Values;
|
||||
var controller = routeValues.GetValueOrDefault<string>(RazorViewEngine.ControllerKey);
|
||||
|
||||
// format is "{viewName}:{controllerName}:{areaName}:"
|
||||
keyBuilder.Append(context.ViewName)
|
||||
.Append(CacheKeySeparator)
|
||||
.Append(controller);
|
||||
|
||||
var area = routeValues.GetValueOrDefault<string>(RazorViewEngine.AreaKey);
|
||||
if (!string.IsNullOrEmpty(area))
|
||||
{
|
||||
keyBuilder.Append(CacheKeySeparator)
|
||||
.Append(area);
|
||||
}
|
||||
|
||||
if (context.Values != null)
|
||||
{
|
||||
var valuesDictionary = context.Values;
|
||||
foreach (var item in valuesDictionary.OrderBy(k => k.Key, StringComparer.Ordinal))
|
||||
{
|
||||
keyBuilder.Append(CacheKeySeparator)
|
||||
.Append(item.Key)
|
||||
.Append(CacheKeySeparator)
|
||||
.Append(item.Value);
|
||||
}
|
||||
}
|
||||
|
||||
var cacheKey = keyBuilder.ToString();
|
||||
return cacheKey;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
// 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>
|
||||
/// Specifies the contracts for caching view locations generated by <see cref="IViewLocationExpander"/>.
|
||||
/// </summary>
|
||||
public interface IViewLocationCache
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a cached view location based on the specified <paramref name="context"/>.
|
||||
/// </summary>
|
||||
/// <param name="context">The <see cref="ViewLocationExpanderContext"/> for the current view location
|
||||
/// expansion.</param>
|
||||
/// <returns>The cached location, if available, <c>null</c> otherwise.</returns>
|
||||
string Get(ViewLocationExpanderContext context);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a cache entry for values specified by <paramref name="context"/>.
|
||||
/// </summary>
|
||||
/// <param name="context">The <see cref="ViewLocationExpanderContext"/> for the current view location
|
||||
/// expansion.</param>
|
||||
/// <param name="value">The view location that is to be cached.</param>
|
||||
void Set(ViewLocationExpanderContext context, string value);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.Razor
|
||||
{
|
||||
/// <summary>
|
||||
/// Specifies the contracts for a view location expander that is used by <see cref="RazorViewEngine"/> instances to
|
||||
/// determine search paths for a view.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Individual <see cref="IViewLocationExpander"/>s are invoked in two steps:
|
||||
/// (1) <see cref="PopulateValues(ViewLocationExpanderContext)"/> is invoked and each expander
|
||||
/// adds values that it would later consume as part of
|
||||
/// <see cref="ExpandViewLocations(ViewLocationExpanderContext, IEnumerable{string})"/>.
|
||||
/// The populated values are used to determine a cache key - if all values are identical to the last time
|
||||
/// <see cref="PopulateValues(ViewLocationExpanderContext)"/> was invoked, the cached result
|
||||
/// is used as the view location.
|
||||
/// (2) If no result was found in the cache or if a view was not found at the cached location,
|
||||
/// <see cref="ExpandViewLocations(ViewLocationExpanderContext, IEnumerable{string})"/> is invoked to determine
|
||||
/// all potential paths for a view.
|
||||
/// </remarks>
|
||||
public interface IViewLocationExpander
|
||||
{
|
||||
/// <summary>
|
||||
/// Invoked by a <see cref="RazorViewEngine"/> to determine the values that would be consumed by this instance of
|
||||
/// <see cref="IViewLocationExpander"/>. The calculated values are used to determine if the view location has
|
||||
/// changed since the last time it was located.
|
||||
/// </summary>
|
||||
/// <param name="context">The <see cref="ViewLocationExpanderContext"/> for the current view location
|
||||
/// expansion operation.</param>
|
||||
void PopulateValues(ViewLocationExpanderContext context);
|
||||
|
||||
/// <summary>
|
||||
/// Invoked by a <see cref="RazorViewEngine"/> to determine potential locations for a view.
|
||||
/// </summary>
|
||||
/// <param name="context">The <see cref="ViewLocationExpanderContext"/> for the current view location
|
||||
/// expansion operation.</param>
|
||||
/// <param name="values">The sequence of view locations to expand.</param>
|
||||
/// <returns>A list of expanded view locations.</returns>
|
||||
IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context,
|
||||
IEnumerable<string> viewLocations);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
// 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 Microsoft.AspNet.Mvc.OptionDescriptors;
|
||||
using Microsoft.Framework.DependencyInjection;
|
||||
using Microsoft.Framework.OptionsModel;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.Razor.OptionDescriptors
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public class DefaultViewLocationExpanderProvider :
|
||||
OptionDescriptorBasedProvider<IViewLocationExpander>, IViewLocationExpanderProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DefaultViewLocationExpanderProvider"/> class.
|
||||
/// </summary>
|
||||
/// <param name="options">An accessor to the <see cref="MvcOptions"/> configured for this application.</param>
|
||||
/// <param name="typeActivator">An <see cref="ITypeActivator"/> instance used to instantiate types.</param>
|
||||
/// <param name="serviceProvider">A <see cref="IServiceProvider"/> instance that retrieves services from the
|
||||
/// service collection.</param>
|
||||
public DefaultViewLocationExpanderProvider(
|
||||
IOptionsAccessor<RazorViewEngineOptions> optionsAccessor,
|
||||
ITypeActivator typeActivator,
|
||||
IServiceProvider serviceProvider)
|
||||
: base(optionsAccessor.Options.ViewLocationExpanders, typeActivator, serviceProvider)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<IViewLocationExpander> ViewLocationExpanders
|
||||
{
|
||||
get { return Options; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.Razor.OptionDescriptors
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides an activated collection of <see cref="IViewLocationExpander"/> instances.
|
||||
/// </summary>
|
||||
public interface IViewLocationExpanderProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a collection of activated <see cref="IViewLocationExpander"/> instances.
|
||||
/// </summary>
|
||||
IReadOnlyList<IViewLocationExpander> ViewLocationExpanders { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -2,8 +2,10 @@
|
|||
// 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 Microsoft.AspNet.Mvc.Razor.OptionDescriptors;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.Core
|
||||
namespace Microsoft.AspNet.Mvc.Razor
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides programmatic configuration for the default <see cref="Microsoft.AspNet.Mvc.Rendering.IViewEngine"/>.
|
||||
|
|
@ -38,5 +40,12 @@ namespace Microsoft.AspNet.Mvc.Core
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a <see cref="IList{T}"/> of descriptors for <see cref="IViewLocationExpander" />s used by this
|
||||
/// application.
|
||||
/// </summary>
|
||||
public IList<ViewLocationExpanderDescriptor> ViewLocationExpanders { get; }
|
||||
= new List<ViewLocationExpanderDescriptor>();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
// 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 Microsoft.AspNet.Mvc.OptionDescriptors;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.Razor.OptionDescriptors
|
||||
{
|
||||
/// <summary>
|
||||
/// Encapsulates information that describes an <see cref="IViewLocationExpander"/>.
|
||||
/// </summary>
|
||||
public class ViewLocationExpanderDescriptor : OptionDescriptor<IViewLocationExpander>
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new instance of <see cref="ViewLocationExpanderDescriptor"/>.
|
||||
/// </summary>
|
||||
/// <param name="type">A <see cref="IViewLocationExpander"/> type that the descriptor represents.
|
||||
/// </param>
|
||||
public ViewLocationExpanderDescriptor([NotNull] Type type)
|
||||
: base(type)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of <see cref="ViewLocationExpanderDescriptor"/>.
|
||||
/// </summary>
|
||||
/// <param name="viewLocationExpander">An instance of <see cref="IViewLocationExpander"/>
|
||||
/// that the descriptor represents.</param>
|
||||
public ViewLocationExpanderDescriptor([NotNull] IViewLocationExpander viewLocationExpander)
|
||||
: base(viewLocationExpander)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
// 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 Microsoft.AspNet.Mvc.Razor.OptionDescriptors;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.Razor
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension methods for adding view location expanders to a collection.
|
||||
/// </summary>
|
||||
public static class ViewLocationExpanderDescriptorExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds a type representing a <see cref="IViewLocationExpander"/> to <paramref name="descriptors"/>.
|
||||
/// </summary>
|
||||
/// <param name="descriptors">A list of <see cref="ViewLocationExpanderDescriptor"/>.</param>
|
||||
/// <param name="viewLocationExpanderType">Type representing an <see cref="IViewLocationExpander"/></param>
|
||||
/// <returns>A <see cref="ViewLocationExpanderDescriptor"/> representing the added instance.</returns>
|
||||
public static ViewLocationExpanderDescriptor Add(
|
||||
[NotNull] this IList<ViewLocationExpanderDescriptor> descriptors,
|
||||
[NotNull] Type viewLocationExpanderType)
|
||||
{
|
||||
var descriptor = new ViewLocationExpanderDescriptor(viewLocationExpanderType);
|
||||
descriptors.Add(descriptor);
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Inserts a type representing a <see cref="IViewLocationExpander"/> in to <paramref name="descriptors"/> at
|
||||
/// the specified <paramref name="index"/>.
|
||||
/// </summary>
|
||||
/// <param name="descriptors">A list of <see cref="ViewLocationExpanderDescriptor"/>.</param>
|
||||
/// <param name="index">The zero-based index at which <paramref name="viewLocationExpanderType"/>
|
||||
/// should be inserted.</param>
|
||||
/// <param name="viewLocationExpanderType">Type representing an <see cref="IViewLocationExpander"/></param>
|
||||
/// <returns>A <see cref="ViewLocationExpanderDescriptor"/> representing the inserted instance.</returns>
|
||||
public static ViewLocationExpanderDescriptor Insert(
|
||||
[NotNull] this IList<ViewLocationExpanderDescriptor> descriptors,
|
||||
int index,
|
||||
[NotNull] Type viewLocationExpanderType)
|
||||
{
|
||||
if (index < 0 || index > descriptors.Count)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(index));
|
||||
}
|
||||
|
||||
var descriptor = new ViewLocationExpanderDescriptor(viewLocationExpanderType);
|
||||
descriptors.Insert(index, descriptor);
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds an <see cref="IViewLocationExpander"/> to <paramref name="descriptors"/>.
|
||||
/// </summary>
|
||||
/// <param name="descriptors">A list of <see cref="ViewLocationExpanderDescriptor"/>.</param>
|
||||
/// <param name="viewLocationExpander">An <see cref="IViewLocationExpander"/> instance.</param>
|
||||
/// <returns>A <see cref="ViewLocationExpanderDescriptor"/> representing the added instance.</returns>
|
||||
public static ViewLocationExpanderDescriptor Add(
|
||||
[NotNull] this IList<ViewLocationExpanderDescriptor> descriptors,
|
||||
[NotNull] IViewLocationExpander viewLocationExpander)
|
||||
{
|
||||
var descriptor = new ViewLocationExpanderDescriptor(viewLocationExpander);
|
||||
descriptors.Add(descriptor);
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Insert an <see cref="IViewLocationExpander"/> in to <paramref name="descriptors"/> at the specified
|
||||
/// <paramref name="index"/>.
|
||||
/// </summary>
|
||||
/// <param name="descriptors">A list of <see cref="ViewLocationExpanderDescriptor"/>.</param>
|
||||
/// <param name="index">The zero-based index at which <paramref name="viewLocationExpander"/>
|
||||
/// should be inserted.</param>
|
||||
/// <param name="viewLocationExpander">An <see cref="IViewLocationExpander"/> instance.</param>
|
||||
/// <returns>A <see cref="ViewLocationExpanderDescriptor"/> representing the added instance.</returns>
|
||||
public static ViewLocationExpanderDescriptor Insert(
|
||||
[NotNull] this IList<ViewLocationExpanderDescriptor> descriptors,
|
||||
int index,
|
||||
[NotNull] IViewLocationExpander viewLocationExpander)
|
||||
{
|
||||
if (index < 0 || index > descriptors.Count)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(index));
|
||||
}
|
||||
|
||||
var descriptor = new ViewLocationExpanderDescriptor(viewLocationExpander);
|
||||
descriptors.Insert(index, descriptor);
|
||||
return descriptor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -266,6 +266,22 @@ namespace Microsoft.AspNet.Mvc.Razor
|
|||
return string.Format(CultureInfo.CurrentCulture, GetString("ViewContextMustBeSet"), p0, p1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// '{0}' must be a {1} that is generated as result of the call to '{2}'.
|
||||
/// </summary>
|
||||
internal static string ViewLocationCache_KeyMustBeString
|
||||
{
|
||||
get { return GetString("ViewLocationCache_KeyMustBeString"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// '{0}' must be a {1} that is generated as result of the call to '{2}'.
|
||||
/// </summary>
|
||||
internal static string FormatViewLocationCache_KeyMustBeString(object p0, object p1, object p2)
|
||||
{
|
||||
return string.Format(CultureInfo.CurrentCulture, GetString("ViewLocationCache_KeyMustBeString"), p0, p1, p2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The '{0}' method must be called before '{1}' can be invoked.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNet.Mvc.Razor.OptionDescriptors;
|
||||
using Microsoft.AspNet.Mvc.Rendering;
|
||||
using Microsoft.Framework.DependencyInjection;
|
||||
|
||||
|
|
@ -16,14 +16,16 @@ namespace Microsoft.AspNet.Mvc.Razor
|
|||
public class RazorViewEngine : IViewEngine
|
||||
{
|
||||
private const string ViewExtension = ".cshtml";
|
||||
internal const string ControllerKey = "controller";
|
||||
internal const string AreaKey = "area";
|
||||
|
||||
private static readonly string[] _viewLocationFormats =
|
||||
private static readonly IEnumerable<string> _viewLocationFormats = new[]
|
||||
{
|
||||
"/Views/{1}/{0}" + ViewExtension,
|
||||
"/Views/Shared/{0}" + ViewExtension,
|
||||
};
|
||||
|
||||
private static readonly string[] _areaViewLocationFormats =
|
||||
private static readonly IEnumerable<string> _areaViewLocationFormats = new[]
|
||||
{
|
||||
"/Areas/{2}/Views/{1}/{0}" + ViewExtension,
|
||||
"/Areas/{2}/Views/Shared/{0}" + ViewExtension,
|
||||
|
|
@ -31,27 +33,44 @@ namespace Microsoft.AspNet.Mvc.Razor
|
|||
};
|
||||
|
||||
private readonly IRazorPageFactory _pageFactory;
|
||||
private readonly IReadOnlyList<IViewLocationExpander> _viewLocationExpanders;
|
||||
private readonly IViewLocationCache _viewLocationCache;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RazorViewEngine" /> class.
|
||||
/// </summary>
|
||||
/// <param name="pageFactory">The page factory used for creating <see cref="IRazorPage"/> instances.</param>
|
||||
public RazorViewEngine(IRazorPageFactory pageFactory)
|
||||
public RazorViewEngine(IRazorPageFactory pageFactory,
|
||||
IViewLocationExpanderProvider viewLocationExpanderProvider,
|
||||
IViewLocationCache viewLocationCache)
|
||||
{
|
||||
_pageFactory = pageFactory;
|
||||
_viewLocationExpanders = viewLocationExpanderProvider.ViewLocationExpanders;
|
||||
_viewLocationCache = viewLocationCache;
|
||||
}
|
||||
|
||||
public IEnumerable<string> ViewLocationFormats
|
||||
/// <summary>
|
||||
/// Gets the locations where this instance of <see cref="RazorViewEngine"/> will search for views.
|
||||
/// </summary>
|
||||
public virtual IEnumerable<string> ViewLocationFormats
|
||||
{
|
||||
get { return _viewLocationFormats; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the locations where this instance of <see cref="RazorViewEngine"/> will search for views within an
|
||||
/// area.
|
||||
/// </summary>
|
||||
public virtual IEnumerable<string> AreaViewLocationFormats
|
||||
{
|
||||
get { return _areaViewLocationFormats; }
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ViewEngineResult FindView([NotNull] ActionContext context,
|
||||
[NotNull] string viewName)
|
||||
{
|
||||
var viewEngineResult = CreateViewEngineResult(context, viewName, partial: false);
|
||||
return viewEngineResult;
|
||||
return CreateViewEngineResult(context, viewName, partial: false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
@ -81,22 +100,77 @@ namespace Microsoft.AspNet.Mvc.Razor
|
|||
}
|
||||
else
|
||||
{
|
||||
var routeValues = context.RouteData.Values;
|
||||
var controllerName = routeValues.GetValueOrDefault<string>("controller");
|
||||
var areaName = routeValues.GetValueOrDefault<string>("area");
|
||||
var potentialPaths = GetViewSearchPaths(viewName, controllerName, areaName);
|
||||
return LocateViewFromViewLocations(context, viewName, partial);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var path in potentialPaths)
|
||||
private ViewEngineResult LocateViewFromViewLocations(ActionContext context,
|
||||
string viewName,
|
||||
bool partial)
|
||||
{
|
||||
// Initialize the dictionary for the typical case of having controller and action tokens.
|
||||
var routeValues = context.RouteData.Values;
|
||||
var areaName = routeValues.GetValueOrDefault<string>(AreaKey);
|
||||
|
||||
// Only use the area view location formats if we have an area token.
|
||||
var viewLocations = !string.IsNullOrEmpty(areaName) ? AreaViewLocationFormats :
|
||||
ViewLocationFormats;
|
||||
|
||||
var expanderContext = new ViewLocationExpanderContext(context, viewName);
|
||||
if (_viewLocationExpanders.Count > 0)
|
||||
{
|
||||
expanderContext.Values = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
|
||||
// 1. Populate values from viewLocationExpanders.
|
||||
foreach (var expander in _viewLocationExpanders)
|
||||
{
|
||||
var page = _pageFactory.CreateInstance(path);
|
||||
if (page != null)
|
||||
{
|
||||
return CreateFoundResult(context, page, path, partial);
|
||||
}
|
||||
expander.PopulateValues(expanderContext);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. With the values that we've accumumlated so far, check if we have a cached result.
|
||||
var viewLocation = _viewLocationCache.Get(expanderContext);
|
||||
if (!string.IsNullOrEmpty(viewLocation))
|
||||
{
|
||||
var page = _pageFactory.CreateInstance(viewLocation);
|
||||
|
||||
if (page != null)
|
||||
{
|
||||
// 2a. We found a IRazorPage at the cached location.
|
||||
return CreateFoundResult(context, page, viewName, partial);
|
||||
}
|
||||
}
|
||||
|
||||
// 2b. We did not find a cached location or did not find a IRazorPage at the cached location.
|
||||
// The cached value has expired and we need to look up the page.
|
||||
foreach (var expander in _viewLocationExpanders)
|
||||
{
|
||||
viewLocations = expander.ExpandViewLocations(expanderContext, viewLocations);
|
||||
}
|
||||
|
||||
// 3. Use the expanded locations to look up a page.
|
||||
var controllerName = routeValues.GetValueOrDefault<string>(ControllerKey);
|
||||
var searchedLocations = new List<string>();
|
||||
foreach (var path in viewLocations)
|
||||
{
|
||||
var transformedPath = string.Format(CultureInfo.InvariantCulture,
|
||||
path,
|
||||
viewName,
|
||||
controllerName,
|
||||
areaName);
|
||||
var page = _pageFactory.CreateInstance(transformedPath);
|
||||
if (page != null)
|
||||
{
|
||||
// 3a. We found a page. Cache the set of values that produced it and return a found result.
|
||||
_viewLocationCache.Set(expanderContext, transformedPath);
|
||||
return CreateFoundResult(context, page, transformedPath, partial);
|
||||
}
|
||||
|
||||
return ViewEngineResult.NotFound(viewName, potentialPaths);
|
||||
searchedLocations.Add(transformedPath);
|
||||
}
|
||||
|
||||
// 3b. We did not find a page for any of the paths.
|
||||
return ViewEngineResult.NotFound(viewName, searchedLocations);
|
||||
}
|
||||
|
||||
private ViewEngineResult CreateFoundResult(ActionContext actionContext,
|
||||
|
|
@ -117,26 +191,5 @@ namespace Microsoft.AspNet.Mvc.Razor
|
|||
{
|
||||
return name[0] == '~' || name[0] == '/';
|
||||
}
|
||||
|
||||
private IEnumerable<string> GetViewSearchPaths(string viewName, string controllerName, string areaName)
|
||||
{
|
||||
IEnumerable<string> unformattedPaths;
|
||||
|
||||
if (string.IsNullOrEmpty(areaName))
|
||||
{
|
||||
// If no areas then no need to search area locations.
|
||||
unformattedPaths = _viewLocationFormats;
|
||||
}
|
||||
else
|
||||
{
|
||||
// If there's an area provided only search area view locations
|
||||
unformattedPaths = _areaViewLocationFormats;
|
||||
}
|
||||
|
||||
var formattedPaths = unformattedPaths.Select(path =>
|
||||
string.Format(CultureInfo.InvariantCulture, path, viewName, controllerName, areaName));
|
||||
|
||||
return formattedPaths;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -165,6 +165,9 @@
|
|||
<data name="ViewContextMustBeSet" xml:space="preserve">
|
||||
<value>'{0} must be set to access '{1}'.</value>
|
||||
</data>
|
||||
<data name="ViewLocationCache_KeyMustBeString" xml:space="preserve">
|
||||
<value>'{0}' must be a {1} that is generated as result of the call to '{2}'.</value>
|
||||
</data>
|
||||
<data name="ViewMustBeContextualized" xml:space="preserve">
|
||||
<value>The '{0}' method must be called before '{1}' can be invoked.</value>
|
||||
</data>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.Razor
|
||||
{
|
||||
/// <summary>
|
||||
/// A context for containing information for <see cref="IViewLocationExpander"/>.
|
||||
/// </summary>
|
||||
public class ViewLocationExpanderContext
|
||||
{
|
||||
public ViewLocationExpanderContext([NotNull] ActionContext actionContext,
|
||||
[NotNull] string viewName)
|
||||
{
|
||||
ActionContext = actionContext;
|
||||
ViewName = viewName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="ActionContext"/> for the current executing action.
|
||||
/// </summary>
|
||||
public ActionContext ActionContext { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the view name
|
||||
/// </summary>
|
||||
public string ViewName { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="IDictionary{TKey, TValue}"/> that is populated with values as part of
|
||||
/// <see cref="IViewLocationExpander.PopulateValues(ViewLocationExpanderContext)"/>.
|
||||
/// </summary>
|
||||
public IDictionary<string, string> Values { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
using Microsoft.AspNet.Mvc.ModelBinding;
|
||||
using Microsoft.AspNet.Mvc.Razor;
|
||||
using Microsoft.AspNet.Mvc.Rendering;
|
||||
using Microsoft.Framework.OptionsModel;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ using Microsoft.AspNet.Mvc.ModelBinding;
|
|||
using Microsoft.AspNet.Mvc.OptionDescriptors;
|
||||
using Microsoft.AspNet.Mvc.Razor;
|
||||
using Microsoft.AspNet.Mvc.Razor.Compilation;
|
||||
using Microsoft.AspNet.Mvc.Razor.OptionDescriptors;
|
||||
using Microsoft.AspNet.Mvc.Rendering;
|
||||
using Microsoft.AspNet.Mvc.Routing;
|
||||
using Microsoft.AspNet.Security;
|
||||
|
|
@ -53,6 +54,11 @@ namespace Microsoft.AspNet.Mvc
|
|||
yield return describe.Singleton<IViewStartProvider, ViewStartProvider>();
|
||||
yield return describe.Transient<IRazorView, RazorView>();
|
||||
|
||||
// Transient since the IViewLocationExpanders returned by the instance is cached by view engines.
|
||||
yield return describe.Transient<IViewLocationExpanderProvider, DefaultViewLocationExpanderProvider>();
|
||||
// Caches view locations that are valid for the lifetime of the application.
|
||||
yield return describe.Singleton<IViewLocationCache, DefaultViewLocationCache>();
|
||||
|
||||
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<IRazorPageFactory, VirtualPathRazorPageFactory>();
|
||||
|
|
|
|||
|
|
@ -101,5 +101,42 @@ component-content";
|
|||
// Assert
|
||||
Assert.Equal(expected, body.Trim());
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> RazorViewEngine_UsesAllExpandedPathsToLookForViewsData
|
||||
{
|
||||
get
|
||||
{
|
||||
var expected1 = string.Join(Environment.NewLine,
|
||||
"expander-index",
|
||||
"gb-partial");
|
||||
yield return new[] { "gb", expected1 };
|
||||
|
||||
var expected2 = string.Join(Environment.NewLine,
|
||||
"fr-index",
|
||||
"fr-partial");
|
||||
yield return new[] { "fr", expected2 };
|
||||
|
||||
var expected3 = string.Join(Environment.NewLine,
|
||||
"expander-index",
|
||||
"expander-partial");
|
||||
yield return new[] { "na", expected3 };
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(RazorViewEngine_UsesAllExpandedPathsToLookForViewsData))]
|
||||
public async Task RazorViewEngine_UsesViewExpandersForViewsAndPartials(string value, string expected)
|
||||
{
|
||||
// Arrange
|
||||
var server = TestServer.Create(_provider, _app);
|
||||
var client = server.CreateClient();
|
||||
|
||||
// Act
|
||||
var body = await client.GetStringAsync("http://localhost/TemplateExpander?language-expander-value=" +
|
||||
value);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, body.Trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -10,7 +10,7 @@ using Microsoft.Framework.Runtime;
|
|||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.Core.Test
|
||||
namespace Microsoft.AspNet.Mvc.Razor
|
||||
{
|
||||
public class ExpiringFileInfoCacheTest
|
||||
{
|
||||
|
|
@ -27,28 +27,28 @@ namespace Microsoft.AspNet.Mvc.Core.Test
|
|||
}
|
||||
}
|
||||
|
||||
public MvcOptions Options
|
||||
public RazorViewEngineOptions Options
|
||||
{
|
||||
get
|
||||
{
|
||||
return new MvcOptions();
|
||||
return new RazorViewEngineOptions();
|
||||
}
|
||||
}
|
||||
|
||||
public IOptionsAccessor<MvcOptions> OptionsAccessor
|
||||
public IOptionsAccessor<RazorViewEngineOptions> OptionsAccessor
|
||||
{
|
||||
get
|
||||
{
|
||||
var options = Options;
|
||||
|
||||
var mock = new Mock<IOptionsAccessor<MvcOptions>>(MockBehavior.Strict);
|
||||
var mock = new Mock<IOptionsAccessor<RazorViewEngineOptions>>(MockBehavior.Strict);
|
||||
mock.Setup(oa => oa.Options).Returns(options);
|
||||
|
||||
return mock.Object;
|
||||
}
|
||||
}
|
||||
|
||||
public ControllableExpiringFileInfoCache GetCache(IOptionsAccessor<MvcOptions> optionsAccessor)
|
||||
public ControllableExpiringFileInfoCache GetCache(IOptionsAccessor<RazorViewEngineOptions> optionsAccessor)
|
||||
{
|
||||
return new ControllableExpiringFileInfoCache(ApplicationEnvironment, optionsAccessor);
|
||||
}
|
||||
|
|
@ -69,16 +69,16 @@ namespace Microsoft.AspNet.Mvc.Core.Test
|
|||
cache.Sleep(offsetMilliseconds);
|
||||
}
|
||||
|
||||
public void Sleep(IOptionsAccessor<MvcOptions> accessor, ControllableExpiringFileInfoCache cache, int offsetMilliSeconds)
|
||||
public void Sleep(IOptionsAccessor<RazorViewEngineOptions> accessor, ControllableExpiringFileInfoCache cache, int offsetMilliSeconds)
|
||||
{
|
||||
var baseMilliSeconds = (int)accessor.Options.ViewEngineOptions.ExpirationBeforeCheckingFilesOnDisk.TotalMilliseconds;
|
||||
var baseMilliSeconds = (int)accessor.Options.ExpirationBeforeCheckingFilesOnDisk.TotalMilliseconds;
|
||||
|
||||
cache.Sleep(baseMilliSeconds + offsetMilliSeconds);
|
||||
}
|
||||
|
||||
public void SetExpiration(IOptionsAccessor<MvcOptions> accessor, TimeSpan expiration)
|
||||
public void SetExpiration(IOptionsAccessor<RazorViewEngineOptions> accessor, TimeSpan expiration)
|
||||
{
|
||||
accessor.Options.ViewEngineOptions.ExpirationBeforeCheckingFilesOnDisk = expiration;
|
||||
accessor.Options.ExpirationBeforeCheckingFilesOnDisk = expiration;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -87,7 +87,7 @@ namespace Microsoft.AspNet.Mvc.Core.Test
|
|||
var optionsAccessor = OptionsAccessor;
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2000, optionsAccessor.Options.ViewEngineOptions.ExpirationBeforeCheckingFilesOnDisk.TotalMilliseconds);
|
||||
Assert.Equal(2000, optionsAccessor.Options.ExpirationBeforeCheckingFilesOnDisk.TotalMilliseconds);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -323,7 +323,7 @@ namespace Microsoft.AspNet.Mvc.Core.Test
|
|||
public class ControllableExpiringFileInfoCache : ExpiringFileInfoCache
|
||||
{
|
||||
public ControllableExpiringFileInfoCache(IApplicationEnvironment env,
|
||||
IOptionsAccessor<MvcOptions> optionsAccessor)
|
||||
IOptionsAccessor<RazorViewEngineOptions> optionsAccessor)
|
||||
: base(env, optionsAccessor)
|
||||
{
|
||||
}
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
// 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 Microsoft.AspNet.PipelineCore;
|
||||
using Microsoft.AspNet.Routing;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.Razor
|
||||
{
|
||||
public class DefaultViewLocationCacheTest
|
||||
{
|
||||
public static IEnumerable<object[]> CacheEntryData
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return new[] { new ViewLocationExpanderContext(GetActionContext(), "test") };
|
||||
|
||||
var areaActionContext = GetActionContext("controller2", "myarea");
|
||||
yield return new[] { new ViewLocationExpanderContext(areaActionContext, "test2") };
|
||||
|
||||
var actionContext = GetActionContext("controller3", "area3");
|
||||
var values = new Dictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
{ "culture", "fr" },
|
||||
{ "theme", "sleek" }
|
||||
};
|
||||
var expanderContext = new ViewLocationExpanderContext(actionContext, "test3")
|
||||
{
|
||||
Values = values
|
||||
};
|
||||
|
||||
yield return new [] { expanderContext };
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(CacheEntryData))]
|
||||
public void Get_GeneratesCacheKeyIfItemDoesNotExist(ViewLocationExpanderContext context)
|
||||
{
|
||||
// Arrange
|
||||
var cache = new DefaultViewLocationCache();
|
||||
|
||||
// Act
|
||||
var result = cache.Get(context);
|
||||
|
||||
// Assert
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(CacheEntryData))]
|
||||
public void InvokingGetAfterSet_ReturnsCachedItem(ViewLocationExpanderContext context)
|
||||
{
|
||||
// Arrange
|
||||
var cache = new DefaultViewLocationCache();
|
||||
var value = Guid.NewGuid().ToString();
|
||||
|
||||
// Act
|
||||
cache.Set(context, value);
|
||||
var result = cache.Get(context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(value, result);
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> CacheKeyData
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return new object[]
|
||||
{
|
||||
new ViewLocationExpanderContext(GetActionContext(), "test"),
|
||||
"test:mycontroller"
|
||||
};
|
||||
|
||||
var areaActionContext = GetActionContext("controller2", "myarea");
|
||||
yield return new object[]
|
||||
{
|
||||
new ViewLocationExpanderContext(areaActionContext, "test2"),
|
||||
"test2:controller2:myarea"
|
||||
};
|
||||
|
||||
var actionContext = GetActionContext("controller3", "area3");
|
||||
var values = new Dictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
{ "culture", "fr" },
|
||||
{ "theme", "sleek" }
|
||||
};
|
||||
var expanderContext = new ViewLocationExpanderContext(actionContext, "test3")
|
||||
{
|
||||
Values = values
|
||||
};
|
||||
|
||||
yield return new object[]
|
||||
{
|
||||
expanderContext,
|
||||
"test3:controller3:area3:culture:fr:theme:sleek"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(CacheKeyData))]
|
||||
public void CacheKeyIsComputedBasedOnValuesInExpander(ViewLocationExpanderContext context, string expected)
|
||||
{
|
||||
// Act
|
||||
var result = DefaultViewLocationCache.GenerateKey(context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
public static ActionContext GetActionContext(string controller = "mycontroller",
|
||||
string area = null)
|
||||
{
|
||||
var routeData = new RouteData
|
||||
{
|
||||
Values = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase)
|
||||
};
|
||||
routeData.Values["controller"] = controller;
|
||||
if (area != null)
|
||||
{
|
||||
routeData.Values["area"] = area;
|
||||
}
|
||||
|
||||
return new ActionContext(new DefaultHttpContext(), routeData, new ActionDescriptor());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
// 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 Microsoft.Framework.DependencyInjection;
|
||||
using Microsoft.Framework.OptionsModel;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.Razor.OptionDescriptors
|
||||
{
|
||||
public class DefaultViewLocationExpanderProviderTest
|
||||
{
|
||||
[Fact]
|
||||
public void ViewLocationExpanders_ReturnsActivatedListOfExpanders()
|
||||
{
|
||||
// Arrange
|
||||
var service = Mock.Of<ITestService>();
|
||||
var expander = Mock.Of<IViewLocationExpander>();
|
||||
var type = typeof(TestViewLocationExpander);
|
||||
var typeActivator = new TypeActivator();
|
||||
var serviceProvider = new Mock<IServiceProvider>();
|
||||
serviceProvider.Setup(p => p.GetService(typeof(ITestService)))
|
||||
.Returns(service);
|
||||
var options = new RazorViewEngineOptions();
|
||||
options.ViewLocationExpanders.Add(type);
|
||||
options.ViewLocationExpanders.Add(expander);
|
||||
var accessor = new Mock<IOptionsAccessor<RazorViewEngineOptions>>();
|
||||
accessor.SetupGet(a => a.Options)
|
||||
.Returns(options);
|
||||
var provider = new DefaultViewLocationExpanderProvider(accessor.Object,
|
||||
typeActivator,
|
||||
serviceProvider.Object);
|
||||
|
||||
// Act
|
||||
var result = provider.ViewLocationExpanders;
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, result.Count);
|
||||
var testExpander = Assert.IsType<TestViewLocationExpander>(result[0]);
|
||||
Assert.Same(service, testExpander.Service);
|
||||
Assert.Same(expander, result[1]);
|
||||
}
|
||||
|
||||
private class TestViewLocationExpander : IViewLocationExpander
|
||||
{
|
||||
public TestViewLocationExpander(ITestService service)
|
||||
{
|
||||
Service = service;
|
||||
}
|
||||
|
||||
public ITestService Service { get; private set; }
|
||||
|
||||
public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context,
|
||||
IEnumerable<string> viewLocations)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void PopulateValues(ViewLocationExpanderContext context)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
public interface ITestService
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
// 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 Microsoft.AspNet.Mvc.Razor.OptionDescriptors;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.Razor
|
||||
{
|
||||
public class ViewLocationExpanderDescriptorExtensionsTest
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(-1)]
|
||||
[InlineData(5)]
|
||||
public void Insert_WithType_ThrowsIfIndexIsOutOfBounds(int index)
|
||||
{
|
||||
// Arrange
|
||||
var collection = new List<ViewLocationExpanderDescriptor>
|
||||
{
|
||||
new ViewLocationExpanderDescriptor(Mock.Of<IViewLocationExpander>()),
|
||||
new ViewLocationExpanderDescriptor(Mock.Of<IViewLocationExpander>())
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentOutOfRangeException>("index",
|
||||
() => collection.Insert(index, typeof(IViewLocationExpander)));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(-2)]
|
||||
[InlineData(3)]
|
||||
public void Insert_WithInstance_ThrowsIfIndexIsOutOfBounds(int index)
|
||||
{
|
||||
// Arrange
|
||||
var collection = new List<ViewLocationExpanderDescriptor>
|
||||
{
|
||||
new ViewLocationExpanderDescriptor(Mock.Of<IViewLocationExpander>()),
|
||||
new ViewLocationExpanderDescriptor(Mock.Of<IViewLocationExpander>())
|
||||
};
|
||||
var expander = Mock.Of<IViewLocationExpander>();
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentOutOfRangeException>("index", () => collection.Insert(index, expander));
|
||||
}
|
||||
|
||||
[InlineData]
|
||||
public void ViewLocationExpanderDescriptors_AddsTypesAndInstances()
|
||||
{
|
||||
// Arrange
|
||||
var expander = Mock.Of<IViewLocationExpander>();
|
||||
var type = typeof(TestViewLocationExpander);
|
||||
var collection = new List<ViewLocationExpanderDescriptor>();
|
||||
|
||||
// Act
|
||||
collection.Add(expander);
|
||||
collection.Insert(0, type);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, collection.Count);
|
||||
Assert.IsType<TestViewLocationExpander>(collection[0].Instance);
|
||||
Assert.Same(expander, collection[0].Instance);
|
||||
}
|
||||
|
||||
private class TestViewLocationExpander : IViewLocationExpander
|
||||
{
|
||||
public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context,
|
||||
IEnumerable<string> viewLocations)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void PopulateValues(ViewLocationExpanderContext context)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
// 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 Microsoft.AspNet.Testing;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.Razor.OptionDescriptors
|
||||
{
|
||||
public class ViewLocationExpanderDescriptorTest
|
||||
{
|
||||
[Fact]
|
||||
public void ConstructorThrows_IfTypeIsNotViewLocationExpander()
|
||||
{
|
||||
// Arrange
|
||||
var viewEngineType = typeof(IViewLocationExpander).FullName;
|
||||
var type = typeof(string);
|
||||
var expected = string.Format("The type '{0}' must derive from '{1}'.",
|
||||
type.FullName, viewEngineType);
|
||||
|
||||
// Act & Assert
|
||||
ExceptionAssert.ThrowsArgument(() => new ViewLocationExpanderDescriptor(type), "type", expected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConstructorSetsViewLocationExpanderType()
|
||||
{
|
||||
// Arrange
|
||||
var type = typeof(TestViewLocationExpander);
|
||||
|
||||
// Act
|
||||
var descriptor = new ViewLocationExpanderDescriptor(type);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(type, descriptor.OptionType);
|
||||
Assert.Null(descriptor.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConstructorSetsViewLocationExpanderAndType()
|
||||
{
|
||||
// Arrange
|
||||
var expander = new TestViewLocationExpander();
|
||||
|
||||
// Act
|
||||
var descriptor = new ViewLocationExpanderDescriptor(expander);
|
||||
|
||||
// Assert
|
||||
Assert.Same(expander, descriptor.Instance);
|
||||
Assert.Equal(expander.GetType(), descriptor.OptionType);
|
||||
}
|
||||
|
||||
private class TestViewLocationExpander : IViewLocationExpander
|
||||
{
|
||||
public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context,
|
||||
IEnumerable<string> viewLocations)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void PopulateValues(ViewLocationExpanderContext context)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,8 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNet.Mvc.Razor.OptionDescriptors;
|
||||
using Microsoft.AspNet.Mvc.Rendering;
|
||||
using Microsoft.AspNet.PipelineCore;
|
||||
using Microsoft.AspNet.Routing;
|
||||
|
|
@ -40,7 +42,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
|
|||
public void FindView_WithFullPathReturnsNotFound_WhenPathDoesNotMatchExtension(string viewName)
|
||||
{
|
||||
// Arrange
|
||||
var viewEngine = CreateSearchLocationViewEngineTester();
|
||||
var viewEngine = CreateViewEngine();
|
||||
var context = GetActionContext(_controllerTestContext);
|
||||
|
||||
// Act
|
||||
|
|
@ -55,7 +57,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
|
|||
public void FindViewFullPathSucceedsWithCshtmlEnding(string viewName)
|
||||
{
|
||||
// Arrange
|
||||
var viewEngine = CreateSearchLocationViewEngineTester();
|
||||
var viewEngine = CreateViewEngine();
|
||||
// Append .cshtml so the viewname is no longer invalid
|
||||
viewName += ".cshtml";
|
||||
var context = GetActionContext(_controllerTestContext);
|
||||
|
|
@ -72,7 +74,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
|
|||
public void FindPartialView_WithFullPathReturnsNotFound_WhenPathDoesNotMatchExtension(string partialViewName)
|
||||
{
|
||||
// Arrange
|
||||
var viewEngine = CreateSearchLocationViewEngineTester();
|
||||
var viewEngine = CreateViewEngine();
|
||||
var context = GetActionContext(_controllerTestContext);
|
||||
|
||||
// Act
|
||||
|
|
@ -87,7 +89,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
|
|||
public void FindPartialViewFullPathSucceedsWithCshtmlEnding(string partialViewName)
|
||||
{
|
||||
// Arrange
|
||||
var viewEngine = CreateSearchLocationViewEngineTester();
|
||||
var viewEngine = CreateViewEngine();
|
||||
// Append .cshtml so the viewname is no longer invalid
|
||||
partialViewName += ".cshtml";
|
||||
var context = GetActionContext(_controllerTestContext);
|
||||
|
|
@ -104,7 +106,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
|
|||
{
|
||||
// Arrange
|
||||
var searchedLocations = new List<string>();
|
||||
var viewEngine = CreateSearchLocationViewEngineTester();
|
||||
var viewEngine = CreateViewEngine();
|
||||
var context = GetActionContext(_areaTestContext);
|
||||
|
||||
// Act
|
||||
|
|
@ -112,7 +114,8 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
|
|||
|
||||
// Assert
|
||||
Assert.False(result.Success);
|
||||
Assert.Equal(new[] {
|
||||
Assert.Equal(new[]
|
||||
{
|
||||
"/Areas/foo/Views/bar/partial.cshtml",
|
||||
"/Areas/foo/Views/Shared/partial.cshtml",
|
||||
"/Views/Shared/partial.cshtml",
|
||||
|
|
@ -123,7 +126,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
|
|||
public void FindPartialViewFailureSearchesCorrectLocationsWithoutAreas()
|
||||
{
|
||||
// Arrange
|
||||
var viewEngine = CreateSearchLocationViewEngineTester();
|
||||
var viewEngine = CreateViewEngine();
|
||||
var context = GetActionContext(_controllerTestContext);
|
||||
|
||||
// Act
|
||||
|
|
@ -141,7 +144,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
|
|||
public void FindViewFailureSearchesCorrectLocationsWithAreas()
|
||||
{
|
||||
// Arrange
|
||||
var viewEngine = CreateSearchLocationViewEngineTester();
|
||||
var viewEngine = CreateViewEngine();
|
||||
var context = GetActionContext(_areaTestContext);
|
||||
|
||||
// Act
|
||||
|
|
@ -160,7 +163,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
|
|||
public void FindViewFailureSearchesCorrectLocationsWithoutAreas()
|
||||
{
|
||||
// Arrange
|
||||
var viewEngine = CreateSearchLocationViewEngineTester();
|
||||
var viewEngine = CreateViewEngine();
|
||||
var context = GetActionContext(_controllerTestContext);
|
||||
|
||||
// Act
|
||||
|
|
@ -182,7 +185,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
|
|||
var page = Mock.Of<IRazorPage>();
|
||||
pageFactory.Setup(p => p.CreateInstance(It.IsAny<string>()))
|
||||
.Returns(Mock.Of<IRazorPage>());
|
||||
var viewEngine = new RazorViewEngine(pageFactory.Object);
|
||||
var viewEngine = CreateViewEngine(pageFactory.Object);
|
||||
var context = GetActionContext(_controllerTestContext);
|
||||
|
||||
// Act
|
||||
|
|
@ -194,17 +197,261 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
|
|||
Assert.Equal("/Views/bar/test-view.cshtml", result.ViewName);
|
||||
}
|
||||
|
||||
private IViewEngine CreateSearchLocationViewEngineTester()
|
||||
[Fact]
|
||||
public void FindView_UsesViewLocationFormat_IfRouteDoesNotContainArea()
|
||||
{
|
||||
// Arrange
|
||||
var pageFactory = new Mock<IRazorPageFactory>();
|
||||
pageFactory.Setup(vpf => vpf.CreateInstance(It.IsAny<string>()))
|
||||
.Returns<RazorPage>(null);
|
||||
var page = Mock.Of<IRazorPage>();
|
||||
pageFactory.Setup(p => p.CreateInstance("fake-path1/bar/test-view.rzr"))
|
||||
.Returns(Mock.Of<IRazorPage>())
|
||||
.Verifiable();
|
||||
var viewEngine = new OverloadedLocationViewEngine(pageFactory.Object,
|
||||
GetViewLocationExpanders(),
|
||||
GetViewLocationCache());
|
||||
var context = GetActionContext(_controllerTestContext);
|
||||
|
||||
var viewEngine = new RazorViewEngine(pageFactory.Object);
|
||||
// Act
|
||||
var result = viewEngine.FindView(context, "test-view");
|
||||
|
||||
// Assert
|
||||
pageFactory.Verify();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindView_UsesAreaViewLocationFormat_IfRouteContainsArea()
|
||||
{
|
||||
// Arrange
|
||||
var pageFactory = new Mock<IRazorPageFactory>();
|
||||
var page = Mock.Of<IRazorPage>();
|
||||
pageFactory.Setup(p => p.CreateInstance("fake-area-path/foo/bar/test-view2.rzr"))
|
||||
.Returns(Mock.Of<IRazorPage>())
|
||||
.Verifiable();
|
||||
var viewEngine = new OverloadedLocationViewEngine(pageFactory.Object,
|
||||
GetViewLocationExpanders(),
|
||||
GetViewLocationCache());
|
||||
var context = GetActionContext(_areaTestContext);
|
||||
|
||||
// Act
|
||||
var result = viewEngine.FindView(context, "test-view2");
|
||||
|
||||
// Assert
|
||||
pageFactory.Verify();
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> FindView_UsesViewLocationExpandersToLocateViewsData
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return new object[]
|
||||
{
|
||||
_controllerTestContext,
|
||||
new[]
|
||||
{
|
||||
"/Views/{1}/{0}.cshtml",
|
||||
"/Views/Shared/{0}.cshtml"
|
||||
}
|
||||
};
|
||||
|
||||
yield return new object[]
|
||||
{
|
||||
_areaTestContext,
|
||||
new[]
|
||||
{
|
||||
"/Areas/{2}/Views/{1}/{0}.cshtml",
|
||||
"/Areas/{2}/Views/Shared/{0}.cshtml",
|
||||
"/Views/Shared/{0}.cshtml"
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(FindView_UsesViewLocationExpandersToLocateViewsData))]
|
||||
public void FindView_UsesViewLocationExpandersToLocateViews(IDictionary<string, object> routeValues,
|
||||
IEnumerable<string> expectedSeeds)
|
||||
{
|
||||
// Arrange
|
||||
var pageFactory = new Mock<IRazorPageFactory>();
|
||||
pageFactory.Setup(p => p.CreateInstance("test-string/bar.cshtml"))
|
||||
.Returns(Mock.Of<IRazorPage>())
|
||||
.Verifiable();
|
||||
var expander1Result = new[] { "some-seed" };
|
||||
var expander1 = new Mock<IViewLocationExpander>();
|
||||
expander1.Setup(e => e.PopulateValues(It.IsAny<ViewLocationExpanderContext>()))
|
||||
.Callback((ViewLocationExpanderContext c) =>
|
||||
{
|
||||
Assert.NotNull(c.ActionContext);
|
||||
c.Values["expander-key"] = expander1.ToString();
|
||||
})
|
||||
.Verifiable();
|
||||
expander1.Setup(e => e.ExpandViewLocations(It.IsAny<ViewLocationExpanderContext>(),
|
||||
It.IsAny<IEnumerable<string>>()))
|
||||
.Callback((ViewLocationExpanderContext c, IEnumerable<string> seeds) =>
|
||||
{
|
||||
Assert.NotNull(c.ActionContext);
|
||||
Assert.Equal(expectedSeeds, seeds);
|
||||
})
|
||||
.Returns(expander1Result)
|
||||
.Verifiable();
|
||||
|
||||
var expander2 = new Mock<IViewLocationExpander>();
|
||||
expander2.Setup(e => e.ExpandViewLocations(It.IsAny<ViewLocationExpanderContext>(),
|
||||
It.IsAny<IEnumerable<string>>()))
|
||||
.Callback((ViewLocationExpanderContext c, IEnumerable<string> seeds) =>
|
||||
{
|
||||
Assert.Equal(expander1Result, seeds);
|
||||
})
|
||||
.Returns(new[] { "test-string/{1}.cshtml" })
|
||||
.Verifiable();
|
||||
|
||||
var viewEngine = CreateViewEngine(pageFactory.Object,
|
||||
new[] { expander1.Object, expander2.Object });
|
||||
var context = GetActionContext(routeValues);
|
||||
|
||||
// Act
|
||||
var result = viewEngine.FindView(context, "test-view");
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Success);
|
||||
Assert.IsAssignableFrom<IRazorView>(result.View);
|
||||
pageFactory.Verify();
|
||||
expander1.Verify();
|
||||
expander2.Verify();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindView_CachesValuesIfViewWasFound()
|
||||
{
|
||||
// Arrange
|
||||
var pageFactory = new Mock<IRazorPageFactory>();
|
||||
pageFactory.Setup(p => p.CreateInstance("/Views/bar/baz.cshtml"))
|
||||
.Verifiable();
|
||||
pageFactory.Setup(p => p.CreateInstance("/Views/Shared/baz.cshtml"))
|
||||
.Returns(Mock.Of<IRazorPage>())
|
||||
.Verifiable();
|
||||
var cache = GetViewLocationCache();
|
||||
var cacheMock = Mock.Get<IViewLocationCache>(cache);
|
||||
|
||||
cacheMock.Setup(c => c.Set(It.IsAny<ViewLocationExpanderContext>(), "/Views/Shared/baz.cshtml"))
|
||||
.Verifiable();
|
||||
|
||||
var viewEngine = CreateViewEngine(pageFactory.Object, cache: cache);
|
||||
var context = GetActionContext(_controllerTestContext);
|
||||
|
||||
// Act
|
||||
var result = viewEngine.FindView(context, "baz");
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Success);
|
||||
pageFactory.Verify();
|
||||
cacheMock.Verify();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindView_UsesCachedValueIfViewWasFound()
|
||||
{
|
||||
// Arrange
|
||||
var pageFactory = new Mock<IRazorPageFactory>(MockBehavior.Strict);
|
||||
pageFactory.Setup(p => p.CreateInstance("some-view-location"))
|
||||
.Returns(Mock.Of<IRazorPage>())
|
||||
.Verifiable();
|
||||
var expander = new Mock<IViewLocationExpander>(MockBehavior.Strict);
|
||||
expander.Setup(v => v.PopulateValues(It.IsAny<ViewLocationExpanderContext>()))
|
||||
.Verifiable();
|
||||
var cacheMock = new Mock<IViewLocationCache>();
|
||||
cacheMock.Setup(c => c.Get(It.IsAny<ViewLocationExpanderContext>()))
|
||||
.Returns("some-view-location")
|
||||
.Verifiable();
|
||||
|
||||
var viewEngine = CreateViewEngine(pageFactory.Object,
|
||||
new[] { expander.Object },
|
||||
cacheMock.Object);
|
||||
var context = GetActionContext(_controllerTestContext);
|
||||
|
||||
// Act
|
||||
var result = viewEngine.FindView(context, "baz");
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Success);
|
||||
pageFactory.Verify();
|
||||
cacheMock.Verify();
|
||||
expander.Verify();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindView_LooksForViewsIfCachedViewDoesNotExist()
|
||||
{
|
||||
// Arrange
|
||||
var pageFactory = new Mock<IRazorPageFactory>();
|
||||
pageFactory.Setup(p => p.CreateInstance("expired-location"))
|
||||
.Returns((IRazorPage)null)
|
||||
.Verifiable();
|
||||
pageFactory.Setup(p => p.CreateInstance("some-view-location"))
|
||||
.Returns(Mock.Of<IRazorPage>())
|
||||
.Verifiable();
|
||||
var cacheMock = new Mock<IViewLocationCache>();
|
||||
cacheMock.Setup(c => c.Get(It.IsAny<ViewLocationExpanderContext>()))
|
||||
.Returns("expired-location");
|
||||
|
||||
var expander = new Mock<IViewLocationExpander>();
|
||||
expander.Setup(v => v.PopulateValues(It.IsAny<ViewLocationExpanderContext>()))
|
||||
.Verifiable();
|
||||
var expanderResult = new[] { "some-view-location" };
|
||||
expander.Setup(v => v.ExpandViewLocations(
|
||||
It.IsAny<ViewLocationExpanderContext>(), It.IsAny<IEnumerable<string>>()))
|
||||
.Returns((ViewLocationExpanderContext c, IEnumerable<string> seed) => expanderResult)
|
||||
.Verifiable();
|
||||
|
||||
var viewEngine = CreateViewEngine(pageFactory.Object,
|
||||
new[] { expander.Object },
|
||||
cacheMock.Object);
|
||||
var context = GetActionContext(_controllerTestContext);
|
||||
|
||||
// Act
|
||||
var result = viewEngine.FindView(context, "baz");
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Success);
|
||||
pageFactory.Verify();
|
||||
cacheMock.Verify();
|
||||
expander.Verify();
|
||||
}
|
||||
|
||||
private IViewEngine CreateViewEngine(IRazorPageFactory pageFactory = null,
|
||||
IEnumerable<IViewLocationExpander> expanders = null,
|
||||
IViewLocationCache cache = null)
|
||||
{
|
||||
pageFactory = pageFactory ?? Mock.Of<IRazorPageFactory>();
|
||||
cache = cache ?? GetViewLocationCache();
|
||||
var viewLocationExpanderProvider = GetViewLocationExpanders(expanders);
|
||||
|
||||
var viewEngine = new RazorViewEngine(pageFactory,
|
||||
viewLocationExpanderProvider,
|
||||
cache);
|
||||
|
||||
return viewEngine;
|
||||
}
|
||||
|
||||
private static IViewLocationExpanderProvider GetViewLocationExpanders(
|
||||
IEnumerable<IViewLocationExpander> expanders = null)
|
||||
{
|
||||
expanders = expanders ?? Enumerable.Empty<IViewLocationExpander>();
|
||||
var viewLocationExpander = new Mock<IViewLocationExpanderProvider>();
|
||||
viewLocationExpander.Setup(v => v.ViewLocationExpanders)
|
||||
.Returns(expanders.ToList());
|
||||
return viewLocationExpander.Object;
|
||||
}
|
||||
|
||||
private static IViewLocationCache GetViewLocationCache()
|
||||
{
|
||||
var cacheMock = new Mock<IViewLocationCache>();
|
||||
cacheMock.Setup(c => c.Get(It.IsAny<ViewLocationExpanderContext>()))
|
||||
.Returns<string>(null);
|
||||
|
||||
return cacheMock.Object;
|
||||
}
|
||||
|
||||
private static ActionContext GetActionContext(IDictionary<string, object> routeValues,
|
||||
IRazorView razorView = null)
|
||||
{
|
||||
|
|
@ -217,5 +464,31 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
|
|||
var routeData = new RouteData { Values = routeValues };
|
||||
return new ActionContext(httpContext, routeData, new ActionDescriptor());
|
||||
}
|
||||
|
||||
private class OverloadedLocationViewEngine : RazorViewEngine
|
||||
{
|
||||
public OverloadedLocationViewEngine(IRazorPageFactory pageFactory,
|
||||
IViewLocationExpanderProvider expanderProvider,
|
||||
IViewLocationCache cache)
|
||||
: base(pageFactory, expanderProvider, cache)
|
||||
{
|
||||
}
|
||||
|
||||
public override IEnumerable<string> ViewLocationFormats
|
||||
{
|
||||
get
|
||||
{
|
||||
return new[] { "fake-path1/{1}/{0}.rzr" };
|
||||
}
|
||||
}
|
||||
|
||||
public override IEnumerable<string> AreaViewLocationFormats
|
||||
{
|
||||
get
|
||||
{
|
||||
return new[] { "fake-area-path/{2}/{1}/{0}.rzr" };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
// 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 Microsoft.AspNet.Mvc;
|
||||
|
||||
namespace RazorWebSite.Controllers
|
||||
{
|
||||
public class TemplateExpander : Controller
|
||||
{
|
||||
public ViewResult Index()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
// 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 Microsoft.AspNet.Mvc;
|
||||
using Microsoft.AspNet.Mvc.Razor;
|
||||
|
||||
namespace RazorWebSite
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see cref="IViewLocationExpander"/> that replaces adds the language as an extension prefix to view names.
|
||||
/// </summary>
|
||||
/// <example>
|
||||
/// For the default case with no areas, views are generated with the following patterns (assuming controller is
|
||||
/// "Home", action is "Index" and language is "en")
|
||||
/// Views/Home/en/Action
|
||||
/// Views/Home/Action
|
||||
/// Views/Shared/en/Action
|
||||
/// Views/Shared/Action
|
||||
/// </example>
|
||||
public class LanguageViewLocationExpander : IViewLocationExpander
|
||||
{
|
||||
private const string ValueKey = "language";
|
||||
private readonly Func<ActionContext, string> _valueFactory;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of <see cref="LanguageViewLocationExpander"/>.
|
||||
/// </summary>
|
||||
/// <param name="valueFactory">A factory that provides tbe language to use for expansion.</param>
|
||||
public LanguageViewLocationExpander(Func<ActionContext, string> valueFactory)
|
||||
{
|
||||
_valueFactory = valueFactory;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void PopulateValues(ViewLocationExpanderContext context)
|
||||
{
|
||||
var value = _valueFactory(context.ActionContext);
|
||||
if (!string.IsNullOrEmpty(value))
|
||||
{
|
||||
context.Values[ValueKey] = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context,
|
||||
IEnumerable<string> viewLocations)
|
||||
{
|
||||
string value;
|
||||
if (context.Values.TryGetValue(ValueKey, out value))
|
||||
{
|
||||
return ExpandViewLocationsCore(viewLocations, value);
|
||||
}
|
||||
|
||||
return viewLocations;
|
||||
}
|
||||
|
||||
private IEnumerable<string> ExpandViewLocationsCore(IEnumerable<string> viewLocations,
|
||||
string value)
|
||||
{
|
||||
foreach (var location in viewLocations)
|
||||
{
|
||||
yield return location.Replace("{0}", value + "/{0}");
|
||||
yield return location;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
using Microsoft.AspNet.Builder;
|
||||
using Microsoft.AspNet.Mvc;
|
||||
using Microsoft.AspNet.Mvc.Razor;
|
||||
using Microsoft.Framework.DependencyInjection;
|
||||
|
||||
namespace RazorWebSite
|
||||
|
|
@ -15,6 +17,12 @@ namespace RazorWebSite
|
|||
// Add MVC services to the services container
|
||||
services.AddMvc(configuration);
|
||||
services.AddTransient<InjectedHelper>();
|
||||
services.SetupOptions<RazorViewEngineOptions>(options =>
|
||||
{
|
||||
var expander = new LanguageViewLocationExpander(
|
||||
context => context.HttpContext.Request.Query["language-expander-value"]);
|
||||
options.ViewLocationExpanders.Add(expander);
|
||||
});
|
||||
});
|
||||
|
||||
// Add MVC to the request pipeline
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
expander-index
|
||||
@await Html.PartialAsync("_Partial")
|
||||
|
|
@ -0,0 +1 @@
|
|||
expander-partial
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fr-index
|
||||
@await Html.PartialAsync("_Partial")
|
||||
|
|
@ -0,0 +1 @@
|
|||
fr-partial
|
||||
|
|
@ -0,0 +1 @@
|
|||
gb-partial
|
||||
Loading…
Reference in New Issue