diff --git a/samples/MvcSample.Web/HomeController.cs b/samples/MvcSample.Web/HomeController.cs
index 00ca9e73a7..30341b570b 100644
--- a/samples/MvcSample.Web/HomeController.cs
+++ b/samples/MvcSample.Web/HomeController.cs
@@ -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()
{
diff --git a/samples/MvcSample.Web/LanguageViewLocationExpander.cs b/samples/MvcSample.Web/LanguageViewLocationExpander.cs
new file mode 100644
index 0000000000..4ec43a9b63
--- /dev/null
+++ b/samples/MvcSample.Web/LanguageViewLocationExpander.cs
@@ -0,0 +1,66 @@
+using System;
+using System.Collections.Generic;
+using Microsoft.AspNet.Mvc;
+using Microsoft.AspNet.Mvc.Razor;
+
+namespace MvcSample.Web
+{
+ ///
+ /// A that replaces adds the language as an extension prefix to view names.
+ ///
+ ///
+ /// 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
+ ///
+ public class LanguageViewLocationExpander : IViewLocationExpander
+ {
+ private const string ValueKey = "language";
+ private readonly Func _valueFactory;
+
+ ///
+ /// Initializes a new instance of .
+ ///
+ /// A factory that provides tbe language to use for expansion.
+ public LanguageViewLocationExpander(Func valueFactory)
+ {
+ _valueFactory = valueFactory;
+ }
+
+ ///
+ public void PopulateValues(ViewLocationExpanderContext context)
+ {
+ var value = _valueFactory(context.ActionContext);
+ if (!string.IsNullOrEmpty(value))
+ {
+ context.Values[ValueKey] = value;
+ }
+ }
+
+ ///
+ public virtual IEnumerable ExpandViewLocations(ViewLocationExpanderContext context,
+ IEnumerable viewLocations)
+ {
+ string value;
+ if (context.Values.TryGetValue(ValueKey, out value))
+ {
+ return ExpandViewLocationsCore(viewLocations, value);
+ }
+
+ return viewLocations;
+ }
+
+ private IEnumerable ExpandViewLocationsCore(IEnumerable viewLocations,
+ string value)
+ {
+ foreach (var location in viewLocations)
+ {
+ yield return location.Replace("{0}", value + "/{0}");
+ yield return location;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/samples/MvcSample.Web/Startup.cs b/samples/MvcSample.Web/Startup.cs
index 81131c7e7f..76d0b496d1 100644
--- a/samples/MvcSample.Web/Startup.cs
+++ b/samples/MvcSample.Web/Startup.cs
@@ -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(options =>
+ {
+ var expander = new LanguageViewLocationExpander(
+ context => context.HttpContext.Request.Query["language"]);
+ options.ViewLocationExpanders.Insert(0, expander);
+ });
// Create the autofac container
ContainerBuilder builder = new ContainerBuilder();
diff --git a/samples/MvcSample.Web/Views/Home/Language.cshtml b/samples/MvcSample.Web/Views/Home/Language.cshtml
new file mode 100644
index 0000000000..9f36c7f0f1
--- /dev/null
+++ b/samples/MvcSample.Web/Views/Home/Language.cshtml
@@ -0,0 +1,5 @@
+
+
COLOR
+
HUMOR
+
ITEMIZE
+
diff --git a/samples/MvcSample.Web/Views/Home/en-gb/Language.cshtml b/samples/MvcSample.Web/Views/Home/en-gb/Language.cshtml
new file mode 100644
index 0000000000..5d750371cb
--- /dev/null
+++ b/samples/MvcSample.Web/Views/Home/en-gb/Language.cshtml
@@ -0,0 +1,5 @@
+
+
COLOUR
+
HUMOUR
+
ITEMISE
+
diff --git a/samples/MvcSample.Web/Views/Home/fr/Language.cshtml b/samples/MvcSample.Web/Views/Home/fr/Language.cshtml
new file mode 100644
index 0000000000..363c8b3f42
--- /dev/null
+++ b/samples/MvcSample.Web/Views/Home/fr/Language.cshtml
@@ -0,0 +1,5 @@
+
+
COULEUR
+
HUMOUR
+
DÉTAILLER
+
diff --git a/samples/MvcSample.Web/Views/Shared/_Layout.cshtml b/samples/MvcSample.Web/Views/Shared/_Layout.cshtml
index 2f5901fdb6..767b0b972e 100644
--- a/samples/MvcSample.Web/Views/Shared/_Layout.cshtml
+++ b/samples/MvcSample.Web/Views/Shared/_Layout.cshtml
@@ -5,6 +5,12 @@
@ViewBag.Title - My ASP.NET Application
+
@RenderSection("header", required: false)
diff --git a/src/Microsoft.AspNet.Mvc.Core/MvcOptions.cs b/src/Microsoft.AspNet.Mvc.Core/MvcOptions.cs
index e47e149eca..45f0ac44a1 100644
--- a/src/Microsoft.AspNet.Mvc.Core/MvcOptions.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/MvcOptions.cs
@@ -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
///
public List InputFormatters { get; private set; }
- ///
- /// Provides programmatic configuration for the default .
- ///
- public RazorViewEngineOptions ViewEngineOptions
- {
- get
- {
- return _viewEngineOptions;
- }
-
- set
- {
- if (value == null)
- {
- throw new ArgumentNullException("value",
- Resources.FormatPropertyOfTypeCannotBeNull("ViewEngineOptions",
- typeof(MvcOptions)));
- }
-
- _viewEngineOptions = value;
- }
- }
-
///
/// 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 s used by
/// .
///
- public List ModelValidatorProviders { get; } =
- new List();
+ public List ModelValidatorProviders { get; }
+ = new List();
///
/// Gets a list of descriptors that represent used
diff --git a/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs
index 3664b0dcd7..be86cd7f05 100644
--- a/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs
@@ -1354,6 +1354,54 @@ namespace Microsoft.AspNet.Mvc.Core
return string.Format(CultureInfo.CurrentCulture, GetString("AttributeRoute_AggregateErrorMessage_ErrorNumber"), p0, p1, p2);
}
+ ///
+ /// Could not find a replacement for view expansion token '{0}'.
+ ///
+ internal static string TemplatedViewLocationExpander_NoReplacementToken
+ {
+ get { return GetString("TemplatedViewLocationExpander_NoReplacementToken"); }
+ }
+
+ ///
+ /// Could not find a replacement for view expansion token '{0}'.
+ ///
+ internal static string FormatTemplatedViewLocationExpander_NoReplacementToken(object p0)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("TemplatedViewLocationExpander_NoReplacementToken"), p0);
+ }
+
+ ///
+ /// {0} must be executed before {1} can be invoked.
+ ///
+ internal static string TemplatedExpander_PopulateValuesMustBeInvokedFirst
+ {
+ get { return GetString("TemplatedExpander_PopulateValuesMustBeInvokedFirst"); }
+ }
+
+ ///
+ /// {0} must be executed before {1} can be invoked.
+ ///
+ internal static string FormatTemplatedExpander_PopulateValuesMustBeInvokedFirst(object p0, object p1)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("TemplatedExpander_PopulateValuesMustBeInvokedFirst"), p0, p1);
+ }
+
+ ///
+ /// The result of value factory cannot be null.
+ ///
+ internal static string TemplatedExpander_ValueFactoryCannotReturnNull
+ {
+ get { return GetString("TemplatedExpander_ValueFactoryCannotReturnNull"); }
+ }
+
+ ///
+ /// The result of value factory cannot be null.
+ ///
+ internal static string FormatTemplatedExpander_ValueFactoryCannotReturnNull()
+ {
+ return GetString("TemplatedExpander_ValueFactoryCannotReturnNull");
+ }
+
///
/// A method '{0}' that defines attribute routed actions must not have attributes that implement '{1}' and do not implement '{2}':{3}{4}
///
diff --git a/src/Microsoft.AspNet.Mvc.Core/Rendering/CompositeViewEngine.cs b/src/Microsoft.AspNet.Mvc.Core/Rendering/ViewEngine/CompositeViewEngine.cs
similarity index 100%
rename from src/Microsoft.AspNet.Mvc.Core/Rendering/CompositeViewEngine.cs
rename to src/Microsoft.AspNet.Mvc.Core/Rendering/ViewEngine/CompositeViewEngine.cs
diff --git a/src/Microsoft.AspNet.Mvc.Core/Rendering/ICompositeViewEngine.cs b/src/Microsoft.AspNet.Mvc.Core/Rendering/ViewEngine/ICompositeViewEngine.cs
similarity index 100%
rename from src/Microsoft.AspNet.Mvc.Core/Rendering/ICompositeViewEngine.cs
rename to src/Microsoft.AspNet.Mvc.Core/Rendering/ViewEngine/ICompositeViewEngine.cs
diff --git a/src/Microsoft.AspNet.Mvc.Core/Rendering/IView.cs b/src/Microsoft.AspNet.Mvc.Core/Rendering/ViewEngine/IView.cs
similarity index 100%
rename from src/Microsoft.AspNet.Mvc.Core/Rendering/IView.cs
rename to src/Microsoft.AspNet.Mvc.Core/Rendering/ViewEngine/IView.cs
diff --git a/src/Microsoft.AspNet.Mvc.Core/Rendering/IViewEngine.cs b/src/Microsoft.AspNet.Mvc.Core/Rendering/ViewEngine/IViewEngine.cs
similarity index 100%
rename from src/Microsoft.AspNet.Mvc.Core/Rendering/IViewEngine.cs
rename to src/Microsoft.AspNet.Mvc.Core/Rendering/ViewEngine/IViewEngine.cs
diff --git a/src/Microsoft.AspNet.Mvc.Core/Rendering/IViewEngineProvider.cs b/src/Microsoft.AspNet.Mvc.Core/Rendering/ViewEngine/IViewEngineProvider.cs
similarity index 100%
rename from src/Microsoft.AspNet.Mvc.Core/Rendering/IViewEngineProvider.cs
rename to src/Microsoft.AspNet.Mvc.Core/Rendering/ViewEngine/IViewEngineProvider.cs
diff --git a/src/Microsoft.AspNet.Mvc.Core/Rendering/ViewEngineResult.cs b/src/Microsoft.AspNet.Mvc.Core/Rendering/ViewEngine/ViewEngineResult.cs
similarity index 100%
rename from src/Microsoft.AspNet.Mvc.Core/Rendering/ViewEngineResult.cs
rename to src/Microsoft.AspNet.Mvc.Core/Rendering/ViewEngine/ViewEngineResult.cs
diff --git a/src/Microsoft.AspNet.Mvc.Core/Resources.resx b/src/Microsoft.AspNet.Mvc.Core/Resources.resx
index ce9383a19a..dc3b141a1a 100644
--- a/src/Microsoft.AspNet.Mvc.Core/Resources.resx
+++ b/src/Microsoft.AspNet.Mvc.Core/Resources.resx
@@ -375,6 +375,15 @@
Error {0}:{1}{2}{0} is the error number, {1} is Environment.NewLine {2} is the error message
+
+ Could not find a replacement for view expansion token '{0}'.
+
+
+ {0} must be executed before {1} can be invoked.
+
+
+ The result of value factory cannot be null.
+
A method '{0}' that defines attribute routed actions must not have attributes that implement '{1}' and do not implement '{2}':{3}{4}{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
diff --git a/src/Microsoft.AspNet.Mvc.Core/project.json b/src/Microsoft.AspNet.Mvc.Core/project.json
index e6aaf6769c..e09ad3b4b5 100644
--- a/src/Microsoft.AspNet.Mvc.Core/project.json
+++ b/src/Microsoft.AspNet.Mvc.Core/project.json
@@ -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-*",
diff --git a/src/Microsoft.AspNet.Mvc.Core/ExpiringFileInfoCache.cs b/src/Microsoft.AspNet.Mvc.Razor/Compilation/ExpiringFileInfoCache.cs
similarity index 91%
rename from src/Microsoft.AspNet.Mvc.Core/ExpiringFileInfoCache.cs
rename to src/Microsoft.AspNet.Mvc.Razor/Compilation/ExpiringFileInfoCache.cs
index e3f4e9f15e..0c37c64e9b 100644
--- a/src/Microsoft.AspNet.Mvc.Core/ExpiringFileInfoCache.cs
+++ b/src/Microsoft.AspNet.Mvc.Razor/Compilation/ExpiringFileInfoCache.cs
@@ -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
{
///
/// A default implementation for the interface.
@@ -37,11 +37,11 @@ namespace Microsoft.AspNet.Mvc.Core
}
public ExpiringFileInfoCache(IApplicationEnvironment env,
- IOptionsAccessor optionsAccessor)
+ IOptionsAccessor 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;
}
///
diff --git a/src/Microsoft.AspNet.Mvc.Core/IExpiringFileInfoCache.cs b/src/Microsoft.AspNet.Mvc.Razor/Compilation/IExpiringFileInfoCache.cs
similarity index 94%
rename from src/Microsoft.AspNet.Mvc.Core/IExpiringFileInfoCache.cs
rename to src/Microsoft.AspNet.Mvc.Razor/Compilation/IExpiringFileInfoCache.cs
index c566219e27..a6408a06b9 100644
--- a/src/Microsoft.AspNet.Mvc.Core/IExpiringFileInfoCache.cs
+++ b/src/Microsoft.AspNet.Mvc.Razor/Compilation/IExpiringFileInfoCache.cs
@@ -3,7 +3,7 @@
using Microsoft.AspNet.FileSystems;
-namespace Microsoft.AspNet.Mvc.Core
+namespace Microsoft.AspNet.Mvc.Razor
{
///
/// Provides cached access to file infos.
diff --git a/src/Microsoft.AspNet.Mvc.Razor/DefaultViewLocationCache.cs b/src/Microsoft.AspNet.Mvc.Razor/DefaultViewLocationCache.cs
new file mode 100644
index 0000000000..28e30608a3
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.Razor/DefaultViewLocationCache.cs
@@ -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
+{
+ ///
+ /// Default implementation of .
+ ///
+ public class DefaultViewLocationCache : IViewLocationCache
+ {
+ private const char CacheKeySeparator = ':';
+
+ // A mapping of keys generated from ViewLocationExpanderContext to view locations.
+ private readonly ConcurrentDictionary _cache;
+
+ ///
+ /// Initializes a new instance of .
+ ///
+ public DefaultViewLocationCache()
+ {
+ _cache = new ConcurrentDictionary(StringComparer.Ordinal);
+ }
+
+ ///
+ public string Get([NotNull] ViewLocationExpanderContext context)
+ {
+ var cacheKey = GenerateKey(context);
+ string result;
+ _cache.TryGetValue(cacheKey, out result);
+ return result;
+ }
+
+ ///
+ 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(RazorViewEngine.ControllerKey);
+
+ // format is "{viewName}:{controllerName}:{areaName}:"
+ keyBuilder.Append(context.ViewName)
+ .Append(CacheKeySeparator)
+ .Append(controller);
+
+ var area = routeValues.GetValueOrDefault(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;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.Razor/IViewLocationCache.cs b/src/Microsoft.AspNet.Mvc.Razor/IViewLocationCache.cs
new file mode 100644
index 0000000000..1689120cba
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.Razor/IViewLocationCache.cs
@@ -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
+{
+ ///
+ /// Specifies the contracts for caching view locations generated by .
+ ///
+ public interface IViewLocationCache
+ {
+ ///
+ /// Gets a cached view location based on the specified .
+ ///
+ /// The for the current view location
+ /// expansion.
+ /// The cached location, if available, null otherwise.
+ string Get(ViewLocationExpanderContext context);
+
+ ///
+ /// Adds a cache entry for values specified by .
+ ///
+ /// The for the current view location
+ /// expansion.
+ /// The view location that is to be cached.
+ void Set(ViewLocationExpanderContext context, string value);
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.Razor/IViewLocationExpander.cs b/src/Microsoft.AspNet.Mvc.Razor/IViewLocationExpander.cs
new file mode 100644
index 0000000000..037a3935d5
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.Razor/IViewLocationExpander.cs
@@ -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
+{
+ ///
+ /// Specifies the contracts for a view location expander that is used by instances to
+ /// determine search paths for a view.
+ ///
+ ///
+ /// Individual s are invoked in two steps:
+ /// (1) is invoked and each expander
+ /// adds values that it would later consume as part of
+ /// .
+ /// The populated values are used to determine a cache key - if all values are identical to the last time
+ /// 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,
+ /// is invoked to determine
+ /// all potential paths for a view.
+ ///
+ public interface IViewLocationExpander
+ {
+ ///
+ /// Invoked by a to determine the values that would be consumed by this instance of
+ /// . The calculated values are used to determine if the view location has
+ /// changed since the last time it was located.
+ ///
+ /// The for the current view location
+ /// expansion operation.
+ void PopulateValues(ViewLocationExpanderContext context);
+
+ ///
+ /// Invoked by a to determine potential locations for a view.
+ ///
+ /// The for the current view location
+ /// expansion operation.
+ /// The sequence of view locations to expand.
+ /// A list of expanded view locations.
+ IEnumerable ExpandViewLocations(ViewLocationExpanderContext context,
+ IEnumerable viewLocations);
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.Razor/OptionDescriptors/DefaultViewLocationExpanderProvider.cs b/src/Microsoft.AspNet.Mvc.Razor/OptionDescriptors/DefaultViewLocationExpanderProvider.cs
new file mode 100644
index 0000000000..98b1c5c2cb
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.Razor/OptionDescriptors/DefaultViewLocationExpanderProvider.cs
@@ -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
+{
+ ///
+ public class DefaultViewLocationExpanderProvider :
+ OptionDescriptorBasedProvider, IViewLocationExpanderProvider
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// An accessor to the configured for this application.
+ /// An instance used to instantiate types.
+ /// A instance that retrieves services from the
+ /// service collection.
+ public DefaultViewLocationExpanderProvider(
+ IOptionsAccessor optionsAccessor,
+ ITypeActivator typeActivator,
+ IServiceProvider serviceProvider)
+ : base(optionsAccessor.Options.ViewLocationExpanders, typeActivator, serviceProvider)
+ {
+ }
+
+ ///
+ public IReadOnlyList ViewLocationExpanders
+ {
+ get { return Options; }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.Razor/OptionDescriptors/IViewLocationExpanderProvider.cs b/src/Microsoft.AspNet.Mvc.Razor/OptionDescriptors/IViewLocationExpanderProvider.cs
new file mode 100644
index 0000000000..e788225028
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.Razor/OptionDescriptors/IViewLocationExpanderProvider.cs
@@ -0,0 +1,18 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Collections.Generic;
+
+namespace Microsoft.AspNet.Mvc.Razor.OptionDescriptors
+{
+ ///
+ /// Provides an activated collection of instances.
+ ///
+ public interface IViewLocationExpanderProvider
+ {
+ ///
+ /// Gets a collection of activated instances.
+ ///
+ IReadOnlyList ViewLocationExpanders { get; }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.Core/Rendering/RazorViewEngineOptions.cs b/src/Microsoft.AspNet.Mvc.Razor/OptionDescriptors/RazorViewEngineOptions.cs
similarity index 75%
rename from src/Microsoft.AspNet.Mvc.Core/Rendering/RazorViewEngineOptions.cs
rename to src/Microsoft.AspNet.Mvc.Razor/OptionDescriptors/RazorViewEngineOptions.cs
index b4dc8ae008..53d534ecd7 100644
--- a/src/Microsoft.AspNet.Mvc.Core/Rendering/RazorViewEngineOptions.cs
+++ b/src/Microsoft.AspNet.Mvc.Razor/OptionDescriptors/RazorViewEngineOptions.cs
@@ -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
{
///
/// Provides programmatic configuration for the default .
@@ -38,5 +40,12 @@ namespace Microsoft.AspNet.Mvc.Core
}
}
}
+
+ ///
+ /// Get a of descriptors for s used by this
+ /// application.
+ ///
+ public IList ViewLocationExpanders { get; }
+ = new List();
}
}
diff --git a/src/Microsoft.AspNet.Mvc.Razor/OptionDescriptors/ViewLocationExpanderDescriptor.cs b/src/Microsoft.AspNet.Mvc.Razor/OptionDescriptors/ViewLocationExpanderDescriptor.cs
new file mode 100644
index 0000000000..eb20fdbe40
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.Razor/OptionDescriptors/ViewLocationExpanderDescriptor.cs
@@ -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
+{
+ ///
+ /// Encapsulates information that describes an .
+ ///
+ public class ViewLocationExpanderDescriptor : OptionDescriptor
+ {
+ ///
+ /// Creates a new instance of .
+ ///
+ /// A type that the descriptor represents.
+ ///
+ public ViewLocationExpanderDescriptor([NotNull] Type type)
+ : base(type)
+ {
+ }
+
+ ///
+ /// Creates a new instance of .
+ ///
+ /// An instance of
+ /// that the descriptor represents.
+ public ViewLocationExpanderDescriptor([NotNull] IViewLocationExpander viewLocationExpander)
+ : base(viewLocationExpander)
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.Razor/OptionDescriptors/ViewLocationExpanderDescriptorExtensions.cs b/src/Microsoft.AspNet.Mvc.Razor/OptionDescriptors/ViewLocationExpanderDescriptorExtensions.cs
new file mode 100644
index 0000000000..a8c7a7f370
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.Razor/OptionDescriptors/ViewLocationExpanderDescriptorExtensions.cs
@@ -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
+{
+ ///
+ /// Extension methods for adding view location expanders to a collection.
+ ///
+ public static class ViewLocationExpanderDescriptorExtensions
+ {
+ ///
+ /// Adds a type representing a to .
+ ///
+ /// A list of .
+ /// Type representing an
+ /// A representing the added instance.
+ public static ViewLocationExpanderDescriptor Add(
+ [NotNull] this IList descriptors,
+ [NotNull] Type viewLocationExpanderType)
+ {
+ var descriptor = new ViewLocationExpanderDescriptor(viewLocationExpanderType);
+ descriptors.Add(descriptor);
+ return descriptor;
+ }
+
+ ///
+ /// Inserts a type representing a in to at
+ /// the specified .
+ ///
+ /// A list of .
+ /// The zero-based index at which
+ /// should be inserted.
+ /// Type representing an
+ /// A representing the inserted instance.
+ public static ViewLocationExpanderDescriptor Insert(
+ [NotNull] this IList 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;
+ }
+
+ ///
+ /// Adds an to .
+ ///
+ /// A list of .
+ /// An instance.
+ /// A representing the added instance.
+ public static ViewLocationExpanderDescriptor Add(
+ [NotNull] this IList descriptors,
+ [NotNull] IViewLocationExpander viewLocationExpander)
+ {
+ var descriptor = new ViewLocationExpanderDescriptor(viewLocationExpander);
+ descriptors.Add(descriptor);
+ return descriptor;
+ }
+
+ ///
+ /// Insert an in to at the specified
+ /// .
+ ///
+ /// A list of .
+ /// The zero-based index at which
+ /// should be inserted.
+ /// An instance.
+ /// A representing the added instance.
+ public static ViewLocationExpanderDescriptor Insert(
+ [NotNull] this IList 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;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.Razor/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.Razor/Properties/Resources.Designer.cs
index 0f8f2058a9..1d07b517ca 100644
--- a/src/Microsoft.AspNet.Mvc.Razor/Properties/Resources.Designer.cs
+++ b/src/Microsoft.AspNet.Mvc.Razor/Properties/Resources.Designer.cs
@@ -266,6 +266,22 @@ namespace Microsoft.AspNet.Mvc.Razor
return string.Format(CultureInfo.CurrentCulture, GetString("ViewContextMustBeSet"), p0, p1);
}
+ ///
+ /// '{0}' must be a {1} that is generated as result of the call to '{2}'.
+ ///
+ internal static string ViewLocationCache_KeyMustBeString
+ {
+ get { return GetString("ViewLocationCache_KeyMustBeString"); }
+ }
+
+ ///
+ /// '{0}' must be a {1} that is generated as result of the call to '{2}'.
+ ///
+ internal static string FormatViewLocationCache_KeyMustBeString(object p0, object p1, object p2)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("ViewLocationCache_KeyMustBeString"), p0, p1, p2);
+ }
+
///
/// The '{0}' method must be called before '{1}' can be invoked.
///
diff --git a/src/Microsoft.AspNet.Mvc.Razor/RazorViewEngine.cs b/src/Microsoft.AspNet.Mvc.Razor/RazorViewEngine.cs
index 6b476b4d50..bdc123112a 100644
--- a/src/Microsoft.AspNet.Mvc.Razor/RazorViewEngine.cs
+++ b/src/Microsoft.AspNet.Mvc.Razor/RazorViewEngine.cs
@@ -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 _viewLocationFormats = new[]
{
"/Views/{1}/{0}" + ViewExtension,
"/Views/Shared/{0}" + ViewExtension,
};
- private static readonly string[] _areaViewLocationFormats =
+ private static readonly IEnumerable _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 _viewLocationExpanders;
+ private readonly IViewLocationCache _viewLocationCache;
///
/// Initializes a new instance of the class.
///
/// The page factory used for creating instances.
- public RazorViewEngine(IRazorPageFactory pageFactory)
+ public RazorViewEngine(IRazorPageFactory pageFactory,
+ IViewLocationExpanderProvider viewLocationExpanderProvider,
+ IViewLocationCache viewLocationCache)
{
_pageFactory = pageFactory;
+ _viewLocationExpanders = viewLocationExpanderProvider.ViewLocationExpanders;
+ _viewLocationCache = viewLocationCache;
}
- public IEnumerable ViewLocationFormats
+ ///
+ /// Gets the locations where this instance of will search for views.
+ ///
+ public virtual IEnumerable ViewLocationFormats
{
get { return _viewLocationFormats; }
}
+ ///
+ /// Gets the locations where this instance of will search for views within an
+ /// area.
+ ///
+ public virtual IEnumerable AreaViewLocationFormats
+ {
+ get { return _areaViewLocationFormats; }
+ }
+
///
public ViewEngineResult FindView([NotNull] ActionContext context,
[NotNull] string viewName)
{
- var viewEngineResult = CreateViewEngineResult(context, viewName, partial: false);
- return viewEngineResult;
+ return CreateViewEngineResult(context, viewName, partial: false);
}
///
@@ -81,22 +100,77 @@ namespace Microsoft.AspNet.Mvc.Razor
}
else
{
- var routeValues = context.RouteData.Values;
- var controllerName = routeValues.GetValueOrDefault("controller");
- var areaName = routeValues.GetValueOrDefault("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(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(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(ControllerKey);
+ var searchedLocations = new List();
+ 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 GetViewSearchPaths(string viewName, string controllerName, string areaName)
- {
- IEnumerable 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;
- }
}
}
diff --git a/src/Microsoft.AspNet.Mvc.Razor/Resources.resx b/src/Microsoft.AspNet.Mvc.Razor/Resources.resx
index 611ea48847..9d0f92e270 100644
--- a/src/Microsoft.AspNet.Mvc.Razor/Resources.resx
+++ b/src/Microsoft.AspNet.Mvc.Razor/Resources.resx
@@ -165,6 +165,9 @@
'{0} must be set to access '{1}'.
+
+ '{0}' must be a {1} that is generated as result of the call to '{2}'.
+
The '{0}' method must be called before '{1}' can be invoked.
diff --git a/src/Microsoft.AspNet.Mvc.Razor/ViewLocationExpanderContext.cs b/src/Microsoft.AspNet.Mvc.Razor/ViewLocationExpanderContext.cs
new file mode 100644
index 0000000000..98d0a4fb82
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.Razor/ViewLocationExpanderContext.cs
@@ -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
+{
+ ///
+ /// A context for containing information for .
+ ///
+ public class ViewLocationExpanderContext
+ {
+ public ViewLocationExpanderContext([NotNull] ActionContext actionContext,
+ [NotNull] string viewName)
+ {
+ ActionContext = actionContext;
+ ViewName = viewName;
+ }
+
+ ///
+ /// Gets the for the current executing action.
+ ///
+ public ActionContext ActionContext { get; private set; }
+
+ ///
+ /// Gets the view name
+ ///
+ public string ViewName { get; private set; }
+
+ ///
+ /// Gets or sets the that is populated with values as part of
+ /// .
+ ///
+ public IDictionary Values { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs b/src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs
index 46484c9f8a..955f03606f 100644
--- a/src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs
+++ b/src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs
@@ -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
diff --git a/src/Microsoft.AspNet.Mvc/MvcServices.cs b/src/Microsoft.AspNet.Mvc/MvcServices.cs
index f175ce64a3..a236e8d32c 100644
--- a/src/Microsoft.AspNet.Mvc/MvcServices.cs
+++ b/src/Microsoft.AspNet.Mvc/MvcServices.cs
@@ -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();
yield return describe.Transient();
+ // Transient since the IViewLocationExpanders returned by the instance is cached by view engines.
+ yield return describe.Transient();
+ // Caches view locations that are valid for the lifetime of the application.
+ yield return describe.Singleton();
+
yield return describe.Singleton();
// Virtual path view factory needs to stay scoped so views can get get scoped services.
yield return describe.Scoped();
diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/OptionDescriptors/ViewEngineDscriptorExtensionsTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/OptionDescriptors/ViewEngineDescriptorExtensionsTest.cs
similarity index 100%
rename from test/Microsoft.AspNet.Mvc.Core.Test/OptionDescriptors/ViewEngineDscriptorExtensionsTest.cs
rename to test/Microsoft.AspNet.Mvc.Core.Test/OptionDescriptors/ViewEngineDescriptorExtensionsTest.cs
diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Rendering/CompositeViewEngineTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Rendering/ViewEngine/CompositeViewEngineTest.cs
similarity index 100%
rename from test/Microsoft.AspNet.Mvc.Core.Test/Rendering/CompositeViewEngineTest.cs
rename to test/Microsoft.AspNet.Mvc.Core.Test/Rendering/ViewEngine/CompositeViewEngineTest.cs
diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/ViewEngineTests.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/ViewEngineTests.cs
index cf5b6b15c5..b1839be16a 100644
--- a/test/Microsoft.AspNet.Mvc.FunctionalTests/ViewEngineTests.cs
+++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/ViewEngineTests.cs
@@ -101,5 +101,42 @@ component-content";
// Assert
Assert.Equal(expected, body.Trim());
}
+
+ public static IEnumerable