46

I need multiple submit buttons to perform different actions in the controller.

I saw an elegant solution here: How do you handle multiple submit buttons in ASP.NET MVC Framework? With this solution, action methods can be decorated with a custom attribute. When the routes are processed a method of this custom attribute checks if the attribute's property matches the name of the clicked submit button.

But in MVC Core (RC2 nightly build) I have not found ActionNameSelectorAttribute (I also searched the Github repository). I found a similar solution which uses ActionMethodSelectorAttribute (http://www.dotnetcurry.com/aspnet-mvc/724/handle-multiple-submit-buttons-aspnet-mvc-action-methods).

ActionMethodSelectorAttribute is available but the method IsValidForRequest has a different signature. There is a parameter of type RouteContext. But I could not find the post data there. So I have nothing to compare with my custom attribute property.

Is there a similar elegant solution available in MVC Core like the ones in previous MVC versions?

Community
  • 1
  • 1
noox
  • 789
  • 1
  • 6
  • 19
  • 1
    What might work, depending on if you like this or not, would be to change the actual URL of the
    element using client side (aka: JQuery). On click even, figure out which button is clicked, changed the [action] attribute of the
    element and submit() the
    .
    – Vlince Apr 11 '16 at 18:22

4 Answers4

82

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.)
Will Ray
  • 10,621
  • 3
  • 46
  • 61
  • Perfect! Seems like I have to look into HTML5 in more detail. Strange is: I had the form on the Index-Action. So: `/AreaName/ControllerName`. After clicking the button I got: `/AreaName/ButtonFormActionName`. controller part got lost. When I start from `/AreaName/ControllerName/ActionName` it works. – noox Apr 11 '16 at 19:51
  • 3
    Ahh, good catch! You will need to use the `Url.Action(actionName, controllerName)` helper method to generate the proper target for the `formaction` attribute in that case. – Will Ray Apr 11 '16 at 20:00
  • Thanks for the hint. Works now also on the Index action! – noox Apr 11 '16 at 20:14
  • 1
    perfect solution, but one pitfal, if we use your way, and irst action is /post/add ,the second one is /post/addcontinue . if user clicks on addcontinue button and the page has validation problema, so we send back the user the form and asks him to correct the errors. but since we return addcontinue action, page url changes to /post/addcontinue , so if user refreshes the page ,the browser makes a get request to /post/addcontinue , and we done have any get action for post/addcontinue. what is the best solution to keep the page url consistent with /post/add in both buttons. – Mohammad Akbari Apr 13 '16 at 07:58
  • 2
    @MohammadAkbari this is a problem inherent to having multiple submit buttons, and unfortunately also occurred with the previous answer for MVC4 and 5. There are a couple of different ways you could achieve what you are looking for, but it depends on the use case. AJAX form submissions would be one option, for example. – Will Ray Apr 13 '16 at 13:59
  • You can use one action and switch multiple posts easily by adding a string parameter named "submit" or same as the name of the button to the post action. Check this http://www.binaryintellect.net/articles/c69d78a3-21d7-416b-9d10-6b812a862778.aspx – Damitha Jun 30 '17 at 15:28
  • 1
    This works in ASP.NET Core 2.0 MVC. It makes using multiple form sections so much easier. It also simplifies hidden fields for ModelView data used between calls. Well done. I had no idea the formaction field for buttons existed. I was having a problem where model fields in forms for different part of the model were cleared as only single form data is retained between posts. – Peter Suwara May 31 '18 at 15:54
50

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".

Shaun Luttin
  • 133,272
  • 81
  • 405
  • 467
  • 2
    hey shuan, one thing i noticed about this is if i validate the model and its incorrect the url changes to the action name. any idea how to address this? something like: { ModelState.AddModelError(string.Empty, "we have an issue here with data in the form"); } return View("Edit", editModel); – nologo May 26 '17 at 02:00
  • Maybe the answer @nologo is to just Redirect? I'm not sure. – Jess Apr 15 '22 at 17:36
6

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.

Jess
  • 23,901
  • 21
  • 124
  • 145
  • I found this no longer works in ASP for .Net 6. Worked in 5 then stopped when I upgraded the project. Only get a null passed for the named parameter. – iCollect.it Ltd Jan 04 '22 at 22:43
  • Hi @GoneCoding, it is working for me in .NET 6. – Jess Apr 15 '22 at 17:52
  • I was using this technique again in .NET 6. One issue I ran into is that you have to not use a hidden input for the button value you are posting. The ModelState will get in your way. – Jess Apr 15 '22 at 18:06
0

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.
Jess
  • 23,901
  • 21
  • 124
  • 145