Supporting multiple submit buttons on an ASP.NET MVC view

Edit on GitHub

Multiple buttons on an ASP.NET MVC view A while ago, I was asked for advice on how to support multiple submit buttons in an ASP.NET MVC application, preferably without using any JavaScript. The idea was that a form could contain more than one submit button issuing a form post to a different controller action.

The above situation can be solved in many ways, one a bit cleaner than the other. For example, one could post the form back to one action method and determine which method should be called from that action method. Good solution, however: not standardized within a project and just not that maintainable… A better solution in this case was to create an ActionNameSelectorAttribute.

Whenever you decorate an action method in a controller with the ActionNameSelectorAttribute (or a subclass), ASP.NET MVC will use this attribute to determine which action method to call. For example, one of the ASP.NET MVC ActionNameSelectorAttribute subclasses is the ActionNameAttribute. Guess what the action name for the following code snippet will be for ASP.NET MVC:

[code:c#]

public class HomeController : Controller
{
    [ActionName("Index")]
    public ActionResult Abcdefghij()
    {
        return View();
    }
}

[/code]

That’s correct: this action method will be called Index instead of Abcdefghij. What happens at runtime is that ASP.NET MVC checks the ActionNameAttribute and asks if it applies for a specific request. Now let’s see if we can use this behavior for our multiple submit button scenario.

kick it on DotNetKicks.com

The view

Since our view should not be aware of the server-side plumbing, we can simply create a view that looks like this.

[code:c#]

<%@ Page Language="C#" Inherits="System.Web.Mvc.ViewPage<MvcMultiButton.Models.Person>" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
    <title>Create person</title>
    <script src="<%=Url.Content("~/Scripts/MicrosoftAjax.js")%>" type="text/javascript"></script>
    <script src="<%=Url.Content("~/Scripts/MicrosoftMvcAjax.js")%>" type="text/javascript"></script>
</head>
<body>

    <% Html.EnableClientValidation(); %>
    <% using (Html.BeginForm()) {>

        <fieldset>
            <legend>Create person</legend>
            <p>
                <%= Html.LabelFor(model => model.Name) %>
                <%= Html.TextBoxFor(model => model.Name) %>
                <%= Html.ValidationMessageFor(model => model.Name) %>
            </p>
            <p>
                <%= Html.LabelFor(model => model.Email) %>
                <%= Html.TextBoxFor(model => model.Email) %>
                <%= Html.ValidationMessageFor(model => model.Email) %>
            </p>
            <p>
                <input type="submit" value="Cancel" name="action" />
                <input type="submit" value="Create" name="action" />
            </p>
        </fieldset>

    <% } %>

    <div>
        <%=Html.ActionLink("Back to List", "Index") %>
    </div>

</body>
</html>

[/code]

Note the two submit buttons (namely “Cancel” and “Create”), both named “action” but with a different value attribute.

The controller

Our controller should also not contain too much logic for determining the correct action method to be called. Here’s what I propose:

[code:c#]

public class HomeController : Controller
{
    public ActionResult Index()
    {
        return View(new Person());
    }

    [HttpPost]
    [MultiButton(MatchFormKey="action", MatchFormValue="Cancel")]
    public ActionResult Cancel()
    {
        return Content("Cancel clicked");
    }

    [HttpPost]
    [MultiButton(MatchFormKey = "action", MatchFormValue = "Create")]
    public ActionResult Create(Person person)
    {
        return Content("Create clicked");
    }
}

[/code]

Some things to note:

  • There’s the Index action method which just renders the view described previously.
  • There’s a Cancel action method which will trigger when clicking the Cancel button.
  • There’s a Create action method which will trigger when clicking the Create button.

Now how do these last two work… You may also have noticed the MultiButtonAttribute being applied. We’ll see the implementation in a minute. In short, this is a subclass for the ActionNameSelectorAttribute, triggering on the parameters MatchFormKey and MatchFormValues. Now let’s see how the MultiButtonAttribute class is built…

The MultiButtonAttribute class

Now do be surprised of the amount of code that is coming…

[code:c#]

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

    public override bool IsValidName(ControllerContext controllerContext, string actionName, MethodInfo methodInfo)
    {
        return controllerContext.HttpContext.Request[MatchFormKey] != null &&
            controllerContext.HttpContext.Request[MatchFormKey] == MatchFormValue;
    }
}

[/code]

When applying the MultiButtonAttribute to an action method, ASP.NET MVC will come and call the IsValidName method. Next, we just check if the MatchFormKey value is one of the request keys, and the MatchFormValue matches the value in the request. Simple, straightforward and re-usable.

kick it on DotNetKicks.com

This is an imported post. It was imported from my old blog using an automated tool and may contain formatting errors and/or broken images.

Leave a Comment

avatar

17 responses

  1. Avatar for Christian Wei&#223;
    Christian Wei&#223; November 27th, 2009

    hi maarten!
    that's a really nice approach. I've also tried to come up with a good solution for this problem, but my ideas were far more complex :-)

    thanks for this post!

    regards!

  2. Avatar for Miguel
    Miguel November 27th, 2009

    Hi Maarten. This is very elegant, bravo! I did something similar where I had a variable number of buttons that could submit a form, I ended up delegating the button html generation in the form to an HtmlHelper in the view and in the controller I had a method parse out the form value to return the correct action which I had defined elsewhere using an enumeration. I think my method works better for what I needed to do since the button pressed was just information that went to my database but when you can justify a button altering the execution path of your controller contingent on the form submission this should come in handy. Thanks.

  3. Avatar for Thomas Eyde
    Thomas Eyde November 27th, 2009

    Nice approach! I learned something this evening.

    However, your approach has one caveat: The action selector now depends on the button value, which, I think, is strictly a view concern. When the whimsy product owner changes his mind, he break our code. And so does localization.

    I need this functionality for my list where I can delete an individual row. What I'd like is to have a button with the row-id embedded in the name:

    <input type="submit" value="Delete" name="deleteRow:99" />

    And have this routed to:

    [HttpPost]
    [MultiButton(Key = "deleteRow")]
    [BindMultiButtonTo("id")]
    public ActionResult DeleteRow(int id, Whatever form) {}

    Maybe it's possible to combine those with some crazy inheritance chain:

    [HttpPost]
    [MultiButton(Key = "deleteRow", BindTo = "id")]
    public ActionResult DeleteRow(int id, Whatever form) {}

    That would be cool!

  4. Avatar for Thomas Eyde
    Thomas Eyde November 27th, 2009

    Inspired by your approach, I decided to take it a little further to satisfy my needs. And I have to tell you, this is way more elegant than my initial approach.

    Now I can write actions in the following manner:

    [HttpPost]
    [MultiButton(Name = "delete", Argument = "id")]
    public ActionResult Delete(string id)
    {
    var response = System.Web.HttpContext.Current.Response;
    response.Write("Delete action was invoked with " + id);
    return View();
    }

    The button can be any of:

    <input type="submit" value="not important" name="delete" />
    <input type="submit" value="not important" name="delete[b]:id[/b]" />

    And your modified attribute is as follows:

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

    public override bool IsValidName(ControllerContext controllerContext, string actionName, MethodInfo methodInfo)
    {
    var key = ButtonKeyFrom(controllerContext);
    var keyIsValid = IsValid(key);

    if (keyIsValid)
    {
    UpdateValueProviderIn(controllerContext, ValueFrom(key));
    }

    return keyIsValid;
    }

    private string ButtonKeyFrom(ControllerContext controllerContext)
    {
    var keys = controllerContext.HttpContext.Request.Params.AllKeys;
    return keys.FirstOrDefault(KeyStartsWithButtonName);
    }

    private static bool IsValid(string key)
    {
    return key != null;
    }

    private static string ValueFrom(string key)
    {
    var parts = key.Split(":".ToCharArray());
    return parts.Length < 2 ? null : parts[1];
    }

    private void UpdateValueProviderIn(ControllerContext controllerContext, string value)
    {
    if (string.IsNullOrEmpty(Argument)) return;
    controllerContext.Controller.ValueProvider[Argument] = new ValueProviderResult(value, value, null);
    }

    private bool KeyStartsWithButtonName(string key)
    {
    return key.StartsWith(Name, StringComparison.InvariantCultureIgnoreCase);
    }
    }

  5. Avatar for dario-g
    dario-g November 27th, 2009

    What about multi-language application. Value of submit button will be different but action should be the same.

    In my solution only checks name of button.
    (In polish) http://dario-g.com/Wykorzys...

  6. Avatar for maartenba
    maartenba November 27th, 2009

    You could simply add support for localized buttons in the MultiButtonAttribute and take care of translated values in there.

  7. Avatar for maartenba
    maartenba November 27th, 2009

    Good one and still very elegant, looks good!

  8. Avatar for Peter Mounce
    Peter Mounce November 27th, 2009

    Why not make the cancel button a link to the cancel-action and style it so it looks like a button?

  9. Avatar for maartenba
    maartenba November 27th, 2009

    Maybe you do want to have teh data in the form and do soemthing with that prior to really canceling teh action. Or other types of buttons.

  10. Avatar for Feroze
    Feroze January 5th, 2010

    MultiButton required a reference. Where is ActionNameSelectorAttribute class?

  11. Avatar for maartenba
    maartenba January 5th, 2010

    In ASP.NET MVC 2

  12. Avatar for Sam
    Sam January 17th, 2011

    Great post! Works perfectly for me.

  13. Avatar for Selvam Sivam
    Selvam Sivam November 27th, 2014

    [MultiButton(MatchFormKey="action", MatchFormValue="Cancel")] in this multibutton namespace could be found error coming for me

  14. Avatar for Svys2nov
    Svys2nov February 22nd, 2015

    I'm sorry for my question. I'm new in asp mvc, but what to do in case when you need to call action method of another controller? If I'm not mistaken this code will search action method in current controller.

  15. Avatar for Kalyan Hulhule
    Kalyan Hulhule June 23rd, 2015

    It Work properly for me. Thank you.

  16. Avatar for ShinJamWinWam
    ShinJamWinWam February 22nd, 2016

    The only problem that I have with this is that I still have to have a unique method signature in my controller, for instance having these two ActionResults.

    [HttpPost]

    [ValidateAntiForgeryToken]

    [Authorize(Roles = "Admin, BCNCAdmin, BCNCSubstitution")]

    [MyAnnotations.MultipleButton(Name = "submitButton", Argument = "Save")]

    public ActionResult Edit(DVM.OrderIndexData viewModel){}
    //////////////////////////////////////////////////////////////////////////////////////////////

    [HttpPost]

    [ValidateAntiForgeryToken]

    [Authorize(Roles = "Admin, BCNCAdmin, BCNCSubstitution")]

    [MyAnnotations.MultipleButton(Name = "submitButton", Argument = "Test")]

    public ActionResult Edit(DVM.OrderIndexData viewModel)

    Throws an error

  17. Avatar for Matt Baker
    Matt Baker November 7th, 2016

    Great solution which works perfectly for what I need. However recently controllerContext.HttpContext.Request[MatchFormKey] has always been returning NULL. Any ideas why and how it can be resolved? Thanks!