Monday, September 29, 2014

Nested routing and folder in ASP.NET MVC Redux

Here's a way to make nested routing and folder in ASP.NET MVC: http://www.ienablemuch.com/2014/09/nested-routing-and-folder-in-aspnet-mvc.html


However, I find the code organization of that approach is not symmetrical enough, I wanted the code organization of controller and views be mirror image of each other, like this:





Here's the supporting code:

using System.Collections.Generic;
using System.Linq;

using System.Web.Mvc;

namespace JustAspNetMvcThing.App
{
    
    public class AreaBaseController : Controller
    {
        public ViewResult View(object model, [System.Runtime.CompilerServices.CallerMemberName] string memberName = "") 
        { 
            return TheView(model, /*viewName*/ null, memberName); 
        }

        
        public ViewResult View(string viewName = null, string masterName = null, [System.Runtime.CompilerServices.CallerMemberName] string memberName = "") 
        {             
            return TheView(/*model*/ (object)null, viewName, memberName); 
        }

        ViewResult TheView(object model, string viewName, string memberName)
        {
            if (viewName != null && model == null)
                return View(viewName, model: null);

            
            string origName = this.GetType().Name;
            // "Controller" string is stripped from "ItController"
            // "Controller" is stringly-typed, there's a better way, just wanted to make a short code
            // Sample output: It
            string controllerName = origName.Substring(0, origName.Length - "Controller".Length);

            // Skip(3) skips JustAspNetMVcThing.App.Controllers
            // SkipLastN excludes the controller, example: Hey/Jude/Dont/Make
            // Sample output: Hey/Jude/Dont/Make/It
            string controllerPath = string.Join("/", this.GetType().FullName.Split('.').Skip(3).SkipLastN(1)) + "/" + controllerName;

            // Sample output: /App/Views/Hey/Jude/Dont/Make/It/Bad.cshtml
            string viewFullPath =  "/App/Views/" + controllerPath + "/" + memberName + ".cshtml";
            
            return View(viewFullPath, model);
        }                
    }

    static class Helper
    {
        // Because .Reverse() is bad: http://stackoverflow.com/questions/4166493/drop-the-last-item-with-linq#comment4498849_4166546
        public static IEnumerable<T> SkipLastN<T>(this IEnumerable<T> source, int n)
        {
            var it = source.GetEnumerator();
            bool hasRemainingItems = false;
            var cache = new Queue<T>(n + 1);

            do
            {
                if (hasRemainingItems = it.MoveNext())
                {
                    cache.Enqueue(it.Current);
                    if (cache.Count > n)
                        yield return cache.Dequeue();
                }
            } while (hasRemainingItems);
        }
    }

}


Then do this on the Area registration:
using System.Web.Mvc;

namespace JustAspNetMvcThing.Areas.Hey
{
    public class HeyAreaRegistration : AreaRegistration
    {
        public override string AreaName { get{ return "Hey"; } }

        public override void RegisterArea(AreaRegistrationContext context)
        {

            // Add this:
            context.MapRoute(
                name: "Hey_Jude_Dont_Make_default",
                url: "Hey/Jude/Dont/Make/{controller}/{action}/{id}",
                defaults: new { action = "Index", id = UrlParameter.Optional },
                namespaces: new[] { "JustAspNetMvcThing.App.Controllers.Hey.Jude.Dont.Make" }
            );

            context.MapRoute(
                "Hey_default",
                "Hey/{controller}/{action}/{id}",
                new { action = "Index", id = UrlParameter.Optional }
            );
        }
    }
}


Voila! Your controller can continue its normal business. The View(object) resolves to View(object,memberName="") hence we are able to hook the normal View signature from the controller that inherits from AreaBaseController
using JustAspNetMvcThing.Models;

using System.Web.Mvc;

namespace JustAspNetMvcThing.App.Controllers.Hey.Jude.Dont.Make
{
    public class ItController : AreaBaseController
    {        
        // GET: /Hey/Jude/Dont/Make/It/Bad/1
        public ViewResult Bad(int id = 0)
        {
            var p = new Person { FirstName = "Paul " + id };

            return View(p);
        }

    }
}


Lastly, we must use @inherits instead of @model on our views. @model is a shorthand for @inherits
@inherits System.Web.Mvc.WebViewPage<JustAspNetMvcThing.Models.Person>  

Hello @Model.FirstName




Happy Coding!

No comments:

Post a Comment