How do you handle multiple submit buttons in ASP.NET MVC Framework?

Solution 1:

Here is a mostly clean attribute-based solution to the multiple submit button issue based heavily on the post and comments from Maarten Balliauw.

[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class MultipleButtonAttribute : ActionNameSelectorAttribute
{
    public string Name { get; set; }
    public string Argument { get; set; }

    public override bool IsValidName(ControllerContext controllerContext, string actionName, MethodInfo methodInfo)
    {
        var isValidName = false;
        var keyValue = string.Format("{0}:{1}", Name, Argument);
        var value = controllerContext.Controller.ValueProvider.GetValue(keyValue);

        if (value != null)
        {
            controllerContext.Controller.ControllerContext.RouteData.Values[Name] = Argument;
            isValidName = true;
        }

        return isValidName;
    }
}

razor:

<form action="" method="post">
 <input type="submit" value="Save" name="action:Save" />
 <input type="submit" value="Cancel" name="action:Cancel" />
</form>

and controller:

[HttpPost]
[MultipleButton(Name = "action", Argument = "Save")]
public ActionResult Save(MessageModel mm) { ... }

[HttpPost]
[MultipleButton(Name = "action", Argument = "Cancel")]
public ActionResult Cancel(MessageModel mm) { ... }

Update: Razor pages looks to provide the same functionality out of the box. For new development, it may be preferable.

Solution 2:

Give your submit buttons a name, and then inspect the submitted value in your controller method:

<% Html.BeginForm("MyAction", "MyController", FormMethod.Post); %>
<input type="submit" name="submitButton" value="Send" />
<input type="submit" name="submitButton" value="Cancel" />
<% Html.EndForm(); %>

posting to

public class MyController : Controller {
    public ActionResult MyAction(string submitButton) {
        switch(submitButton) {
            case "Send":
                // delegate sending to another controller action
                return(Send());
            case "Cancel":
                // call another action to perform the cancellation
                return(Cancel());
            default:
                // If they've submitted the form without a submitButton, 
                // just return the view again.
                return(View());
        }
    }

    private ActionResult Cancel() {
        // process the cancellation request here.
        return(View("Cancelled"));
    }

    private ActionResult Send() {
        // perform the actual send operation here.
        return(View("SendConfirmed"));
    }

}

EDIT:

To extend this approach to work with localized sites, isolate your messages somewhere else (e.g. compiling a resource file to a strongly-typed resource class)

Then modify the code so it works like:

<% Html.BeginForm("MyAction", "MyController", FormMethod.Post); %>
<input type="submit" name="submitButton" value="<%= Html.Encode(Resources.Messages.Send)%>" />
<input type="submit" name="submitButton" value="<%=Html.Encode(Resources.Messages.Cancel)%>" />
<% Html.EndForm(); %>

and your controller should look like this:

// Note that the localized resources aren't constants, so 
// we can't use a switch statement.

if (submitButton == Resources.Messages.Send) { 
    // delegate sending to another controller action
    return(Send());

} else if (submitButton == Resources.Messages.Cancel) {
     // call another action to perform the cancellation
     return(Cancel());
}

Solution 3:

You can check the name in the action as has been mentioned, but you might consider whether or not this is good design. It is a good idea to consider the responsibility of the action and not couple this design too much to UI aspects like button names. So consider using 2 forms and 2 actions:

<% Html.BeginForm("Send", "MyController", FormMethod.Post); %>
<input type="submit" name="button" value="Send" />
<% Html.EndForm(); %>

<% Html.BeginForm("Cancel", "MyController", FormMethod.Post); %>
<input type="submit" name="button" value="Cancel" />
<% Html.EndForm(); %>

Also, in the case of "Cancel", you are usually just not processing the form and are going to a new URL. In this case you do not need to submit the form at all and just need a link:

<%=Html.ActionLink("Cancel", "List", "MyController") %>

Solution 4:

Eilon suggests you can do it like this:

If you have more than one button you can distinguish between them by giving each button a name:

<input type="submit" name="SaveButton" value="Save data" />
<input type="submit" name="CancelButton" value="Cancel and go back to main page" />

In your controller action method you can add parameters named after the HTML input tag names:

public ActionResult DoSomeStuff(string saveButton, string
cancelButton, ... other parameters ...)
{ ... }

If any value gets posted to one of those parameters, that means that button was the one that got clicked. The web browser will only post a value for the one button that got clicked. All other values will be null.

if (saveButton != null) { /* do save logic */ }
if (cancelButton != null) { /* do cancel logic */ }

I like this method as it does not rely on the value property of the submit buttons which is more likely to change than the assigned names and doesn't require javascript to be enabled

See: http://forums.asp.net/p/1369617/2865166.aspx#2865166

Solution 5:

Just written a post about that: Multiple submit buttons with ASP.NET MVC:

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;
    }
} 

To use just define a form like 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.