ASP.NET MVC 2 How are urls/routes, views related to controllers? - asp.net

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

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.

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.

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.

Related

Why are these routes being matched?

The following code:
<li>#Html.ActionLink(metaTapp.Nav_About, "Mayla", "About")</li>
<li>#Html.ActionLink(metaTapp.Nav_Support, "Support", "About")</li>
<li>#Html.ActionLink(metaTapp.Nav_Exchange, "Index", "Exchange")</li>
<li>#Html.ActionLink("Post Rfq", "Create", "Rfq")</li>
is producing the following links:
<li>About</li>
<li>Support</li>
<li>Exchange</li>
<li>Post Rfq</li>
My Global Application Start looks like this:
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
UploadRouteConfig.RegisterRoutes(RouteTable.Routes);
LocalizationConfig.RegisterRoutes(RouteTable.Routes);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
DatabaseFactory.SetDatabaseProviderFactory(new DatabaseProviderFactory());
}
}
UploadRouteConfig.RegisterRoutes:
public static void RegisterRoutes(RouteCollection routes)
{
routes.MapRoute("Upload", "Upload/Image", null).RouteHandler = new UploadMvcRouteHandler();
}
LocalizationConfig.RegisterRoutes
public static void RegisterRoutes(RouteCollection routes)
{
routes.MapRoute(
"Account", // Route name
"Account/{action}", // URL with parameters
new { controller = "Account", action = "Index" } // Parameter defaults
);
routes.MapRoute(
"RfqCategory",
string.Format("{{{0}}}/Rfq/CategoryFilter/{{category}}", Constants.ROUTE_PARAMNAME_LANG),
new { controller = "Rfq", action = "CategoryFilter", category = Guid.Empty.ToString() }
);
routes.MapRoute(
Constants.ROUTE_NAME,
string.Format("{{{0}}}/{{controller}}/{{action}}/{{rfqid}}", Constants.ROUTE_PARAMNAME_LANG),
new { controller = "About", action = "Home", rfqid = "00000000-0000-0000-0000-000000000000" }
);
}
RouteConfig.RegisterRoutes:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute("TappDefault", "{controller}/{action}/{id}", new { controller = "About", action = "Home", id = UrlParameter.Optional }
);
I cant understand how upload is being matched to everything. If the route doesn't start with Upload/Image it should fall through to the localizationconfig routes?
Ok so the short answer to my problem is that I am rendering ActionLinks when I should be using RouteLinks. ActionLink will perform a match based on route table entries which seems like what I want but because I'm using a static Url:
"Upload/Image"
every url is matched. Why? Because routes are not filters. Routes work by matching supplied route values to the parameters of segments of the Url, but since:
"Upload/Image"
has no parameters i.e. {controller} then technically EVERYTHING is a match. RouteLink on tyhe pother hand allows me to specify which route to use when rendering the link:
#Html.RouteLink(
linkText: "route: Home",
routeName: "TappDefault",
routeValues: new {controller="About", action="Home"}
)
..
From Professional ASP.NET.MVC4 (Jon Galloway, Phil Haack, Brad Wilson, K. Scott Allen)
Page 232 Chapter 9 Routing:
Let’s suppose you add the following page route at the beginning of
your list of routes so that the URL /static/url is handled by the
page /aspx/SomePage.aspx:
routes.MapPageRoute("new", "static/url", "~/aspx/SomePage.aspx");
Note that you can’t put this route at the end of the list of routes
within the RegisterRoutes method because it would never match
incoming requests. Why wouldn’t it? Well, a request for /static/url
would be matched by the default route and never make it through
the list of routes to get to the new route. Therefore, you need to
add this route to the beginning of the list of routes before the
default route.
NOTE This problem isn’t specific to Routing with Web Forms. There are many cases where you might route to a non-ASP.NET MVC route
handler.
Moving this route to the beginning of the defined list of routes seems
like an innocent enough change, right? For incoming requests, this
route will match only requests that exactly match /static/url but will
not match any other requests. This is exactly what you want. But
what about generated URLs? If you go back and look at the result of
the two calls to Url.RouteLink, you’ll find that both URLs are broken:
/url?controller=section&action=Index&id=123
and
/static/url?controller=Home&action=Index&id=123
This goes into a subtle behavior of Routing, which is admittedly
somewhat of an edge case, but is something that people run into from
time to time.
Typically, when you generate a URL using Routing, the route values
you supply are used to “fill in” the URL parameters as discussed
earlier in this chapter.
When you have a route with the URL {controller}/{action}/{id}, you’re
expected to supply values for controller, action, and id when
generating a URL. In this case, because the new route doesn’t have
any URL parameters, it matches every URL generation attempt because
technically, “a route value is supplied for each URL parameter.” It
just so happens that there aren’t any URL parameters. That’s why all
the existing URLs are broken, because every attempt to generate a URL
now matches this new route.
This might seem like a big problem, but the fix is very simple. Use
names for all your routes and always use the route name when
generating URLs. Most of the time, letting Routing sort out which
route you want to use to generate a URL is really leaving it to
chance, which is not something that sits well with the
obsessive-compulsive control freak developer. When generating a
URL, you gener- ally know exactly which route you want to link to, so
you might as well specify it by name. If you have a need to use
non-named routes and are leaving the URL generation entirely up to
Routing, we recommend writing unit tests that verify the expected
behavior of the routes and URL generation within your application.
Specifying the name of the route not only avoids ambiguities, but it
may even eke out a bit of a per- formance improvement because the
Routing engine can go directly to the named route and attempt to use
it for URL generation.
In the previous example, where you generated two links, the following
change fixes the issue. We changed the code to use named parameters to
make it clear what the change was. #Html.RouteLink( linkText: "route:
Test", routeName: "test", routeValues: new {controller="section",
action="Index", id=123})

MVC3 ActionName attribute, its behaviour and effects

While reading about mcv3 I came across an attribute name called [ActionName]. It actually gives a new name to the action method. I tested a scenario which made me think; how are the internals working. When I have the following two action methods in my controller class
[ActionName("Test")]
public ActionResult Index()
{
return View();
}
[ActionName("Index")]
public ActionResult Test()
{
return View();
}
I thought this will end up in some kind of infinite loop or will give some ambiguity exception. But the same works fine and the second method is called when i give this url http://mysite:1234/mycontroller
What made MVC engine to choose the second method and not the first?
Any idea why this happens?
Phil Haack has a post on this matter: How a method becomes an action
In short: the ControllerActionInvoker uses reflection to find a method matches the action name.
The ActionNameAttribute redefines the name of the method.
Also be aware that the name of your View matches the ActionName, not the MethodName: the method Index will search for a view with the name "Test"
This is the magic of the Routing engine. Somewhere within the global.asax.cs file there would be routing patterns defined, mostly which defaults to
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = "" } // Parameter defaults
);
This is a routing pattern defined for your application. The action name attribute maps to the 'action' parameter within the parameter collection (3rd parameter for MapRoute).
In your case if you map the action 'Index' to method 'Test'. It should call Test() method. I am not sure whether it is still calling Index() for you. In fact the routing engine does not care about the method name if it finds the ActionName attribute over your public method.
ActionNameAttribute it represents an attribute that is used for the name of an action. If it is not present the name of the method is used.

MVC3 how to minimize the number of controllers

i am new to MVC3 and our project needs something like:
http://www.abc.com/product_1/product_1_subpage/...
http://www.abc.com/product_2/product_2_subpage/...
right now, i have product_1 as a controller; product_1_subpage as an action of this controller, however, think about i have over 100 different products, i cannot keep create over 100 controllers for each single product, i need to do something on this structure, any idea?
thank you very much for your help, really appreciate any input.
You would probably want to have only a single Controller called Products, for all your products, instead of having to add a new controller for every product.
With custom routes, or the default routing for that matter, you would still be able to generate individual links for individual products. Also, if you were to use your approach with a new controller for every product (which you really shouldn't!), you would have to re-compile and deploy your application every time you want to add another product - which would be a pain to maintain.
It sounds like you should have a look at the tutorials about MVC provided by the .Net team to get some basic understand of MVC, and how to think about it.
Use custom routes:
routes.MapRoute(
"ProductsRoute", // Route name
"products/{productName}/{subName}/{id}", // URL with parameters
new { controller = "Product", action = "View", id = UrlParameter.Optional } // Parameter defaults
);
That would make the following work:
public class ProductController : Controller
{
// http://yourweb/products/goggles/xray/Elite2000
public ActionResult View(string productName, string subName, string id)
{
}
}
how about changing the format of the url a little to take advantage of the routing:
http://www.abc.com/product/subpage/1

ASP.NET MVC Route All Requests

We need to present the same site, with a different theme (and different data) depending on the "route" into the site.
www.example.com/Trade
www.example.com/Public
NB: These are not dynamic themes which the user can pick. Certain customers will be coming via a third party link which will always point them to one of the urls.
The value trade/public also needs to be used in queries from the UI to the database (to pull back varying data depending on the route into the site).
So what are my options?
Create custom view engine which uses the querystring (mvc route param) value to load the relevant master page.
In each controller action, grab the parameter (trade/public etc) and pass it off to the database queries.
public ActionResult List(string siteType){
products.ListFor(siteType);
}
The problem here is having to alter every controller action to pass the querystring value through.
This also presents an issue with any route defined in global.asax having to accept the parameter.
I'm wondering if there's another way, perhaps some combination of a custom controller base and host name e.g. trade.example.com, public.example.com?
First define routing:
routes.MapRoute(
"Default", // Route name
"{siteType}", // URL with parameters
new { controller = "Home", action = "Index", id = "" } // Parameter defaults
);
and then use it in controller:
RouteData.Values["siteType"]
How about this:
Create a base controller that all your controllers inherit from
Add a property: public string SiteType { get; protected set; }
Add an OnActionExecuting method to set it before any action is called, e.g.
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
//use filterContext to set SiteType somehow, e.g. you can look at the URL or the route data
}

Best way for ASP.NET MVC routing for not-so common scenarios?

UPDATE - 1/21/09:
The only way I've been able to get this working moderately well is to create routes that have additional path info... in other words instead of http://company.com/myDepartment/myTeam/action/id, the routes need to be built to handle paths
like http://company.com/department/myDepartment/team/myTeam and so on.
END UPDATE - 1/21/09:
I have two possible URL conventions that I need to be able to route to the same controllers/views
http://team.department.company.com/Action/id
http://department.company.com/Action/id
http://company.com/Action/id
The above has been pretty easy to handle, since the department/team aliases are in the hostname of the Uri.
However, when I want to implement the following, I've been unable to get it working satisfactorily:
http://company.com/department/team/Action/id
http://company.com/department/Action/id
http://company.com/Action/id
Here are the routes I've defined:
routes.MapRoute("departmentAndTeam1", "{departmentId}/{teamId}/{controller}/{action}/{id}", new { controller = "Home", action = "Index", id = "" });
routes.MapRoute("departmentAndTeam2", "{departmentId}/{teamId}/{controller}/{action}", new { controller = "Home", action = "Index" });
routes.MapRoute("departmentAndTeam3", "{departmentId}/{teamId}/{controller}", new { controller = "Home", action = "Index" });
routes.MapRoute("department", "{department}/{controller}/{action}/{id}", new {controller = "Home", action = "Index", id = ""});
routes.MapRoute("departmentAndTeam4", "{departmentId}/{teamId}/", new { controller = "Home", action = "Index"});
routes.MapRoute("Default", "{controller}/{action}/{id}", new { controller = "Home", action = "Index", id = "" });
The problem I'm having is now every action method in my controllers require a departmentId & teamId parameter. I would really prefer just to be able to "pre-process" the department/team id values in the base controller class that I've got in the OnActionExecuting event handler. Am I barking up the wrong tree? Do I need to implement my own Mvc Route handler?
Note: This is replacing a legacy app and the URL formats need to be kept/maintained.
EDIT: First off, thanks for the responses so far. Based on the responses I've received I think I've not been clear enough on one point. This is (loosely) what my controller looks like:
public class ControllerBase : Controller
{
DepartmentDTO department;
TeamDTO team;
protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
PopulateDept(filterContext.ActionParameters("departmentId");
PopulateTeam(filterContext.ActionParameters("teamId");
}
}
However, with this scheme, all controllers that inherit from this class (and are routed appropriately with the routes listed above) expect (what I perceive to be redundant/uneeded) parameters in their action handlers. I want to avoid having each handler require these "extra" parameters and am looking for suggestions.
Thanks!
-Giorgio
First off, you don't need departmentAndTeam2 or departmentAndTeam3. departmentAndTeam1 can handle it because it has the defaults for controller, action and id.
Another thing I notice is that none of your example URLs have the controller specified in them. In that case, leave it out of the route and just leave it as the default. eg
for the URL
http://company.com/department/team/Action/id
use the route
routes.MapRoute("departmentAndTeam1", "{departmentId}/{teamId}/{action}/{id}", new { controller = "Home", action = "Index", id = "" });
Notice how "controller" will always be "Home" because it's never mentioned in the URL.
For the actions and URLs that you don't want to specify the department and team IDs, they should just be passing by all the other routes and just using the Default route. The fact that they aren't means that they are getting caught by one of the earlier routes. I think if you fix the earlier route's problems then it should fix the problems of needing the extra IDs.
Finally, if you are still haveing trouble, try using an MVC route debugger like this one from Phil Haacked http://haacked.com/archive/2008/03/13/url-routing-debugger.aspx. I've used it to work out the problems with some of my routes and it is really helpful to see what it is actually doing and runtime.
How about this (I haven't tested because I'm not sure what requirements you have other than those 3 particular urls mapping)?
routes.MapRoute("1", "{department}/{team}/{action}/{id}", new { controller = "Home" });
routes.MapRoute("2", "{department}/{action}/{id}", new { controller = "Home" });
routes.MapRoute("3", "{action}/{id}", new { controller = "Home" });

Resources