asp.net mvc dynamic/relative routing - asp.net-mvc

I'm developing a website which has a modular structure.
Every segment of the url presents an content item.
For example url: www.mysite.com/blogs/programming/2010/01/
Root item is 'blogs' of type 'area'. It has a child item 'programming' of type 'blog'.
Now there's '2010/01' left of the url.
Last valid (routable) item 'programming' was a blog so I need to map '2010/01' to action
BlogController.Date(int blogid, int year, int? month, int? day)
Every controller comes from a module (separate dll), which registers some item types (blog registers types 'blog' (routable) and 'post' (not routable). 'blog' can have children of type 'post').
When last valid (routable) item of the url is detected, logic knows which assembly and controller to look for. Now I need a way to invoke correct action with correct parameters.
Some routes for item of type 'blog'
{year}/
{year}/{month}
{year}/{month}/{day}
feed/
category/{category-name}/
tag/{tag-name}/
search/{*phrase}
{*post-name}
Any suggestions what would be a simple way to do the routing?

To solve the action parameter signature problem, I personally would create a new Model class "BlogModel" and have only that as your single parameter. This way, you'd have a consistent action parameter signature. However, this would require a bit more work, as you would need to create a custom ModelBinder object "BlogModelBinder" and register it to the ModelBinderFactory (or in MVC3 the DependencyResolver). In the "BlogModelBinder" you simply look up the RouteData's parameters and values and bind it to the corresponding field in your "BlogModel."
From my personal experience, I don't think there's an easy way to register your routes: you still would have to individually register the route urls to a specific action. Unless someone has an efficient way of registering the route urls, you can take solace in knowing that we all have to get our hands dirty with the plumbing code.

Related

How to set {city} in all the routes MVC 5

I am developing a marketplace application which is supposed to have different products and vendors which are mapped to different cities.
The idea is that I need to store the CurrentCity in context so that I can use it to construct urls, filter data, fetch delivery areas etc.
e.g.
www.mywebsite.com/cityA/listings
www.mywebsite.com/cityB/listings
www.mywebsite.com/cityA/cart
www.mywebsite.com/cityB/cart
Something like the MoonPig website (https://www.moonpig.com/uk/Gift/Flowers/)
Currently, I am passing the city as a parameter in almost all the controller methods and also storing it in a cookie.
Alternatively, I am also thinking of creating a BaseController and possibly inject it in the OnActionExecuting(ActionExecutingContext context).
But the problem with the first approach is that all the Action methods need to have "city" as a parameter and I need to have it in context for doing something like
#Url.Action("Index", "Listings", new {city = cityName})
If I use the second approach, then I don't think I'll get the urls which have city in them.
I am ideally looking for a solution with which I can inject a city parameter as Base route / segment in the MVC RouteDictionary so that all the Urls are generated accordingly (with #url helper).
Is this possible or is there a better way to tackle this problem?
Would really appreciate if someone can show me a direction.
But the problem with the first approach is that all the Action methods need to have "city" as a parameter
This assumption is incorrect, since MVC automatically passes values from the current context when generating URLs, so there is no need to explicitly pass city as long as it is configured in the route and present in the URL. See this answer for how you can utilize this behavior for localization, which is similar to what you are doing.

MVC: ActionLink Remembers ID Field?

This is a follow-on to an earlier stackoverflow question (link text).
If you use the default routing definition, which ends with {id}, then if you have an ActionLink whose target is the same method as generated the page the ActionLink is on, the framework automagically includes the id in the callback url, even if you didn't request it.
For example, if you're displaying a page from the following URL:
http://www.somedomain.com/AController/SameMethod/456
and the page cshtml file has an ActionLink like the following:
#Html.ActionLink("some text", "SameMethod", ARouteValueDictionary, SomeHtmlAttributes)
then whether or not you have "id" included in ARouteValueDictionary, it will show up in the generated URL.
This only occurs if you call back to the same method that generated the page in the first place. If you call back to a different method on the same controller the {id} field does not get inserted into the generated URL.
I don't necessarily have a problem with this. But I am curious as to why the designers took this approach.
FYI, I discovered this feature because I'd inadvertently been depending on it in my website design. I have to pass the ID field back to the server, along with a bunch of other information...only I'd never explicitly added the ID information to the RouteValueDictionary. But because most of my callbacks were to the same action method that had generated the page in the first place I was having the information included anyway.
You can imagine my surprise when a new component -- which I was sure was "essentially identical" to what was already working -- failed. But because the new component had a different target action method, the magic went away.
Edit:
Modified the explanation to clarify that including the {id} field in the generated URL is contingent upon calling the same method as generated the page in the first place.
...the framework automagically includes the id in the callback url,
even if you didn't request it.
I would prefer the term "ambiently" over "automagically". You can think of route tokens already in the URL as "ambient" to your HtmlHelper and UrlHelpers.
But I am curious as to why the designers took this approach.
Consider a Controller that groups together, say 5 actions. Those 5 may have links to each other, but not a lot of links outside the group. The simplest overload of Html.Action takes only 2 args: the text to render, and the action name.
This makes shorthand for linking around from action to action within these views. Since they are all on the same controller, and that controller is already in the path for the current action, MVC reuses this value when you don't specify the controller name in the helper method. The same behavior extends to {id}, or any other route token you define.

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.)

How Can I Stop ASP.Net MVC Html.ActionLink From Using Existing Route Values?

The website I'm working on has some fairly complicated routing structures and we're experiencing some difficulties working with the routing engine to build URLs the way we need them to be built.
We have a search results page that uses RegEx based pattern matching to group several variables into a single route segment (i.e. "www.host.com/{structuralParameters}" can be the following: "www.host.com/variableA-variableB-variableC" - where variables A through C are all optional). This is working for us fine after a bit of work.
The problem we are experiencing resolves around an annoying feature of the ActionLink method: if you point to the same controller/action it will retain the existing route values whether you want them or not. We prefer to have control over what our links look like and, in some cases, cannot have the existing parameters retained. An example would be where our site's main navigation leads to a search results page with no parameters set - a default search page, if you like. I say this is an annoying feature because it is a rare instance of the ASP.Net MVC Framework seemingly dictating implementation without an obvious extension point - we would prefer not to create custom ActionLink code to write a simple navigation link in our master page!
I've seen some say that you need to explicitly set such parameters to be empty strings but when we try this it just changes the parameters from route values into query string parameters. It doesn't seem right to me that we should be required to explicitly exclude values we aren't explicitly passing as parameters to the ActionLink method but if this is our only option we will use it. However at present if it is displaying in the query string then it is as useless to us as putting the parameters directly into the route.
I'm aware that our routing structure exasperates this problem - we probably wouldn't have any issue if we used a simpler approach (i.e. www.host.com/variableA/variableB/variableC) but our URL structure is not negotiable - it was designed to meet very specific needs relating to usability, SEO, and link/content sharing.
How can we use Html.ActionLink to generate links to pages without falling back on the current route data (or, if possible, needing to explicitly excluding route segments) even if those links lead to the same action methods?
If we do need to explicitly exclude route segments, how can we prevent the method from rendering the routes as query string parameters?
This seemingly small problem is causing us a surprising amount of grief and I will be thankful for any help in resolving it.
EDIT: As requested by LukLed, here's a sample ActionLink call:
// I've made it generic, but this should call the Search action of the
// ItemController, the text and title attribute should say "Link Text" but there
// should be no parameters - or maybe just the defaults, depending on the route.
//
// Assume that this can be called from *any* page but should not be influenced by
// the current route - some routes will be called from other sections with the same
// structure/parameters.
Html.ActionLink(
"Link Text",
"Search",
"Item",
new { },
new { title = "Link Text" }
);
Setting route values to be null or empty string when calling Html.ActionLink or Html.RouteLink (or any URL generation method) will clear out the "ambient" route values.
For example, with the standard MVC controller/action/id route suppose you're on "Home/Index/123". If you call Html.RouteLink(new { id = 456 }) then MVC will notice the "ambient" route values of controller="Home" and action="Index". It will also notice the ambient route value of id="123" but that will get overwritten by the explicit "456". This will cause the generated URL to be "Home/Index/456".
The ordering of the parameters matters as well. For example, say you called Html.RouteLink(new { action = "About" }). The "About" action would overwrite the current "Index" action, and the "id" parameter would get cleared out entirely! But why, you ask? Because once you invalidate a parameter segment then all parameter segments after it will get invalidated. In this case, "action" was invalidated by a new explicit value so the "id", which comes after it, and has no explicit value, also gets invalidated. Thus, the generated URL would be just "Home/About" (without an ID).
In this same scenario if you called Html.RouteLink(new { action = "" }) then the generated URL would be just "Home" because you invalidated the "action" with an empty string, and then that caused the "id" to be invalidated as well because it came after the invalidated "action".
Solution at the root of the problem
It seems that the optimal solution (that doesn't smell like a workaround) is the one that solves the problem where it has roots and that's in routing.
I've written a custom Route class called RouteWithExclusions that is able to define route value names that should be excluded/removed when generating URLs. The problem is when routing falls through routes table and subsequent routes don't have the same route value names...
The whole problem is detailed and explained in my blog post and all the code is provided there as well. Check it out, it may help you solve this routing problem. I've also written two additional MapRoute extension methods that take an additional parameter.
If you want total control of the link, just build the link yourself:
Click Here
Substitute whatever you need inside the href attribute.

asp.net MVC:hierarchical domain model and controllers

My domain model is this: we have a bunch of schools as the root of the "hierarchy". Each school has teachers and courses, and each course has one teacher. I am trying to model this with the logic of the mvc framework and I 'm quite confused. For example, the \school\details\x should give the first page of a school. That should contain a link to a list of its teachers, and a list to each courses.
A list of teachers means that the index action should be parametric to the school the user is looking at: \teacher\id where id is the school. The same with the course list. And then create teacher or course should also be parametric to what school we are looking at:\teacher\create\x where x=school.
How do I carry around the school id? Is there some neat way to do it, or do I need to pass it around all the time, into every view that needs it? It also makes the site URLs very cryptic. I was thinking of a way to make the url structure like {school-alias}\{controller}\{action}\{id}, still I have to find a way to pass around the school. If this is accomplished, then I need to implement some kind of filter that will not allow a user to perform certain actions if the schoolId he is requesting does not match the one in his profile.
I figure that if I 'm carrying the schoolid around the URL, the site is more REST-like, compared to, for example, getting the schoolId from the user's profile.
I would create acronym for every school. For example:
School no. 1 - ABC
School no. 2 - DEF
If i wanted to list teachers, I would write
http://site-address/ABC/teachers/list or just http://site-address/ABC/teachers
To show basic information about school
http://site-address/ABC
The code for routing would be:
routes.MapRoute(
"Default", // Route name
"{acronym}/{controller}/{action}/{id}", // URL with parameters
new {controller = "School", action = "Details", id = ""} // Parameter defaults
);
I would create authorization action filter on teachers,school and classes controller to check if user has access to school defined by acronym parameter in URL. You can check it by comparing filterContext.RouteData.Values["acronym"] with data stored in profile.
Write an extension method to overload rendering of links that extracts the school identifier ( acronym or whatever you choose to use ) from the routing data and adds it to the route values already passed in. This way your action can choose to use the identifier if it is present but is not required to add it to the view data and you do not have to remember to include it in any action links ( you just have to remember to use your action link overload ).
I would make the action link overload quite obviously different so anyone following behind you can see you are doing something unusual. This could be as simple as Html.SchoolActionLink( ...).
For example:
If your url is http://mydomain.com/abc/teachers/list and your route is defined as {school}/{controller}/{action} then the route value dictionary will have the value "abc" at the key "school". The route values can be accessed via HtmlHelper.ViewContext.RouteData.Values.
In the end I 'm answering my own question.
The real solution to this is :Restfull Routing. It implements the functionality in RoR, which is exactly what I need. Too bad this is not a requirement from more people so that it can go into mvc-trunk.

Resources