Skip navigation

Dealing with Multi-Button Forms in ASP.NET MVC

Attach different operations to distinct submit buttons in MVC forms

Some input HTML forms don't require more than a plain submit button; other forms require submit and reset buttons. The HTML standard syntax covers both of these scenarios effectively. The HTML 4.x standard provides two kinds of special input buttons—Submit and Reset—that browsers know how to deal with. Sometimes, however, you require multiple submit buttons on the same HTML form, and you also have to trigger different operations on the server, depending on which button was clicked. For example, a given input form might display details about a given customer, and the form might also offer two options: Update and Delete.

As you can see, each button is going to trigger a distinct operation and require its own special handling code on the server. How would you address this code requirement? Interestingly, implementing this type of code was never perceived as a problem in ASP.NET Web Forms because of the more abstract programming interface that Web Forms offers. But what about ASP.NET MVC?

In ASP.NET MVC, you can afford much more freedom in the way in which you organize and express the view. However, you lose some of the infrastructure capabilities that characterize ASP.NET Web Forms. In the end, it's a tradeoff. Still, ASP.NET MVC includes its own full bag of programming goodies that, properly used, simplify form programming.

Multi-Button Forms in ASP.NET Web Forms
Imagine a classic two-button form in ASP.NET Web Forms in which you have a couple of submit buttons in addition to several input fields. For example, you have one button to save the current content and one to delete it. Figure 1 shows some sample code.

For a Web Forms developer, this code is a no-brainer. The code that handles the act of any user clicking the Update button or the Delete button is explicitly written through the handler methods that are associated with the OnClick event. For example, when the user clicks to update the database by using the current content of the form, the btnUpdate_Click method runs. The method is typically defined in the code-behind class of the page, such as in the following example.

void btnUpdate_Click(Object sender, EventArgs e)
\\{
    :
\\}


The ASP.NET Web Forms markup actually produces a markup that's similar to the following example.

<form action="...">
  <input type="text" ... />
  <input type="text" ... />
    :
  <input type="submit" value="Save" name="btnUpdate" />
  <input type="submit" value="Delete" name="btnDelete" />
</form>


When the browser processes the post, it serializes the content of the form's input fields to a string. This string is then uploaded as the body of the resulting HTTP packet. The form's content includes the name of the button that the user clicked to post the form. Based on that piece of information, the ASP.NET runtime determines which server component is responsible for handling the event, and invokes any code that is associated with the OnClick handler. In summary, the runtime infrastructure in ASP.NET Web Forms figures out which button was clicked and what code is associated with it. In Web Forms, you don't have to know which button was clicked.

The Form Action Mechanism
In ASP.NET MVC, you usually express the form by using plain HTML syntax. This process is both good and bad news. Good because it allows you to control exactly what's going on; bad because you are responsible for setting any of the attributes, and because you can't rely on any of the high-level facilities of Web Forms.

When the user posts a form in ASP.NET MVC, the action URL typically points to a controller method that takes control of the operation on the server. You help ASP.NET MVC find the controller method by using the parameters of the <form> tag. The following is an example of such code.

<% Html.BeginForm("demo1", "Home"); %>
  <input type="text" ... />
  <input type="text" ... />
    :
  <input type="submit" value="Update" name="btnUpdate" />
  <input type="submit" value="Delete" name="btnDelete" />
<% Html.EndForm(); %>


In this case, the form is posted to the Home controller and is handled by the Demo1 method. The form is posted when the user clicks either the Update button or the Delete button. The code probably has to take different actions to update and to delete. How can you write such code? Figure 2 shows a possible solution to implement in a controller.

ASP.NET MVC offers a feature known as model binding. Model binding is used to expose posted data to controller methods. If the controller method exposes a parameter whose name matches one of the posted data (form, query string, route values, and so on), ASP.NET MVC automatically passes the matched value to the controller method. Now, imagine that you have the following markup in which all submit buttons within a given form share the same name.

<input type="submit" value="Update" name="serverAction" />
<input type="submit" value="Delete" name="serverAction" />

Regardless of which button you click, the posted data includes a parameter named serverAction. The parameter value depends on the clicked button and is given by its value attribute. By adding a parameter named serverAction to the controller method, you tell ASP.NET MVC to automatically bind button information to the parameter. The binding assigns to the parameter any value that is posted. Therefore, it assigns the content of the value attribute on the submit button. You might first think to define the signature of the controller method to accept a string. You can use strings, but an enumeration is even better. Nicely enough, ASP.NET MVC can resolve the enumerated value for us as long as the posted string matches one of the entries in the enumeration. For example, if the button caption is Update, and if the enumeration contains a member named Update, the corresponding value is picked up. The net effect is to produce quite elegant code that is based on IF statements (see Figure 2).

Using Attributes
Although functional, this solution has some inherent shortcomings. In particular, it is strictly dependent on the captions of the buttons. This is a problem both for maintenance and for localization. Also, the level of Separation of Concerns (SoC) within the controller code is not as high as it should be. Ideally, we would have two distinct controller methods that are automatically invoked based on which button was clicked. To do this, we must briefly recall action selectors.

In ASP.NET, an action selector is a special attribute that you attach to controller methods. The action selector is called by the action invoker during the process that determines which action method has to be invoked. The action selector receives some runtime information, and then it validates the proposed action name.

There is an alternative solution for the multi-button scenario, as shown in Figure 3. First, the form posts to a common action method. For example, the form posts to Demo2. In the controller, you have a plain GET handler for the Demo2 action, and you have two or more POST handlers—one for each submit button that you have.

Both handlers for the POST action must have the same action name. Otherwise, you receive an HTTP 404 error. You also have to add some code to make sure that only one of the two handlers is picked up. If more than one method is a candidate to take the request at the end of the action selection process, an exception is returned from ASP.NET MVC because of ambiguous action references. For this reason, you see a custom attribute (OnlyIfPostedFromButton) used in Figure 3. This attribute lets the method match a Demo2 action request only if the form contains a parameter named deleteAction or updateAction.

Where do these parameter names come from? These are the names of the submit buttons within the form, such as the following examples.

<input type="submit" value="Update" name="updateAction" />
<input type="submit" value="Delete" name="deleteAction" />


Figure 4 shows the code of the custom attribute.

As you can see, the attribute is an action selector. After the action invoker determines the name of the action to execute, you should map the action to a controller method. By default, a match between the action name and a controller method is found on the name of the method. The action selector is an optional component that can add some logic to help the method resolution process. Interestingly, any method that's defined on a controller class is queried, and any selectors it may have are invoked. Let's delve a bit deeper into the code we see in Figure 4.

The method IsValidRequest returns a Boolean answer to indicate whether the method is a good match to serve the request. The code first checks whether the posted data includes an entry that has the name of a submit button. The name of the submit button is set through the SubmitButton property. If such an entry is found, this means that the user clicked that button and, therefore, the form has posted. At this point, the attribute rewrites the action name to be the name of the current method.

Written in this manner, the attribute forces you to be rather verbose in your configuration of the controller method, such as in the following example.

 

\\[HttpPost\\]
\\[ActionName("Demo2")\\]
\\[OnlyIfPostedFromButton(SubmitButton = "deleteAction")\\]
public ActionResult Demo2_Delete()
\\{
   ViewData\\["ActionName"\\] = "Delete";
   return View("demo2");
\\}


The Demo2_Delete method is executed if the request is a POST, if the original action name is Demo2, and if the OnlyIfPostedFromButton selector confirms the operation. You have to specify all these bits of information.

As a way to simplify this code, you can merge the ActionName and OnlyIfPostedFromButton attributes. The ActionName attribute is a sealed class, so you can't derive a new attribute. Instead, you must start from scratch, as shown in Figure 5.

Figure 5 illustrates an action name selector component. This component is invoked in a similar manner as action method selectors to help resolve the action name. The key method is IsValidName. This method returns a Boolean answer that states whether the specified action name is valid. The SubmitActionName attribute first checks whether the proposed action name matches the one that's required for the method. This is the behavior you would expect from the system ActionName attribute. In addition, the SubmitActionName attribute checks the posted data, and then it rewrites the action name, if it is necessary. The following code shows how to use this attribute in your controller code.

 

\\[SubmitActionName(ActionName="demo3", SubmitButton = "updateAction")\\]
public ActionResult Demo3_Update()
\\{
    ViewData\\["ActionName"\\] = "Update";
    return View("demo3");
\\}


Using Script Code
All the solutions that we have considered are based on the idea that you don't change the action attribute of the form based on the button that the user clicks. If you could change the action attribute on the fly, you would not have to have attributes on the controller methods to instruct the runtime about what to look for. To change the action on the fly, however, you have to add some JavaScript, as Figure 6 shows.

By using this markup, the controller method doesn't require extra attributes—except, perhaps, the HttpPost attribute. In any case, the controller doesn't require any attributes to help it handle multiple submit buttons.

Multiple Choices for Multi-Button Forms
An HTML form is a collection of input fields whose value will be, most of the time, simply uploaded. But in the event that you want your application to provide multiple opportunities to the user for performing many tasks on the same input data, you need an effective way of handling posts from multiple buttons. I've shown you three made-to-measure solutions for ASP.NET MVC that will do just that. Just be mindful that dealing with multiple submit buttons in MVC will require giving some more thought to your coding than it does in Web Forms, where the infrastructure handles the usage of multiple submit buttons on a form.

Dino Esposito, author of Programming Microsoft ASP.NET MVC (Microsoft Press), is a trainer and consultant specializing in web architectures and effective code design principles and practices. Get in touch via weblogs.asp.net/despos.

Hide comments

Comments

  • Allowed HTML tags: <em> <strong> <blockquote> <br> <p>

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.
Publish