Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

ASP.NET MVC: Part 2

0.00/5 (No votes)
5 Jul 2008 1  
A continued look at ASP.NET MVC and URL Routing.

Introduction

This article is a continuation of ASP.NET MVC Part 1 in which I will attempt to elaborate on the URL Routing portion of MVC. A word of caution though, the previous article was based on the Preview 2 release of ASP.NET MVC; however, this article uses the Preview 3 release. There are some things that have changed between these releases.

Prerequisites

Review of Model-View-Controller Pattern

To recap a little from the first part of this article, let's briefly discuss the MVC pattern. MVC is a pattern that divides an application into separate areas of responsibility: Model, View, and Controller.

  • Model: Models are responsible for maintaining the state of the application, often by using a database.
  • View: These components are strictly for displaying data, they provide no functionality beyond formatting it for display.
  • Controller: Controllers are the central communication mechanism in an MVC application. They communicate actions from the Views to the Model and back.

One of the main points with the MVC pattern is that there is no direct communication between the Model and the View. The point of this article is to detail how this communication takes place using URL Routing.

URL Routing

URL Routing is a part of the MVC Web Application framework. However, Microsoft has chosen to also make it a part of the .NET Framework 3.5 Service Pack 1, though its functionality may differ from the implementation in MVC. We can take a look at how URL Routing is accomplished in MVC to get an understanding of how it may be implemented in non-MVC web applications. The URL Routing components are, appropriately enough, in the System.Web.Routing namespace. The images below show the two versions of each namespace.

URL Routing makes use of HTTPHandlers and HTTPModules as described below to perform the desired actions, as we will see.

Routing HTTPModule

As a brief review, HTTPModules are inserted into the ASP.NET pipeline as a means to preprocess requests. HTTPModules for authentication, session state, and others are already built in to ASP.NET applications. Since these modules process requests before they get to the page level, it is a perfect solution for URL Routing; requests are handled by the module, and then routed appropriately. Adding the below entry into the web.config file, under the httpModules element, places the UrlRoutingModule into the pipeline for a web application.

<add name="UrlRoutingModule" 
  type="System.Web.Routing.UrlRoutingModule, 
        System.Web.Routing, Version=3.5.0.0, 
        Culture=neutral, PublicKeyToken=31BF3856AD364E35"/> 

As a word of caution, Preview 3 uses the following to avoid conflicts with SP1:

<add name="UrlRoutingModule" 
   type="System.Web.Routing.UrlRoutingModule, 
         System.Web.Routing, Version=0.0.0.0, Culture=neutral, 
         PublicKeyToken=31BF3856AD364E35"/>

The module registers two event handlers: OnApplicationPostMapRequestHandler, OnApplicationPostMapRequestHandler, in its Init method. The latter is, of course, used to handle the PostMapRequestHandler event on the HttpApplication object. This event is triggered after the ASP.NET engine has mapped a request to an event handler. The former is used to handler the PostResolveRequestCache event which occurs when the ASP.NET engine bypasses an event handler to serve a request from the cache. The handler for this event is where we can see the magic for URL Routing occurring. Here, a new RequestData object is created and added to the HttpContext collection. An instance of an IRouteHandler interface is added, and HttpContext.RewritePath is used to redirect the request to UrlRouting.axd.

RequestData data2 = new RequestData();
data2.OriginalPath = context.Request.Path;
data2.HttpHandler = httpHandler;
context.Items[_requestDataKey] = data2;
context.RewritePath("~/UrlRouting.axd");

Routing HttpHandler

Again, as a brief review, HttpHandlers are used simply to take requests and process them. In the case of URL Routing, the UrlRoutingHandler will be used to process requests coming from the UrlRouting.axd that we saw above. In the ProcessRequest method of the UrlRoutingHandler, an instance of the controller for the request being processed is attempted to be created from an IControllerFactory instance and then the Execute method called.

string controllerName = RequestContext.RouteData.GetRequiredString("controller");
// Instantiate the controller and call Execute
IControllerFactory factory = ControllerBuilder.GetControllerFactory();
IController controller = 
   factory.CreateController(RequestContext, controllerName);
...
controller.Execute(controllerContext);

Controller

As we saw in part 1, Controllers are like the middleware of ASP.NET MVC web applications. They take the request for a certain action, do any processing, such as retrieving data from the model, then invoke a View. As described above, the Controller for the request is found, then its Execute method is called. In the Execute method, the Action for the request is found and attempted to be invoked.

string actionName = RouteData.GetRequiredString("action");
if (!InvokeAction(actionName))
{
    HandleUnknownAction(actionName);
}

The InvokeAction method uses some Reflection to find the method for the action and pass any parameters necessary.

This is a brief look at the inner workings of the URL Routing mechanisms. A much more detailed review can be found here: http://www.cnblogs.com/shanyou/archive/2008/03/22/1117573.html.

Defining and Creating Routes

Now that we have an understanding of what is happening behind the scenes, we can now concentrate on creating and defining routes to be used in an application.

Routes are added to the RouteTable for the application in the Application_Start event.

protected void Application_Start(object sender, EventArgs e)
{
    RegisterRoutes(RouteTable.Routes);
}
public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

    routes.MapRoute("Default", "{controller}/{action}/{id}",
        new { controller = "Home", action = "Index", id = "" }
    );
}

This is a default implementation for an ASP.NET MVC application. Routes are evaluated in the order in which they have been entered into the RouteTable. The first entry, IgnoreRoute, is an extension method on the RouteCollection provided by MVC.

public static void IgnoreRoute(this RouteCollection routes, string url, object constraints)
{
    if (routes == null)
    {
        throw new ArgumentNullException("routes");
    }
    if (url == null)
    {
        throw new ArgumentNullException("url");
    }
    Route route2 = new Route(url, new StopRoutingHandler());
    route2.Constraints = new RouteValueDictionary(constraints);
    Route item = route2;
    routes.Add(item);
}

This method creates a new Route that uses the StopRoutingHandler to stop processing requests for HTTPHandlers. This is essential for processing requests in MVC since, as we saw above, requests are redirected to UrlRouting.axd. Routes are added to the collection using MapRoute, which is an extension method to RouteCollection provided by MVC with three overrides.

public static void MapRoute(this RouteCollection routes, string name, string url)
public static void MapRoute(this RouteCollection routes, string name, 
                            string url, object defaults)
public static void MapRoute(this RouteCollection routes, string name, 
                            string url, object defaults, object constraints)

The first two overrides call the last, which creates a Route that uses the MvcRouteHandler, then adds it to the collection.

Route route = new Route(url, new MvcRouteHandler())
{
    Defaults = new RouteValueDictionary(defaults),
    Constraints = new RouteValueDictionary(constraints)
};

if(String.IsNullOrEmpty(name))
{
    // Add unnamed route if no name given
    routes.Add(route);
}
else
{
    routes.Add(name, route);
}

If it is a Route that did not use the MvcRouteHandler, it could, of course, be added directly to the collection.

routes.Add(new Route("Default.aspx", 
   new{ controller = "Home", action = "Index"}, new MyRouteHandler);

Routing in Action

With URL Routing, you can accept requests such as http://www.mysite.com/Products/Bikes/Schwinn, and have it, for instance, display all bikes made by Schwinn. Rather than using query string parameters, this produces a much cleaner URL, and routing details can be hidden from ordinary users. ASP.NET MVC expects URLs to have at least two elements: controller and action. In the URL, http://www.mysite.com/Home/Index, Home is the controller that should be used, and Index is the action to be invoked. A Route for this would be as follows:

Route("{controller/{action}", 
new RouteValueDictionary( new { controller = "Home", action = "Index" }), 
                          new MvcRouteHandler());

The first parameter is the URL pattern to match requests against. The second parameter is a RouteValueDictionary that can be used to provide default values if not found in the requested URL. A request for http://www.mysite.com would be rewritten as http://www.mysite.com/Home/Index. A request for http://www.mysite.com/Home would also be rewritten as http://www.mysite.com/Home/Index.

Routes are evaluated in the order they were added to the RouteCollection. For instance, given the routes added in this order:

routes.MapRoute("DefaultRoute", "{controller}/{action}/{id}",
    new { controller = "Home", action = "Index", id = 0 }
);

routes.MapRoute("NameRoute", "Artist/{name}",
    new { controller = "Music", action = "ArtistsByName", name = "AC/DC" }
);

A URL such as http://www.mysite.com/Artist/Elvis will be matched by the first route despite it having the word Artist. The first Route assumes that Artist is meant to be the controller and Elvis the action, and a default ID of 0 will be inserted. To get the desired results, the Routes need to be reversed in the collection. Given the two Routes below, the problem is determining which is used for requests such as http://www.mysite.com/Artist/1394 and http://www.mysite.com/Artist/U2.

routes.MapRoute("ArtistByID", "Artist/{id}",
    new { controller = "Artist", action = "AlbumsByArtistId", id = 0 }
);

routes.MapRoute("ArtistByName", "Artist/{name}",
    new { controller = "Artist", action = "AlbumsByArtistName", name = "" }
);

In the former URL, it's obvious that what is being provided is an ID; however, in the letter URL, U2 is assumed by the engine to be an ID, and will attempt to case it to an integer as required by the AlbumsByArtistId action method. If the Routes were reversed, 1394 would be converted to string as required by AlbumsByArtistName. To solve this problem, we can make use of the Constraints parameter when creating the Route. This parameter is a RouteValueDictionary that is used as a Regular Expressions to be used to evaluate a specified parameter in the requested URL.

protected virtual bool ProcessConstraint(HttpContextBase httpContext, 
          object constraint, string parameterName, 
          RouteValueDictionary values, RouteDirection routeDirection)
{
    object obj2;
    IRouteConstraint constraint2 = constraint as IRouteConstraint;
    if (constraint2 != null)
    {
        return constraint2.Match(httpContext, this, 
               parameterName, values, routeDirection);
    }
    string str = constraint as string;
    if (str == null)
    {
        throw new InvalidOperationException(
              string.Format(CultureInfo.CurrentUICulture, 
              RoutingResources.Route_ValidationMustBeStringOrCustomConstraint, 
              new object[] { parameterName, this.Url }));
    }
    values.TryGetValue(parameterName, out obj2);
    string input = Convert.ToString(obj2, CultureInfo.InvariantCulture);
    string pattern = "^(" + str + ")$";
    return Regex.IsMatch(input, pattern, 
           RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
}

The above Routes can be modified as follows:

routes.MapRoute("ArtistByID", "Artist/{id}",
    new { controller = "Artist", action = "AlbumsByArtistId", id = 0 },
    new { id = @"\d{1,}" }
);

routes.MapRoute("ArtistByName", "Artist/{name}",
    new { controller = "Artist", action = "AlbumsByArtistName", name = "" },
    new { name = @"[a-zA-Z]{1,}" }
);

Now the request for http://www.mysite.com/Artist/1394 will be evaluated as matching the ArtistByID Route and http://www.mysite.com/Artist/U2 by the ArtistByName Route regardless of the order they appear in the RouteTable.

To be Continued...

ASP.NET MVC is a very rich technology that can't be covered in a single article. Hopefully this article has illustrated the basic concepts and can be used to evaluate the potential of this technology. Future articles in this series will cover more aspects such as unit testing and forms.

References

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here