ASP.NET MVC Core/6: Multiple submit buttons

You can use the HTML5 formaction attribute for this, instead of routing it server-side.

<form action="" method="post">
    <input type="submit" value="Option 1" formaction="DoWorkOne" />
    <input type="submit" value="Option 2" formaction="DoWorkTwo"/>
</form>

Then simply have controller actions like this:

[HttpPost]
public IActionResult DoWorkOne(TheModel model) { ... }

[HttpPost]
public IActionResult DoWorkTwo(TheModel model) { ... }

A good polyfill for older browsers can be found here.

Keep in mind that...

  1. The first submit button will always be chosen when the user presses the carriage return.
  2. If an error - ModelState or otherwise - occurs on the action that was posted too, it will need to send the user back to the correct view. (This is not an issue if you are posting through AJAX, though.)

I have done this before and in the past I would have posted the form to different controller actions. The problem is, on a server side validation error you are either stuck with:

  1. return View(vm) leaves the post action name in the url… yuck.
  2. return Redirect(...) requires using TempData to save the ModelState. Also yuck.

Here is what I chose to do.

  1. Use the name of the button to bind to a variable on POST.
  2. The button value is an enum to distinguish the submit actions. Enum is type safe and works better in a switch statement. ;)
  3. POST to the same action name as the GET. That way you don't get the POST action name in your URL on a server side validation error.
  4. If there is a validation error, rebuild your view model and return View(viewModel), following the proper PGR pattern.

Using this technique, there is no need to use TempData!

In my use case, I have a User/Details page with an "Add Role" and "Remove Role" action.

Here are the buttons. They can be button instead of input tags... ;)

<button type="submit" class="btn btn-primary" name="SubmitAction" value="@UserDetailsSubmitAction.RemoveRole">Remove Role</button>
<button type="submit" class="btn btn-primary" name="SubmitAction" value="@UserDetailsSubmitAction.AddRole">Add Users to Role</button>

Here is the controller action. I refactored out the switch code blocks to their own functions to make them easier to read. I have to post to 2 different view models, so one will not be populated, but the model binder does not care!

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Details(
    SelectedUserRoleViewModel removeRoleViewModel, 
    SelectedRoleViewModel addRoleViewModel,
    UserDetailsSubmitAction submitAction)
{
    switch (submitAction)
    {
        case UserDetailsSubmitAction.AddRole:
        {
            return await AddRole(addRoleViewModel);
        }
        case UserDetailsSubmitAction.RemoveRole:
        {
            return await RemoveRole(removeRoleViewModel);
        }
        default:
            throw new ArgumentOutOfRangeException(nameof(submitAction), submitAction, null);
    }
}

private async Task<IActionResult> RemoveRole(SelectedUserRoleViewModel removeRoleViewModel)
{
    if (!ModelState.IsValid)
    {
        var viewModel = await _userService.GetDetailsViewModel(removeRoleViewModel.UserId);
        return View(viewModel);
    }

    await _userRoleService.Remove(removeRoleViewModel.SelectedUserRoleId);

    return Redirect(Request.Headers["Referer"].ToString());
}

private async Task<IActionResult> AddRole(SelectedRoleViewModel addRoleViewModel)
{
    if (!ModelState.IsValid)
    {
        var viewModel = await _userService.GetDetailsViewModel(addRoleViewModel.UserId);
        return View(viewModel);
    }

    await _userRoleService.Add(addRoleViewModel);

    return Redirect(Request.Headers["Referer"].ToString());
}

As an alternative, you could post the form using AJAX.


An even better answer is to use jQuery Unobtrusive AJAX and forget about all the mess.

  1. You can give your controller actions semantic names.
  2. You don't have to redirect or use tempdata.
  3. You don't have the post action name in the URL on server side validation errors.
  4. On server side validation errors, you can return a form or simply the error message.

ASP.NET Core 1.1.0 has the FormActionTagHelper that creates a formaction attribute.

<form>
    <button asp-action="Login" asp-controller="Account">log in</button>
    <button asp-action="Register" asp-controller="Account">sign up</button>
</form>

That renders like this:

<button formaction="/Account/Login">log in</button>
<button formaction="/Account/Register">sign up</button>

It also works with input tags that are type="image" or type="submit".