How do you override route table default values using Html.ActionLink? - asp.net

Global.asax route values
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional, filterDate = DateTime.Now.AddDays(-1), filterLevel = "INFO" } // Parameter defaults
);
Here's my actionlink
#Html.ActionLink(item.MachineName, "Machine", new { id = item.MachineName, filterLevel = "hello" }, null)
When the filterlevel is specified in the actionlink, it generates a url like this:
http://localhost:1781/LoggingDashboard/log/level/VERBOSE
Which is the same page as I am currently on. If I change the actionlink to use a property other than one that has a default value in the route table (yes, if I use filterDate it messes up too), it generates a link like this:
#Html.ActionLink(item.MachineName, "Machine", new { id = item.MachineName, foo = "bar" }, null)
http://localhost:1781/LoggingDashboard/log/Machine/C0UPSMON1?foo=bar
Is this behavior correct? Should I not be able to override the defaults set up in the route table? I have confirmed that if I remove the filterLevel default from the route table this works as I expect:
http://localhost:1781/LoggingDashboard/log/Machine/C0UPSMON1?filterLevel=VERBOSE
---EDIT---
sorry, here is the action
public ActionResult Machine(string id, DateTime filterDate, string filterLevel)
{
...
var model = new LogListViewModel { LogEntries = logEntries };
return View(model);
}
For the bounty I want to know how to override the "default" values that are specified in the routes from global.asax. i.e. I want to be able to override filterLevel and filterDate.

SLaks already said what is probably the best way to handle this problem. I don't know if this will work, but, what happens if you put this above the existing route (so there would be two in your global.asax now)?
routes.MapRoute(
"Filtered",
"{controller}/{action}/{id}?filterLevel={filterLevel}&filterDate={filterDate}",
new
{
controller = "Home",
action = "Index",
id = UrlParameter.Optional,
filterDate = DateTime.Now.AddDays(-1),
filterLevel = "INFO"
}
);
Also, it just occurred to me that the reason you don't like SLaks' solution is that it could be repetitive. Since you only have one route, these parameters probably indicate a global functionality, instead of an action-scoped functionality. You could fix this by adding the values in an action filter on each controller, or your could use a custom route handler to apply this globally. Either of these would allow you to take the filterLevel and filterDate fields out of your route definition and still get the scope you want. Then it should be no problem to pass the parameters in a querystring with Html.ActionLink().
To do this with the route handler, change your route definition to:
routes.Add(
new Route(
"{controller}/{action}/{id}",
new RouteValueDictionary(new{ controller = "Home", action = "Index", id = UrlParameter.Optional}),
new CustomRouteHandler()));
Your implementation of the route handler would be something like this:
public class CustomRouteHandler : IRouteHandler
{
public IHttpHandler GetHttpHandler(RequestContext requestContext)
{
var routeValues = requestContext.RouteData.Values;
if(!routeValues.ContainsKey("filterLevel"))
{
routeValues.Add("filterLevel","INFO");
}
if(!routeValues.ContainsKey("filterDate"))
{
routeValues.Add("filterDate", DateTime.Now.AddDays(-1));
}
var mvcRouteHandler = new MvcRouteHandler();
return (mvcRouteHandler as IRouteHandler).GetHttpHandler(requestContext);
}
}

I thought the defaults were always for entries defined in the URL, that you can't define a default to omit something not in the core URL, and anything else is passed as a querystring.
Interesting question.
HTH.

You should specify the default value in your method, like this:
public ActionResult Machine(string id, DateTime? filterDate = null, string filterLevel = "INFO")
{
filterDate = filterDate ?? DateTime.Now.AddDays(-1);
var model = new LogListViewModel { LogEntries = logEntries };
return View(model);
}

If there are default values that are not specified in the URL pattern, then you can't override them because they are used to determine route selection when matching routes for URL generation.
Let me give you an example. Suppose you had the following two routes.
routes.MapRoute(
"Default1", // Route name
"foo/{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional
, filterLevel = "INFO" } // Parameter defaults
);
routes.MapRoute(
"Default2", // Route name
"bar/{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional
, filterLevel = "DEBUG" } // Parameter defaults
);
Notice that there's a default value for "filterLevel", but there is no "{filterLevel}" parameter within the URL pattern.
Which URL should match when you do this?
#Html.ActionLink(item.MachineName, "Machine",
new { id = item.MachineName, filterLevel = "RANDOM" }, null)
If you could override the default value for filterLevel, then you'd expect both of the routes to match. But that doesn't make sense. In this case, neither matches because filterLevel isn't in the URL pattern and therefore the supplied filterLevel must match the default value. That way, you can do this:
#Html.ActionLink(item.MachineName, "Machine",
new { id = item.MachineName, filterLevel = "INFO" }, null)
//AND
#Html.ActionLink(item.MachineName, "Machine",
new { id = item.MachineName, filterLevel = "DEBUG" }, null)
to generate a URL for the first and second route respectively.
This confusion is why I always recommend to always use named routes.

Related

Html.RenderAction not hitting default route as expected

I have a default route specified like this:
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = "", RouteName = "Default" }, // Parameter defaults
new { controller = #"[^\.]*" } // Constraints (Ignore urls with periods in them)
);
I have a controller called Test and an action on Test called DoSomething that is defined like this:
public ActionResult DoSomething(int someId, int someotherId, IEnumerable<string> listOfSomething, bool flag, bool flag2)
I am trying to call the action like this:
var parameters = new RouteValueDictionary();
parameters.Add("someId", id);
parameters.Add("someotherId", otherId);
parameters.Add("flag", flag1);
parameters.Add("flag2", flag2);
for (int i = 0; i < thisList.Count; i++)
{
parameters.Add("listOfSomething[" + i + "]", thisList[i]);
}
Html.RenderAction("DoSomething", "Test", parameters);
The Html.RenderAction call is failing with the InvalidOperationException : No route in the route table matches the supplied values.
What would be causing this? The default route should pick this call up?
No route in the route table matches the supplied values indicates that no valid route in RouteCollection matches the requested URL, i.e. your default route parameters count doesn't match with DoSomething action parameters (1 parameter : 5 parameters).
Also note that IEnumerable<string> considered as complex object, therefore you can't pass it as part of RouteValueDictionary parameters in URL. Hence, you should pass only 4 value parameters & pass IEnumerable<string> object as Session or TempData content.
First, define a custom route with 4 parameters as such on top of default one (avoid modifying default route which placed last in route order, it may required for other routes):
routes.MapRoute(
"Custom", // Route name
"{controller}/{action}/{someId}/{someotherId}/{flag}/{flag2}", // URL with parameters
new { controller = "Home", action = "Index", someId = "", someotherId = "", flag = "", flag2 = "" }, // Parameter defaults
new { controller = #"[^\.]*" } // Constraints (Ignore urls with periods in them)
);
Then, edit controller action method to receive 4 parameters (strip off IEnumerable<string> from parameters list, based from explanation above):
public class TestController : Controller
{
public ActionResult DoSomething(int someId, int someotherId, bool flag, bool flag2)
{
// other stuff
}
}
And pass parameters for redirect thereafter, with IEnumerable object stored in TempData or Session variable:
var parameters = new RouteValueDictionary();
parameters.Add("someId", id);
parameters.Add("someotherId", otherId);
parameters.Add("flag", flag1);
parameters.Add("flag2", flag2);
var list = new List<string>();
for (int i = 0; i < thisList.Count; i++)
{
list.Add(thisList[i]);
}
TempData["listOfSomething"] = list;
Html.RenderAction("DoSomething", "Test", parameters);
Or define parameters directly in RedirectToAction:
var list = new List<string>();
for (int i = 0; i < thisList.Count; i++)
{
list.Add(thisList[i]);
}
TempData["listOfSomething"] = list;
Html.RenderAction("DoSomething", "Test", new { someId = id, someotherId = otherId, flag = flag1, flag2 = flag2 });
If thisList is already IEnumerable<string> instead, remove for loop & just assign it straight to Session/TempData:
TempData["listOfSomething"] = thislist;
The list of parameters can be retrieved using this way:
var listOfParameters = TempData["listOfSomething"] as List<string>;
Similar issues as reference:
How to pass List in Redirecttoaction
Sending a list using RedirectToAction in MVC4
Passing an array or list of strings from one action to another when redirecting
Routing with Multiple Parameters using ASP.NET MVC
How to pass multiple objects using RedirectToAction() in Asp.NET MVC?

html.ActionLink using wrong route

I have set up the following routes so that I can use duplicate controller names (in different namespaces). This works fine but when I use html.actionlink from any controller it always includes the “CRUD” subfolder to the link.
var route1 = routes.MapRoute(
"CRUD",
"CRUD/{controller}/{action}/{id}",
new { action = "Index", id = UrlParameter.Optional }
);
route1.DataTokens["Namespaces"] = new string[] { "College.Controllers.CRUD" };
route1.DataTokens["UseNamespaceFallback"] = false;
var route2 = routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
namespaces: new[] { "College.Controllers" }
);
route2.DataTokens["Namespaces"] = new string[] { "College.Controllers" };
route2.DataTokens["UseNamespaceFallback"] = false;
So an html.actionlink in http://localhost/students/index looks like this
http://localhost/CRUD/students/Edit/1
What I want is this
http://localhost/students/Edit/1
I know I could fix this by specifying the route in the actionlink but I don’t want to do this because I want to re-scaffold in future and my changes would be overwritten.
The issue here is that your 2 routes are ambiguous when building the URL. There are basically 3 ways to fix this:
Add another route value to match that is not part of the URL.
Use RouteLink to specify the route by name (along with the other route value criteria to make it match).
Create a custom route to handle "constraining" it to a specific namespace or make a custom route constraint.
Since you explicitly stated the second option is unacceptable, here is an example of the first:
var route1 = routes.MapRoute(
"CRUD",
"CRUD/{controller}/{action}/{id}",
new { crud = "crud", action = "Index", id = UrlParameter.Optional }
);
route1.DataTokens["Namespaces"] = new string[] { "College.Controllers.CRUD" };
route1.DataTokens["UseNamespaceFallback"] = false;
var route2 = routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
route1.DataTokens["Namespaces"] = new string[] { "College.Controllers" };
route1.DataTokens["UseNamespaceFallback"] = false;
Now when you call #Html.ActionLink("Students", "Index", "Students", new { crud = (string)null }, null) it will not match the CRUD route, it will match the Default route.
To make it match the CRUD route, you have to explicitly add the route value to the ActionLink: #Html.ActionLink("Students", "Index", "Students", new { crud = "crud" }, null) or leave it out entirely: #Html.ActionLink("Students", "Index", "Students")
Constraining the Route
Here is an example of the 3rd option.
Unfortunately, we can't use a regular route constraint because Microsoft decided not to make the RequestContext object available in the IRouteConstraint interface. This means the namespace information about what controller the request is bound for is not available. So, we need to drop to a lower level and make a custom RouteBase class that implements the decorator pattern to wrap our existing Route class configuration.
This class simply checks to see if the namespace from the request matches a specific namespace before generating the URL.
public class NamespaceConstrainedRoute : RouteBase
{
private readonly string namespaceToMatch;
private readonly RouteBase innerRoute;
public NamespaceConstrainedRoute(string namespaceToMatch, RouteBase innerRoute)
{
if (string.IsNullOrEmpty(namespaceToMatch))
throw new ArgumentNullException("namespaceToMatch");
if (innerRoute == null)
throw new ArgumentNullException("innerRoute");
this.namespaceToMatch = namespaceToMatch;
this.innerRoute = innerRoute;
}
public override RouteData GetRouteData(HttpContextBase httpContext)
{
return innerRoute.GetRouteData(httpContext);
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
object namespaces;
if (requestContext.RouteData.DataTokens.TryGetValue("Namespaces", out namespaces)
&& namespaces is IList<string>
&& ((IList<string>)namespaces).Contains(namespaceToMatch))
{
return innerRoute.GetVirtualPath(requestContext, values);
}
// null indicates to try to match the next route in the route table
return null;
}
}
Usage
var route1 = new Route(
url: "CRUD/{controller}/{action}/{id}",
defaults: new RouteValueDictionary(new { action = "Index", id = UrlParameter.Optional }),
routeHandler: new MvcRouteHandler()
)
{
DataTokens = new RouteValueDictionary
{
{ "Namespaces", new string[] { "College.Controllers.CRUD" }},
{ "UseNamespaceFallback", false }
}
};
var route2 = new Route(
url: "{controller}/{action}/{id}",
defaults: new RouteValueDictionary(new { controller = "Home", action = "Index", id = UrlParameter.Optional }),
routeHandler: new MvcRouteHandler()
)
{
DataTokens = new RouteValueDictionary
{
{ "Namespaces", new string[] { "College.Controllers" }},
{ "UseNamespaceFallback", false }
}
};
routes.Add(
name: "CRUD",
item: new NamespaceConstrainedRoute(
namespaceToMatch: "College.Controllers.CRUD",
innerRoute: route1));
routes.Add(
name: "Default",
item: new NamespaceConstrainedRoute(
namespaceToMatch: "College.Controllers",
innerRoute: route2));
From this point, you could build your own MapRoute extension methods if you so choose to make the above configuration look cleaner.

URL rewriting using similar MapRoutes

I have 2 similar maproute requests but I'm trying to target different routes.
Basically I'm creating a picture project using ASP.NET MVC.
What I want is to have the URL as:
website.com/pictures/username
and
website.com/pictures/pictureid
I'm using this as the map routes atm. Hoped that the different signatures would be enough to distinguish which action i would need.
The pictures controller has the action methods as
ActionResult Index (string username) { ... }
ActionResult Index (long id) { ... }
routes.MapRoute(
"UsersPicturesRoute",
"Pictures/{username}",
new { controller = "Pictures", action = "Index", username = UrlParameter.Optional }
);
routes.MapRoute(
"SinglePictureRoute",
"Pictures/{id}",
new { controller = "Pictures", action = "Index", id = UrlParameter.Optional }
);
Is there a way to have this desired outcome?
You can change your RegisterRoutes in below sequence then you will get your required output
routes.MapRoute(
"SinglePictureRoute",
"Pictures/{id}",
new { controller = "Home", action = "abcd", id = UrlParameter.Optional },
new { id = #"\d+" } // Parameter constraints
);
routes.MapRoute(
"UsersPicturesRoute",
"Pictures/{username}",
new { controller = "Home", action = "abcTest", username = UrlParameter.Optional }
);

Route value with dashes

I have this route:
routes.MapRoute(
"News",
"News/{id}-{alias}",
new { controller = "News", action = "Show" },
new
{
id = #"^[0-9]+$"
},
namespaces: new[] { "Site.Controllers" }
);
This route working for url's like this:
http://localhost:54010/News/6-news
But not working for url's like this:
http://localhost:54010/News/6-nice-news
How use dashes in my route value "alias"?
EDITED
Route like this:
"News/{id}_{alias}"
works for both url's:
http://localhost:54010/News/6_news
http://localhost:54010/News/6_nice-news
The problem is with your pattern: News/{id}-{alias} because the Routeing is parsing the patterns greedily.
So the url http://localhost:54010/News/6-news generates the following tokens:
id = 6, alias = news
But the http://localhost:54010/News/6-nice-news generates the following tokens:
id = 6-nice, alias = news
And the id = 6-nice token will fail your routing contraint #"^[0-9]+$". so you will get 404.
There is now way to configure this behavior of MVC so you have the following options:
Use something else than dashes. As you noted combining dashes and hyphens works.
Take flem approach and parse inside the id and alias inside your controller action
You can create a custom Route which will take of the re-parsing. E.g transforming id = 6-nice, alias = news to id = 6, alias = news-nice
I will show you a raw (without any error handling or good coding practices!) implementation of the option 3 to get you started.
So you need to inherit from Route:
public class MyRoute : Route
{
public MyRoute(string url,
RouteValueDictionary defaults,
RouteValueDictionary constraints,
RouteValueDictionary dataTokens)
: base(url, defaults, constraints, dataTokens, new MvcRouteHandler())
{
}
protected override bool ProcessConstraint(HttpContextBase httpContext,
object constraint, string parameterName, RouteValueDictionary values,
RouteDirection routeDirection)
{
var parts = ((string) values["id"]).Split('-');
if (parts.Length > 1)
{
values["id"] = parts[0];
values["alias"] = // build up the alias part
string.Join("-", parts.Skip(1)) + "-" + values["alias"];
}
var processConstraint = base.ProcessConstraint(httpContext, constraint,
parameterName, values, routeDirection);
return processConstraint;
}
}
Then you just need to register your route:
routes.Add("News",
new MyRoute("News/{id}-{alias}",
new RouteValueDictionary(new {controller = "News", action = "Show"}),
new RouteValueDictionary(new
{
id = #"^[0-9]+$"
}),
new RouteValueDictionary()));

MVC3 Areas how to use dashes in page names

We're using Areas with a project. And we're also using this fix to replaces dashes in page names
routes.Add(
new Route("{controller}/{action}/{id}",
new RouteValueDictionary(
new { controller = "Home", action = "Index", id = UrlParameter.Optional }),
new HyphenatedRouteHandler())
);
public class HyphenatedRouteHandler : MvcRouteHandler
{
protected override IHttpHandler GetHttpHandler(RequestContext requestContext)
{
requestContext.RouteData.Values["controller"] = requestContext.RouteData.Values["controller"].ToString().Replace("-", "_");
requestContext.RouteData.Values["action"] = requestContext.RouteData.Values["action"].ToString().Replace("-", "_");
return base.GetHttpHandler(requestContext);
}
}
This Fix works fine with the normal top level pages. Home/some-page.
But when it comes to an Area this no longer works. I tried modifying the AreaRegistration.cs file to be an equivalent to the Global.asax.cs route but that didn't work either. I can rename the pages with [ActionName("some-page")] but it doesn't solve the problem of the controller still having underscores Area/some_folder/some-page and I don't want my URLs to look like that.
Edit:
When I use the route that Darin Dimitrov suggest I get this error:
Multiple types were found that match the controller named 'page'. This can happen if the route that services this request ('admin/{controller}/{action}/{id}') does not specify namespaces to search for a controller that matches the request. If this is the case, register this route by calling an overload of the 'MapRoute' method that takes a 'namespaces' parameter.
We have multiple "segments" that have the same names but different content to target the segment audience
The following should work in your area registration:
public override void RegisterArea(AreaRegistrationContext context)
{
context.Routes.Add(
"Admin_default",
new Route("Admin/{controller}/{action}/{id}",
new RouteValueDictionary(
new { action = "Index", id = UrlParameter.Optional }
),
null,
new RouteValueDictionary(
new
{
area = AreaName
}
),
new HyphenatedRouteHandler()
)
);
}
UPDATE:
You seem to be having the same controller name in multiple areas which is not possible without defining a namespace constraint when registering your routes as the error message you are getting suggests you to do.
So in your Global.asax:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.Add(
"Default",
new Route(
"{controller}/{action}/{id}",
new RouteValueDictionary(
new { controller = "Home", action = "Index", id = UrlParameter.Optional }
),
null,
new RouteValueDictionary(
new { Namespaces = new[] { "AppName.Controllers" } }
),
new HyphenatedRouteHandler()
)
);
}
and in your AreaRegistration:
public override void RegisterArea(AreaRegistrationContext context)
{
context.Routes.Add(
"Admin_default",
new Route(
"Admin/{controller}/{action}/{id}",
new RouteValueDictionary(
new { action = "Index", id = UrlParameter.Optional }
),
null,
new RouteValueDictionary(
new
{
Namespaces = new[] { "AppName.Areas.Admin.Controllers" },
area = AreaName
}
),
new HyphenatedRouteHandler()
)
);
}
You might need to adjust the namespace in the constraint to match yours.

Resources