I’ve been putting off working with MVC until three days ago.. what a fool I have been! Long story short, it has made (for me at least) web development fun again. The more I work with the MVC Framework, the more tickled I get.
As I continue down the path of discovery and self-education by re-creating a personal project site, I get to the administration section of things… Typically, it has been my “convention” for the last 15 years or so that all of my administration related bits go into a ~/Admin path. Each individual administration functional group would fall into a sub folder under that.
- ~/Admin/User – User Management
- ~/Admin/News – News Management
- ~/Admin/Gallery – Gallery Management
Following this structure allowed me to keep each functional piece separate, but all under the same “Admin” umbrella.
I was grinning ear to ear when I discovered that MVC would allow me to organize my controllers how I saw fit, so I put all of my administration controllers under the ~/Controllers/Admin folder
Once that was done, I set forth the task of setting up a few simple routes in the Global.asax.cs file. I wanted my URL’s to all follow under the http://<site>/Admin/<function> design. For example:
- http://www.mysite.com/Admin/User
- http://www.mysite.com/Admin/User/Edit/312
- http://www.mysite.com/Admin/News/Create
instead of:
- http://www.mysite.com/UserAdmin
- http://www.mysite.com/UserAdmin/Edit/312
- http://www.mysite.com/NewsAdmin/Create
I found that (at least in my case) it’s best to define routes from the bottom up. In other words, write the most generic route map first, then with each route that I “fine grain” the control of the URL on, add that above the previous one. This is what my routes looked like when I was done with the admin section.
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"UserAdmin",
"Admin/User/{action}/{id}",
new { controller = "UserAdmin", action = "Index", id = "" }
);
routes.MapRoute(
"NewsAdmin",
"Admin/News/{action}/{id}",
new { controller = "NewsAdmin", action = "Index", id = "" }
);
routes.MapRoute(
"GalleryAdmin",
"Admin/Gallery/{action}/{id}",
new { controller = "GalleryAdmin", action = "Index", id = "" }
);
routes.MapRoute(
"BlogAdmin",
"Admin/Blog/{action}/{id}",
new { controller = "BlogAdmin", action = "Index", id = "" }
);
routes.MapRoute( // Controller for the Admin Home Page
"AdminHome",
"Admin/{action}/{id}",
new { controller = "AdminHome", action = "Index", id = "" }
);
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = "" } // Parameter defaults
);
}
So everything is awesome at this point. The music is cranked, birds are chirping and the clouds of web development are parting… so I start to create my views for the admin section
~/Views/AdminHome/Index.aspx
<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage" %>
<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">Admin Home</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
<h2>Admin Home</h2>
<ul>
<li><a href="/Admin/User">User Admin</a></li>
<li><a href="/Admin/News">News Admin</a></li>
<li><a href="/Admin/Gallery">Gallery Admin</a></li>
<li><a href="/Admin/Blog">Blog Admin</a></li>
</ul>
</asp:Content>
~/Views/UserAdmin/Index.aspx (all of the others are virtually the same.. this is a simple demo)
<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage" %>
<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">User Admin</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
<h2>User Admin</h2>
<a href="/Admin">Back to Admin Home</a>
</asp:Content>
and this is what my ~/Views folder turned out to be:
Not quite the organizational utopia that I was hoping for. So I figure, what the heck, MVC was cool enough to let me put my controllers in their own folder.. let’s see what happens when I do what I want with my views. So after some re-arranging, I was left with this:
There! Like a nice, comfortable pair of shoes… it just feels right! So let’s fire it up and look at our magic!
And clicking on “Admin” gives us this:
Well bollocks to that! It seems that the ViewResult is still looking at the “conventional” ~/Views/Controller location for the Index.aspx file. That just won’t do.
ASP.Net MVC is still fairly “young” as far as platforms go.. so the documentation (as expected) is sparse at best and terse when you can find it.. but I did happen to stumble upon a this post: ASP.NET MVC Tip #24 – Retrieve Views from Different Folders by Stephen Walther
Of course, he warns his readers that this breaks all convention with ASP.Net MVC:
Now, I want to be the first to warn you that you should never, never, never use this tip (Please delete this entry from your news aggregator immediately). There is a good reason for following the conventions inherent in an MVC application. Placing your files in known locations makes it easier for everyone to understand your application.
So I ask myself: “Self, Are you sure that you want to stick to your old ways and organize the administration views under the ~/Views/Admin/<function> scheme?”. To whit I readily replied to myself: “Why yes, yes I most certainly do!”
So I came up with a set of known facts and assumptions:
- All administration views should reside within the ~/Views/Admin/<function> structure
- I can specify a path to a view by passing it in as the viewName parameter to the View() method of the controller
- MVC doesn’t require viewName to be passed in to the View() method.
- When viewName is not supplied, it is inferred by the action context of RouteData
- I wish to follow MVC Convention for the rest of the site with only /Admin being the edge case
- I don’t want to roll my own controller factory for this single edge case.
- Up until this point, I’ve been focusing on full views (.aspx) but I also need to take partials into account (.ascx)
- I don’t want to have to modify my admin controllers to make this happen
- The MVC Framework should be unaffected by this edge case
Knowing these facts, I dug into MVC with reflector (I know I can download the source, but I like the way reflector let’s me navigate) and came up with this little gem inside the abstract class ViewResultBase
public override void ExecuteResult(ControllerContext context)
{
if (context == null)
{
throw new ArgumentNullException("context");
}
if (string.IsNullOrEmpty(this.ViewName))
{
this.ViewName = context.RouteData.GetRequiredString("action");
}
[ ... snip for brevity ... ]
}
So now we know how the viewName is inferred when not passed into the View() method of the controller… And since Controller is defined as an abstract, I realized that what I really needed to do was create a base class for my admin controllers to inherit from…
using System.Web.Mvc;
namespace MvcApplication1.Controllers.Admin
{
public abstract class AdminBaseController : Controller
{
private readonly string _viewRoot = "~/Views/Admin/";
protected AdminBaseController(string subPath)
{
_viewRoot += subPath + "/";
}
protected internal new virtual ViewResult View()
{
return base.View(ViewPath(RouteData.GetRequiredString("action")));
}
protected internal new virtual ViewResult View(object model)
{
return base.View(ViewPath(RouteData.GetRequiredString("action")), model);
}
protected internal new virtual ViewResult View(string viewName)
{
return base.View(ViewPath(viewName));
}
protected internal new virtual ViewResult View(string viewName, object model)
{
return base.View(ViewPath(viewName), model);
}
protected internal new virtual ViewResult View(string viewName, string masterName)
{
return base.View(ViewPath(viewName), masterName);
}
protected internal new virtual ViewResult View(string viewName, string masterName, object model)
{
return base.View(ViewPath(viewName), masterName, model);
}
protected internal new PartialViewResult PartialView()
{
return base.PartialView(ViewPath(RouteData.GetRequiredString("action"), true), null);
}
protected internal new PartialViewResult PartialView(object model)
{
return base.PartialView(ViewPath(RouteData.GetRequiredString("action"), true), model);
}
protected internal new PartialViewResult PartialView(string viewName)
{
return base.PartialView(ViewPath(viewName, true), null);
}
protected internal new virtual PartialViewResult PartialView(string viewName, object model)
{
return base.PartialView(ViewPath(viewName, true), model);
}
private string ViewPath(string viewName)
{
return ViewPath(viewName, false);
}
private string ViewPath(string viewName, bool partial)
{
if (partial)
return _viewRoot + viewName + ".ascx";
return _viewRoot + viewName + ".aspx";
}
}
}
So what we’ve done is created our base class for our admin controllers that takes a subPath as part of the constructor and builds out the root path to the views that the controller has. Then we created a few small helper methods called ViewPath that appends the viewName to the root path and adds our extension based on if the request is for a partial view or not.
The magic all happens because the base class hides each iteration of the controller’s View() methods replacing them with it’s own implementation. You can see that in the empty View() method, we are getting the action name from the RouteData, getting our translated path location with ViewPath and armed with that information, we can now call into the base.View() method passing our fully qualified viewName as a parameter.
The implementation of this base class in our admin controllers is very painless:
namespace MvcApplication1.Controllers.Admin
{
public class UserAdminController : AdminBaseController
{
public UserAdminController() : base("User") { }
//
// GET: /UserAdmin/
public ActionResult Index()
{
return View();
}
}
}
As you can see, all we had to do is change where UserAdminController is inheriting from (Controller to AdminBaseController) and add the public parameter less constructor passing in our “subPath” into the AdminBaseController’s constructor. None of the other code in the controller has to change.
With those changes put it to place it’s time to run our application and click on the admin link again:
In order to facilitate the “Admin Home” under this new design, I renamed AdminHomeController to AdminController and left it as inheriting from Controller. This allows for the placement of the views in the ~/Views/Admin location. Once that was done, I modified the AdminHome route to this:
routes.MapRoute( // Controller for the Admin Home Page
"AdminHome",
"Admin/{action}/{id}",
new { controller = "Admin", action = "Index", id = "" }
);
In Conclusion
ASP.Net MVC favors “convention over configuration”. With that said though, the framework does not prohibit you from defining your own conventions as the need for it arises. I’ve shown you how it’s possible (and rather simple) to create a convention where the views for your controller are organized in a different fashion than what the “vanilla” MVC expects. More importantly, I’ve shown you that you can adopt “edge case” conventions within your application without affecting how MVC behaves with the rest of your site.
I am really excited about ASP.Net MVC. For the first time since .Net 1.0 was released I don’t feel like my web development is constrained or that I have to fight, scratch and claw my way through development because the tools that I’m using have tried to abstract (and dumb down) the process to the point it is a hindrance. It just makes sense. BIG KUDOS to Phil Haack Scott Guthrie and the rest of the ASP.Net MVC Team on a truly wonderful product!
Download the Source here: