Asp.net MVC2 URL structure - best practices - c#

What are the best practices for setting up projects with multiple interfaces in asp.net MVC2? I'm starting a project where we need a typical arrangement like:
example.com/ - end-user interface
example.com/Admin/ - site management
The site will be involved enough that simply having all the user logic in HomeController and all the admin logic in AdminController isn't really an option. I've read about using multiple Route definitions, but the following doesn't seem to work properly:
routes.MapRoute(
"Admin", // Route name
"Admin/{controller}/{action}/{id}", // URL with parameters
new { controller = "Admin", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
It causes all the links on the homepage to point to example.com/Admin/local-part instead of example.com/local-part.
I'd like to avoid specifying a different MapRoute for each Admin controller (omitting {controller} from the mapping each time).
I've also read about setting up different Areas within a project, but that seems like it would be too involved for the scope of this project.
Finally, I've also read that you can put constraints on MapRoutes, but the documentation on that part seems confusing to me. Is there something obvious I'm missing? Failing that, are there any best practices for asp.net structure that I should keep in mind?

It sounds like Area's is ready made for what you want to do. Setting up an area isn't really all that involved, basically you just have to register it. By default the area routing will match the default in the global.asax, the exception being the extra "\area" slug in the url. I'm fairly certain it only took me a few minutes when I set this up on a project a few months ago.
If your Admin controller is complicated enough to exceed the scope of a single controller then that indicates that an area may be warranted.

Related

How to ignore all characters after "controller/action" in an ASP.NET MVC route?

I would like my ASP.NET MVC4 application to only serve the base HTML markup for a specific page, and after that I'm processing everything else on client-side with knockout.js/history.js/AJAX, including the initial page load.
So when someone refers to URL http://example.com/products/list/food/fruits, the MVC router should simply ignore everything what is behind "products/list" and route the request to ProductsController and List action. Then on client-side I will handle the rest and load the requested data accordingly.
I was playing with the route definitions, I tried to completely skip the "products/list" route, I also tried to add a "products/list/*" route, but didn't have success yet.
You can use an asterisk as part of the last variable in a route. For example, when configuring your routes:
routes.MapRoute(
"ProductRoute",
"products/list/{*otherArgs}",
new { controller = "Products", action = "List" });
You can learn more in MSDN's Documentation on routing under the section "Handling a Variable Number of Segments in a URL Pattern"
You will need to create your own route.
Something like this should do the trick:
routes.MapRoute("Products", "Products/{List}",
new {controller = "Products", action = "List"}
);
Note: I´m not sure if the other parameters are required in the route.

MVC routing with "optional" routes

I'm a bit baffled by how MVC is figuring out my routing details. Let me see if I can explain this right.
So ... given that I have the default route ...
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "CMS", action = "GetPage", id = UrlParameter.Optional }
);
And my app is a content management system so I want to create nice urls from the site structure so i'll map a wildcard url that lets me determine if i need to render a 404 based on what's in my database ...
routes.MapRoute(
"CMS",
"{*path}",
new { controller = "CMS", action = "GetPage", path = string.Empty }
);
Herein lies the problem.
MVC will basically match everything to the default route because technically no params are required assuming "GetPage" on the "CMS" controller requires no params, which is not what i want.
What i'm trying to say to it is something like "given 2 or 3 url parts, look for a controller and action match with an optional id parameter but for all other urls including ones that you can't match to this route fall down in to the CMS route".
The only "easy" way I found to do this is to change the first route to something like this ...
routes.MapRoute(
"Default",
"Get/{controller}/{action}/{id}",
new { controller = "CMS", action = "GetPage", id = UrlParameter.Optional }
);
Then any url that starts "Get/" will match that route and all other routes automatically fall down in to the second route, but that doesn't sit right somewhere in my head and I can't figure out quite why yet (i think it's because it doesn't really solve the problem it simply moves it).
My problem is that I don't really want a route that says "given no values match this route anyway" so I changed it to this ...
routes.MapRoute(
"Default",
"{controller}/{action}/{id}"
);
Now for some odd reason literally every request is hitting the catch all (not quite what i want but close).
So any ideas guys?
EDIT:
I came a touch closer with this ...
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { id = UrlParameter.Optional}
);
... but that now matches all urls with 2 parts "foo/bar" rather than dropping through like it should to the other route because there is no "foo" controller.
Ok I have a solution, it works for me because 99% of requests need to map to the CMS route but in your case it may not if you have a lot of controllers you need to map to.
I was hoping to find an ideal solution for all but this is merely an ideal in my scenario ...
So assuming you have (like me) only a cms controller and a an accounts controller you can do this:
routes.MapRoute("Account", "Account/{action}", new { controller = "Account" });
routes.MapRoute(
"Default",
"{*path}",
new { controller = "CMS", action = "GetPage", path = string.Empty }
);
That way only urls starting with "Account" get caught by the first rule, and everything else falls through to the default route and gets handled by the cms controller.
I plan to simply add more routes as I add more controllers. It's not an ideal solution because it could mean i end up with a lot of route mappings in the long term but its a good enough solution to meet my needs.
Hope it helps someone else out there.
Maybe I don't understand your problem fully but why not have this route:
routes.MapRoute(
"CMS",
"CMS/{action}/{path}",
new { action = "GetPage", path = string.Empty }
);
and add it into the route collection before your default path...

Can I delete xxxxxxxAreaRegistration.cs?

If I am registering the route to an area (say it's called Admin) in the global.asax file rather than the area's AdminAreaRegistration.cs file, is there any reason why I can't delete the AdminAreaRegistration.cs file? Is there any other code in the framework that may call into it at some stage which might throw an exception if it is missing?
As asked in comments, here is the code to register an Area in global.asax
routes.MapRoute(
"AdminAreaRoute",
"Admin/{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional },
new string[] { "Payntbrush.Presentation.Demo.MVC3.Areas.Admin.Controllers" }
).DataTokens.Add("Area", "Admin");
routes.MapRoute(
"SiteAreaRoute",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional },
new { controller = "/|Home|Account" }, // Constraint's on the URL (second param above) {controller} segment
new string[] { "Payntbrush.Presentation.Demo.MVC3.Areas.Site.Controllers" }
).DataTokens.Add("Area", "Site");
The key is in using the DataTokens collection, as highlighted in this excellent post by Phillip Haydon. You can add as many as you want, but make sure you put the root site area after the explicitly named areas so that {controller}/{action}/{id} doesn't catch all the requests before the other Map Routes can.
Could you post the code that you are using to register your area in the global.asax?
The reason that I ask, is I don't think that it is technically possible to get an area working correctly without the xxxAreaRegistration.cs class implemented somewhere (even if in another project.
It might seem like it is working without it but in reality I think you might simply telling .net that your route is pointing to a sub directory.
I would expect you to get an error at some point.
Have a look at msdn AreaRegistration Class documentation it says
Provides a way to register one or more areas in an ASP.NET MVC application.
while it doesn't say that its the only way, I think that might be the case.
I've deleted these files and haven't had any problems so far. I'm going to assume that it is OK until I run into any issues.

MVC non-area route problems (alongside areas)

How should I configure the following non area routes?
/foo/{controller}/{action}/{id}
maps to controllers in namespace myapp.foo.
/{controller}/{action}/{id}
maps to controllers in namespace myapp.
I also have 2 areas, bar and baz, they are registered with registeraAllAreas.
My current setup
This is my current setup. It gives the problem below when I use the url /Home/Index.
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.IgnoreRoute("myapp/elmah.axd/{*pathInfo}");
AreaRegistration.RegisterAllAreas();
routes.MapRoute(
"foo", // Route name
"foo/{controller}/{action}/{id}", // URL with parameters
new { action = "Index", id = UrlParameter.Optional }, // Parameter defaults
new string[] { "myapp.Controllers.foo" }
);
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional }, // Parameter defaults
new string[] { "myapp.Controllers" }
);
Multiple types were found that match the controller named 'Menu'. This
can happen if the route that services this request
('foo/{controller}/{action}/{id}') does not specify namespaces to
search for a controller that matches the request.
The request for 'Menu' has found the following matching controllers:
myapp.Controllers.MenuController
myapp.Areas.bar.Controllers.MenuController
myapp.Areas.baz.Controllers.MenuController
Clearly there's something I'm doing the wrong way.
Update
I also get the wrong adress generated when I use:
<% using (Ajax.BeginForm("SaveSomething", "Home", ...
It renders <form target="/foo/Home/SaveSomething"
I'm guessing that one cannot reliably use {controller} in two routes in the same area.
Update 2
It seems to work much better when I put the /foo route registration at the bottom.
This raises the question, what is considered a/the default route? (As the default route is reccomended to be put at the very end.)
You have two controllers that has the name MenuController so MVC doesn't know which one to use if you don't give it more information. In you areas you probably have a files named something like <YourAreaName>AreaRegistration. Open those files and update the RegisterArea method so you route the request to the right controller.
From your error message it seems like the route is getting mapped to foo/{controller}/{action}/{id}, which doesn't have a MenuController. My guess is that you have a action link on a page under foo something something. That will generate an incorrect link if you don't specify the area for the link.
Try this to use the default route with ActionLink:
#Html.ActionLink("Some text", "action", "controller", new { area = "" }, null)
If you want the request to go to a specific area just write it down in the call.
UPDATE: The problem is that when you write something like Ajax.BeginForm("SaveSomething", "Home",...) it will match the first route. You can't solve this by putting the area in the BeginForm statement as I suggested before since the foo route is not an area. You have two options, 1: move the foo part to an area, 2: put the foo route after the default route. If you put the default route before the foo route you will get a hard time rendering urls as long as you have foo in the same area as the default route (the default area), since the route engine will always find the default one first. However, you will be able to catch request to the foo route. So my best suggestion is to put the foo route in an area.

Why does my route get redirected, this doesn't make any sense to me

I have these 2 routes mapped out:
routes.MapRoute(
"Admin",
"admin/{controller}/{action}/{id}",
new { controller = "Admin", action = "index", id = "" }
);
and then I have:
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = "" } // Parameter defaults
);
So the 2 routes are identical, except the first one has /admin prefixed in the URLS.
This is what is happening, I have no idea how to explain this:
When I go to:
www.example.com/user/verify
it redirects to
www.example.com/admin/user/complete
instead of
www.example.com/user/complete
The action Verify simply redirects to Complete like this:
return RedirectToAction("complete", "user");
And all the complete action does is populate the ViewModel, and then calls the view.
How can it be redirecting and adding the prefix /admin/ to the URL?
I believe it is redirecting to the Admin route because the Admin route is the first with all the matching parameters (controller and action in the case provided). If you want to use something like this you will need to either look into using areas (MVC2) or using a named route redirect.
admin is your controller, you dont need an admin/controller/action the default route works just fine
all you need is an admin controller and the default route will find it for you
ie {controller}/{action}/{id}
will send /admin/addproduct to a controller named admin and an action called addproduct
you only need to add routes if you want something custom for example
/products/televisions/hdtv/2
where products would be a controller and the last 3 are category,subcategory and pagenumber
on the controller you point it to within your route.
hope that makes sense
Not sure exactly how your controllers are structured, but you can add a constraint to the first MapRoute to limit it to the specific controllers you want the route to apply to:
routes.MapRoute(
"Admin",
"admin/{controller}/{action}/{id}",
new { controller = "Admin", action = "index", id = "" } ,
new { controller = "[Some regex Expression - e.g. Admin]" }
);
Which will make the route only applicable for those controllers related routes. You can also use this tool to debug your routes. Depends how you have things structured, but like #NickLarson said - sounds like your using area functionality of MVC 2.
mvc goes from top to bottom while matching router, that' why you are dealing with this problem

Categories