How to work with RouteValues with multiple values

2019-07-21 15:38发布

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?

4条回答
干净又极端
2楼-- · 2019-07-21 16:20

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.

查看更多
霸刀☆藐视天下
3楼-- · 2019-07-21 16:26

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;

  }
查看更多
该账号已被封号
4楼-- · 2019-07-21 16:29

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)

查看更多
我欲成王,谁敢阻挡
5楼-- · 2019-07-21 16:36

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

查看更多
登录 后发表回答