Passing an array to Html.ActionLink() - c#

I followed this example:
ASP.NET MVC - Pass array object as a route value within Html.ActionLink(...)
But, my Action is always called with null. What am I doing wrong?
foreach (OrderDetail od in order.OrderDetails)
{
rvd.Add("key" + count++, productID);
rvd.Add("key" + count++, productName);
}
#Html.ActionLink(linkText, "Renew", "Orders", rvd, new Dictionary<string, object>())
The query string is correctly generated, like ?key0=dog&key1=cat&key2=fish..., but I get a null parameter in my Action below:
public ActionResult Renew(RouteValueDictionary rvd)
{
// 'rvd' is null here!
}
Please note: I don't know the number of parameters in advance.

The query string is correctly generated, like ?key0=dog&key1=cat&key2=fish...
No, this is not a correct url. A correct url would have looked like this:
?%5B0%5D.Key=123&%5B0%5D.Value=dog&%5B1%5D.Key=456&%5B1%5D.Value=cat...
which would have mapped to:
public ActionResult Renew(Dictionary<int, string> rvd)
{
...
}
You could write a custom ActionLink to generate this url:
public static class LinkExtensions
{
public static IHtmlString MyActionLink(
this HtmlHelper html,
string linkText,
string actionName,
string controllerName,
IDictionary<string, string> parameters
)
{
var a = new TagBuilder("a");
var urlHelper = new UrlHelper(html.ViewContext.RequestContext);
var query = string.Join("&", parameters.Select((x, i) => string.Format("[{0}].Key={1}&[{0}].Value={2}", i, urlHelper.Encode(x.Key), urlHelper.Encode(x.Value))));
var url = string.Format(
"{0}?{1}",
urlHelper.Action(actionName, controllerName, null, html.ViewContext.HttpContext.Request.Url.Scheme),
query
);
a.Attributes["href"] = url;
a.SetInnerText(linkText);
return new HtmlString(a.ToString());
}
}
which you could use like this in your view:
#Html.MyActionLink(
linkText,
"Renew",
"Orders",
order.OrderDetails.ToDictionary(x => x.ProductID.ToString(), x => x.ProductName)
)
You can read more about the correct wire format for binding to various collections in this blog post.

I imagine what is happening is you are expecting the model binder to bind your array to a RouteValueDictionary, but the model binder doesn't know that key0=dog&key1=cat&key2=fish is supposed to be a dictionary. I would recommend changing your code to accept a string array. To do this, your query string needs to look something like this: ?rvd=dog&rvd=cat&rvd=fish
And your Action...
public ActionResult Renew(string[] rvd)
{
// 'rvd' is no longer null here!
}
The important part is rvd is the parameter name in your action, as well as the name of each element in the querystring: ?rvd=dog&rvd=cat&rvd=fish. If you really want to use a dictionary instead of a string array, then your querystring should look like this: ?rvd[0]=dog&rvd[1]=cat&rvd[2]=fish, giving each item an array index, but you would probably have to change your parameter from RouteValueDictionary to Dictionary<string,string>, I'm not quite sure. More info here. EDIT: See Darin's comment about binding to a dictionary, as I believe his is correct.
You may have to write your own extension for Html.ActionLink that accepts an array (or whatever OrderDetails is) and creates the querystring as an array. This looks like a pretty good starting place.

Related

How to create keyless URL parameters in ASP.NET

I have seen this answer describing ASP.NET support for keyless (not valueless) parameters, like http://some.url?param1&param2, and confirmed them to be viewable on Request.QueryString like:
var values = this.Request.QueryString.GetValues(null);
values.Any(o => o == "param1");
This is fine and dandy but now I want to generate urls like this. My first intuition was to use the RouteValueDictionary: routeValues parameter of Url.Action with null as a key:
#{
var dict = new RouteValueDictionary();
dict.Add(null, "param1");
dict.Add(null, "param2");
}
Very link, amaze
But apparently C# forbids nulls as dictionary keys because of reasons.
I have also tried the empty string as the key, but it results in a query string like: ?=param1,=param2 which contains 2 more equal signs that I want it to.
Of course I can string manipulate the heck out of my URL and add the &param1 part to the query string, but I was hoping for a concise solution.
You want to add the key values, but leaving the value null isn't allowed.
RouteValueDictionary ignores empty values
You could add a value like 1 for instance, but you lose your fine and dandy solution.
#{
var dict = new RouteValueDictionary();
dict.Add("param1",1);
}
Very link, amaze
For another solution you will have to write some custom code.
Since there's no built-in helper for this why don't you roll your own:
public static class UrlHelperExtensions
{
public static string MyAction(this UrlHelper urlHelper, string actionName, IList<string> parameters)
{
string url = urlHelper.Action(actionName);
if (parameters == null || !parameters.Any())
{
return url;
}
return string.Format("{0}?{1}", url, string.Join("&", parameters));
}
}
and then:
#{
var parameters = new List<string>();
parameters.Add("param1");
parameters.Add("param2");
}
#Url.MyAction("ActionName", parameters)

GET string with many queries

I need to make a query with a string, variable long and can have several simultaneous queries:
String:
http://localhost:39051/api/values​​/punto1?name=125.25
http://localhost:39051/api/values/punto1?name=125.25&name1=1&name2=23.98
http://localhost:39051/api/values/punto1?name=125.25&name1=1&name2=23.98&name3=12.5
http://localhost:39051/api/values/punto1?name=125.25&name1=1&name2=23.98&name3=12.5&name6=34&name23=3
I have this configuration in webApiConfig.cs
config.Routes.MapHttpRoute(
name: "name",
routeTemplate: "api/{controller}/{id}/{name}/{name1}/{name2}",
defaults: new { id = RouteParameter.Optional, action = "GetProductsByName", name = string.Empty, name1 = string.Empty, name2 = string.Empty });
And I call GetProductsByName (....), which is in ValuesController.cs, where is the GET, POST, etc.
public string GetProductsByName (string name, string name1, string name2)
{
return "Requested name:" + ":" + name + ":" + name1 + ":" + name2;
}
It works and brings me the parameters name, name1 name2 and. But if I want to see more parameters, I have to define the in config.Routes. Which makes the system more complicated.
Need to separate the data into two parts and put them in string variables.
For example:
http://localhost:39051/api/values/punto1?name=125.25&name1=1&name2=23.98
string1: punto1
and the other string is put everything after the?
string2: name=125.25&name1=1&name2=23.98
or
string2: name=125.25&name1=1&name2=23.98&name3=12.5&name6=34&name23=3
Depending on the case
Then string2 separate process to scan values.
It works perfectly, I tried it and brings the data correctly.
One more query, if I get 4 different parameters, in consultation, as would be the separation of parameters
for example:
http://localhost:39051/api/values/punto1.25?name=5.25&name1=1&valor1=23.98&valor2=0.125&book2=17&book1=8&nivel15=9&nivel20=8
So you are searching by name(s) but you could have any number of names passed in?
You could bind to a List<string> to prevent you needing to add parameters and edit your routes.
public string Get([FromUri]List<string> Names)
{
string output = "Requested name";
foreach (var item in Names)
{
output += ":" + item;
}
return output;
}
To get that to work with your URL you need to send the parameters in the form
names[0]=firstparam&names1=secondParam
for example:
http://localhost:39051/api/values?names[0]=name1&names[1]=name2&names[2]=name3
will produce the output
Requested name:name1:name2:name3
Alternatively, as long as you keep the name the same you can remove the indexing and pass the parameters like this:
http://localhost:39051/api/values?names=name1&names=name2&names=name3
Note the [FromUri] is telling the model binder to take the values from the Uri. If you have a Post rather than a Get then you won't need the [FromUri] as the data will be in the body.
This way you won't need to change your routing at all when you want to add new parameters.
Be aware that there are limits to the length of the Uri that browsers allow. See this Stackoverflow question for more information.
Edit
As you can't change the URL, the only thing I think you could do is create your own ModelBinder.
You could create a binder that checks the parameters that match a particular criteria and then convert those into a list or dictionary in your action method.
To create a ModelBinder you need to create a class that implements IModelBinder then in your action method you need to decorate the parameter you wish to be bound using your binder with the ModelBinder attribute.
A very rough example that only handles parameters on the querystring would be:
public class MyModelBinder : IModelBinder
{
public bool BindModel(System.Web.Http.Controllers.HttpActionContext actionContext, ModelBindingContext bindingContext)
{
List<string> names = new List<string>();
if (!string.IsNullOrEmpty(actionContext.Request.RequestUri.Query))
{
foreach (var item in actionContext.Request.GetQueryNameValuePairs().ToDictionary(x => x.Key, x => x.Value))
{
if (item.Key.ToLower().StartsWith("name"))
{
//it's a "nameX" parameter so let's add it to the list
names.Add(item.Value);
}
}
}
//this is what will be bound to "name" in the action method
bindingContext.Model = names;
return true;
}
}
Then in the action method you can wire up your binder:
public string Get([ModelBinder(typeof(MyModelBinder))] List<string> name)
{
string output = "Requested name";
foreach (var item in name)
{
output += ":" + item;
}
return output;
}
Given the querystring
http://localhost:39051/api/values?name=name1&name1=name2&name2=name3
will again produce the output
Requested name:name1:name2:name3
It works perfectly, I tried it and brings the data correctly.
One more query, if I get 4 different parameters, in consultation, as would be the separation of parameters
for example:
http://localhost:39051/api/values/punto1.25?name=5.25&name1=1&valor1=23.98&valor2=0.125&book2=17&book1=8&nivel15=9&nivel20=8

RedirectToAction with unnamed querystring parameters

In MVC 2 I have a RedirectToAction call which I need to pass all of the querystring parameters. Unfortunately I can only find a way to pass named querystring parameters is there a way of passing all querystring parameters regardless.
We have one named parameter, id but we just want to append all of the rest onto the end of the URL without setting them explicitly.
return RedirectToAction("index", "enquiry", new { id = enquiryCacheId.ToString()});
You cannot pass COMPLEX objects in URLs, so that kills the option of passing on Complex types in new { }.
One option what you are left with is to encode the querystring and then send that in 'id'. Say for examples, you have querystring as follows name=rami&gender=male. Then you can encode it with HttpUtility.UrlEncode(), then set it to id=[encoded string]. On the other action (retrieving side) you can get id value and then use HttpUtility.UrlDecode() to decode the string. Then finally you can use HttpUtility.ParseQueryString() to split the querystring into NameValueCollection.
If above suggestion is not what you are looking for then. you need to add all querystring parameters to new { } in the RedirectToAction(). If you want to customize it then you might need to go to ASP.Net MVC Source code #CodePlex and make your own builds (which I think not worthy for this kind of requirement).
I have an extension method that I use to modify the querystring of the current URL:
public static string ModifyQueryString(this UrlHelper helper, NameValueCollection updates, IEnumerable<string> removes)
{
var request = helper.RequestContext.HttpContext.Request;
var url = request.Url.AbsolutePath;
var query = HttpUtility.ParseQueryString(request.QueryString.ToString());
updates = updates ?? new NameValueCollection();
foreach (string key in updates.Keys)
{
query.Set(key, updates[key]);
}
removes = removes ?? new List<string>();
foreach (string param in removes)
{
query.Remove(param);
}
if (query.HasKeys())
{
return string.Format("{0}?{1}", url, query.ToString());
}
else
{
return url;
}
}
But, if you need to modify an arbitrary URL, it should be easy enough to modify. You would just need to add a parameter to accept an arbitrary URL, and then instead of getting the URL/querystring from the HttpContext, you just split the passed URL at ?. The rest of the code should work the same.

URL.Action with a string array?

I have an array of strings that I need to pass in a query string of Url.Action.
Url.Action("Index", "Resource", new { FormatIds = Model.FormatIDs})
Right now the link is showing up in my browser as System.String[] instead of a query string. Is it possible to have MVC do this automatically with model binding?
I need it to bind with my controller action like:
public ActionResult Index(string[] formatIDs)
To get the list of string to automatically bind using the default binder, you will need to provide them as:
name=value&name=value2&name=value3
So you'll need to convert your list to something like:
Index?formatIDs=1&formatIDs=2&formatIDs=3
For use the default model binder, you should end up with something like :
Index?formatIDs=value1&formatIDs=value2&formatIDs=value3
you can returns a private collection named HttpValueCollection even the documentation says it's a NameValueCollection using the ParseQueryString utility.
Then add the keys manually, HttpValueCollection do the encoding for you.
And then just append the QueryString manually :
var qs = HttpUtility.ParseQueryString("");
new string[] { "value1", "value2", "value3" }.ToList().ForEach(x => qs.Add("formatIDs", x));
Url.Action("Index", "Resource")?#qs
There is another way using the RouteValueDictionary with an array:
#{
var parameters = new RouteValueDictionary();
for (var i = 0; i < Model.CustomList.Count; ++i)
{
parameters.Add($"customListId[{i}]", Model.CustomList[i]);
}
}
usage:
var url = '#Html.Raw(Url.Action("ControllerActioon", "Controller", parameters))';
Still not very elegant - but working.

How do I accept an array as an ASP.NET MVC controller action parameter?

I have an ASP.net MVC controller called Designs that has an action with the following signature:
public ActionResult Multiple(int[] ids)
However, when I try to navigate to this action using the url:
http://localhost:54119/Designs/Multiple?ids=24041,24117
The ids parameter is always null. Is there any way to get MVC to convert the ?ids= URL query parameter into an array for the action? I've seen talk of using an action filter but as far as I can tell that will only work for POSTs where the array is passed in the request data rather than in the URL itself.
The default model binder expects this url:
http://localhost:54119/Designs/Multiple?ids=24041&ids=24117
in order to successfully bind to:
public ActionResult Multiple(int[] ids)
{
...
}
And if you want this to work with comma separated values you could write a custom model binder:
public class IntArrayModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (value == null || string.IsNullOrEmpty(value.AttemptedValue))
{
return null;
}
return value
.AttemptedValue
.Split(',')
.Select(int.Parse)
.ToArray();
}
}
and then you could apply this model binder to a particular action argument:
public ActionResult Multiple([ModelBinder(typeof(IntArrayModelBinder))] int[] ids)
{
...
}
or apply it globally to all integer array parameters in your Application_Start in Global.asax:
ModelBinders.Binders.Add(typeof(int[]), new IntArrayModelBinder());
and now your controller action might look like this:
public ActionResult Multiple(int[] ids)
{
...
}
To extend on Darin Dimitrov's answer, something you can get away with is accepting a simple string in your URL parameter and converting it to an array yourself:
public ActionResult Multiple(string ids){
int[] idsArray = ids.Split(',').Select(int.Parse).ToArray();
/* ...process results... */
}
If you get a parse error while doing this (because someone passed you a malformed array), you can cause your exception handler to return a 400 Bad Request error instead of the default, more unfriendly 404 Not Found error that MVC returns when an endpoint is not found.
You can also use this URL format, and ASP.NET MVC will do everything for you. But, remember to apply URL encoding.
?param1[0]=3344&param1[1]=2222
I don't know where Groky's URL string was coming from, but I had the same problem with some javascript calling my controller/action. It would build up a URL of null, 1, or many "IDs" from a multiple-select list (which is unique to the solution I'm going to share).
I copy/pasted Darin's custom model binder and decorated my action/parameter, but it didn't work. I still got null valued int[] ids. Even in the "safe" case where I actually did have many IDs.
I ended up changing the javascript to produce an ASP.NET MVC friendly parameter array like
?ids=1&ids=2
I had to do some silly stuff, though
ids || [] #=> if null, get an empty array
[ids || []] #=> if a single item, wrap it in an array
[].concat.apply([], ...) #=> in case I wrapped an array, flatten it
So, the full block was
ids = [].concat.apply([], [ids || []])
id_parameter = 'ids=' + ids.join('&ids=')
It's messy, but it's the first time I had to hack like this in javascript.
.Net Core Answer
For those coming here in recent times, you can do this in .Net Core with:
http://localhost:54119/Designs/Multiple?ids=24041&ids=24117
and:
public ActionResult Multiple([FromQuery] int[] ids)
{
...
}

Categories

Resources