views:

580

answers:

3

Could someone explain how routes are associated with controllers in MVC 2? Currently, I have a controller in /Controllers/HomeController.cs and a view /Home/Index.aspx.

My route registration method looks like this:

 public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
            routes.IgnoreRoute("{resource}.aspx/{*pathInfo}");
            routes.MapRoute(
               "Default",
                // Route name
               "{controller}/{action}/{id}",
                // URL with parameters
               new { controller = "Home", action = "Index", id = "" }
                // Parameter defaults
              );
        }

If I request the URL: http://localhost/Home/Index, then the request is correctly handled by HomeController.Index().

However, for the life of me, I can't figure out how the url /Home/Index gets pointed to HomeController. The view aspx doesn't, as far as I can tell, reference HomeController, HomeController doesn't reference the view, and the route table doesn't explicitly mention HomeController. How is this magically happening? Surely I'm missing something obvious.

then

+2  A: 

Inside the action Index there is a statement return View(). When a blank View is returned, the DefaultViewEngine searches several default folders for the name of the Controller method(specifically inside the FindView method). One of them is the Views/Home directory because Home is the name of the controller. There it finds the Index file, and uses it to display the result.

Yuriy Faktorovich
That explains how the correct View is found, but how is the correct controller method to execute found?
Robert Harvey
+6  A: 

This is the convention in ASP.NET MVC.

When using the DefaultControllerFactory this convention is buried inside the internal sealed class System.Web.Mvc.ControllerTypeCache (typical for Microsoft to write internal sealed classes). Inside you will find a method called EnsureInitialized which looks like this:

public void EnsureInitialized(IBuildManager buildManager)
{
    if (this._cache == null)
    {
        lock (this._lockObj)
        {
            if (this._cache == null)
            {
                this._cache = GetAllControllerTypes(buildManager).GroupBy<Type, string>(delegate (Type t) {
                    return t.Name.Substring(0, t.Name.Length - "Controller".Length);
                }, StringComparer.OrdinalIgnoreCase).ToDictionary<IGrouping<string, Type>, string, ILookup<string, Type>>(delegate (IGrouping<string, Type> g) {
                    return g.Key;
                }, delegate (IGrouping<string, Type> g) {
                    return g.ToLookup<Type, string>(t => t.Namespace ?? string.Empty, StringComparer.OrdinalIgnoreCase);
                }, StringComparer.OrdinalIgnoreCase);
            }
        }
    }
}

Pay attention how the grouping is made. So basically the DefaultControllerFactory will look inside all the referenced assemblies for types implementing the Controller base class and will strip the "Controller" from the name.

If you really want to dissect in details ASP.NET MVC's pipeline I would recommend you this excellent article.

Darin Dimitrov
That's bizarre. The name of the controller class must be related to the URL?! What if I wanted to use the same controller for a url which didn't have the name of the controller?
David Lively
Then you specify it in your routing table: `routes.MapRoute("MyRoute", "someSpecialUrl/blabla", new { controller = "Home", action = "Index", id = "" });` but you still have to specify the controller to use.
Darin Dimitrov
Also, with the StringComparer.OrdinalIgnoreCase flag set, doesn't that mean that the framework will be unable to differentiate between HomeController and homecontroller?
David Lively
@ztech: Yes, but then so's the Windows file system - you can't have (on a Windows based server) a file called HomeController.cs and one called homecontroller.cs with different content. Admittedly you can have two classes in an application called that, but the standard practice is usually to have one class per file, and have the filename the same as the class name ;)
Zhaph - Ben Duguid
+1 Hmm, great answer ztech, and great comments, but 4 upvotes and not the selected answer? I think you got gypped.
Robert Harvey
+4  A: 

The default views engine that comes with ASP.NET MVC works on the following conventions:

You have a folder structure like this:

- Controllers\
|-  HomeController.cs
- Views\
|- Home\
|-- Index.aspx
|- Shared\

When a request comes in, and matches a route defined in the RegisterRoutes method (see things like URL routing for more on that), then the matching controller is called:

routes.MapRoute(
  "Default", // Route name, allows you to call this route elsewhere
  "{controller}/{action}/{id}", // URL with parameters
  new { controller = "Home", action = "Index", id = "" } // Parameter defaults
  );

In the default route, you are also specifying a default controller (without the "Controller" suffix) - the routing engine will automatically add Controller onto the controller name for you - and a default action.

In your controller, you call the simple method:

public ActionResult Index(){
  return View();
}

The default view engine then looks for an aspx file called Index (the same as the action) in a folder called "Home" (the same as the controller) in the "Views" folder (convention).

If it doesn't find one in there, it will also look for an index page in the Shared folder.

From the ASP.NET MVC Nerd Dinner sample chapter

ASP.NET MVC applications by default use a convention-based directory naming structure when resolving view templates. This allows developers to avoid having to fully-qualify a location path when referencing views from within a Controller class. By default ASP.NET MVC will look for the view template file within the \Views\[ControllerName]\ directory underneath the application.

The \Views\Shared subdirectory provides a way to store view templates that are re-used across multiple controllers within the application. When ASP.NET MVC attempts to resolve a view template, it will first check within the \Views\[Controller] specific directory, and if it can’t find the view template there it will look within the \Views\Shared directory.

When it comes to naming individual view templates, the recommended guidance is to have the view template share the same name as the action method that caused it to render. For example, above our "Index" action method is using the "Index" view to render the view result, and the "Details" action method is using the "Details" view to render its results. This makes it easy to quickly see which template is associated with each action.

Developers do not need to explicitly specify the view template name when the view template has the same name as the action method being invoked on the controller. We can instead just pass the model object to the View() helper method (without specifying the view name), and ASP.NET MVC will automatically infer that we want to use the \Views\[ControllerName]\[ActionName] view template on disk to render it.


Edit to add:

Some example routes I've set up, that explicitly set the controller are:

routes.MapRoute(
  "PhotoDetailsSlug",
  "Albums/{albumId}/{photoId}/{slug}",
  new {controller = "Albums", action = "PhotoDetails"},
  new {albumId = @"\d{1,4}", photoId = @"\d{1,8}"}
  );

Here I'm explicitly stating that I'm using the Albums controller, and the PhotoDetails action on that, and passing in the various ids, etc to the that action.

Zhaph - Ben Duguid
Most of that makes sense. However, the defaults object passed to .MapRoute in your second code snippet references a controller called "Home". My controller class is called "HomeController." How can I explicitly specify the controller used for a particular route/url?
David Lively
You'll need to add an explicit rule for those routes, and tell it in the default parameters which Controller (less the "Controller" suffix) to use.
Zhaph - Ben Duguid
Thanks - I'll mark this as accepted. However, do I understand correctly that in the above example, "Albums" must have a corresponding controller class named "AlbumsController"?
David Lively
Yes, because I've set the controller to "Albums", if I'd set it to "Photos" then there would need to be one called "PhotosController" instead.
Zhaph - Ben Duguid