Saturday, December 24, 2011

Razor, MVC3, Client-Side Validation and Multiple Submit Buttons

In a details view for creating a new model object I wanted to replace the Cancel link with a Cancel button. I wanted both to be buttons as I felt that to be more consistent. I didn’t realize that I would be up for a surprise when I placed two submit buttons into a single form since, of course, not matter what button I would click I would always end up in the same controller method. It was even worse. As client-side validation was enabled, clicking the Cancel button would perform client-side validation and not even make it to the server.

I googled and found a couple of ideas for how to handle multiple submit buttons. For example one type of suggestions is based on implementing a class derived from ActionMethodSelectorAttribute, for example here or here. Another type of suggestions is based on implementing a class derived from ActionNameSelectorAttribute, for example here.

I couldn’t get any of these to work the way I wanted. I found that they would work only when I disabled client-side validation or JavaScript on the client altogether. Only in these cases, and by giving the submit button a name using the ‘name’ attribute, would the button value be included in the request. Therefore this was just a partial solution as I wanted to support both scenarios (JavaScript disabled, JavaScript enabled). I tried the actionmethod attribute on the input element but either I used it incorrectly or the handling of this new HTML 5 element is not yet correctly handled by either the browsers or MVC on the server. I didn’t investigate this in more detail.

Here is the solution that worked for me.

First I implemented a JavaScript function which I placed at the end of the view (.cshtml):

<script type="text/javascript">
   function OnFormButtonClick(action, validate) {
      if (!validate) {
         document.forms[0].noValidate = true;
         document.forms[0].action = '' + action;
         document.forms[0].submit();
      }
}
</script>

The first parameter of this short method is the name of the action (controller method) to be invoked on the server side while the second parameter determines whether or not to validate on the client side. Of course the latter would only kick in when JavaScript is enabled on the client side. If you want to use multiple submit buttons in several place you may want to put this function in a shared file such as ‘_Layout.cshtml’ or similar.

With the JavaScript function in place I then added the two submit buttons to the Razor view (HTML form) as follows:

<p>
   <input type="submit" value="Create" name="button"
    onclick="OnFormButtonClick('Create', true);" />
   <input type="submit" value="Cancel" name="button"
    onclick="OnFormButtonClick('Cancel', false);" />
</p>

This would now work with JavaScript enabled on the client. How about the scenario when JavaScript was disabled? In that case submitting the form would always submit to the same action (‘Create’ in my case) no matter what button was clicked. Some server side code was required to distinguish this. This is the point I added a class which is derived from ActionNameSelectorAttribute:

/// <summary>
/// Attribute that helps MVC with selecting the proper method when multiple submit buttons
/// exist in a single form.
/// </summary>
/// <remarks>The implementation is partially based on  </remarks>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class MultiSubmitButtonAttribute : ActionNameSelectorAttribute {
   public override bool IsValidName(ControllerContext controllerContext, 
                                    string actionName, 
                                    MethodInfo methodInfo) {
      // Implementation derived from:
      // http://blog.maartenballiauw.be/post/2009/11/26/Supporting-multiple-submit-buttons-on-an-ASPNET-MVC-view.aspx
      if (controllerContext.RequestContext.HttpContext.Request["button"] != null) {
         // JavaScript disabled.
         return controllerContext.RequestContext.HttpContext.Request["button"] == methodInfo.Name;
      }

      return actionName.Equals(methodInfo.Name, StringComparison.InvariantCultureIgnoreCase) 
         || actionName.Equals("Action", StringComparison.InvariantCultureIgnoreCase);
   }
}

The first if()-statement in this implementation yields to true only if JavaScript is disabled on the client. In that case the request contains the information about which submit button was clicked and this can be compared to the name of the controller method (bear with me for a little longer to see how this will work). Having this new attribute available I was now able to add it to the controller methods handling the two cases (‘Create’ and ‘Cancel’):

[HttpPost]
[MultiSubmitButton]
public ActionResult Create(FormCollection collection) {
   // TODO: insert your 'Create' logic here
   return RedirectToAction("Index");
}

[HttpPost]
[MultiSubmitButton]
public ActionResult Cancel() {
   return RedirectToAction("Index");
}

When MVC tries to route the action to a controller method it will now also invoke the IsValidName method in the MultiSubmitButtonAttribute class. The two scenarios now work as follows:

Case 1 - JavaScript enabled: The little JavaScript function is executed on the client side and an appropriate action will be requested. The action is simply routed to the controller method of the same name. Depending on the second parameter of the call to the JavaScript function (and subject to server side settings in web.config) client-side validation is executed or not.

Case 2 – JavaScript disabled: The client includes the name of the submit button and the MultiSubmitButtonAttribute helps MVC to select the correct controller method.

Extending HtmlHelper to Simplify View Implementation

There is a way to simplify the view implementation by extending the HtmlHelper class as follows:

public static class HtmlHelperExtensions {
   public static MvcHtmlString SubmitButton<T>(this HtmlHelper<T> helper, string value, string action, bool validate) {
      return new MvcHtmlString(String.Format("<input type=\"submit\" value=\"{0}\" name=\"button\" onclick=\"OnFormButtonClick('{1}', {2});\" />", value, action, validate ? "true" : "false"));
   }
}

With this the submit buttons can be coded like this:

@* Namespace of the HtmlHelper.SubmitButton() implementation: *@
@using Web.Helpers; 

... other code for Razor based view

<p>
   @Html.SubmitButton("Create", "Create", true)
   @Html.SubmitButton("Cancel", "Cancel", false)
</p>

Note that although it appears as if the first two parameters here are duplicated, they are not. The first parameter is the label that appears on the submit button and you may want to localize it. The second parameter is the name of the action which you typically don’t want to change. Even if everything is in the same language (e.g. English) they can be the same but don’t have to.

Automatically Injecting JavaScript

So far the solution requires manually adding the OnFormButtonClick() function either to the Razor view or to have it in some shared file. I would like to remove this by making the HtmlHelper.SubmitButton<T>() implementation a little smarter:

public static class HtmlHelperExtensions {
   public static MvcHtmlString SubmitButton<T>(this HtmlHelper<T> helper, string value, string action, bool validate) {
      var javaScript = string.Empty;
      const string functionName = "_OnFormButtonClicked";
      if (!helper.ViewData.ContainsKey(functionName)) {
         helper.ViewData.Add(functionName, true);
         const string linefeed = " \n\r";
         // Inspiration for the following JavaScript function from:
         // http://www.javascript-coder.com/html-form/html-form-action.phtml
         javaScript = "<script type=\"text/javascript\">" + linefeed
                    + "   function " + functionName + "(action, validate) {" + linefeed
                    + "      if (!validate) {" + linefeed
                    + "         document.forms[0].noValidate = true;" + linefeed
                    + "         document.forms[0].action = '' + action;" + linefeed
                    + "         document.forms[0].submit();" + linefeed
                    + "      }" + linefeed
                    + "   }" + linefeed
                    + "</script>" + linefeed;
      }
      return new MvcHtmlString(String.Format("{0}<input type=\"submit\" value=\"{1}\" name=\"button\" onclick=\"{2}('{3}', {4});\" />", javaScript, value, functionName, action, validate ? "true" : "false"));
   }
}

The basic idea is that if the JavaScript function doesn’t exist yet in the view this code adds it. Then it adds one key to the ViewData. If that key is already present this code assumes the JavaScript function has already been added. With this, the JavaScript function can now be removed from the view and all that is needed is this:

  1. MultiSubmitButtonAttribute applied to appropriate controller methods
  2. Use of HtmlHelper.SubmitButton() in view definition

The JavaScript injection is taken care of automatically. Happy coding!

0 comments:

Post a Comment

All comments, questions and other feedback is much appreciated. Thank you!