Html.RenderAction not hitting default route as expected - asp.net

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?

Related

How to get all url parameters in asp.net mvc?

My test url:
localhost:61578/?type=promotion&foo=bar
I usually use this way to get the value of type parameter:
public IActionResult Index(string type)
{
// type = "promotion"
}
My question: How to detect all parameters in the url? I want to prevent to access the page with some unknown parameter.
Something like this:
public IActionResult Index(string type, string foo)
{
if (!string.IsNullOrEmpty(foo))
{
return BadRequest(); // page 404
}
}
The problem is: I don't know exactly the name what user enters. So, it can be:
localhost:61578/?bar=baz&type=promotion
You can use the HttpContext Type to grab the query string
var context = HttpContext.Current;
then, you can grab the entire query string:
var queryString = context.Request.QueryString
// "bar=baz&type=promotion"
or, you can get a list of objects:
var query = context.Request.Query.Select(_ => new
{
Key = _.Key.ToString(),
Value = _.Value.ToString()
});
// { Key = "bar", Value = "baz" }
// { Key = "type", Value = "promotion" }
or, you could make a dictionary:
Dictionary<string, string>queryKvp = context.Request.GetQueryNameValuePairs()
.ToDictionary(_=> _.Key, _=> _.Value, StringComparer.OrdinalIgnoreCase);
// queryKvp["bar"] = "baz"
// queryKvp["type"] = "promotion"

ASP.Net MVC Handling Segments with Route

I am new to ASP.Net MVC and facing a problem. Here it is.
routes.MapRoute(
"SearchResults",// Route name
"{controller}/{action}/{category}/{manufacturer}/{attribute}",
new {
controller = "Home",
action = "CategoryProducts",
category = UrlParameter.Optional,
manufacturer = UrlParameter.Optional,
attribute = UrlParameter.Optional
}
);
And here is my controller method.
public ActionResult CategoryProducts(string category, string manufacturer, string attribute)
{
string[] categoryParameter = category.Split('_');
.
.
.
return View();
}
when i hit the url i always get null in category parameter
http://localhost:50877/Home/CategoryProducts/c_50_ShowcasesDisplays
I get this error
Object reference not set to an instance of an object
How can i fix this problem. I need to extract the id from segment and use it. Similarly i need to process the manufacturer and attribute strings too.
One more thing
How can i make my function get at least one parameter regardless of order? I mean i want to make functions like that i can handle category or manufacturer or attributes or category + manufacturer and all the combinations/
A placeholder (such as {category}) acts like a variable - it can contain any value. The framework must be able to understand what the parameters in the URL mean. You can do this one of three ways:
Provide them in a specific order, and for a specific number of segments
Put them in the query string so you have name/value pairs to identify what they are
Make a series of routes with literal segments to provide names to identify what the parameters are
Here is an example of option #3. It is a bit involved compared to using query string parameters, but it is certainly possible as long as you provide some sort of identifier for each route segment.
IEnumerable Extensions
This adds LINQ support for being able to get every possible permutation of parameter values.
using System;
using System.Collections.Generic;
using System.Linq;
public static class IEnumerableExtensions
{
// Can be used to get all permutations at a certain level
// Source: http://stackoverflow.com/questions/127704/algorithm-to-return-all-combinations-of-k-elements-from-n#1898744
public static IEnumerable<IEnumerable<T>> Combinations<T>(this IEnumerable<T> elements, int k)
{
return k == 0 ? new[] { new T[0] } :
elements.SelectMany((e, i) =>
elements.Skip(i + 1).Combinations(k - 1).Select(c => (new[] { e }).Concat(c)));
}
// This one came from: http://stackoverflow.com/questions/774457/combination-generator-in-linq#12012418
private static IEnumerable<TSource> Prepend<TSource>(this IEnumerable<TSource> source, TSource item)
{
if (source == null)
throw new ArgumentNullException("source");
yield return item;
foreach (var element in source)
yield return element;
}
public static IEnumerable<IEnumerable<TSource>> Permutations<TSource>(this IEnumerable<TSource> source)
{
if (source == null)
throw new ArgumentNullException("source");
var list = source.ToList();
if (list.Count > 1)
return from s in list
from p in Permutations(list.Take(list.IndexOf(s)).Concat(list.Skip(list.IndexOf(s) + 1)))
select p.Prepend(s);
return new[] { list };
}
}
RouteCollection Extensions
We extend the MapRoute extension method, adding the ability to add a set of routes to match all possible permutations of the URL.
using System;
using System.Collections.Generic;
using System.Web.Mvc;
using System.Web.Routing;
public static class RouteCollectionExtensions
{
public static void MapRoute(this RouteCollection routes, string url, object defaults, string[] namespaces, string[] optionalParameters)
{
MapRoute(routes, url, defaults, null, namespaces, optionalParameters);
}
public static void MapRoute(this RouteCollection routes, string url, object defaults, object constraints, string[] namespaces, string[] optionalParameters)
{
if (routes == null)
{
throw new ArgumentNullException("routes");
}
if (url == null)
{
throw new ArgumentNullException("url");
}
AddAllRoutePermutations(routes, url, defaults, constraints, namespaces, optionalParameters);
}
private static void AddAllRoutePermutations(RouteCollection routes, string url, object defaults, object constraints, string[] namespaces, string[] optionalParameters)
{
// Start with the longest routes, then add the shorter ones
for (int length = optionalParameters.Length; length > 0; length--)
{
foreach (var route in GetRoutePermutations(url, defaults, constraints, namespaces, optionalParameters, length))
{
routes.Add(route);
}
}
}
private static IEnumerable<Route> GetRoutePermutations(string url, object defaults, object constraints, string[] namespaces, string[] optionalParameters, int length)
{
foreach (var combination in optionalParameters.Combinations(length))
{
foreach (var permutation in combination.Permutations())
{
yield return GenerateRoute(url, permutation, defaults, constraints, namespaces);
}
}
}
private static Route GenerateRoute(string url, IEnumerable<string> permutation, object defaults, object constraints, string[] namespaces)
{
var newUrl = GenerateUrlPattern(url, permutation);
var result = new Route(newUrl, new MvcRouteHandler())
{
Defaults = CreateRouteValueDictionary(defaults),
Constraints = CreateRouteValueDictionary(constraints),
DataTokens = new RouteValueDictionary()
};
if ((namespaces != null) && (namespaces.Length > 0))
{
result.DataTokens["Namespaces"] = namespaces;
}
return result;
}
private static string GenerateUrlPattern(string url, IEnumerable<string> permutation)
{
string result = url;
foreach (string param in permutation)
{
result += "/" + param + "/{" + param + "}";
}
System.Diagnostics.Debug.WriteLine(result);
return result;
}
private static RouteValueDictionary CreateRouteValueDictionary(object values)
{
IDictionary<string, object> dictionary = values as IDictionary<string, object>;
if (dictionary != null)
{
return new RouteValueDictionary(dictionary);
}
return new RouteValueDictionary(values);
}
}
Usage
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
url: "Home/CategoryProducts",
defaults: new { controller = "Home", action = "CategoryProducts" },
namespaces: null,
optionalParameters: new string[] { "category", "manufacturer", "attribute" });
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
}
This adds a complete set of routes to match the URL patterns:
Home/CategoryProducts/category/{category}/manufacturer/{manufacturer}/attribute/{attribute}
Home/CategoryProducts/category/{category}/attribute/{attribute}/manufacturer/{manufacturer}
Home/CategoryProducts/manufacturer/{manufacturer}/category/{category}/attribute/{attribute}
Home/CategoryProducts/manufacturer/{manufacturer}/attribute/{attribute}/category/{category}
Home/CategoryProducts/attribute/{attribute}/category/{category}/manufacturer/{manufacturer}
Home/CategoryProducts/attribute/{attribute}/manufacturer/{manufacturer}/category/{category}
Home/CategoryProducts/category/{category}/manufacturer/{manufacturer}
Home/CategoryProducts/manufacturer/{manufacturer}/category/{category}
Home/CategoryProducts/category/{category}/attribute/{attribute}
Home/CategoryProducts/attribute/{attribute}/category/{category}
Home/CategoryProducts/manufacturer/{manufacturer}/attribute/{attribute}
Home/CategoryProducts/attribute/{attribute}/manufacturer/{manufacturer}
Home/CategoryProducts/category/{category}
Home/CategoryProducts/manufacturer/{manufacturer}
Home/CategoryProducts/attribute/{attribute}
Now when you use the following URL:
Home/CategoryProducts/category/c_50_ShowcasesDisplays
The action CategoryProducts on the HomeController will be called. Your category parameter value will be c_50_ShowcasesDisplays.
It will also build the corresponding URL when you use ActionLink, RouteLink, Url.Action, or UrlHelper.
#Html.ActionLink("ShowcasesDisplays", "CategoryProducts", "Home",
new { category = "c_50_ShowcasesDisplays" }, null)
// Generates URL /Home/CategoryProducts/category/c_50_ShowcasesDisplays

ASP.NET route with arbitrary number of key-value pairs - is it possible?

I'd like to handle URLs like this:
/Id/Key1/Value1/Key2/Value2/Key3/Value3/
Right now, I have set up a rule like this:
/{id}/{*parameters}
The parameters object is passed as a single string to all the actions that are involved in forming the response. This does work, but I have a few problems with it:
Each action must resolve the string for itself. I've, of course, made an extension method that turns the string to a Dictionary<string, string>, but I'd prefer it if the dispatching mechanism gave my methods a Dictionary<string, string> directly - or, better yet, the actual pairs as separate arguments.
Action links will still add parameters using the traditional format (?Key1=Value1). I guess I could write specialized helpers with my desired format, but I'd prefer it if there was a way to make the existing overloads follow the above routing rule.
Is there a way to do the above?
You could write a custom route:
public class MyRoute : Route
{
public MyRoute()
: base(
"{controller}/{action}/id/{*parameters}",
new MvcRouteHandler()
)
{
}
public override RouteData GetRouteData(HttpContextBase httpContext)
{
var rd = base.GetRouteData(httpContext);
if (rd == null)
{
return null;
}
string parameters = rd.GetRequiredString("parameters");
IDictionary<string, string> parsedParameters = YourExtensionMethodThatYouAlreadyWrote(parameters);
rd.Values["parameters"] = parsedParameters;
return rd;
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
object parameters;
if (values.TryGetValue("parameters", out parameters))
{
var routeParameters = parameters as IDictionary<string, object>;
if (routeParameters != null)
{
string result = string.Join(
"/",
routeParameters.Select(x => string.Concat(x.Key, "/", x.Value))
);
values["parameters"] = result;
}
}
return base.GetVirtualPath(requestContext, values);
}
}
which could be registered like that:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.Add("my-route", new MyRoute());
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
and now your controller actions could take the following parameters:
public ActionResult SomeAction(IDictionary<string, string> parameters)
{
...
}
As far as generating links following this pattern is concerned, it's as simple as:
#Html.RouteLink(
"Go",
"my-route",
new {
controller = "Foo",
action = "Bar",
parameters = new RouteValueDictionary(new {
key1 = "value1",
key2 = "value2",
})
}
)
or if you wanted a <form>:
#using (Html.BeginRouteForm("my-route", new { controller = "Foo", action = "Bar", parameters = new RouteValueDictionary(new { key1 = "value1", key2 = "value2" }) }))
{
...
}
Write your own model binder for a specialized dictionary. If you will have one there will be no need for parsing the string in each action method.

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()));

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

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.

Resources