Several solutions exist to handle multiple buttons on a form, however, none of the existing solutions were sufficiently complete to handle the cases I wanted to handle in a clean way. In this post, I present the features of the button handler I created.
Introduction
One of the common problems with ASP.NET MVC is the handling of buttons on a form when multiple 'Submit' buttons are present. Take, for instance, the following demonstration form (from the enclosed sample app):
Every button posts the form so that the information filled in does not get lost. But every button should have a different effect. In addition, some buttons are repeated per element in collections. For instance, there is an “Add Employee” button for each department, and an “+50” button for each employee.
Several solutions exist to handle multiple buttons on a form, see:
However, I found none of the existing solutions to be sufficiently complete to handle the cases I wanted to handle in a clean way.
The features of the button handler I present here include:
- Simple and intuitive HTML-code in the view
- Simple attribute based controller action method selection
- Supports button with values
- Supports indexing of buttons (for button per array/collection element)
- No localization issues
- Fully JavaScript/JQuery free
Some Examples
A Simple Button
For a first example, let’s see how the HTML/Razor code looks like for a simple button as the “Add Department” button at the bottom of the above screen dump:
<button type="submit" name="AddDepartment">Add Department</button>
Or, using the INPUT
element:
<input type="submit" name="AddDepartment" value="Add Department" />
The code in the MVC controller to handle this button is the following:
[ButtonHandler]
public ActionResult AddDepartment(Company model)
{
model.Departments.Add(new Department());
return View(model);
}
It is a regular action method returning an ActionResult
. The differences are that the method has a [ButtonHandler]
attribute, and that the name of the method does not match the action (the post action in this sample is “Index
”), but matches the button name!
However, if you like, you could mention the action name, either by using an ActionName
attribute, or by setting the ActionName
property of the ButtonHandler
. You could also explicitly set the ButtonName
property, in which case the name of the method does not matter any more.
Following are valid alternatives of the above ButtonHandler
attribute:
[ActionName("Index"), ButtonHandler()]
[ActionName("Index"), ButtonHandler(ButtonName = "AddDepartment")]
[ButtonHandler(ActionName = "Index")]
[ButtonHandler(ActionName = "Index", ButtonName = "AddDepartment")]
So the [ButtonHandler]
attribute is used to mark the action method that will handle a button action.
Buttons with Values
Now let’s take a look at the company budget buttons:
It would have been possible to create two distinct buttons with distinct names and distinct button handler action methods. But in this case, I’ve solved this differently. The HTML/Razor code is the following:
<label>Remaining budget of the company :</label>
@Html.EditorFor(m => m.Budget, @readonly)
<button type="submit" name="UpdateCompanyBudget" value="100">Budget+100</button>
<button type="submit" name="UpdateCompanyBudget" value="-100">Budget-100</button>
As you can see, both buttons have the same name ! However, they also have a (different) value. This allows them to be handled by the same MVC controller action method, which looks like:
[ButtonHandler]
public ActionResult UpdateCompanyBudget(Company model, decimal value)
{
model.ShareHoldersBenefit -= value;
model.Budget += value;
return View(model);
}
Still we have a simple [ButtonHandler]
attribute, and an action method of which the name matches the button name. In addition, we have a ‘value
’ argument. This value
argument will contain the value of the button (100 or -100-).
The name of the argument (‘value
’) is hardcoded but can be overwritten by using the ‘ValueArgumentName
’ of the ButtonHandler
property. For instance:
[ButtonHandler(ValueArgumentName = "amount")]
public ActionResult UpdateCompanyBudget(Company model, decimal amount) ...
Of course, you can still explicitly mention ActionName
and/or ButtonName
properties.
Having buttons with values helps in a better separation between controller code and view: the view can decide to add even more buttons with different values without impacting the controller code.
For the INPUT
element in HTML, the value represents also the displayed button caption. Therefore, if you want to use INPUT
elements instead of BUTTON
elements, either consider button values as not supported, or accept that they will also be the caption of your button.
Indexed Buttons
Another situation are buttons that are repeated per element in a collection or array. That is, for instance, the case of the “Delete” button to delete a department:
If you add multiple departments, you will have multiple instances of this Delete button. How can we detect which instance of the button was deleted?
We could, of course, have used the value of the button to hold an index value. That’s OK for a single loop level, but collections can be nested in which case a single index value is not sufficient.
ASP.NET MVC solves collection element indexing issue by adding index values within the names of the HTML input control elements. For instance, the rendering of the name of a department (knowing that the model contains a collection of departments) is done using the following Razor expression:
@Html.EditorFor(m => m.Departments[d].Name)
Where ‘d
’ is the for
-loop index of the department.
This translates in the rendered HTML into:
<input id="Departments_0__Name" name="Departments[0].Name" type="text" value="Los Angeles" />
The name of the input field contains an index number between square brackets. Well, we’ll use the same trick to identify the instance of our delete
button:
<button type="submit" name="DeleteDepartment[@(d)]">Delete</button>
Or, using an INPUT
element:
<input type="submit" name="DeleteDepartment[@(d)]" value="Delete" />
To handle this button, we need an action method named “DeleteDepartment
”, that takes an argument for the button
index. Here it is:
[ButtonHandler(ArgumentNames = "departmentId")]
public ActionResult DeleteDepartment(Company model, int departmentId)
{
model.Departments.RemoveAt(departmentId);
return View(model);
}
When our button
has arguments, we need to declare the arguments with the ArgumentNames
parameter (which takes a comma-separated list of argument names). These arguments will then be used to bind the real method arguments to.
It is possible to have multiple arguments (for nested loops, for instance) combined with a value on the button
. Let’s take a look at an example that combines nested loops and button
values.
Multiple Indexes and Values Combined
The “+50”, “+100”, “-50”, “-100” buttons on the above screen dump are an example of a combination of multiple indexes combined with values. On the controller side, a single action method handles all those buttons.
Let’s first take a look at a simplified version of the Razor view containing these buttons:
...
@for (int d = 0; d < Model.Departments.Count; d++)
{
...
for(int e = 0; e < Model.Departments[d].Employees.Count; e++)
{
<li>
@Html.EditorFor(m => m.Departments[d].Employees[e].Name)
Assigned bonus :
@Html.EditorFor(m => m.Departments[d].Employees[e].Bonus)
<button type="submit" name="UpdateBonus[@(d)][@(e)]" value="50">+50</button>
<button type="submit" name="UpdateBonus[@(d)][@(e)]" value="100">+100</button>
<button type="submit" name="UpdateBonus[@(d)][@(e)]" value="-50">-50</button>
<button type="submit" name="UpdateBonus[@(d)][@(e)]" value="-100">-100</button>
</li>
}
}
...
The four buttons are placed within a nested loop. Therefore, the button name takes two indexes. The rendered HTML will be similar to (for the first employee of the fourth department):
<li>
<input id="Departments_3__Employees_0__Name"
name="Departments[3].Employees[0].Name"
type="text" value="Lauren Walker" />
Assigned bonus :
<input id="Departments_3__Employees_0__Bonus"
name="Departments[3].Employees[0].Bonus"
type="text" value="0,00" />
<button type="submit" name="UpdateBonus[3][0]" value="50">+50</button>
<button type="submit" name="UpdateBonus[3][0]" value="100">+100</button>
<button type="submit" name="UpdateBonus[3][0]" value="-50">-50</button>
<button type="submit" name="UpdateBonus[3][0]" value="-100">-100</button>
</li>
The controller action method to handle the UpdateBonus
button is:
[ButtonHandler(ArgumentNames = "departmentId, employeeId")]
public ActionResult UpdateBonus(Company model, int departmentId, int employeeId, decimal value)
{
model.Departments[departmentId].Budget -= value;
model.Departments[departmentId].Employees[employeeId].Bonus += value;
return View(model);
}
Our button handler takes two index arguments and a value argument (as well as the model argument to hold the form postback).
ButtonHandler Reference
The ButtonHandler
attribute has following properties:
ActionName
The name of the MVC action. By default, the action name is not checked, only the button name is checked. Alternatively, you can use the [ActionName] attribute.
ButtonName
The name of the button to be handled. By default, this is the name of the action method.
ArgumentNames
A comma-separated ordered list of argument names matching arguments of the action method.
ValueArgumentName
The name of the action method argument to bind the button value to.
AllowGetRequests
Whether the button handler accepts Http GET
requests. By default, GET
requests are not accepted.
ButtonHandler Code
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using System.Web.Mvc;
namespace MvcMultiButtonSampleApp
{
public class ButtonHandlerAttribute : ActionNameSelectorAttribute
{
private readonly Regex ButtonNameParser =
new Regex("^(?<name>.*?)(\\[(?<arg>.+?)\\])*$",
RegexOptions.CultureInvariant | RegexOptions.IgnoreCase |
RegexOptions.Compiled);
private string argumentNames;
private string[] arguments;
public ButtonHandlerAttribute()
{
this.ValueArgumentName = "value";
}
public bool AllowGetRequests { get; set; }
public string ActionName { get; set; }
public string ButtonName { get; set; }
public string ArgumentNames
{
get
{
return this.argumentNames;
}
set
{
this.argumentNames = value;
if (String.IsNullOrWhiteSpace(value))
this.arguments = null;
else
this.arguments = value.Split(',').Select(s => s.Trim()).ToArray();
}
}
public string ValueArgumentName { get; set; }
public override bool IsValidName(ControllerContext controllerContext,
string actionName, System.Reflection.MethodInfo methodInfo)
{
if (!AllowGetRequests)
if (controllerContext.HttpContext.Request.GetHttpMethodOverride().Equals
("GET", StringComparison.OrdinalIgnoreCase))
return false;
if (this.ActionName != null)
if (!this.ActionName.Equals(actionName, StringComparison.OrdinalIgnoreCase))
return false;
var values = new NameValueCollection();
if ((this.arguments == null) || (this.arguments.Length == 0))
{
var buttonName = this.ButtonName ?? methodInfo.Name;
if (controllerContext.HttpContext.Request[buttonName] == null)
return false;
if (this.ValueArgumentName != null)
values.Add(this.ValueArgumentName,
controllerContext.HttpContext.Request[buttonName]);
}
else
{
var buttonName = this.ButtonName ?? methodInfo.Name;
var buttonNamePrefix = buttonName + "[";
string buttonFieldname = null;
string[] args = null;
foreach (var fieldname in controllerContext.HttpContext.Request.Form.AllKeys
.Union(controllerContext.HttpContext.Request.QueryString.AllKeys))
{
if (fieldname.StartsWith
(buttonNamePrefix, StringComparison.OrdinalIgnoreCase))
{
var match = ButtonNameParser.Match(fieldname);
if (match == null) continue;
args = match.Groups["arg"].Captures.OfType<Capture>().Select
(c => c.Value).ToArray();
if (args.Length != this.arguments.Length) continue;
buttonFieldname = fieldname;
break;
}
}
if (buttonFieldname == null)
return false;
if (this.ValueArgumentName != null)
values.Add(this.ValueArgumentName,
controllerContext.HttpContext.Request[buttonFieldname]);
for(int i=0; i<this.arguments.Length; i++)
{
values.Add(this.arguments[i], args[i]);
}
}
var valueProviders = new List<IValueProvider>();
valueProviders.Add(new NameValueCollectionValueProvider
(values, Thread.CurrentThread.CurrentCulture));
valueProviders.Add(controllerContext.Controller.ValueProvider);
controllerContext.Controller.ValueProvider =
new ValueProviderCollection(valueProviders);
return true;
}
}
}
The Sample
The sample that comes with this article contains a simple MVC application to distribute budget over employee bonuses. When you start the application, the company has $1000 of shareholder benefit and $500 reserved for employee bonuses. When you increase that budget, the shareholder benefit is lowered. When you add budget to a department, that budget is taken from the companies budget. When you increase the bonus of an employee, that amount is taken from the departments budget.
It’s just a sample app to try out multiple functional buttons on a form.
There’s no backend database.
History
- 24th September, 2012: Initial version