Wednesday, October 30, 2013

Passing an array as a route value within Html.ActionLink

MVC framework has a limitation when it comes to passing an array object as a route value to such Html helpers as Html.ActionLink, Url.Action and Html.BeginForm. Let's assume you want to render a link to a certain action method and pass in a list of strings. You would probably try to achieve that with the following line of code.

@Html.ActionLink("Click on me", "Index", new { names = new string[] { "foo", "boo", "moo"} })

The problem with this is that the provided array values are not included into the generated URL.

.../Index?names=System.String%5B%5D

You would rather expect the URL to look like this.

.../Index?names=foo&names=boo&names=moo

The expected behavior can be achieved by extending ASP.NET routing. In my previous post, I showed how to append certain parameters to all links generated by the MVC. The described approach can be also used to extend a URL with array based data which comes as a part of route values. To do that, create a custom route and override the GetVirtualPath function as follows.

public class ArrayAwareRoute : Route
{
  public ArrayAwareRoute(string url, IRouteHandler routeHandler)
    : base(url, routeHandler)
  {
  }

  public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary routeValues)
  {
    NameValueCollection enumerables = new NameValueCollection();

    foreach (KeyValuePair<string, object> routeValue in routeValues)
    {
      IEnumerable<string> values = routeValue.Value as IEnumerable<string>;

      // collects all enumerable route values
      if (values != null)
      {
        foreach (string value in values)
        {
          enumerables.Add(routeValue.Key, value);
        }
      }
    }

    // removes all enumerable route values so they are not processed by the base class
    foreach (string key in enumerables.AllKeys)
    {
      routeValues.Remove(key);
    }

    // lets the base class generate a URL
    VirtualPathData path = base.GetVirtualPath(requestContext, routeValues);

    Uri requestUrl = requestContext.HttpContext.Request.Url;
    if (enumerables.Count > 0 && requestUrl != null && path != null)
    {
      string authority = requestUrl.GetLeftPart(UriPartial.Authority);
      Uri authorityUri = new Uri(authority);
      Uri url = new Uri(authorityUri, path.VirtualPath);
      UriBuilder builder = new UriBuilder(url);

      NameValueCollection queryString = HttpUtility.ParseQueryString(builder.Query);

      // extends the URL's query string with the provided enumerable route values
      queryString.Add(enumerables);

      builder.Query = queryString.ToString();

      path.VirtualPath = builder.Uri.PathAndQuery.TrimStart('/');
    }

    return path;
  }
}

Next, register the created route within your RegisterRoutes method. In the previous post, you can find more details on how to do that.

If you now open your view in the browser, you will notice that MVC renders the URL as expected.

.../Index?names=foo&names=boo&names=moo

1 comment:

  1. Thank you so much for this! Worked the very first time. You saved me hours of work and frustration!

    ReplyDelete