They always say time changes things, but you actually have to change them yourself. -Andy Warhol
ASP.NET Web Forms started getting old the day that Ajax conquered the masses. As some have said, Ajax has been the poisonous arrow shot in the heel of ASP.NET-another Achilles. Ajax made getting more and more control over HTML and client-side code a true necessity. Over time, this led to different architectures and made ASP.NET Web Forms a little less up to the task with each passing day.
Based on the same run-time environment as Web Forms, ASP.NET MVC makes developing web applications a significantly different experience. At its core, ASP.NET MVC just separates behavior from the generation of the response-a simple change, but one that has a huge impact on applications and developers. ASP.NET MVC is action-centric, disregards the page-based architecture of Web Forms, and pushes a web-adapted implementation of the classic Model-View-Controller pattern.
In ASP.NET MVC, each request results in the execution of an action-ultimately, a method on a specific class. Results of executing the action are passed down to the view subsystem along with a view template. The results and template are then used to build the final response for the browser. Users don’t point the browser to a page; users just place a request. Doesn’t that sound like a big change?
So everything looks different for developers in the beginning, but everything looks sort of familiar after a bit of practice. Your actions can serve HTML as well as any other type of response, including JSON, script, graphic, and binary files. You don’t have to forgo using roles on methods, forms authentication, session state, and cache-the run-time environment is the same, and MVC and Web Forms applications can happily coexist on the same site.
Unlike Web Forms, ASP.NET MVC is made of various layers of code connected together but not intertwined and not forming a single monolithic block. For this reason, it’s easy to replace any of these layers with custom components that enhance the maintainability as well as the testability of the solution. With ASP.NET MVC, you gain total control over the markup and can apply styles and inject script code at will using the JavaScript frameworks you like most.
The bottom line is that although you might decide to keep using Web Forms, for today’s web development ASP.NET MVC is a much better choice. Worried about productivity? My best advice is that you start making the transition as soon as possible. You don’t need to invest a huge amount of time, but you need to understand exactly what’s going on and the philosophy behind MVC. If you do that, any investment you make will pay you back sooner than you expect.
ASP.NET MVC doesn’t change the way a web application works on the ASP.NET and Internet Information Services (IIS) platforms. ASP.NET MVC, however, changes the way developers write web applications. In this chapter, you’ll discover the role and structure of the controller-the foundation of ASP.NET MVC applications-and how requests are routed to controllers.
Note This book is based on ASP.NET MVC 3. This version of ASP.NET MVC is backward compatible with the previous version, MVC 2. This means you can install both versions side by side on the same machine and play with the new version without affecting any existing MVC code you might have already. Of course, the same point holds for web server machines. You can install both ASP.NET MVC 2 and ASP.NET MVC 3 on the same server box without unpleasant side effects. The same level of backward compatibility is expected with the upcoming version, MVC 4.
Routing Incoming Requests
Originally, the whole ASP.NET platform was developed around the idea of serving requests for physical pages. It turns out that most URLs used within an ASP.NET application are made of two parts: the path to the physical Web page that contains the logic, and some data stuffed in the query string to provide parameters. This approach has worked for a few years, and it still works today. The ASP.NET run-time environment, however, doesn’t limit you to just calling into resources identified by a specific location and file. By writing an ad hoc HTTP handler and binding it to a URL, you can use ASP.NET to execute code in response to a request regardless of the dependencies on physical files. This is just one of the aspects that most distinguishes ASP.NET MVC from ASP.NET Web Forms. Let’s briefly see how to simulate the ASP.NET MVC behavior with an HTTP handler.
Note In software, the term URI (short for Uniform Resource Identifier) is used to refer to a resource by location or a name. When the URI identifies the resource by location, it’s called a URL, or Uniform Resource Locator. When the URI identifies a resource by name, it becomes a URN, or Uniform Resource Name. In this regard, ASP.NET MVC is designed to deal with more generic URIs, whereas ASP.NET Web Forms was designed to deal with location-aware physical resources.
Simulating the ASP.NET MVC Runtime
Let’s build a simple ASP.NET Web Forms application and use HTTP handlers to figure out the internal mechanics of ASP.NET MVC applications. You can start from the basic ASP.NET Web Forms application you get from your Microsoft Visual Studio project manager.
Defining the Syntax of Recognized URLs
In a world in which requested URLs don’t necessarily match up with physical files on the web server, the first step to take is listing which URLs are meaningful for the application. To avoid being too specific, let’s assume you support only a few fixed URLs, each mapped to an HTTP handler component. The following code snippet shows the changes required to be made to the default web.config file:
<httpHandlers>
<add verb="*"
path="home/test/*"
type="MvcEmule.Components.MvcEmuleHandler" />
</httpHandlers>
Whenever the application receives a request that matches the specified URL, it will pass it on to the specified handler.
Defining the Behavior of the HTTP Handler
In ASP.NET, an HTTP handler is a component that implements the IHttpHandler interface. The interface is simple and consists of two members, as shown here:
public class MvcEmuleHandler : IHttpHandler
{
public void ProcessRequest(HttpContext context)
{
// Logic goes here
...
}
public Boolean IsReusable
{
get { return false; }
}
}
Most of the time, an HTTP handler has a hardcoded behavior influenced only by some input data passed over the query string. Nothing prevents us, however, from using the handler as an abstract factory for adding one more level of indirection. The handler, in fact, can use information from the request to determine an external component to call to actually serve the request. In this way, a single HTTP handler can serve a variety of requests and just dispatch the call among a few more specialized components.
The HTTP handler could parse out the URL in tokens and use that information to identify the class and the method to invoke. Here’s an example of how it could work:
public void ProcessRequest(HttpContext context)
{
// Parse out the URL and extract controller, action, and parameter
var segments = context.Request.Url.Segments;
var controller = segments[1].TrimEnd('/');
var action = segments[2].TrimEnd('/');
var param1 = segments[3].TrimEnd('/');
// Complete controller class name with suffix and (default) namespace
var fullName = String.Format("{0}.{1}Controller",
this.GetType().Namespace, controller);
var controllerType = Type.GetType(fullName, true, true);
// Get an instance of the controller
var instance = Activator.CreateInstance(controllerType);
// Invoke the action method on the controller instance
var methodInfo = controllerType.GetMethod(action,
BindingFlags.Instance |
BindingFlags.IgnoreCase |
BindingFlags.Public);
var result = String.Empty;
if (methodInfo.GetParameters().Length == 0)
{
result = methodInfo.Invoke(instance, null) as String;
}
else
{
result = methodInfo.Invoke(instance, new Object[] { param1 }) as String;
}
// Write out results
context.Response.Write(result);
}
The preceding code just assumes the first token in the URL past the server name contains the key information to identify the specialized component that will serve the request. The second token refers to the name of the method to call on this component Finally, the third token indicates a parameter to pass.
Invoking the HTTP Handler
Given a URL such as home/test/*, it turns out that home identifies the class, test identifies the method, and whatever trails is the parameter. The name of the class is further worked out and extended to include a namespace and a suffix. According to the example, the final class name is MvcEmule.Components.HomeController. This class is expected to be available to the application. The class is also expected to expose a method named Test, as shown here:
namespace MvcEmule.Components
{
public class HomeController
{
public String Test(Object param1)
{
var message = "<html><h1>Got it! You passed '{0}'</h1></html>";
return String.Format(message, param1);
}
}
}
Figure 1-1 shows the effect of invoking a page-agnostic URL in an ASP.NET Web Forms application.
This simple example demonstrates the basic mechanics used by ASP.NET MVC. The specialized component that serves a request is the controller. The controller is a class with just methods and no state. A unique system-level HTTP handler takes care of dispatching incoming requests to a specific controller class so that the instance of the class executes a given action method and produces a response.
What about the scheme of URLs? In this example, you just use a hardcoded URL. In ASP.NET MVC, you have a very flexible syntax you can use to express the URLs the application recognizes. In addition, a new system component in the run-time pipeline intercepts requests, processes the URL, and triggers the ASP.NET MVC HTTP handler. This component is the URL Routing HTTP module.
The URL Routing HTTP Module
The URL routing HTTP module processes incoming requests by looking at the URLs and dispatching them to the most appropriate executor. The URL routing HTTP module supersedes the URL rewriting feature of older versions of ASP.NET. At its core, URL rewriting consists of hooking up a request, parsing the original URL, and instructing the HTTP run-time environment to serve a “possibly related but different” URL.
Superseding URL Rewriting
URL rewriting comes into play if you need to make tradeoffs between needing human-readable and SEO-friendly URLs and needing to programmatically deal with tons of URLs. For example, consider the following URL:
http://northwind.com/news.aspx?id=1234
The news.aspx page incorporates any logic required to retrieve, format, and display any given news. The ID for the specific news to retrieve is provided via a parameter on the query string. As a developer, implementing the page couldn’t be easier: you get the query string parameter, run the query, and create the HTML. As a user or as a search engine, by simply looking at the URL you can’t really understand the intent of the page and you aren’t likely to remember the address easily enough to pass it around.
URL rewriting helps you in two ways. It makes it possible for developers to use a generic frontend page, such as news.aspx, to display related content. In addition, it also enables users to request friendly URLs that will be programmatically mapped to less intuitive, but easier to manage, URLs. In a nutshell, URL rewriting exists to decouple the requested URL from the physical webpage that serves the requests.
In the latest version of ASP.NET 4 Web Forms, you can use URL routing to match incoming URLs to other URLs without incurring the costs of HTTP 302 redirects. In ASP.NET MVC, on the other hand, URL routing serves the purpose of mapping incoming URLs to a controller class and an action method.
Note Originally developed as an ASP.NET MVC component, the URL routing module is now a native part of the ASP.NET platform and, as mentioned, offers its services to both ASP.NET MVC and ASP.NET Web Forms applications, though through a slightly different API.
Routing the Requests
What happens exactly when a request knocks at the IIS gate? Figure 1-2 gives you an overall picture of the various steps involved and how things work differently in ASP.NET MVC and ASP.NET Web Forms applications.
The URL routing module intercepts any requests for the application that could not be served otherwise by IIS. If the URL refers to a physical file (for example, an ASPX file), the routing module ignores the request, unless it’s otherwise configured. The request then falls down to the classic ASP.NET machinery to be processed as usual in terms of a page handler.
Otherwise, the URL routing module attempts to match the URL of the request to any of the application-defined routes. If a match is found, the request goes into the ASP.NET MVC space to be processed in terms of a call to a controller class. If no match is found, the request will be served by the standard ASP.NET runtime in the best possible way and likely results in an HTTP 404 error.
In the end, only requests that match predefined URL patterns (also known as routes) are allowed to enjoy the ASP.NET MVC runtime. All such requests are routed to a common HTTP handler that instantiates a controller class and invokes a defined method on it. Next, the controller method, in turn, selects a view component to generate the actual response.
Internal Structure of the URL Routing Module
In terms of implementation, I should note that the URL routing engine is an HTTP module that wires up the PostResolveRequestCache event. The event fires right after checking that no response for the request is available in the ASP.NET cache.
The HTTP module matches the requested URL to one of the user-defined URL routes and sets the HTTP context to using the ASP.NET MVC standard HTTP handler to serve the request. As a developer, you’re not likely to deal with the URL routing module directly. The module is system provided and doesn’t need you to perform any specific form of configuration. You are responsible, instead, for providing the routes that your application supports and that the routing module will actually consume.
Application Routes
By design, an ASP.NET MVC application is not forced to depend on physical pages. In ASP.NET MVC, users place requests for acting on resources. The framework, however, doesn’t mandate the syntax for describing resources and actions. I’m aware that the expression “acting on resources” will likely make you think of Representational State Transfer (REST). And, of course, you will not be too far off the mark in thinking so.
Although you can definitely use a pure REST approach within an ASP.NET MVC application, I would rather say that ASP.NET MVC is loosely REST-oriented in that it does acknowledge concepts like resource and action, but it leaves you free to use your own syntax to express and implement resources and actions. As an example, in a pure REST solution you would use HTTP verbs to express actions-GET, POST, PUT, and DELETE-and the URL to identify the resource. Implementing a pure REST solution in ASP.NET MVC is possible but requires some extra work on your part.
The default behavior in ASP.NET MVC is using custom URLs where you make yourself responsible for the syntax through which actions and resources are specified. This syntax is expressed through a collection of URL patterns, also known as routes.
URL Patterns and Routes
A route is a pattern-matching string that represents the absolute path of a URL-namely, the URL string without protocol, server, and port information. A route might be a constant string, but it will more likely contain a few placeholders. Here’s a sample route:
/home/test
The route is a constant string and is matched only by URLs whose absolute path is /home/test. Most of the time, however, you deal with parametric routes that incorporate one or more placeholders. Here are a couple of examples:
/{resource}/{action}
/Customer/{action}
Both routes are matched by any URLs that contain exactly two segments. The latter, though, requires that the first segment equals the string “Customer”. The former, instead, doesn’t pose specific constraints on the content of the segments.
Often referred to as a URL parameter, a placeholder is a name enclosed in curly brackets { }. You can have multiple placeholders in a route as long as they are separated by a constant or delimiter. The forward slash (/) character acts as a delimiter between the various parts of the route. The name of the placeholder (for example, action) is the key that your code will use to programmatically retrieve the content of the corresponding segment from the actual URL.
Here’s the default route for an ASP.NET MVC application:
{controller}/{action}/{id}
In this case, the sample route contains three placeholders separated by the delimiter. A URL that matches the preceding route is the following:
/Customers/Edit/ALFKI
You can add as many routes as you want with as many placeholders as appropriate. You can even remove the default route.
Defining Application Routes
Routes for an application are usually registered in the global.asax file, and they are processed at the application startup. Let’s have a look at the section of the global.asax file that deals with routes:
public class MvcApplication : HttpApplication
{
protected void Application_Start()
{
RegisterRoutes(RouteTable.Routes);
// Other code
...
}
public static void RegisterRoutes(RouteCollection routes)
{
// Other code
...
// Listing routes
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new {
controller = "Home",
action = "Index",
id = UrlParameter.Optional
});
}
}
As you can see, the Application_Start event handler calls into a public static method named RegisterRoutes that lists all routes. Note that the name of the RegisterRoutes method, as well as the prototype, is arbitrary and can be changed if there’s a valid reason.
Supported routes must be added to a static collection of Route objects managed by ASP.NET MVC. This collection is RouteTable.Routes. You typically use the handy MapRoute method to populate the collection. The MapRoute method offers a variety of overloads and works well most of the time. However, it doesn’t let you configure every possible aspect of a route object. If there’s something you need to set on a route that MapRoute doesn’t support, you might want to resort to the following code:
// Create a new route and add it to the system collection
var route = new Route(...);
RouteTable.Routes.Add("NameOfTheRoute", route);
A route is characterized by a few attributes, such as name, URL pattern, default values, constraints, data tokens, and a route handler. The attributes you set most often are name, URL pattern, and default values. Let’s expand on the code you get for the default route:
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new {
controller = "Home",
action = "Index",
id = UrlParameter.Optional
});
The first parameter is the name of the route; each route should have a unique name. The second parameter is the URL pattern. The third parameter is an object that specifies default values for the URL parameters.
Note that a URL can match the pattern even in an incomplete form. Let’s consider the root URL- http://yourserver.com. At first sight, such a URL wouldn’t match the route. However, if a default value is specified for a URL parameter, the segment is considered optional. As a result, for the preceding example, when you request the root URL, the request is resolved by invoking the method Index on the Home controller.
Processing Routes
The ASP.NET URL routing module employs a number of rules when trying to match an incoming requested URL to a defined route. The most important rule is that routes must be checked in the order they were registered in global.asax.
To ensure that routes are processed in the right order, you must list them from the most specific to the least specific. In any case, keep in mind that the search for a matching route always ends atthe first match. This means that just adding a new route at the bottom of the list might not work and might also cause you a bit of trouble. In addition, be aware that placing a catch-all pattern at the top of the list will make any other patterns-no matter how specific-pass unnoticed.
Beyond order of appearance, other factors affect the process of matching URLs to routes. As mentioned, one is the set of default values that you might have provided for a route. Default values are simply values that are automatically assigned to defined placeholders in case the URL doesn’t provide specific values. Consider the following two routes:
{Orders}/{Year}/{Month}
{Orders}/{Year}
If in the first route you assign default values for both {Year} and {Month}, the second route will never be evaluated because, thanks to the default values, the first route is always a match regardless of whether the URL specifies a year and a month.
A trailing forward slash (/) is also a pitfall. The routes {Orders}/{Year} and {Orders}/{Year}/ are two very different things. One won’t match to the other, even though logically, at least from a user’s perspective, you’d expect them to. Another factor that influences the URL-to-route match is the list of constraints that you optionally define for a route. A route constraint is an additional condition that a given URL parameter must fulfill to make the URL match the route. The URL not only should be compatible with the URL pattern, it also needs to contain compatible data. A constraint can be defined in various ways, including through a regular expression. Here’s a sample route with constraints:
routes.MapRoute(
"ProductInfo",
"{controller}/{productId}/{locale}",
new { controller = "Product", action = "Index", locale="en-us" },
new { productId = @"\d{8}",
locale = ""[a-z]{2}-[a-z]{2}" });
In particular, the route requires that the productId placeholder must be a numeric sequence of exactly eight digits, whereas the locale placeholder must be a pair of two-letter strings separated by a dash. Constraints don’t ensure that all invalid product IDs and locale codes are stopped at the gate but at least they cut off a good deal of work.
To purchase this book and get the entire chapter go to: http://shop.oreilly.com/product/0790145336064.do