ASP.Net MVC with complex routes - how to keep it "sane"? - asp.net-mvc

I have a client who wishes to use a URL naming convention along the lines of:
/{subjectarea}/{subject}/{action}
Which is fine - this works brilliantly, with one controller per subject area, and having the action after the id (subject) is no issue at all.
However, it then gets complicated, as the client then wants to further continue the hierarchy:
/{subjectarea}/{subject}/{action}/{tightlyrelatedsubject}/{tightlyrelatedsubjectvariables}/{tightlyrelatedsubjectaction}
I have a controller for the tightly related subject (its just another subject area) which handles all of the admin side, but the client insists on having the public view hung off of the parent subject rather than its own root.
How can I do this while avoiding breaking the entire principals of MVC, and also avoiding re-implementing a ton of ASP.Net MVC provided functionality in my subject area controller just to be able to handle the related subjects from that same controller?
Is it possible to somehow call the related subjects controller from within the parent subject controller, and return the resulting view (as this would keep the separation of functionality for the subjects to their own controllers)? If that is possible, it would solve a heck of a lot of issues with this.

Here is the solution which solves my given issue - hope it solves someone elses.
As mentioned in my comment to Robert Harvey, all I actually need is another route which doesn't use the first two or three components as the controller, action and id, but instead takes those values from later on - if you hang this off of a static value in the route as well, its much easier to do.
So, here is the url I decided on to simplify the route:
/{subjectarea}/{subject}/related/{tightlyrelatedsubject}/{tightlyrelatedsubjectvariables}/{tightlyrelatedsubjectaction}
The route which satisfies this URL is as follows:
routes.MapRoute(
"RelatedSubjects",
"{parentcontroller}/{parentsubject}/related/{controller}/{id}/{action}",
new { controller = "shoes", action = "view", id = "all" }
);
On the subsequent controller action, I can ask for parameter values for parentcontroller and parentsubject so I can filter out the related item to just be specific to the given parent subject - problem solved!
This route needs to be above the ones which just deal with the first two values, otherwise you run the risk of another route map hijacking the request.
I could do this entirely without the /related/ static portion as the route could easily match on number of values alone, and infact I may indeed do so - however, I consider it better for later administration if there is a static item in there to confirm the use of the route.
I hope this helps someone!

One way you can do it is specify a wildcard route (notice the asterisk):
routes.MapRoute("subjects", "{action}/{*path}",
new { controller = "Subjects", action = "Index" });
This allows the controller to receive the entire path string after the action.
You can then obtain the hierarchy of subjects in the controller method like so:
string[] subjects = path.Split('/');
Once you have that, you can do anything you want, including dispatching different subjects to different handling methods for processing.

Related

ASP.NET MVC - CMS Questions

I'm looking at developing an application that will include a CMS. I'm a seasoned web forms developer but only really just moving into MVC.
I have a couple of questions that I hope some of you guys can answer:
First, my current web forms CMS allows users to create a page, and then "drop" any number of user controls onto that page they have created. The way I do this is to create an entry in the DB together with the path and then use the LoadControl method.
I can see I can do this with partial views, but partial views have no code behind. If I've potentially got 100 controls that people can drop onto a page, does this mean that the ViewBag in the controller needs to cater for all 100 controls just in case they are used on the view? For example, a web forms user control will contain logic: rptItems.DataSource = blah; rptItems.DataBind()
With MVC, I'm assuming that logic will be in the view controller and the view would access it by the ViewBag? I'm a little confused at how to do this.
Secondly, how would you handle deep routing?
EG:
Store/Products/Category is fine, but what about Store/Products/Category/Delivery/UK ? Would I need to set up a route in global.asax for each route I need? In web forms, I just called the ReWritePath method and handled the routing myself using regular expressions.
Thanks for the time to read this, and hopefully answer some of my queries
For your second question, (ie, "deep routing"), you can handle this within your controller instead of adding real routes. Each part of the url is available via the RouteData.Values collection inside of your controller action. So, your route may look like
~/Store/Products/Category/{*params}
Assuming typical route configuration, this would call the Category(...) action method on ~/areas/store/controllers/storeController, which could then grap delivery and uk from the RouteData.Values collection.
There are a lot of other approaches to this - storing routes in a database and using associated metadata to find the correct controller and method - but I think this is the simplest. Also, it may be obvious, but if you really only need two parameters beyond 'Category' in your example, you could just use
public ActionResult Category(string category, string region)
{
...
}
and a route:
~/store/{controller}/{action}/{category}/{region}/{*params}
Delivery and UK would be mapped to the the category and region parameters, respectively. Anything beyond uk would still be available via the RouteData.Values collection. This assumes that you don't have more specific routes, like
~/store/{controller}/{action}/{category}/{region}/{foo}/{bar}/{long_url}/{etc}
that would be a better match. ({*params} might conflict with the second route; you'll have to investigate to see if it's a problem.)
For your first question:
You can dynamically generate the view source and return it as a string from the controller, eliminating the need to pass a lot of stuff via ViewBag. If a virtual page from your CMS database requires inclusion of partial views, you would add the references to those components when generating the page. (This may or may not address your problem - if not, please provide more information.)

What is the appropriate route design for this ASP.NET MVC application?

My url http://server/region/section/item
Now if someone goes to http://server/us/ I want to display a page to choose a section
if someone goes to http://server/us/beer/ I want to display a list of all beers.
My question is should I just have 1 route with defaults and then return different views depending on how much the URL is filled in or should I create multiple routes going to the same controller different actions? or even different controllers, I just want to know what the best practice is.
The typical route looks like this:
http://www.domain.com/controller/action/id
[domain/controller/action/id]
In your case, it's short one part:
http://server/us/beer
[domain/controller/action?/?]
As Robert Harvey said, you wouldn't want to have an action for every product. So how about something like:
http://server/us/product/beer
[domain/controller/action/id]
domain = server
controller = us (tho, this doesn't seem like it would be a good name for the controller)
action = product
id = beer
Then you'd develop a product view that would show the beer data to your visitors.
This isn't architect-ed very well, but without knowing your situation it would be difficult for anyone to answer this. I hope this helps though.
In your particular case, "beer" can probably be a filter to a controller action, rather than another controller action, in which case you need only one route entry.
It doesn't seem wise to create a strategy that will require you to add new routes, controller methods and/or views every time you add a product category. Unless, of course, you want to highly customize each category's look and behavior.

Newbie MVC Routing

I'm clearly missing the concept of routing - for an experiment I've set the route as
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute("Standard",
"{devicetype}/{devicesub}/{language}/{culture}/{controller}/{action}/{id}",
new
{
devicetype = "pc",
devicesub = "def",
language = "en",
culture = "int",
controller = "Home",
action = "Index",
id = ""
}
);
My index page is in Views/pc/def/en/int/Home
When I run it I get an error searching for /Home/Index.aspx
It seems to still use the default structure and not my more complex one - what am I not understanding?
The way the content of the site is stored does not reflect the route but is defined by the Controllers and the Views so although your route is complex you're still ending up at the home controller so MVC is going to be looking in /views/home for the appropriate view which in this case is index.
One of the hardest things I've found to get my head around is the separation of URL from the processing and more importantly content - its right and its clever but the fact that routing and result can be radically different (in terms of finding things in your directory structure) is, erm, interesting (-:
As a practical experiment, do nothing other than relocate your index page to /views/home/index.aspx and see if that resolves the problem...
I'm a bit of a newbie myself so this might not be correct, but as far as I know, the path of your views are always located in the "Controller/Action" path. The additional properties you have specified are simply just querystring values being submitted additionally with the request.
Hope it helps...
G
So, the relevant lines if your controller simply ends in return View() or return View(modelData) are:
controller = "Home",
action = "Index",
All URLs matching your above route will land there, unless your URL is for something like /pc/def/en/int/Widgets, in which case you will route to WidgetsController/index.
It sounds like you want to have different views for the same action. If you want to have different views depending on the parameters passed to your controller, you can do that. You need to be explicit about it when you return your ViewResult. You can return View("SpecialView",model) and the view engine will look for SpecialView.aspx in your controller's view directory. Of course, "SpecialView" could be replaced with an appropriate string for your app, and could be generated programmatically if it makes sense.
Many thanks for the input folks - i think i'm begining to understand
It does indeed work if i place the form in views/home - however most commercial sites are much more complex than 2 levels
As you have probably gathered from the structure what i was trying to experiment with was different forms for device type (pc, phone, mobile) and culture but using a single controller as the business logic is the same regardless of the style and language of the presentation
Just for further info
I've changed the directory structure to Views/Home/pc/def/int and auto generated the path as Jason suggests and this works fine - I had to change the structure as Views/Home seems to get added to the front of the search regardless of the string you supply in the View command

Handling complex URLs in ASP MVC

I have - I think - a complex URL to deal with in ASP MVC 1.0:
All my actions in most of the controllers require two parameters all the time: Account and Project. This is on top of each Action's requirements. This means a typical URL is like this:
http://abcd.com/myaccount/projects/project_id/sites/edit/12
In this example:
myaccount is the account name. projects can be a controller, others options are like locations, employees. project_id is the id of a project within myaccount, sites could be a controller, other options are like staff or payments. edit is an action and 12 is the id of the site edited.
(hope this is clear enough)
Now one option is to create a route and pass project_id and account into all actions of controllers by adding two extra parameters to all actions. This is not really desired and also I'm not sure the two controllers (projects and sites) are going to work here.
My ideal situation is to use some kind of context that travels with the call to the controller action and store project_id and myaccount in there. The rest of the parameters then can be dealt with in a normal way like:
// sitescontroller
public ActionResult Edit(string id)
{
string account = somecontext["account"];
string project_id = somecontext["project"];
// do stuff
}
Any ideas as to how/where this can happen? Also how is this going to work with ActionLink (i.e. generating correct links based on this context)?
Thanks!
You first need to add the tokens to your routes like {company}/projects/{project}{controller}/{action}/{id}. Then if you wrote your own IControllerFactory then it would be very easy to push the values from the RouteData into the controller via the constructor or however you wanted to do it. Probably the easiest way to get started would be to subclass DefaultControllerFactory and override the CreateController method.
This doesn't quite make sense to me. Why would you have a route that is akin to the following:
{controller}/{id}/{controller}/{id}
?

ASP.Net MVC - handling bad URL parameters

What's the best way to handle a visitor constructing their own URL and replacing what we expect to be an ID with anything they like?
For example:
ASP.Net MVC - handling bad URL parameters
But the user could just as easily replace the URL with:
https://stackoverflow.com/questions/foo
I've thought of making every Controller Function parameter a String, and using Integer.TryParse() on them - if that passes then I have an ID and can continue, otherwise I can redirect the user to an Unknown / not-found or index View.
Stack Overflow handles it nicely, and I'd like to too - how do you do it, or what would you suggest?
Here's an example of a route like yours, with a constraint on the number:
routes.MapRoute(
"Question",
"questions/{questionID}",
new { controller = "StackOverflow", action = "Question" },
new { questionID = #"\d+" } //Regex constraint specifying that it must be a number.
);
Here we set the questionID to have at least one number. This will also block out any urls containing anything but an integer, and also prevents the need for a nullable int.
Note: This does not take into account numbers that larger than the range of Int32 (-2147483647 - +2147483647). I leave this as an exercise to the user to resolve. :)
If the user enters the url "questions/foo", they will not hit the Question action, and fall through it, because it fails the parameter constraint. You can handle it further down in a catchall/default route if you want:
routes.MapRoute(
"Catchall",
"{*catchall}", // This is a wildcard routes
new { controller = "Home", action = "Lost" }
);
This will send the user to the Lost action in the Home controller. More information on the wildcard can be found here.
NB: The Catchall should reside as the LAST route. Placing it further up the chain will mean that this will handle all others below it, given the lazy nature of routes in ASP.NET MVC.
Here is some useful infromation that might help.
If you have a action method
public ActionResult Edit(int? id)
{}
then if someone types in
/Home/Edit/23
the parameter id will be 23.
however if someone types in
/Home/Edit/Junk
then id will be null which is pretty cool. I thought it would throw a cast error or something. It means that if id is not a null value then it is a valid integer and can be passed to your services etc. for db interaction.
Hope this provides you with some info that I have found whilst testing.
In ASP.NET MVC, you can define a filter implementing IActionFilter interface. You will be able to decorate your action with this attribute so that it will be executed on, before or after your action.
In your case, you will define it to be executed "before" your action. So that, you will be able to cancel it if there is an error in the passed parameters. The key benefit here that you only write the code which checking the passed paramaters once (i.e you define it in your filter) and use it wherever you want in your controller actions.
Read more about MVC filters here: http://haacked.com/archive/2008/08/14/aspnetmvc-filters.aspx
You can specify constraints as regular expressions or define custom constraints. Have a look at this blog post for more information:
http://weblogs.asp.net/stephenwalther/archive/2008/08/06/asp-net-mvc-tip-30-create-custom-route-constraints.aspx
You will still need to deal with the situation where id 43243 doesn't map to anything which could be dealt with as an IActionFilter or in your controller directly.
The problem with that approach is that they still might pass an integer which doesn't map to a page. Just return a 404 if they do that, just as you would with "foo". It's not something to worry about unless you have clear security implications.

Resources