Most people coming to ASP.NET MVC tend to have similar problems. Most of them are related to refreshed understanding of HTML+HTTP limitations. Some are mitigated by ASP.NET MVC (checkboxes), but a lot require custom solutions.

One of the more common problems I get asked about by every second person learning MVC is how to make a form with two independent submit buttons, where both should submit the same data, but a have a different processing logic. For example, form may have a Save Draft button and Publish button.

Until now, I always said that it is possible, Google/StackOverflow and find out. But I just had to build such kind of thing myself, so I finally took some time to find/build the final solution.

So, let’s look at the solutions available online.

StackOverflow question “How do you handle multiple submit buttons in ASP.NET MVC Framework?” is the starting point, but it does not provide a good solution for the situation where you need to send same data for both buttons, except the solution to switch by button name, which is duct-taping.

Post “ASP.NET MVC – Multiple buttons in the same form” by David Findley is much more interesting, since his AcceptParameterAttribute is very similar to my solution. However, this has several (small) shortcomings: first, you have to specify what is actually an action you want to do in an attribute. So even if you name your action “SaveDraft”, you will still need to specify AcceptParameter(Name=“button”, Value=“saveDraft”). Another thing is need to put [ActionName] on your actions, which is understandable, but a bit confusing for people who do not yet know the idea.

So, I wanted to build the solution that would require at most one attribute, and where the name of action method corresponds to the attributes of the button. Also, since <input> value is the thing being shown in the button, and <button> value has issues in IE, I decided to go with name attribute and ignore value completely (which seems to be consistent with David Findley’s commenters).

Now, the solution. Basically, instead of using ActionMethodSelectorAttribute, I am using ActionNameSelectorAttribute, which allows me to pretend the action name is whatever I want it to be. Fortunately, ActionNameSelectorAttribute does not just make me specify action name, instead I can choose whether the current action matches request.

So there is my class (btw I am not too fond of the name):

public class HttpParamActionAttribute : ActionNameSelectorAttribute {
    public override bool IsValidName(ControllerContext controllerContext, string actionName, MethodInfo methodInfo) {
        if (actionName.Equals(methodInfo.Name, StringComparison.InvariantCultureIgnoreCase))
            return true;

        if (!actionName.Equals("Action", StringComparison.InvariantCultureIgnoreCase))
            return false;
        
        var request = controllerContext.RequestContext.HttpContext.Request;
        return request[methodInfo.Name] != null;
    }
}

How to use it? Just have a form similar to this:

<% using (Html.BeginForm("Action", "Post")) { %>
  <!— …form fields… -->
  <input type="submit" name="saveDraft" value="Save Draft" />
  <input type="submit" name="publish" value="Publish" />
<% } %>

and controller with two methods

public class PostController : Controller {
    [HttpParamAction]
    [AcceptVerbs(HttpVerbs.Post)]
    public ActionResult SaveDraft(…) {
        //…
    }

    [HttpParamAction]
    [AcceptVerbs(HttpVerbs.Post)]
    public ActionResult Publish(…) {
        //…
    }
}

As you see, the attribute does not require you to specify anything at all. Also, name of the buttons are translated directly to the method names. Additionally (I haven’t tried that) these should work as normal actions as well, so you can post to any of them directly.

There is some room for improvements (hardcoded “action” as a default action name), but in general I’m satisfied with this solution.

  • Junk

    1.) public ActionResult Login(LoginModel viewModel, string returnUrl)public ActionResult Register(LoginModel viewModel)

    2.)

    Â
    3)
    @using (Html.BeginForm(“Action”, “Secure”)) 

    Thanks for your help.

  • http://blog.ashmind.com Andrey Shchekin

    Do you have any other inputs named login or register in your form?
    (one example might be user name textbox called “login”)

  • Krzysztof Jeske

    === MY SOLUTION ==

    == JavaScript (jQuery): ==

        $(‘form input[data-action]‘).click(function () {        this.form.action = $(this).data(‘action’);    });

    ===

    And that’s it.

  • Lenkowski Andrzej

     This one is good :)

  • Pingback: Multiple submit buttons in a single ASP.NET MVC Form | Blog

  • Flippingemail

    Make sure you are setting the type to type=”button” otherwise it will submit the form.

  • PaweÅ‚

    @David – consider using PRG pattern.

  • sankar

    Hi im a new user  for MVC4.How I will add Cancel button in JSON register form.Once i will click the Cancel  button it will goes to Homepage.AnyOne help me for this…
    My code is………………………..
    Register.cshtml
    @model CapeShop.Web.Models.RegisterModel

    @{
        ViewBag.Title = “Register”;
    }

        @ViewBag.Title.
        Use the form below to create a new account.

        Passwords are required to be a minimum of @Membership.MinRequiredPasswordLength characters in length.

    @*

    *@

    @using (Html.BeginForm((string)ViewBag.FormAction, “Account”)) {
        @Html.ValidationSummary(true, “Account creation was unsuccessful. Please correct the errors and try again.”)

      Â
            Registration Form
          Â
              Â
                    @Html.LabelFor(m => m.UserName)
                    @Html.TextBoxFor(m => m.UserName)
                    @Html.ValidationMessageFor(m => m.UserName)
              Â
             Â
              Â
                    @Html.LabelFor(m => m.Password)
                    @Html.PasswordFor(m => m.Password)
                    @Html.ValidationMessageFor(m => m.Password)
              Â
              Â
                    @Html.LabelFor(m => m.ConfirmPassword)
                    @Html.PasswordFor(m => m.ConfirmPassword)
                    @Html.ValidationMessageFor(m => m.ConfirmPassword)
                     Â
              Â
               Â
                    @Html.LabelFor(m => m.Email)
                    @Html.TextBoxFor(m => m.Email)
                    @Html.ValidationMessageFor(m => m.Email)
                        Â
                         Â
          Â
          Â
         Â
            Cancel
           

         Â
      Â
    }

  • Jaime

    You can expose a property instead of hardcode a default action name like “Action”. Using as 

    [HttpParamAction(Name="Action")]
    public ActionResult …

  • Ty

    Very useful.  Thanks for sharing!

  • Pingback: It’s all or nothing – MVC Action Attributes « Mark Gilbert's Blog

  • Samuel Tremblay

     I have resharper installed and when I use “BeginForm(“Action” …”, I also get an error but it’s an error that don’t prevent the compiler to build the application. For this reason, I put a dummy Action method in my controller with a throw new NotImplementedException to prevent resharper crying.

  • PureDevelopers

    Your using HTML.Begin Form statement does not work. You aren’t specifying a controller name nor are you specifying a valid action, so how does it work?
    What am I missing?

    My controller name is: ClientPayment, so I tried using the following:
    using

    (Html.BeginForm(“Action”, “ClientPayment”, FormMethod.Post))

    which then hits the action result for the button, but then it throws the following error: The view ‘Action’ or its master was not found or no view engine supports the searched locations. The following locations were searched:

    Do you have a fully working example that I could look at?

  • http://blog.ashmind.com Andrey Shchekin

    Interesting. If you return View(), yes, it might assume the View name is Action.
    Try to specify the view name explicitly (or do a redirect).

  • Puredevelopers

    What do you mean by specify the view name explicitly? Specify Action as a view?
    Yes, I am returning a view. Any chance you would put together a sample project with this functionality working?
    I don’t want to do a redirect though, because the model has a grid on it with search filters, and I don’t want to lose those values.

  • http://blog.ashmind.com Andrey Shchekin

    If you do not want to do a redirect, try returning View(“YourViewName”) while making sure view YourViewName exists.
    If you just call View() without specifying view name it may assume your want a view called “Action”, which you probably do not have.

  • Eugene

    +1

  • Thor

    One of the best and most inspirational articles I have read in a long time! Saved me hours of work and presents a beautiful solution! Suggest adding alternative constructors to HttpParamActionAttribute for the action name and maybe method name

  • baker_tony

    Really good, although I added a data-action for the standard action as well, as I’m using Ajax.BeginForm

  • Manish
  • C. Pottinger

    Thank you Andrey,
    This solved my problem perfectly.

  • Lakshman

    Hi,

    I am getting below error

    “An item with the same key has already been added” when i was calling that controller after an event

  • swarmttied

    Great solution! Thanks so much!

  • Sriram

    It’s not working in mvc 3. With the razor framework
    The action name should the view page name right

  • neelsh

    hi Andrey Shchekin,

    It did not work for me. Below is the solution

    –Controller
    ========================

    public class TestController : Controller
    {
    public ActionResult Index()
    {
    return View();
    }

    [HttpPost]
    [AcceptVerbs(HttpVerbs.Post)]
    // [httpParamAction(Name = "Index", Argument = "cancel")]
    public ActionResult Index(string button)
    {
    return View();
    }

    [HttpPost]
    [AcceptVerbs(HttpVerbs.Post)]
    // [httpParamAction(Name = "saveDraft", Argument = "saveDraft")]
    public ActionResult saveDraft(int a, bool? asHtml)
    {
    // Do something here
    return View();
    }

    [HttpPost]
    [AcceptVerbs(HttpVerbs.Post)]
    // [httpParamAction(Name = "publish", Argument = "publish")]
    public ActionResult publish(float b)
    {
    //do something here
    return View();
    }

    public class httpParamActionAttribute : ActionNameSelectorAttribute
    {
    public string Name { get; set; }

    public string Argument { get; set; }
    public override bool IsValidName(ControllerContext controllerContext, string actionName, MethodInfo methodInfo)
    {
    if (actionName.Equals(methodInfo.Name, StringComparison.InvariantCultureIgnoreCase))
    return true;
    if (!actionName.Equals(“Index”, StringComparison.InvariantCultureIgnoreCase))
    return false;
    var request = controllerContext.RequestContext.HttpContext.Request;
    return request[methodInfo.Name] != null;
    }
    }
    }
    }

    my view is as below
    ===================
    @model IEnumerable
    @{
    ViewBag.Title = “Index”;
    }

    @using (Html.BeginForm(“Index”, “Test”))
    {

    }

    If I dont use the attribute [httpParamAction(Name..)], whenever I click on any button, it goes only to below menthod

    [HttpPost]

    public ActionResult Index(string button)
    {

    }
    but if I use this attribute it throws error as below

    The current request for action ‘Index’ on controller type ‘TestController’ is ambiguous between the following action methods:
    System.Web.Mvc.ActionResult saveDraft(Int32, System.Nullable`1[System.Boolean]) on type TestMVC.Controllers.TestController
    System.Web.Mvc.ActionResult Index(System.String) on type TestMVC.Controllers.TestController
    .
    I tried all the possible solutions discussed here, but it did not help.
    .

    .

  • Kalle

    Works great for me in an Umbraco-SurfaceController. Thanks a million!

  • lavnit

    .net MVC input button click then controller function not a call