In one of my earlier post I announced that I will start playing around with Microsoft ASP.NET MVC framework with the intent of seeing if I can find a compelling reason to replace the custom implementation of MVP (based on Billy McCafferty's article) that currently powers TacticSheet with a Microsoft ASP.NET MVC implementation.
As I started playing around with ASP.NET MVC I discovered that Microsoft ASP.NET MVC isn't just about separation of concerns. As Scott Hanselman right points out, to a typical ASP.NET developer, it's a completely new philosophy:
This is a not just a different tune, but a whole different band playing all new music. Not everyone will like the music, and that's why the world has more than one band. This is a Good Thing.
I like to think of ASP.NET MVC as the raw, acoustic version of the more heavily produced and multi-layered ASP.NET WebForms we use today.
After years of View-stating and Post-backing joining a completely different band that plays completely new music is a big change. A change this big also demands that folks wanting to be quick adopters and play this all new music will need to spend a bit of time and understand a lot of new concepts out there.
Most concepts with ASP.NET MVC Framework are easy to understand. If you're someone who believes in simplicity, writing clean testable code and controlling your own angle brackets ASP.NET MVC is mostly very straight forward to figure out. If you have been following posts by Scott Guthrie, posts by Phil Haack and posts by Scott Hanselman, none of the MVC concepts should sound new.
Where I did have to pause a bit, think and spend a little bit of time was while figuring out how Routes and Routing Tables work with the ASP.NET MVC framework. If there were simple explanations of routing and how one could make routing dance to his tune out there, at-least I wasn't able to find these simple explanations.
After wasting quite a bit of time trying to figure out how default routing in ASP.NET works I decided to write this post, so that you dear reader, won't have to spend time going through multiple blog posts. I've always thought of myself as a hands on person and I don't like reading a lot of theory just to understand fundamental concepts. Here's my humble attempt to simplify how Microsoft MVC routes and routing tables work and bring it to dummies like you and me. :)
Let's get started!
Without going into a lot of theory let's just start a new ASP.NET MVC Project and take a look at our global.asax file. Here are the first three lines you would see in the Application_Start.
Let's analyze these three lines and what they mean.
Using line one we're telling ASP.NET MVC Framework that my URLs are going to be following the pattern of passing a controller name followed by action name followed by an a parameter called 'id'. Line 3 tells ASP.NET MVC that when I do hit a URL that follows that pattern it should use the MvcRouteHandler as it's route handler. MvcRouteHandler is the default Route Handler shipped with ASP.NET MVC framework and for most applications (unless we have a special need to write our own route handler) that's pretty much what we're going to use.
Line two indicates that if I forget to give the name of an action in the URL while browsing / running the application, ASP.NET MVC invokes the "Index" action by default and if I forget to specify the "id" in the URL, it passes null to the action method by default. In the above declaration I'm not allowed the luxury of forgetting my controller name in the URL, but if you wanted a route that allowed you that, you could do that as well. (We'll shortly see how).
All of this basically means means that if I had the following code snippet in my HomeController, the OpenEmployee method would get invoked with Id passed as 2 if I entered the URL - http://[ApplicationUrl]/Home/OpenEmployee/2
For a typical ASP.NET developer there is a little bit of magic going on here. The magic is mostly inspired out of design paradigms like Convention over Configuration and some of it also seems to have slight inspirations from Duck Typing.
Notice how you didn't type "HomeController" in your URL. You just typed "/Home/OpenEmployee/2". The moment ASP.NET MVC sees the controller name in your URL it uses 'convention' to figure out that you intend to invoke a class which has the name you have specified in your URL followed by the word 'controller'. So it ends up invoking the 'HomeController'.
Additionally a little bit of magic you might have noticed in the above code snippet is that we have not specified anywhere in our code that the Id in the URL has to be treated as an integer. However the parameter in the OpenEmployee method is defied as an integer. Which means when you pass "/Home/OpenEmployee/2" ASP.NET MVC assumes that 2 is an integer, implicitly converts "2" to an integer and passes it to your Open Employee method.
On the other hand if your OpenEmployee expected a string called id, ASP.NET MVC would figure out that the conversion to Integer from your URL isn't necessary. The below code snippet still works:
In the above snippet because your controller now expects a string you could pass anything to it. In other words URL's like http://[ApplicationUrl]/Home/OpenEmployee/2 or http://[ApplicationUrl]/Home/OpenEmployee/Hello would pass "2" and "Hello" to your controller.
Let's get back to the global.asax and take a look at the next 3 lines of the routing code that was generated by Visual Studio 2008:
Remember I told you that you also build a route where the name of the controller would be assumed by default if you didn't specify it ? The highlighted lines of code above are an example. By now if you've got to the soul of what we've been talking about the three lines highlighted above shouldn't need any explanation. In the above three lines we're telling ASP.NET MVC a few very simple things:
If someone hits the URL "Default.aspx" (and does not follow the controller/action/id pattern) we just want the default controller to be "Home", the default action to be "Index" and the default 'id' to be null. This is useful because when someone tries to visit your application using the URL http://[ApplicationUrl]/ IIS 7(based on it's default settings) will attempt to open default.aspx. When this happens ASP.NET MVC will automatically instantiate the Home controller and will invoke the default action 'Index' on it.
This logic also explains why all Microsoft ASP.NET MVC applications must have a Default.aspx in it's root even when the file isn't directly used or invoked manually:
Ok, so far so good. This post was written with an intent of making the basic concepts of ASP.NET MVC routing concepts stupid simple to understand. Now that we understand these concepts let's see if we can tweak our routing slightly and make it dance to our tune a little bit.
In most Web based applications if you are using SQL server identity columns (or any column that's an auto-generated integer) as primary keys the whole approach of having id's being passed to Action methods directly from the URL makes a lot of sense. However, at times you want to pass more that just the id from the URL.
For example in TacticSheet, where a person can be assigned to more then one project I would want to open his allocation to a specific project; this means that to open allocations of an employee I would have to include "/Allocations/Open/5" in the URL, where Allocation is the controller, Open is the action and 5 is the ID of the employee whose allocation I want to open.
But in the above approach we have a problem. An employee might be allocated to more than one project and I want to open his allocation for a specific project. In other words, I want my URLs to work like this - "/Allocations/Open/5/Projectx" - where Allocations is the controller, Open is the action, 5 is the ID of the employee and Projectx is the name of the project.
If I can trap this URL from my controller and have both '5' and 'Projectx' passed to my Action method, I can effectively open employee 5's allocation for project x.
To archive this I open my global.asax and add one more route to it:
Notice how I added projectname to the URL and told ASP.NET MVC framework that it should now start expecting project name after the id; so now paths like "/Allocations/Open/5/Projectx" become perfectly valid paths in my application as long as we have underlying controllers and actions which can handle these paths.
So let's go ahead and create my AllocationsController and add the following Action method to it:
Now when I visit the URL - http://[ApplicationUrl]/Allocations/Open/5/Projectx it results in the Open action method of the AllocationsController being invoked. 5 is passed in as 'id' of the employee that you want to open and 'Projectx' is passed as the project name of the project for which you want to open Employee ID 5's allocation.
If you've followed along, the basic explanation of how routing works and how you can add simple routes yourself might sound stupid simple. If you're looking for more control over your routes, refer to Scott Guthrie's post on routes.