Generate a URL From Route Data ONLY - asp.net-mvc

I'm trying to do something simple in ASP.NET MVC:
RouteValuesDictionary routeValues = GetMyRouteData();
var url = new UrlHelper(Html.ViewContext.RequestContext);
return url.RouteUrl(routeValues);
The problem is that no matter what I do, the url includes route data from the current request context. I want to generate a URL based on ONLY the route values from GetMyRouteData().
Thanks

The problem is that no matter what I do, the url includes route data
from the current request context
That's by design. You have to explicitly set the route values that were present in the original request and that you don't want in the resulting url to null:
var routeValues = GetMyRouteData();
// remove values that you want to exclude from the resulting url
// by setting their values to null
routeValues["id"] = null;
var url = new UrlHelper(Html.ViewContext.RequestContext);
return url.RouteUrl(routeValues);

This is not specific to ASP.NET MVC, but due to ASP.NET Routing's route resolution. The entry point to this is RouteCollection.GetVirtualPath, which has two signatures.
The first takes a RequestContext and a RouteValueDictionary. This is used for default route resolution, which relies on pattern matching to find a route. The route search incorporates all tokens from RequestContext as well as RouteValueDictionary; in other words, the two sets of route tokens are combined to form the basis for the route search. A special case exists whereby null parameters in the RouteValueDictionary remove that parameter from search. However, if such a null-valued parameter has a value in the RequestContext, that value will still appear in the generated URL as a query string value.
The other signature additionally accepts a route name. It is a little strange because it alters both the route resolution as well as the query string creation. Routes are found, obviously, using name resolution. Given the named route is found, only tokens for those parameters specified in route's URL pattern will appear in the generated URL.
Why is it this way? It's ASP.NET MVC's interpretation of Ruby on Rails' parameter-handling convention.
So default route resolution and "fallback" token resolution are comingled. If you don't want tokens to fallback to the RequestContext, (and you still want to use ASP.NET Routing) you have to use named route resolution.

This might help clarify. Use the source, Luke!
The RouteUrl helper calls into this static method to generate the URL:
public static string GenerateUrl(string routeName, string actionName, string controllerName, RouteValueDictionary routeValues, RouteCollection routeCollection, RequestContext requestContext, bool includeImplicitMvcValues) {
if (routeCollection == null) {
throw new ArgumentNullException("routeCollection");
}
if (requestContext == null) {
throw new ArgumentNullException("requestContext");
}
RouteValueDictionary mergedRouteValues = RouteValuesHelpers.MergeRouteValues(actionName, controllerName, requestContext.RouteData.Values, routeValues, includeImplicitMvcValues);
VirtualPathData vpd = routeCollection.GetVirtualPathForArea(requestContext, routeName, mergedRouteValues);
if (vpd == null) {
return null;
}
string modifiedUrl = PathHelpers.GenerateClientUrl(requestContext.HttpContext, vpd.VirtualPath);
return modifiedUrl;
}
Note the line:
RouteValueDictionary mergedRouteValues = RouteValuesHelpers.MergeRouteValues(actionName, controllerName, requestContext.RouteData.Values, routeValues, includeImplicitMvcValues);
This is merging in the current requestcontext values under the covers. So, you could make your own helper that just calls into this static method and passes empty collections to avoid getting the current route context values being merged in. Experiment and debug into the MVC code and you should be able to see which values you need to nuke.

Related

Routing: RedirectToAction(string action)

This question is about the latest DNN platform release: DNN 8.0.1.
I am trying to redirect to an action in my controller-classes, but I am not able to do it.
For example the following check:
// Check if user is voting on his own item
if (item.CreatedByUserId == User.UserID)
return RedirectToAction("Index");
This is the error I receive when the controller tries to return an action.
DotNetNuke.Services.Exceptions.ModuleLoadException: No route in the route table matches the supplied values. ---> System.InvalidOperationException: No route in the route table matches the supplied values.
I don't know how routing is working in DNN (MVC) and I couldn't find anything helpful about is.
Thank you!
DNN's routing is different from MVCs because it has to untangle different modules. You could create a method in your controller to override the default method in this way:
public new ActionResult RedirectToAction(string actionName, string controllerName, object routeValues)
{
var routeVals = TypeHelper.ObjectToDictionary(routeValues);
routeVals["controller"] = controllerName;
routeVals["action"] = actionName;
return Redirect(ModuleRoutingProvider.Instance().GenerateUrl(routeVals, ModuleContext));
}

Is there a way to generate the post URL in the GET action if both the GET and the POST methods share the same name?

Is there a way to generate the post URL in the GET action if both the GET and the POST methods share the same name?
The reason for needing to generate the post URL in the controller is that we aren't dealing with the views but only providing the ViewModel that contains a payload in the form of JSON. And the front-end needs to know where to post form values to.
[HttpGet]
public ActionResult MethodName(string id)
{
...
}
[HttpPost]
public JsonResult MethodName(ViewModel model)
{
...
}
e.g. the get URL would be:
/controller/methodname/123
and the post URL would be:
/controller/methodname
in the get action, I need to feed the front-end with the post URL of which the form values would be consumed.
using Url.Action("methodname", "controller", new { id = string.Empty}) would generate a URL that's the same to the post URL, but it's sort of hack-ish way
Another option would be to define a route for the post method and use Url.RouteUrl("PostMethodName") to generate the URL.
Neither of these seem ideal, so is there another (cleaner) way to get this done?
Walter,
I am failing to understand how this is "hack-ish." You are providing a restricted post action with a strongly typed model, the only difference if your action result type. The URL's can be the same if you wish.
Alternativtly you can create just an action for your aJax method such as.
[HttpPost]
public JsonResult ajaxMethod(ViewModel model){
//...
}
This is restrictive to just the HttpPost method and contains your view model. Actions dont need views they simply return an action result. If you wanted to get very bare-bones and action can be an object returning anything. Your route for this action is similar to all other routes:
#Url.Action("ajaxMethod", "controller")
to generate
/controller/ajaxMethod
I may have completly missed the question, please let me know if this doesnt help.
EDIT: Updated from comments.
Just to add if you would like to generate a route or action url in your controller action you can simply use the Url helper in your action such as.
ViewBag.Url = Url.Action("MethodName", "Controller");
Another option is to create a custom Route implementation. A very basic one that includes a line to fix your problem (Drop the existing route parameters) can look like this:
public class CustomRoute : Route
{
public CustomRoute(string url, IRouteHandler routeHandler)
: base(url, routeHandler)
{
}
public override RouteData GetRouteData(HttpContextBase httpContext)
{
var routeData = base.GetRouteData(httpContext);
if (routeData == null)
{
return null;
}
/* custom implementation here */
return routeData;
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
/*
* GetVirtualPath is called until a suitable route is found.
* This means modifying the passed RouteValueDictionary has consequenses for subsequent calls.
* Therefore, always work with a copy if you want to modify any values: new RouteValueDictionary(values)
*/
/*
* Drop the existing route parameters. Implicitness can only cause confusion.
*/
requestContext = new RequestContext(requestContext.HttpContext, new RouteData());
return base.GetVirtualPath(requestContext, values);
}
}
You probably also want to have MapRoute methods for your custom route. For this, you just need to look at the original implementation, copy paste it and replace Route with CustomRoute.

Replace character in url before routing in ASP.NET MVC

Can I manipulate the url before routing it, i.e. before MVC goes through my route configuration to find the route to use.
I'd like to replace some characters in the url, for example "www.test.com/ä/ö" to "www.test.com/a/o". That way, if a user typed those letter in the url, the right route would still be used.
Maybe there´s something that I can hook into to manipulate the url?
Edit:
To clarify what I want I'll add an example. Let's say I have a routing configuration that looks like this: "{controller}/{action}". The user types www.test.com/MyCöntroller/MyÄction and I want to route that to the controller "MyController" and the action method "MyAction". I have to do the character replacement before the routing is done, otherwise no matching route will be found. Thus I'd like to replace all "ö" with "o" and all "ä" with "a" (and some more characters) BEFORE the routing is done. Is there any way to do this?
Edit2:
After some research it seems like it is UrlRoutingModule that is the first to get the url in ASP.NET MVC. Maybe there is some way to hook into that?
Take a loot at this post, by creating custom route handler it is possible.
using System.Web.Routing;
namespace My.Services
{
public class MyRouteHander : IRouteHandler
{
ApplicationDbContext Db = new ApplicationDbContext();
public IHttpHandler GetHttpHandler(RequestContext requestContext)
{
// Get route data values
var routeData = requestContext.RouteData;
var action = routeData.GetRequiredString("action");
var controller = routeData.GetRequiredString("controller");
//modify your action name here
requestContext.RouteData.Values["action"] = actionName;
requestContext.RouteData.Values["controller"] = "SpecialController";
return new MvcHandler(requestContext);
}
}
}
Check out the answer to this question.
Basically you'll want to use the FilterAttribute with IActionFilter, and then apply the annotation to the ActionResult that services the route. This way you have an intermediary method to manipulate the URL before it's processed by your route configuration.

How to route legacy QueryString parameters in ASP.Net MVC 3?

I am using a third party service that does an async callback to a URL I provide to them.
So I tell them to use http://www.mysite.com/Status/Incoming.
This must obviously map to an Incoming() method on my StatusController.
However, what I don't have control over is the format of the parameters they call my URL with.
E.g. They will do a callback such as: http://www.mysite.com/Status/Incoming?param1=val1&param2=val2&param3=val3
I want to map this to the parameters of my action method: Incoming(string param1, string param2, int param3)
How do I do this?
I have found a lot of stuff about custom routing, but nothing about legacy QueryString parameters.
There is no such thing as legacy query string parameters. There are query string parameters and they are part of the HTTP specification. And assuming that the http://www.mysite.com/Status/Incoming?param1=val1&param2=val2&param3=val3 url is called you don't need any route to make it map to the following action (the default route will do just fine):
public ActionResult Incoming(string param1, string param2, string param3)
{
...
}
The default model will take care of binding those values.
Why not use a catch all?
routes.MapRoute(
"Incoming",
"Status/Incoming/{*path}", // URL with parameters
new { controller = "Status", action = "Incoming"}
);
then in your controller,
public ActionResult Incoming(string path){
// the next line would probably be better off in a model binder, but this works:
var dictionary = path
.Substring(path.IndexOf("?")+1)
.Split("&")
.Select(x =>
{
var kvArray = x.Split("=");
return new KeyValuePair<string, string>(kvArray[0], kvArray[1]);
})
.ToDictionary(x=>x.Key,x=>x.Value);
return Incoming(dictionary);
}
public ActionResult Incoming(Dictionary<string,string> dictionary){
//do stuff
}
All that being said, I think using the Request.QueryString is probably a better approach. As long as you are using MVC, it is accessible from your controller. However, if you can guarantee that the correct parameters will be passed then Darin's approach is going to be the best choice.
When I've had to deal with this before now, I just use the "legacy" call of Request.QueryString. It still works, even if it isn't very graceful.

Is it possible to localize a URL / routing in ASP.NET MVC?

I'm working with a client that wants the URLs in our web application to be in French. I'm an English developer and we also have English clients. This is an interesting problem but I don't think its something the ASP.NET MVC Framework would support.
Here's the scenario. The route...
Specific EXAMPLE
English URL
www.stackoverflow.com/questions/ask
would also support
French URL
www.stackoverflow.com/problème/poser
Generic EXAMPLE
English URL
http://clientA.product.com/AreaNameEnglish/ControllerNameEnglish/ActionNameEnglish/params
also needs to support
French URL
http://clientB.product.com/AreaNameFrench/ControllerNameFrench/ActionNameFrench/params
So in MVC my Area, Controller and Actions all need to have both English and French translations.
Obviously maintainability would be a HUGE issue if I were to go and hardcode all my Controllers, Views and Action names to French. Is there anyway to localize the route that is presented in the browser without doing this? Keeping in mind there are lots of different routes in the application. A couple Areas each with a handful of Controller each with many Actions?
Thanks,
Justin
EDIT
Thanks to #womp here is what I've come up with so far... Although in the end I took the approach which I posted as an answer.
public class LocalizedControllerFactory : DefaultControllerFactory
{
public override IController CreateController(RequestContext requestContext, string controllerName)
{
if (string.IsNullOrEmpty(controllerName))
throw new ArgumentNullException("controllerName");
if (CultureInfo.CurrentCulture.TwoLetterISOLanguageName == "fr")
{
controllerName = this.ReplaceControllerName(requestContext, controllerName);
this.ReplaceActionName(requestContext);
this.ReplaceAreaName(requestContext);
}
return base.CreateController(requestContext, controllerName);
}
private string ReplaceControllerName(RequestContext requestContext, string controllerName)
{
// would use the language above to pick the propery controllerMapper. For now just have french
Dictionary<string, string> controllerMapper = new Dictionary<string, string>()
{
{"frenchControllerA", "englishControllerA"},
{"frenchControllerB", "englishControllerB"}
};
return this.ReplaceRouteValue(requestContext, "controller", controllerMapper);
}
private void ReplaceAreaName(RequestContext requestContext)
{
// would use the language above to pick the propery areaMapper. For now just have french
Dictionary<string, string> areaMapper = new Dictionary<string, string>()
{
{"frenchAreaX", "englishAreaX"},
{"frenchAreaY", "englishAreaY"}
};
this.ReplaceRouteValue(requestContext, "area", areaMapper);
}
private void ReplaceActionName(RequestContext requestContext)
{
// would use the language above to pick the propery actionMapper. For now just have french
Dictionary<string, string> actionMapper = new Dictionary<string, string>()
{
{"frenchAction1", "englishAction1"},
{"frenchAction2", "englishAction2"}
};
this.ReplaceRouteValue(requestContext, "action", actionMapper);
}
private string ReplaceRouteValue(RequestContext requestContext, string paramName, Dictionary<string, string> translationLookup)
{
if (requestContext.RouteData.Values[paramName] == null)
{
return null;
}
string srcRouteValue = requestContext.RouteData.Values[paramName] as string;
if (srcRouteValue != null && translationLookup.ContainsKey(srcRouteValue))
{
requestContext.RouteData.Values[paramName] = translationLookup[srcRouteValue];
}
return requestContext.RouteData.Values[paramName] as string;
}
}
A decent start. If I localize just the ControllerName and ActionName in the Url it will find and render the proper View. However I have the following problems.
Area Name can't be translated
Localizing the Area means the Controller.View() method fails to find Views.
Even though I've replaced the Area name in the request context the ViewEngineCollection.Find() method doesn't seem to pick it up. Anywhere in my Controller class that does "return View()" fails to find the default view for its action. If I don't localize the Area then the other steps work.
RedirectToAction or Html.ActionLink
Anytime the application calls RedirectToAction or if I use an Html.ActionLink helper or something similiar the Urls generate are the English ones. It looks like I'm going to have to add logic somewhere possibly in multiple spots to convert an English Url to the French (or other language) one.
The following blog contains a complete solution this exact problem. Its actually a very elegant solution which I highly recommend.
https://blog.maartenballiauw.be/post/2010/01/26/translating-routes-(aspnet-mvc-and-webforms).html
Note to get it working for AREAs I had to add the following extension method to his "TranslatedRouteCollectionExtensions.cs" class:
public static Route MapTranslatedRoute(this AreaRegistrationContext areaContext, string name, string url, object defaults, object routeValueTranslationProviders, bool setDetectedCulture)
{
TranslatedRoute route = new TranslatedRoute(
url,
new RouteValueDictionary(defaults),
new RouteValueDictionary(routeValueTranslationProviders),
setDetectedCulture,
new MvcRouteHandler());
route.DataTokens["area"] = areaContext.AreaName;
// disabling the namespace lookup fallback mechanism keeps this areas from accidentally picking up
// controllers belonging to other areas
bool useNamespaceFallback = (areaContext.Namespaces == null || areaContext.Namespaces.Count == 0);
route.DataTokens["UseNamespaceFallback"] = useNamespaceFallback;
areaContext.Routes.Add(route);
return route;
}
However, even with this a translated route with an AREA can be read and interpreted the routes generated always seem to include an English AREA name but localized everything else.
I was directed to a blog via the same question asked on the ASP.NET MVC Forums
The MVC framework supports pretty much any routing scenario you can think of, but not necessarily with the default routing classes.
Most localization solutions I've run across involve using the same Controller and Action method names, but specifying a culture parameter in the route which dictates which translated version of the View is presented. For example,
http://clientA.product.com/AreaName/Controller/Action //en-US
http://clientB.product.com/es-MX/AreaName/Controller/Action // spanish
If you really must have translated URL's though, I don't see much other choice then to maintain a mapping table somewhere. If I understand your question correctly, you need to be able to map all the different language translations of "questions" (controller) and "ask" (action) to the same controller/action method combination.
However, once you've built this table somewhere (resource files?), you can easily override the DefaultControllerFactory that the framework is using, and implement your own logic for determining the controller to instantiate. So instead of just matching the {controller} token from the URL as a simple string comparison, you can implement logic to check it against your mapping table to pick the right controller.
For a walkthrough of creating a custom controller factory, check this great blog post. It's actually a localization example as well, but it's based on the user's culture settings, rather than the language of the URL.

Resources