Celui-ci m'a intéressé, et j'ai finalement eu l'occasion de m'y intéresser. Apparemment, d'autres personnes n'ont pas compris qu'il s'agit d'un problème de trouver la vue pas un problème avec le routage lui-même - et c'est probablement parce que le titre de votre question indique qu'il s'agit de routage.
Quoi qu'il en soit, comme il s'agit d'un problème lié à la vue, la seule façon d'obtenir ce que vous voulez est de remplacer le moteur d'affichage par défaut . Normalement, lorsque vous faites cela, c'est dans le simple but de changer votre moteur de vue (c'est-à-dire vers Spark, NHaml, etc.). Dans ce cas, ce n'est pas la logique de création de la vue que nous devons remplacer, mais la logique de création de la vue. FindPartialView
y FindView
dans les VirtualPathProviderViewEngine
classe.
Vous pouvez remercier votre bonne étoile que ces méthodes soient en fait virtuelles, parce que tout le reste du programme VirtualPathProviderViewEngine
n'est même pas accessible - c'est privé, et ça le rend muy ennuyeux de remplacer la logique de recherche parce que vous devez réécrire la moitié du code déjà écrit si vous voulez qu'il soit compatible avec le cache de localisation et les formats de localisation. Après quelques recherches dans Reflector, j'ai finalement réussi à trouver une solution fonctionnelle.
Ce que j'ai fait ici est de créer d'abord une abstraction AreaAwareViewEngine
qui dérive directement de VirtualPathProviderViewEngine
au lieu de WebFormViewEngine
. Je l'ai fait pour que si vous voulez créer des vues Spark à la place (ou autre), vous puissiez toujours utiliser cette classe comme type de base.
Le code ci-dessous est assez long, alors pour vous donner un résumé rapide de ce qu'il fait réellement : il vous permet de mettre un fichier de type {2}
dans le format de l'emplacement, qui correspond au nom de la zone, de la même manière que {1}
correspond au nom du contrôleur. Voilà, c'est fait ! C'est pour ça qu'on a dû écrire tout ce code :
BaseAreaAwareViewEngine.cs
public abstract class BaseAreaAwareViewEngine : VirtualPathProviderViewEngine
{
private static readonly string[] EmptyLocations = { };
public override ViewEngineResult FindView(
ControllerContext controllerContext, string viewName,
string masterName, bool useCache)
{
if (controllerContext == null)
{
throw new ArgumentNullException("controllerContext");
}
if (string.IsNullOrEmpty(viewName))
{
throw new ArgumentNullException(viewName,
"Value cannot be null or empty.");
}
string area = getArea(controllerContext);
return FindAreaView(controllerContext, area, viewName,
masterName, useCache);
}
public override ViewEngineResult FindPartialView(
ControllerContext controllerContext, string partialViewName,
bool useCache)
{
if (controllerContext == null)
{
throw new ArgumentNullException("controllerContext");
}
if (string.IsNullOrEmpty(partialViewName))
{
throw new ArgumentNullException(partialViewName,
"Value cannot be null or empty.");
}
string area = getArea(controllerContext);
return FindAreaPartialView(controllerContext, area,
partialViewName, useCache);
}
protected virtual ViewEngineResult FindAreaView(
ControllerContext controllerContext, string areaName, string viewName,
string masterName, bool useCache)
{
string controllerName =
controllerContext.RouteData.GetRequiredString("controller");
string[] searchedViewPaths;
string viewPath = GetPath(controllerContext, ViewLocationFormats,
"ViewLocationFormats", viewName, controllerName, areaName, "View",
useCache, out searchedViewPaths);
string[] searchedMasterPaths;
string masterPath = GetPath(controllerContext, MasterLocationFormats,
"MasterLocationFormats", masterName, controllerName, areaName,
"Master", useCache, out searchedMasterPaths);
if (!string.IsNullOrEmpty(viewPath) &&
(!string.IsNullOrEmpty(masterPath) ||
string.IsNullOrEmpty(masterName)))
{
return new ViewEngineResult(CreateView(controllerContext, viewPath,
masterPath), this);
}
return new ViewEngineResult(
searchedViewPaths.Union<string>(searchedMasterPaths));
}
protected virtual ViewEngineResult FindAreaPartialView(
ControllerContext controllerContext, string areaName,
string viewName, bool useCache)
{
string controllerName =
controllerContext.RouteData.GetRequiredString("controller");
string[] searchedViewPaths;
string partialViewPath = GetPath(controllerContext,
ViewLocationFormats, "PartialViewLocationFormats", viewName,
controllerName, areaName, "Partial", useCache,
out searchedViewPaths);
if (!string.IsNullOrEmpty(partialViewPath))
{
return new ViewEngineResult(CreatePartialView(controllerContext,
partialViewPath), this);
}
return new ViewEngineResult(searchedViewPaths);
}
protected string CreateCacheKey(string prefix, string name,
string controller, string area)
{
return string.Format(CultureInfo.InvariantCulture,
":ViewCacheEntry:{0}:{1}:{2}:{3}:{4}:",
base.GetType().AssemblyQualifiedName,
prefix, name, controller, area);
}
protected string GetPath(ControllerContext controllerContext,
string[] locations, string locationsPropertyName, string name,
string controllerName, string areaName, string cacheKeyPrefix,
bool useCache, out string[] searchedLocations)
{
searchedLocations = EmptyLocations;
if (string.IsNullOrEmpty(name))
{
return string.Empty;
}
if ((locations == null) || (locations.Length == 0))
{
throw new InvalidOperationException(string.Format("The property " +
"'{0}' cannot be null or empty.", locationsPropertyName));
}
bool isSpecificPath = IsSpecificPath(name);
string key = CreateCacheKey(cacheKeyPrefix, name,
isSpecificPath ? string.Empty : controllerName,
isSpecificPath ? string.Empty : areaName);
if (useCache)
{
string viewLocation = ViewLocationCache.GetViewLocation(
controllerContext.HttpContext, key);
if (viewLocation != null)
{
return viewLocation;
}
}
if (!isSpecificPath)
{
return GetPathFromGeneralName(controllerContext, locations, name,
controllerName, areaName, key, ref searchedLocations);
}
return GetPathFromSpecificName(controllerContext, name, key,
ref searchedLocations);
}
protected string GetPathFromGeneralName(ControllerContext controllerContext,
string[] locations, string name, string controllerName,
string areaName, string cacheKey, ref string[] searchedLocations)
{
string virtualPath = string.Empty;
searchedLocations = new string[locations.Length];
for (int i = 0; i < locations.Length; i++)
{
if (string.IsNullOrEmpty(areaName) && locations[i].Contains("{2}"))
{
continue;
}
string testPath = string.Format(CultureInfo.InvariantCulture,
locations[i], name, controllerName, areaName);
if (FileExists(controllerContext, testPath))
{
searchedLocations = EmptyLocations;
virtualPath = testPath;
ViewLocationCache.InsertViewLocation(
controllerContext.HttpContext, cacheKey, virtualPath);
return virtualPath;
}
searchedLocations[i] = testPath;
}
return virtualPath;
}
protected string GetPathFromSpecificName(
ControllerContext controllerContext, string name, string cacheKey,
ref string[] searchedLocations)
{
string virtualPath = name;
if (!FileExists(controllerContext, name))
{
virtualPath = string.Empty;
searchedLocations = new string[] { name };
}
ViewLocationCache.InsertViewLocation(controllerContext.HttpContext,
cacheKey, virtualPath);
return virtualPath;
}
protected string getArea(ControllerContext controllerContext)
{
// First try to get area from a RouteValue override, like one specified in the Defaults arg to a Route.
object areaO;
controllerContext.RouteData.Values.TryGetValue("area", out areaO);
// If not specified, try to get it from the Controller's namespace
if (areaO != null)
return (string)areaO;
string namespa = controllerContext.Controller.GetType().Namespace;
int areaStart = namespa.IndexOf("Areas.");
if (areaStart == -1)
return null;
areaStart += 6;
int areaEnd = namespa.IndexOf('.', areaStart + 1);
string area = namespa.Substring(areaStart, areaEnd - areaStart);
return area;
}
protected static bool IsSpecificPath(string name)
{
char ch = name[0];
if (ch != '~')
{
return (ch == '/');
}
return true;
}
}
Comme indiqué, il ne s'agit pas d'un moteur concret, vous devez donc le créer également. Cette partie, heureusement, est mucho plus facile, il ne nous reste plus qu'à définir les formats par défaut et à créer réellement les vues :
AreaAwareViewEngine.cs
public class AreaAwareViewEngine : BaseAreaAwareViewEngine
{
public AreaAwareViewEngine()
{
MasterLocationFormats = new string[]
{
"~/Areas/{2}/Views/{1}/{0}.master",
"~/Areas/{2}/Views/{1}/{0}.cshtml",
"~/Areas/{2}/Views/Shared/{0}.master",
"~/Areas/{2}/Views/Shared/{0}.cshtml",
"~/Views/{1}/{0}.master",
"~/Views/{1}/{0}.cshtml",
"~/Views/Shared/{0}.master"
"~/Views/Shared/{0}.cshtml"
};
ViewLocationFormats = new string[]
{
"~/Areas/{2}/Views/{1}/{0}.aspx",
"~/Areas/{2}/Views/{1}/{0}.ascx",
"~/Areas/{2}/Views/{1}/{0}.cshtml",
"~/Areas/{2}/Views/Shared/{0}.aspx",
"~/Areas/{2}/Views/Shared/{0}.ascx",
"~/Areas/{2}/Views/Shared/{0}.cshtml",
"~/Views/{1}/{0}.aspx",
"~/Views/{1}/{0}.ascx",
"~/Views/{1}/{0}.cshtml",
"~/Views/Shared/{0}.aspx"
"~/Views/Shared/{0}.ascx"
"~/Views/Shared/{0}.cshtml"
};
PartialViewLocationFormats = ViewLocationFormats;
}
protected override IView CreatePartialView(
ControllerContext controllerContext, string partialPath)
{
if (partialPath.EndsWith(".cshtml"))
return new System.Web.Mvc.RazorView(controllerContext, partialPath, null, false, null);
else
return new WebFormView(controllerContext, partialPath);
}
protected override IView CreateView(ControllerContext controllerContext,
string viewPath, string masterPath)
{
if (viewPath.EndsWith(".cshtml"))
return new RazorView(controllerContext, viewPath, masterPath, false, null);
else
return new WebFormView(controllerContext, viewPath, masterPath);
}
}
Notez que nous avons ajouté quelques entrées à la norme ViewLocationFormats
. Ce sont les nouvelles {2}
les entrées, où le {2}
sera mis en correspondance avec le area
nous avons mis dans le RouteData
. J'ai laissé le MasterLocationFormats
seul, mais vous pouvez évidemment changer cela si vous le souhaitez.
Modifiez maintenant votre global.asax
pour enregistrer ce moteur de vue :
Global.asax.cs
protected void Application_Start()
{
RegisterRoutes(RouteTable.Routes);
ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new AreaAwareViewEngine());
}
...et enregistrer la route par défaut :
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"Area",
"",
new { area = "AreaZ", controller = "Default", action = "ActionY" }
);
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = "" }
);
}
Créez maintenant le AreaController
dont nous venons de parler :
DefaultController.cs (dans ~/Controllers/)
public class DefaultController : Controller
{
public ActionResult ActionY()
{
return View("TestView");
}
}
Il est évident que nous avons besoin de la structure du répertoire et de la vue qui va avec - nous allons rester très simples :
TestView.aspx (dans ~/Areas/AreaZ/Views/Default/ ou ~/Areas/AreaZ/Views/Shared/)
<%@ Page Title="" Language="C#" Inherits="System.Web.Mvc.ViewPage" %>
<h2>TestView</h2>
This is a test view in AreaZ.
Et c'est tout. Enfin, nous avons terminé .
Dans la plupart des cas, vous devriez être en mesure de simplement prendre le BaseAreaAwareViewEngine
y AreaAwareViewEngine
et le déposer dans n'importe quel projet MVC. Ainsi, même s'il a fallu beaucoup de code pour réaliser cette opération, vous n'aurez à l'écrire qu'une seule fois. Après cela, il suffit de modifier quelques lignes dans le fichier global.asax.cs
et la création de la structure de votre site.