Note that there are some explanatory texts on larger screens.

plurals
  1. PO
    primarykey
    data
    text
    <p>This one interested me, and I finally had a chance to look into it. Other folks apparently haven't understood that this is an issue with <strong>finding the view</strong>, not an issue with the <em>routing</em> itself - and that's probably because your question title indicates that it's about routing.</p> <p>In any case, because this is a View-related issue, the only way to get what you want is to <strong>override the default view engine</strong>. Normally, when you do this, it's for the simple purpose of switching your view engine (i.e. to Spark, NHaml, etc.). In this case, it's not the View-creation logic we need to override, but the <code>FindPartialView</code> and <code>FindView</code> methods in the <code>VirtualPathProviderViewEngine</code> class.</p> <p>You can thank your lucky stars that these methods are in fact virtual, because everything else in the <code>VirtualPathProviderViewEngine</code> is not even <em>accessible</em> - it's private, and that makes it <em>very</em> annoying to override the find logic because you have to basically rewrite half of the code that's already been written if you want it to play nice with the location cache and the location formats. After some digging in Reflector I finally managed to come up with a working solution.</p> <p>What I've done here is to first create an abstract <code>AreaAwareViewEngine</code> that derives directly from <code>VirtualPathProviderViewEngine</code> instead of <code>WebFormViewEngine</code>. I did this so that if you want to create Spark views instead (or whatever), you can still use this class as the base type.</p> <p>The code below is pretty long-winded, so to give you a quick summary of what it actually does: It lets you put a <code>{2}</code> into the location format, which corresponds to the area name, the same way <code>{1}</code> corresponds to the controller name. That's it! That's what we had to write all this code for:</p> <h1>BaseAreaAwareViewEngine.cs</h1> <pre><code>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) &amp;&amp; (!string.IsNullOrEmpty(masterPath) || string.IsNullOrEmpty(masterName))) { return new ViewEngineResult(CreateView(controllerContext, viewPath, masterPath), this); } return new ViewEngineResult( searchedViewPaths.Union&lt;string&gt;(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 &lt; locations.Length; i++) { if (string.IsNullOrEmpty(areaName) &amp;&amp; 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; } } </code></pre> <p>Now as stated, this isn't a concrete engine, so you have to create that as well. This part, fortunately, is <em>much</em> easier, all we need to do is set the default formats and actually create the views:</p> <h1>AreaAwareViewEngine.cs</h1> <pre><code>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); } } </code></pre> <p>Note that we've added few entries to the standard <code>ViewLocationFormats</code>. These are the new <code>{2}</code> entries, where the <code>{2}</code> will be mapped to the <code>area</code> we put in the <code>RouteData</code>. I've left the <code>MasterLocationFormats</code> alone, but obviously you can change that if you want.</p> <p>Now modify your <code>global.asax</code> to register this view engine:</p> <h1>Global.asax.cs</h1> <pre><code>protected void Application_Start() { RegisterRoutes(RouteTable.Routes); ViewEngines.Engines.Clear(); ViewEngines.Engines.Add(new AreaAwareViewEngine()); } </code></pre> <p>...and register the default route:</p> <pre><code>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 = "" } ); } </code></pre> <p>Now Create the <code>AreaController</code> we just referenced:</p> <h1>DefaultController.cs (in ~/Controllers/)</h1> <pre><code>public class DefaultController : Controller { public ActionResult ActionY() { return View("TestView"); } } </code></pre> <p>Obviously we need the directory structure and view to go with it - we'll keep this super simple:</p> <h1>TestView.aspx (in ~/Areas/AreaZ/Views/Default/ or ~/Areas/AreaZ/Views/Shared/)</h1> <pre><code>&lt;%@ Page Title="" Language="C#" Inherits="System.Web.Mvc.ViewPage" %&gt; &lt;h2&gt;TestView&lt;/h2&gt; This is a test view in AreaZ. </code></pre> <p>And that's it. <strong>Finally, we're done</strong>.</p> <p>For the most part, you should be able to just take the <code>BaseAreaAwareViewEngine</code> and <code>AreaAwareViewEngine</code> and drop it into any MVC project, so even though it took a lot of code to get this done, you only have to write it once. After that, it's just a matter of editing a few lines in <code>global.asax.cs</code> and creating your site structure.</p>
    singulars
    1. This table or related slice is empty.
    plurals
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    1. VO
      singulars
      1. This table or related slice is empty.
    2. VO
      singulars
      1. This table or related slice is empty.
    3. VO
      singulars
      1. This table or related slice is empty.
 

Querying!

 
Guidance

SQuiL has stopped working due to an internal error.

If you are curious you may find further information in the browser console, which is accessible through the devtools (F12).

Reload