Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Multiple Parameterized (Localizable) Form Buttons in ASP.NET MVC

0.00/5 (No votes)
27 Sep 2012 1  
Easy handling of buttons in ASP.NET MVC
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):

Image 1

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:

Image 2

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)
{
    // Increase the bonus budget by lowering the benefits of the shareholders:
    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:

Image 3

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)
{
    // Delete the given department:
    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)
{
    // Increase the bonus of the employee by lowering his departments budget:
    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
{
    /// <summary>
    /// An MVC ActionName Selector for actions handling form buttons.
    /// </summary>
    public class ButtonHandlerAttribute : ActionNameSelectorAttribute
    {
        private readonly Regex ButtonNameParser = 
                         new Regex("^(?<name>.*?)(\\[(?<arg>.+?)\\])*$",
                         RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | 
                         RegexOptions.Compiled);
 
        private string argumentNames;
        private string[] arguments;
 
        /// <summary>
        /// Indicates this action handles actions for a button with the name 
        /// of the action method.
        /// </summary>
        public ButtonHandlerAttribute()
        {
            this.ValueArgumentName = "value";
        }
 
        /// <summary>
        /// Whether GET-requests are allowed (by default not allowed).
        /// </summary>
        public bool AllowGetRequests { get; set; }
 
        /// <summary>
        /// Name of the MVC action.
        /// </summary>
        public string ActionName { get; set; }
 
        /// <summary>
        /// Name of the button (without arguments).
        /// </summary>
        public string ButtonName { get; set; }
 
        /// <summary>
        /// Comma-separated list of argument names to bind to the button arguments.
        /// </summary>
        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();
            }
        }
 
        /// <summary>
        /// Name of the method argument to bind to the button value.
        /// </summary>
        public string ValueArgumentName { get; set; }
 
        /// <summary>
        /// Determines whether the action name is valid in the specified controller context.
        /// </summary>
        public override bool IsValidName(ControllerContext controllerContext, 
               string actionName, System.Reflection.MethodInfo methodInfo)
        {
            // Reject GET requests if not allowed:
            if (!AllowGetRequests)
                if (controllerContext.HttpContext.Request.GetHttpMethodOverride().Equals
                   ("GET", StringComparison.OrdinalIgnoreCase))
                    return false;
 
            // Check ActionName if given:
            if (this.ActionName != null)
                if (!this.ActionName.Equals(actionName, StringComparison.OrdinalIgnoreCase))
                    return false;
 
            // Check button name:
            var values = new NameValueCollection();
            if ((this.arguments == null) || (this.arguments.Length == 0))
            {
                // Buttonname has no args, perform an exact match:
                var buttonName = this.ButtonName ?? methodInfo.Name;
 
                // Return false if button not found:
                if (controllerContext.HttpContext.Request[buttonName] == null)
                    return false;
 
                // Button is found, add button value:
                if (this.ValueArgumentName != null)
                    values.Add(this.ValueArgumentName, 
                               controllerContext.HttpContext.Request[buttonName]);
            }
            else
            { 
                // Buttonnname has arguments, perform a match up to the first argument:
                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;
                    }
                }
 
                // Return false if button not found:
                if (buttonFieldname == null)
                    return false;
 
                // Button is found, add button value:
                if (this.ValueArgumentName != null)
                    values.Add(this.ValueArgumentName, 
                               controllerContext.HttpContext.Request[buttonFieldname]);
 
                // Also add arguments:
                for(int i=0; i<this.arguments.Length; i++)
                {
                    values.Add(this.arguments[i], args[i]);
                }
            }
 
            // Install a new ValueProvider for the found values:
            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 success:
            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

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here