Monday, September 29, 2014

Modular code organization in ASP.NET MVC

Hated the way ASP.NET MVC imposes the sock drawer approach to your code organization?


Instead of sock drawer approach, we could organize our code into modules


Here's how we wanted our code organization be like:




With modular approach, the controller and its views are grouped together. Though you will lose the IDE navigation aspect of controller<->view, you won't miss it much as your contoller and its views are near each other



Notice that we didn't include {controller} in the url, the controller and module are the same thing

And notice too that we hardcoded the controller to "_"

"_Controller" won't work, ASP.NET MVC always append Controller to the controller parameter. Hence "_" is enough

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_SandwichModule_default",
                url: "Hey/Jude/Dont/Make/SandwichModule/{action}/{id}",
                defaults: new { action = "Index", id = UrlParameter.Optional, controller = "_" },                
                // instead of this string-based code: "JustAspNetMvcThing.App.Hey.Jude.Dont.Make.SandwichModule" parameter, we could use typeof:
                namespaces: new[] { typeof(JustAspNetMvcThing.App.Hey.Jude.Dont.Make.SandwichModule._Controller).Namespace }
            );

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

using System.Web.Mvc;

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

        public override void RegisterArea(AreaRegistrationContext context)
        {

            // Add this:
            context.MapRoute(
                name: "Let_It_BeModule_default",
                url: "Let/It/BeModule/{action}/{id}",
                defaults: new { action = "Index", id = UrlParameter.Optional, controller = "_" },
                namespaces: new[] { typeof(JustAspNetMvcThing.App.Let.It.BeModule._Controller).Namespace }
            );
  

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



Here's the base controller for our modules:
using System.Collections.Generic;
using System.Linq;

using System.Web.Mvc;

namespace JustAspNetMvcThing.App
{
    
    public class AppBaseController : 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((object)null, viewName, memberName);
        }


        ViewResult TheView(object model, string viewName, string memberName)
        {                        
            // Skip(1) skips JustAspNetMVcThing
            // SkipLastN excludes the controller

            // Sample output: App/Hey/Jude/Dont/Make/SandwichModule
            string modulePath = string.Join("/", this.GetType().FullName.Split('.').Skip(1).SkipLastN(1));

            // Sample output: /App/Hey/Jude/Dont/Make/SandwichModule/Bad.cshtml
            string viewFullPath =  "/" + modulePath + "/" + memberName + ".cshtml";
            
            return View(viewFullPath, model);
        }                
    }// class AppBaseController

    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);
        }
    }//class Helper
 
}//namespace


The Sandwich module, notice the _Controller name? We do that, so the controller will always sort first in the folder
using JustAspNetMvcThing.Models;

using System.Web.Mvc;

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

            return View(p);
        }

    }
}

Here's one of the SandwichModule's views. Noticed that we can't use @model anymore. @model ModelHere is just a shorthand for @inherits System.Web.Mvc.WebViewPage<ModelHere>
@inherits System.Web.Mvc.WebViewPage<JustAspNetMvcThing.Models.Person>

Hello @Model.FirstName


The Be module:
using System.Web.Mvc;

namespace JustAspNetMvcThing.App.Let.It.BeModule
{
    public class _Controller : AppBaseController
    {        
        // GET: /Let/It/BeModule/ThePersonYouWantToBe
        public ActionResult ThePersonYouWantToBe()
        {
            return View();
        }

    }
}


Here's one of the BeModule's views:
@inherits System.Web.Mvc.WebViewPage

I am me!




Happy Coding!

2 comments:

  1. Ever heard of areas?

    ReplyDelete
    Replies
    1. Yes have used Areas, it has a limitation though, it can't be nested

      Delete