I am trying to construct a route that routes to a specific product page.
The product exists in a category. Categories may also exist in categories. I am trying to construct the URL like
my-site.com/products/category/category/category/product
The amount of categories is able to change but the last parameter will always be the name of the product.
Is there any way to construct a route for this?
A solution exactly for you
I faced the same problem in the past and solved it a bit differently to what others will advise you here. Most of solutions will talk about *catch-all parameter. In your case this means that you'd have to parse the product out yourself. Manually. Because catch-all parameter may only be the last parameter in route definition.
Catch-all anywhere in the route
If you think of it carefully, you can actually realise that catch all parameter can actually be defined anywhere in the route as long as you have all other segments present. So I've written such route class that does all that and successfully runs in production on a heavy traffic website.
My blog post has all the information about it as well as all the code that will solve your problem:
Custom Asp.net MVC route class with catch-all segment anywhere in the URL
This makes it possible for you to define your route as:
products/{*categories}/{productId}
If you think of the catch-all parameter even further you could also get to the point that a single route definition could have several catch-all parameters as long as at least one segment between them is static. But my class isn't able to do this, because your scenario with just one arbitrary segment set is much more common.
Related
For some reason it thinks the target is an Apache server - I suspect?
The MVC is V5.2.3 and its dependancies are correct as per nuget
. I have searched and searched to no avail.
EDIT
The code is simply #Url.Action("Action", "Controller")
So I created a brand new MVC project and using exactly the same code the correct code was returned.
http://localhost:53143/Controller/Action
EDIT 2
I removed my web.config files as the problem is not there.
I got in touch with an expert and he looked at the project and answered as below.
(BTW I have 'cgi-bin' in a route as there are old URLs out in the wild that relate to my domain previously being hosted on an Apache server and which I cannot change.)
The answer
I digged a little more into the source code of the mvc helpers and, yes, the two issues (I had a similar problem Html.BeginForm with overload causes form action to be '/cgi-bin?action=Index&controller=Home' and so the HomeController is bypassed) are related since Url.Action and Html.BeginForm boil down to calling one and the same method: UrlHelper.GetUrl... Now, what this method does is:
Retrieve the current URL including controller, action, area...
Add or replace the parameter(s) you specify,
Find the best matching route! <==
If there are any route variables - push the provided parameters into those variables.
Stick the rest of the parameters into the query string <==
I have deliberately highlighted point 3 & 5, with point 3 being the most important. So, UrlHelper.GetUrl (and Url.Action and Html.BeginForm respectively) needs a route and it searches through the available ones to find the first match.
Now, here comes the problem with your mixed webforms-mvc app - an issue which is not present when you are dealing purely with MVC: You are using MapPageRoute!!! Please, note that it is different from MapRoute. And MapPageRoute uses the PageRouteHandler class to create the route whereas MapRoute uses the MvcRouteHandler class and it makes all the difference because PageRouteHandler creates the route in such a way that it's always a good match for UrlHelper.GetUrl("ActionName", "ControllerName") with the action name and controller name being thrown into the query string as parameters (point 5).
So, what happens with your set-up is that Url.Action is searching for a route and is hitting the first one created by MapPageRoute and in your case this is:
routes.MapPageRoute("cgi-bin", "cgi-bin/{*theRestcgi-bin}", "~/home/Search.aspx");
That's where that arcane cgi-bin part of the query string comes from, giving the impression that the framework is actually searching for some virtual/physical folder.
As for the proper solution: either define a suitable route or specify the url as a simple string the way you have done. I think, your solution is the better one as you won't have to move around the route definitions in the RouteConfig class.
I'm learning how to do routing in MVC. It seems to me that the routing API only solves half the problem. I can easily see how to map incoming URLs to controller actions and params. However, it is not obvious to me how to generate these routed URLs in the source code of my pages.
For example, in one of my views, I use this code to get the route URL:
<a class="listingResult" href="#Url.RouteUrl("ListingSEO", new { id = Model.Listing.ID, seoName = ListingController.SeoName(Model.Listing.Title) })">
This seems like poor coding practice to me for several reasons:
If the route changes in the future, I may have many places in my View code that will need updating.
The View now requires knowledge of the ListingController (maybe this is not a big deal?)
I've lost strong typing on my input params, and if I misspell the param names, my code is broken, but this doesn't generate compile warnings.
How do I observe good coding standards when I am generating route URLs? The alternative seems to be putting static functions in the controller to generate routes, which would at least address concerns #1 and #3. If I worked with you and you saw the code above, how unhappy would you be?
My recommendations:
Generate URLs in the ViewModel, not the View: This will keep your views cleaner and logic free. You can pass the UrlHelper instance from the controller to the ViewModel, which will also help for my next point...
Use a strongly-typed URL generation technique: Such as delegate-based, expression-based or code generation.
One of the purposes of using named routes is to abstract the controller/action. Your named routes shouldn't really change. At the most, you'd just change the controller/action they hit, but that happens seamlessly behind the scenes because you're using named routes.
Your view requires knowledge of the controller because you've added a dependency on it. This is bad for a number of reasons. There's many different ways you could handle this that wouldn't require a dependency on the controller, depending on what it is you're actually doing here, but at the very least, you should simply use a utility class, so at least it wouldn't be controller-specific.
The route params are intentionally not strongly-typed, because routes are flexible by design. You can pass anything you want to the action, with or without a parameter to catch it (you can use something like Request to get at it without a param).
I am in the process of generating a URL dynamically in my cshtml page.
What is the difference between Url.RouteUrl() & Url.Action()?
Which one should I use to generate the URL & what difference do both have in terms of implementation ?
Thanks in advance.
RouteUrl generated the url based on route name. If you have multiple routes with similar parameters the Action method may pick a wrong one - it works based on the order of route definitions. This may take place when your routes have optional parameters.
If you want to make sure that a certain route url will be used you need to call RouteUrl passing this route name. Route names are unique and clearly identifies a route.
One more difference is that Action is MVC specific (it uses controller and action names), while RouteUrl is generic is and can be used without MVC (you can have routing in WebForms).
Url.RouteUrl allows you to specify a particular route by name. This will force the usage of that route. Url.Action will simply pick the first route that matches the criteria.
I'm using AnchorLink on a very simple site with just two routes defined, one standard route and another area route for admin/{controller}/{action}/{id}. I'm at a URL like:
/admin/release/push/255
In that view I'm using:
#Html.AnchorLink("Confirm", "Confirm")
AnchorLink is rendering a link without the current request {id} included, ie. /admin/release/confirm! If I inspect the RouteValues I can see {id} is in there. If I explicity pass in the route values from teh current request, as in:
#Html.AnchorLink("Confirm this release", "Confirm", Url.RequestContext.RouteData.Values)
Then it works, I get the URL /admin/release/confirm/255. Why does the explicit version where I pass in the current request route values work, but the implicit one, without the route values argument, which I thought would pick up the current request route values, doesn't? I know the above is a solution, but it's ugly and there's some underlying reason why the AnchorLink without the route values isn't working as I expect?!
MVC is doing exactly the right thing here. It's not to know you require the Id parameter for your anchor link or not -- in other words its not trying anything clever to pre-parse and examine the link. It will only do that when its being rendered. And in your case without the id parameter specified somewhere its going to use the default route.
If you find yourself writing that same code all over the place you could easily extract it out into a static helper class. That can get rid of the ugliness.
I am trying to write a route that matches the following URL format:
/category1/category2/S/
where the number of categories is unknown, so there could be 1 category or there could be 10 (1..N).
I cannot use a catch all becuase the categories are not at the end of the URL.
I am actually routing to a web form here (using Phil Haack's example http://haacked.com/archive/2008/03/11/using-routing-with-webforms.aspx), but that is beside the point really.
Any ideas?
To be honest, I found the answer here to be more useful: Using the greedy route parameter in the middle of a route definition
The blog post linked to in the question was extremely useful: http://www.thecodejunkie.com/2008/11/supporting-complex-route-patterns-with.html
I think it's impossible but you could try work around it using this route:
{categories}/S
Then split the categories using the '/' character yourself.
I made a site where I just fixed it to 1-3 categories by registering 3 routes, but I had to work around a lot of things and wasn't really happy with it afterwards.
EDIT: Using S/{*categories} will catch the categories. You can only use it at the end of the URL.
Exactly what you need(ed)
This is a long time lost shot, but I seem to have exactly what you need. I've written a GreedyRoute class that allows greedy segment anywhere in the URL (at the beginning, in the middle or at the end - which is already supported).
You can read all details on my blog as well as getting the code of this particular class.
The main thing is it supports any of these patterns:
{segment}/{segment}/{*segment}
{segment}/{*segment}/{segment}
{*segment}/{segment}/{segment}
It doesn't support multiple greedy segments though (which is of course possible as well but has some restrictions that should be obeyed in that scenario), but I guess that's a rare example where that could be used.