3

In my ASP.NET MVC 4 application I can filter on multiple tags. In HTML, it looks like this:

<form>
  <label>
    <input type="checkbox" name="tag" value="1">One
  </label>
  <label>
    <input type="checkbox" name="tag" value="2">Two
  </label>
  <label>
    <input type="checkbox" name="tag" value="3">Three
  </label>
  <input type="submit" name="action" value="Filter">
</form>

When checking the first and third checkbox, the querystring is serialized as ?tag=1&tag=3 and my controller nicely passes an object with the type of the following class:

// Filter class
public class Filter { 
    public ICollection<int> tag { get; set; }
}

// Controller method
public ActionResult Index(AdFilter filter)
{
    string url = Url.Action("DoFilter", filter);
    // url gets this value:
    // "/controller/Index?tag=System.Collections.Generic.List%601%5BSystem.Int32%5D"
    // I would expect this:
    // "/controller/Index?tag=1&tag=3"
    ...
 }

However, a call to Url.Action results in the typename of the collection being serialized, instead of the actual values.

How can this be done?


The standard infrastructure of MVC can handle the multi-keys described as input. Is there not standard infrastructure that can handle it the other way around? Am I missing something?

doekman
  • 18,750
  • 20
  • 65
  • 86
  • Thanks for the answers, but both answers seem to be some kind of a hack to me. I was under the impression that I was doing something wrong. Can't RouteValueDictionary work with "multiple" (`` with the same name) values? It is difficult to imagine Microsoft came up with an abstraction that didn't incorporate the whole HTML FORM model... – doekman Feb 13 '15 at 11:40

4 Answers4

4

You can do it in the following way:

string url = Url.Action("DoFilter", TypeHelper2.ObjectToDictionary(filter) );

The TypeHelper2.ObjectToDictionary is a modified version of an internal method of .NET, and can be found in this two file gist.

Changed behavior: when an item implements IEnumerable, for each item an entry is created in the returned dictionary with as key "Name[index]" (the index is 0 based). This is possible because the binder of the MVC controller can handle both tag=1&tag=3 and tag[0]=1&tag[1]=3 query strings.

doekman
  • 18,750
  • 20
  • 65
  • 86
Joanvo
  • 5,677
  • 2
  • 25
  • 35
  • 1
    I did not know the controller handles the QueryStrings `?tag=1&tag=3` and ?tag[0]=1&tag[1]=3` the same. Internal `Url.Action` converts an `object` to `RouteValueDictionary`. This is not the problem, since the value of the dictionary is still the `IEnumerable`. However, this is also an easy solution because in this conversion you can add an item in the dictionary with it's own key. This solution is pretty transparent, and only needs two files copied from the MVC-project (one file slightly changed). Shall I edit this answer, because this answer lead me to this solution? – doekman Feb 20 '15 at 15:48
  • Here is the [2 file-gist](https://gist.github.com/doekman/b5fbe3944bfe0283670e) solution I made. – doekman Feb 20 '15 at 16:10
3

An easy yet no so elegant solution can be:

  public ActionResult Index(AdFilter filter)
  {
     string parameters = "?";
     foreach (var item in filter.tag)
        {
            parameters += string.Format("tag={0}&", item);
        }
     //trimming the last "&"
     parameters = parameters.TrimEnd(parameters[parameters.Length - 1]);
     string url = Url.Action("DoFilter") + parameters;

  }
Ziv Weissman
  • 4,400
  • 3
  • 28
  • 61
  • I also like this answer, but it's a bit hacky and might be hard to get right. The item needs encoding, and also this code is not taking bookmarks into consideration.. But then again, it is not so elegant, but when there are troubles, it's easy to debug this code. – doekman Feb 20 '15 at 15:51
2

Have upvoted the above 2 answers - they're both perfectly good answers. On this side of things (redirecting to an action or generaring a URL to redirect to) MVC doesn't "like" arrays at all.

There are 2 futher approaches I can see:

1) use TempData to persist an retrieve the array (e.g at the bottom of this article)

2) Write and use a custom model binder for AdFilter

I'd go for the latter myself - testable and more deterministic (also, I don't know how TempData works when you get into a server farm scenario)

Another thing you might want to consider is using something like as the return from the Index action.

return View("DoFilter", new AdFilter(){tag = tag});

(This will return the Dofilter view whilst maintaining the "index?tag[0]=1&tag1=2" url in the browser)

Finally - it feels like you're taking the filter criteria only to send them back to the browser so the browser can then ask again for the filter results. Maybe a better option would be to set the action on the form to post to "DoFilter" from the start:

<form  method="post" action="DoFilter"> 

HTH

Community
  • 1
  • 1
Andrew Hewitt
  • 331
  • 1
  • 6
  • In my case I need a querystring, because I use a [PagedList](https://github.com/troygoode/PagedList). (BTW: I actually upvoted the answers, because the are good answers. However, my question is about MVC handling arrays). – doekman Feb 19 '15 at 10:57
  • Ah - ok - didn't quite get that from your question (thanks for the upvote tho!). Essentially, if you're needing a click-able URL (using a GET via links on a pager?) then you're trying to replicate "when a form gets posted to a url" (from your first example), with doing a "get with just a basic url" - this won't fly - MVC doesn't handle that sort of thing naturally (see other answers). If it was me, and I had to support this, then I'd definitely create a custom model binder and use that on the way in (so you have a PagedAdFilterModel and a binder for it that pulls the HTTPGET to pieces). HTH – Andrew Hewitt Feb 19 '15 at 11:26
1

You using the overload of Url.Action() that accepts a object to generate the route parameters. Internally this works by using reflection to build a dictionary of each properties name and its .ToString() value.

In the case of a simple value type this might be Name = "doekman" (which becomes /DoFilter?Name=doekman), but in the case of a property which is a complex object or a collection it returns the typename (i.e. the .ToString() value). No recursion is done and this makes sense because query strings have length limits so if recursion was done on complex objects and collections, you would soon exceed the limit and throw an exception if the collection contained a lot of items (and it would create a really ugly query string).

So in answer to How can this be done? You can't unless you manually generate the RouteValueDictionary (or write your own helper to generate the query string)

  • One other option is to write my own `Url.Action()` that also can handle `IEnumerable`. Is `Url.Action()` already open-sourced? I can't find it on GitHub... – doekman Feb 19 '15 at 10:58
  • 1
    Yes (that's what I meant by create you own helper). The source code for the current UrlHelper is [here](http://aspnetwebstack.codeplex.com/SourceControl/latest#src/System.Web.Mvc/UrlHelper.cs) and you can download it all. –  Feb 19 '15 at 11:08