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

Tuesday, October 15, 2013

Propagate additional URL parameter through all links generated by MVC

If you need to add some parameters to all links generated by MVC, the likely best way to do this is by adjusting ASP.NET routing. Such behavior might be interesting if you want to append some kind of a token to all links in your application. ASP.NET utilizes this technique when cookieless sessions are activated. The framework stores a session ID inside an URL and extracts it back once the server becomes a request for this URL. Another scenario where you can benefit from the parameter propagation is while making links contain a navigation history of a website visitor. The history may be further utilized by a navigation bar, which would show where the user has come from.

If you want to modify URLs before they get rendered in a view, you should create your own route class and extend the GetVirtualPath function with your customization logic.

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

  public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
  {
    values.Add("token", "E0ECE7CB54EA1");

    return base.GetVirtualPath(requestContext, values);
  }
}

Next you should instruct MVC to use the created route. To this end, replace a default route inside your RegisterRoutes method with the tokenized one.

public static void RegisterRoutes(RouteCollection routes)
{
  routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

  TokenizedRoute route = new TokenizedRoute("{controller}/{action}/{id}", new MvcRouteHandler());
  object defaults = new
    {
      controller = "Home",
      action = "Index",
      id = UrlParameter.Optional
    };
  route.Defaults = new RouteValueDictionary(defaults);

  routes.Add("Default", route);
}

If several routes are used in your application, just write a MapRoute extension method that creates a tokenized route and appends it to your route collection. By using this extension method, you would eliminate duplicates in the source code.

To test the previously created route, I have used a simple view with a form and an action link.

<body>
  @using (Html.BeginForm("Save", "Home"))
  {
    <div>
      @Html.LabelFor(m => m.Name)
      @Html.EditorFor(m => m.Name)
    </div>

    <button type="submit">Send</button>
  }
    
  <div>
    @Html.ActionLink("Go to About", "Index", "About")
  </div>
</body>

As you can notice, I have not passed any route values to the Html.BeginForm and Html.ActionLink helpers. However, if you would open the view in browser, you would see that each rendered URL contains a token parameter.

<form action="/Home/Save?token=E0ECE7CB54EA1" method="post">

<a href="/About?token=E0ECE7CB54EA1">Go to About</a>

The tokenized route has done a good job!

It is important to notice that such behavior is to be expected not only from Html.BeginForm and Html.ActionLink helpers, but also from other HTML helpers that utilize the ASP.NET routing mechanism. To the latter belong Html.Action, Url.Action, Url.RouteUrl, Ajax.BeginForm etc.

Monday, October 7, 2013

Handling multiple submit buttons in MVC

In this post I want to show you how to use multiple submit buttons on a single form and how to handle their requests in a controller.

This kind of scenario might be interesting when you need to process form data in two or more different ways. If you, for example, create a post you would probably expect to have two buttons once finished writing it. The first one is to preview your post and the second one is to publish it. Technically you would end up creating a view with a couple of input fields and two submit buttons. The problem with that approach is that the MVC framework can only link one controller action method to an html form out of the box.

However, you can work around this limitation by creating a custom ActionNameSelector attribute. The idea is to mark submit buttons with the names of controller action methods they are related to and to utilize those names while searching for an appropriate action method on the server side.

Here is my view for creating a post:

<!DOCTYPE html>
<html>
<body>
  @using (Html.BeginForm())
  {
    <div>
      Post title:
      @Html.TextBox("title")
    </div>
            
    if (ViewBag.NotificationMessage != null)
    {
    <div>
      @ViewBag.NotificationMessage
    </div>
    }

    <button type="submit" name="action" value="Save">Save my post</button>
    <button type="submit" name="action" value="Publish">Publish my post</button>
  }
</body>
</html>

As you can see, the buttons' value attributes have been set to the names of controller action methods. As next, we create a custom action name selector attribute which is able to pick an appropriate action method.

public class SubmitActionAttribute : ActionNameSelectorAttribute
{
  public override bool IsValidName(ControllerContext controllerContext, string actionName, MethodInfo methodInfo)
  {
    HttpRequestBase request = controllerContext.RequestContext.HttpContext.Request;
    string action = request["action"];

    return methodInfo.Name.Equals(action, StringComparison.InvariantCultureIgnoreCase);
  }
}

And finally, we implement a controller with Save and Publish methods, which are tagged with the previously created attribute.

public class PostController : Controller
{
  public ActionResult Create()
  {
    // this is an entry point to the creation form
    return View();
  }

  [SubmitAction]
  [HttpPost]
  public ActionResult Save(string title)
  {
    ViewBag.NotificationMessage = string.Format("Post {0} has been saved", title);

    return View("Create");
  }

  [SubmitAction]
  [HttpPost]
  public ActionResult Publish(string title)
  {
    ViewBag.NotificationMessage = string.Format("Post {0} has been published", title);

    return View("Create");
  }
}

Voila! Now you are able to handle multiple submit requests inside your MVC controller.