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).
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 know there are a lot of utility and helper classes/methods for generating URLs and links from internal routes and controllers. But how would you tackle the below in MVC 3?
In a razor file someone has defined this:
Website
ExternalURL in this instance will hold values like www.yoursite.com, without any prefixes. Hard-coding an http:// at the start is an obvious no-no but how best to handle this?
It's not so bad to hardcode http:// in your case, but if you want to avoid it, I see few options, but maybe most correct will be to extend your model with property #Model.Details.ExternalUrlLink or something like that. In getter you can do any logic what you want over original value, e.g. concatenate http:// prefix if it's not presented
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.
I know this question has been asked many times. But people suggest creating custom derived route classes, or writing lowercase everywhere in code (for action links) which is a really dirty way (what if I just decide to make'em all Pascal Cased again? changing hundreds of links?), or they suggest to create HTML helpers to do that (which is not a bad answer). But isn't there a more simple way? I mean something like setting some configuration in web.config file, or using an HttpModule or something else which is both simple, and centralized?
Apart from the options you have already listed, I can think of no other way of producing this result.
In short, the URL needs to be processed by 'something', be it .ToLower(), a Helper Method or HTTPModule.
In most of our applications, we use a Global Static method that performs actions on the desired URI and returns the result.
The following will allow that.. http://mvccoderouting.codeplex.com/ - and much more besides.
I'm calling a controller action to do a search with an AJAX request from 2 different pages and want to render a different rjs file based on which page requested the action. I could just make 2 actions to do this but it doesn't seem very DRY when it's the same code in the action just need different rjs as it's displaying the search results differently in the view.
Using Rails 2.3.4 and Ruby 1.8.7
If I understand your question correctly, three ways come to mind to solve this:
In your action, check the current request's http_referrer and try to figure out what page initiated the request. Depending on how you've got your routing set up, this may or may not work, but it does have the advantage of being pretty simple to do.
Have your AJAX request include an extra GET parameter to identify which page the request is from. Then, have the Rails action test for that parameter, and render RJS accordingly.
Do something clever with Routes and have page A hit the action from one distinct URL, and page B hit the action from another, and include the page identification parameter in the route configuration.
My preference would be for approach #2, as it seems way less likely to break randomly when your routing changes, and #3 strikes me as being overly complicated. There's probably a million other ways to do this, but those are the three that came to mind right off the bat. Hope that helps...
How much code is in the action? You could just factor that out into a common subroutine and call that from each action. It would keep the code simple and easy to understand, without resorting to clever tricks.
I usually do like #2 from Steven's answer, but with a twist. A filter in my ApplicationController attributes a custom mime type corresponding to the extra parameter.
That way, the names of my view files are clearer (i.e.: "show.employees-autocomplete.rjs", "show.quotation-autofill.rjs").