Practical .NET

Performing Multiple Actions from a Single ASP.NET MVC Form

Peter responds to a question: How, in an ASP.NET MVC form, can the user be given two submit Buttons that do two different things?

In addition to doing development work for my clients and writing these articles, I teach and write courses for Learning Tree International. One of my class participants got in touch with me recently with a question about implementing an ASP.NET form that performed two different actions, each tied to a different submit Button. One Button would allow the user to save their current work; the other Button would also save the current work … and then go on to mark the work as "completed." His question was simple: How could he make this work?

Working with HTML
Of course, in a desktop application this is a trivial problem, but, because the way HTML works, it's a little more interesting in a Web page. I'll begin, therefore, by looking at how your HTML will be processed in the browser and on the server.

Here's a page that represents the problem as it appears in Visual Studio:

@Html.BeginForm()
  @Html.EditorFor(c => c.FirstName)
  ...more fields...  
  <input type="submit" name="SaveWork" id="SaveWork" value="Save Your Work" />
  <input type="submit" name="SaveWork" id="FinishWork" value="Complete" />
</form>

The resulting HTML that appears in the browser would look something like this:

<form action="/Customer/Update" method="post">System.Web.Mvc.Html.MvcForm
  <input class="text-box single-line" id="FirstName" name="FirstName" type="text" 
    value="Peter" />
  ...more fields...
  <input type="submit" name="SaveWork" id="SaveWork" value="Save Your Work" />
  <input type="submit" name="SaveWork" id="FinishWork" value="Complete" />
</form>

When the user clicks either of the submit Buttons, the browser gathers up all the values from the input elements on the page and sends the results back to the server tagged with the HTML POST verb. The URL the values are sent to is controlled by the action attribute on the form tag: /Customer/Update, in this case (and the HTTP verb used is controlled by the form's method attribute). The values are sent to the server as name/value pairs with each value tied to the name of the HTML element from which it came (in my example, the value "Peter" would be tied to the name "FirstName").

The issue is that, even though you want the user to do two different things (and, therefore, have two different action methods on the server), there can only be one action associated with the form.

It might seem obvious that the solution is to use the ASP.NET MVC ActionLink helper to add an anchor tag to the page. You can, after all, associate any URL you want with an ActionLink so it could point to either one of the server's two processing option action methods. However, when the user clicks on an anchor tag, the browser doesn't gather the values up from the page and send them to the server: The browser just issues a request for the URL in the ActionLink (it also uses the GET verb rather than the POST verb, but that doesn't really affect this problem). Because the data from the page isn't sent to the server with an ActionLink, your action method won't have access to the values on the page. In this example, it means I can't save the user's work.

Potential Solutions
One solution is to "Ajaxify" the page by tying the Buttons to JavaScript code that will communicate with WebMethods back at the server. An alternative to this option is to use JavaScript associated with each Button to rewrite the form's action attribute before submitting. However, there's no other JavaScript on the site (other than whatever might be generated by ASP.NET MVC), so that solution seems like overkill.

The answer I prefer is tied to the way the ASP.NET MVC model binder deals with the name/value pairs when they arrive at the server. The model binder uses the name associated with each value to slot the value into parameters in the action method that MVC invokes. For my sample page, the value "Peter" will be passed to the FirstName parameter in this method:

[HttpPost]
public ActionResult Update(string FirstName)
{

Alternatively (and assuming that my Customer object has a property called FirstName), the model binder would slot the value into the FirstName property of the Customer object passed to this version of the method:

[HttpPost]
public ActionResult Update(Customer cust)
{

But, in addition to sending the data from the page, the browser also sends, as a name/value pair, the caption of the submit Button the user clicked to trigger processing. If you look at the two submit Buttons on my sample page, you can see they both have the same value in their name attribute: SaveWork. Regardless of which Button is clicked, therefore, a name/value pair with the name of "SaveWork" will be sent to the server. The value tied to the name will be the caption on the Button as set in the Button tag's text attribute. I don't want to incorporate the Button on the page as another property on my Customer object so, to catch the value of the Button, I'd write a method with parameters like this:

[HttpPost]
public ActionResult Update(Customer cust, string SaveWork)
{
  ...save the customer data...
  if (SaveWork == "Complete")
  {
    ...mark as complete...
  }

While this solution will work, I'm not a big fan of it. If someone decides to change the caption on the Complete Button, for example, this code will stop working (and if, for example, the application is internationalized, then the caption is guaranteed to change). Instead, I'd set up the Buttons with two different names:

<input type="submit" name="SaveWork" id="SaveWork" value="Save Your Work" />
<input type="submit" name="CompleteWork" id="FinishWork" value="Complete" />

I'd then rewrite my method to accept both Buttons (even though only one Button -- the one clicked by the user -- will ever be sent to the server at any one time). In my code, I'd check to see which Button was set to determine which button the user clicked and base my processing on that. The code for that solution looks like this:

[HttpPost]
public ActionResult Index(Customer cust, string SaveWork, string CompleteWork)
{
  ...save the customer data...
  if (!string.IsNullOrWhiteSpace(CompleteWork))
  {
    ...mark as complete...
  }

This solution can, of course, be extended for as many submit Buttons as you wish to put on the form. And, of course, more Buttons translates into the most important criteria for any project I work on: More billable hours!

About the Author

Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter tweets about his VSM columns with the hashtag #vogelarticles. His blog posts on user experience design can be found at http://blog.learningtree.com/tag/ui/.

comments powered by Disqus

Featured

  • Compare New GitHub Copilot Free Plan for Visual Studio/VS Code to Paid Plans

    The free plan restricts the number of completions, chat requests and access to AI models, being suitable for occasional users and small projects.

  • Diving Deep into .NET MAUI

    Ever since someone figured out that fiddling bits results in source code, developers have sought one codebase for all types of apps on all platforms, with Microsoft's latest attempt to further that effort being .NET MAUI.

  • Copilot AI Boosts Abound in New VS Code v1.96

    Microsoft improved on its new "Copilot Edit" functionality in the latest release of Visual Studio Code, v1.96, its open-source based code editor that has become the most popular in the world according to many surveys.

  • AdaBoost Regression Using C#

    Dr. James McCaffrey from Microsoft Research presents a complete end-to-end demonstration of the AdaBoost.R2 algorithm for regression problems (where the goal is to predict a single numeric value). The implementation follows the original source research paper closely, so you can use it as a guide for customization for specific scenarios.

  • Versioning and Documenting ASP.NET Core Services

    Building an API with ASP.NET Core is only half the job. If your API is going to live more than one release cycle, you're going to need to version it. If you have other people building clients for it, you're going to need to document it.

Subscribe on YouTube