41

I have a method that returns an array (string[]) and I'm trying to pass this array of strings into an Action Link so that it will create a query string similar to:

/Controller/Action?str=val1&str=val2&str=val3...etc

But when I pass new { str = GetStringArray() } I get the following url:

/Controller/Action?str=System.String%5B%5D

So basically it's taking my string[] and running .ToString() on it to get the value.

Any ideas? Thanks!

tvanfosson
  • 524,688
  • 99
  • 697
  • 795

6 Answers6

14

Try creating a RouteValueDictionary holding your values. You'll have to give each entry a different key.

<%  var rv = new RouteValueDictionary();
    var strings = GetStringArray();
    for (int i = 0; i < strings.Length; ++i)
    {
        rv["str[" + i + "]"] = strings[i];
    }
 %>

<%= Html.ActionLink( "Link", "Action", "Controller", rv, null ) %>

will give you a link like

<a href='/Controller/Action?str=val0&str=val1&...'>Link</a>

EDIT: MVC2 changed the ValueProvider interface to make my original answer obsolete. You should use a model with an array of strings as a property.

public class Model
{
    public string Str[] { get; set; }
}

Then the model binder will populate your model with the values that you pass in the URL.

public ActionResult Action( Model model )
{
    var str0 = model.Str[0];
}
tvanfosson
  • 524,688
  • 99
  • 697
  • 795
  • 1
    Just thought I'd mention that it looks like you've given another alternative to a similar question over here at: [ASP.Net MVC RouteData and arrays] (http://stackoverflow.com/questions/1752721/asp-net-mvc-routedata-and-arrays). Is there a way to link these two questions so that people can see both of your alternatives? – GuyIncognito Jan 20 '10 at 17:16
  • I think you just did. Actually this won't work any more. I'll update the action method to use a model. – tvanfosson May 30 '10 at 03:08
  • 2
    The model binding is not the issue. It seems MVC 2 still generates query strings like `?str=System.String%5B%5D` when a `RouteValueDictionary` value contains an array/list/etc. Still no way around that? – Crescent Fresh May 31 '10 at 02:39
  • @Crescent - are you sure you're using the signature that has both route values and html attributes? – tvanfosson May 31 '10 at 03:45
  • 4
    @tvanfosson: I believe so, yes. Passing route data like `new { foo = new[] { "a", "b" } }` to any of `Html.ActionLink`, `Url.Action`, `HtmlHelper.GenerateLink`, etc will generate a query string parameter like `?foo=System.String%5B%5D`, rather than the expected `?foo=a&foo=b`. Do you get different results if you specify html attributes? – Crescent Fresh May 31 '10 at 04:34
  • @Crescent - sorry I misunderstood. The solution I propose adds each of the elements of the array individually to the RouteValueDictionary, avoiding the problem. The serializer iterates over the properties of the object adding them as key/value pairs to the dictionary internally. This is obviously **not** what you want for an array. See my other answer (quoted above by @Guy) for a helper extension that handles IEnumerables differently. Note that I don't use this -- typically I'll use post and Phil Haack's method of binding lists of objects. – tvanfosson May 31 '10 at 13:17
  • Perfect, but not working on dotnet Core. In dotnet core i got escaped string. https://stackoverflow.com/questions/42461590/asp-net-core-routing-with-routevaluedictionary – Petr Tomášek Feb 26 '17 at 09:06
4

This really annoyed me so with inspiration from Scott Hanselman I wrote the following (fluent) extension method:

public static RedirectToRouteResult WithRouteValue(
    this RedirectToRouteResult result, 
    string key, 
    object value)
{
    if (value == null)
        throw new ArgumentException("value cannot be null");

    result.RouteValues.Add(key, value);

    return result;
}

public static RedirectToRouteResult WithRouteValue<T>(
    this RedirectToRouteResult result, 
    string key, 
    IEnumerable<T> values)
{
    if (result.RouteValues.Keys.Any(k => k.StartsWith(key + "[")))
        throw new ArgumentException("Key already exists in collection");

    if (values == null)
        throw new ArgumentNullException("values cannot be null");

    var valuesList = values.ToList();

    for (int i = 0; i < valuesList.Count; i++)
    {
        result.RouteValues.Add(String.Format("{0}[{1}]", key, i), valuesList[i]);
    }

    return result;
}

Call like so:

return this.RedirectToAction("Index", "Home")
           .WithRouteValue("id", 1)
           .WithRouteValue("list", new[] { 1, 2, 3 });
dav_i
  • 27,509
  • 17
  • 104
  • 136
2

Another solution that just came to my mind:

string url = "/Controller/Action?iVal=5&str=" + string.Join("&str=", strArray); 

This is dirty and you should test it before using it, but it should work nevertheless. Hope this helps.

ViRuSTriNiTy
  • 5,017
  • 2
  • 32
  • 58
Krisztián Balla
  • 19,223
  • 13
  • 68
  • 84
1

There is a library called Unbinder, which you can use to insert complex objects into routes/urls.

It works like this:

using Unbound;

Unbinder u = new Unbinder();
string url = Url.RouteUrl("routeName", new RouteValueDictionary(u.Unbind(YourComplexObject)));
Krisztián Balla
  • 19,223
  • 13
  • 68
  • 84
0

This is a HelperExtension solving array and IEnumerable properties troubles :

public static class AjaxHelperExtensions
{
    public static MvcHtmlString ActionLinkWithCollectionModel(this AjaxHelper ajaxHelper, string linkText, string actionName, object model, AjaxOptions ajaxOptions, IDictionary<string, object> htmlAttributes)
    {
        var rv = new RouteValueDictionary();

        foreach (var property in model.GetType().GetProperties())
        {
            if (typeof(ICollection).IsAssignableFrom(property.PropertyType))
            {
                var s = ((IEnumerable<object>)property.GetValue(model));
                if (s != null && s.Any())
                {
                    var values = s.Select(p => p.ToString()).Where(p => !string.IsNullOrEmpty(p)).ToList();
                    for (var i = 0; i < values.Count(); i++)
                        rv.Add(string.Concat(property.Name, "[", i, "]"), values[i]);
                }
            }
            else
            {
                var value = property.GetGetMethod().Invoke(model, null) == null ? "" : property.GetGetMethod().Invoke(model, null).ToString();
                if (!string.IsNullOrEmpty(value))
                    rv.Add(property.Name, value);
            }
        }
        return System.Web.Mvc.Ajax.AjaxExtensions.ActionLink(ajaxHelper, linkText, actionName, rv, ajaxOptions, htmlAttributes);
    }
}
GGO
  • 2,678
  • 4
  • 20
  • 42
-7

I'd use POST for an array. Aside from being ugly and an abuse of GET, you risk running out of URL space (believe it or not).

Assuming a 2000 byte limit. The query string overhead (&str=) reduces you to ~300 bytes of actual data (assuming the rest of the url is 0 bytes).

Greg Dean
  • 29,221
  • 14
  • 67
  • 78