65.9K
CodeProject 正在变化。 阅读更多。
Home

ASP.NET MVC 中多个参数化(可本地化)表单按钮

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.45/5 (7投票s)

2012年9月24日

CPOL

6分钟阅读

viewsIcon

48052

downloadIcon

798

轻松处理 ASP.NET MVC 中的按钮

引言

ASP.NET MVC 中一个常见的问题是在表单上处理按钮,当存在多个“提交”按钮时。例如,考虑以下演示表单(来自附带的示例应用程序)

每个按钮都会提交表单,这样输入的信息就不会丢失。但每个按钮应该有不同的效果。此外,一些按钮会为集合中的每个元素重复出现。例如,每个部门都有一个“添加员工”按钮,每个员工都有一个“+50”按钮。

有几种解决方案可以处理表单上的多个按钮,请参阅

然而,我发现现有的解决方案都不够完善,无法以干净的方式处理我想处理的情况。

我在此介绍的按钮处理器的功能包括

  • 视图中的简单直观的 HTML 代码
  • 简单的基于属性的控制器操作方法选择
  • 支持带值的按钮
  • 支持按钮的索引(用于数组/集合元素的按钮)
  • 无本地化问题
  • 完全不使用 JavaScript/JQuery

一些示例

一个简单的按钮

首先,让我们看看上面屏幕截图底部“添加部门”按钮的简单按钮的 HTML/Razor 代码

<button type="submit" name="AddDepartment">Add Department</button>

或者,使用 INPUT 元素

<input type="submit" name="AddDepartment" value="Add Department" />

处理此按钮的 MVC 控制器中的代码如下

[ButtonHandler]
public ActionResult AddDepartment(Company model)
{
    model.Departments.Add(new Department());
 
    return View(model);
}

这是一个返回 ActionResult 的常规操作方法。不同之处在于该方法具有 [ButtonHandler] 属性,并且方法名称不匹配操作(本示例中的 post 操作是“Index”),而是匹配按钮名称!

但是,如果您愿意,可以通过使用 ActionName 属性,或通过设置 ButtonHandlerActionName 属性来指定操作名称。您也可以显式设置 ButtonName 属性,在这种情况下,方法名称不再重要。

以下是上述 ButtonHandler 属性的有效替代方案

[ActionName("Index"), ButtonHandler()]
[ActionName("Index"), ButtonHandler(ButtonName = "AddDepartment")]
[ButtonHandler(ActionName = "Index")]
[ButtonHandler(ActionName = "Index", ButtonName = "AddDepartment")]

因此,[ButtonHandler] 属性用于标记将处理按钮操作的操作方法。

带值的按钮

现在让我们来看看公司预算按钮

本可以创建两个不同的按钮,具有不同的名称和不同的按钮处理程序操作方法。但在这种情况下,我用不同的方式解决了这个问题。HTML/Razor 代码如下

<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>

正如您所见,两个按钮的名称相同!但是,它们也有一个(不同的)值。这允许它们由同一个 MVC 控制器操作方法处理,该方法如下

[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);
}

我们仍然有一个简单的 [ButtonHandler] 属性,以及一个名称与按钮名称匹配的操作方法。此外,我们还有一个“value”参数。这个 value 参数将包含按钮的值(100 或 -100)。

参数名称(“value”)是硬编码的,但可以使用 ButtonHandler 属性的“ValueArgumentName”来覆盖。例如

[ButtonHandler(ValueArgumentName = "amount")]
public ActionResult UpdateCompanyBudget(Company model, decimal amount) ...

当然,您仍然可以显式提及 ActionName 和/或 ButtonName 属性。

具有值的按钮有助于更好地分离控制器代码和视图:视图可以决定添加更多具有不同值的按钮,而不会影响控制器代码。

对于 HTML 中的 INPUT 元素,其值也代表显示的按钮标题。因此,如果您想使用 INPUT 元素而不是 BUTTON 元素,要么认为按钮值不受支持,要么接受它们也将是按钮的标题。

索引按钮

另一种情况是为集合或数组中的元素重复出现的按钮。例如,删除部门的“删除”按钮就是这种情况

如果您添加多个部门,您将有多个此 删除 按钮的实例。我们如何检测删除了哪个按钮实例?

当然,我们可以使用按钮的值来保存索引值。这对于单层循环来说是可以的,但集合可能嵌套,在这种情况下,单个索引值是不够的。

ASP.NET MVC 通过在 HTML 输入控件元素的名称中添加索引值来解决集合元素索引问题。例如,部门名称的渲染(知道模型包含部门集合)是通过以下 Razor 表达式完成的

@Html.EditorFor(m => m.Departments[d].Name)

其中,“d”是部门的 for 循环索引。

这在渲染的 HTML 中转换为

<input id="Departments_0__Name" name="Departments[0].Name" type="text" value="Los Angeles" />

输入字段的名称包含方括号内的索引号。嗯,我们将使用相同的技巧来识别我们 delete 按钮的实例

<button type="submit" name="DeleteDepartment[@(d)]">Delete</button>

或者,使用 INPUT 元素

<input type="submit" name="DeleteDepartment[@(d)]" value="Delete" />

要处理此按钮,我们需要一个名为“DeleteDepartment”的操作方法,该方法接受一个用于 button 索引的参数。这是

[ButtonHandler(ArgumentNames = "departmentId")]
public ActionResult DeleteDepartment(Company model, int departmentId)
{
    // Delete the given department:
    model.Departments.RemoveAt(departmentId);
 
    return View(model);
}

当我们的 button 有参数时,我们需要使用 ArgumentNames 参数(它接受一个逗号分隔的参数名称列表)来声明参数。然后将使用这些参数来绑定实际方法参数。

可以将多个参数(例如,用于嵌套循环)与 button 上的值结合起来。让我们来看一个结合了多个索引和按钮值的示例。

多个索引和值组合

上面屏幕截图中的“+50”、“+100”、“-50”、“-100”按钮是多个索引与值组合的示例。在控制器端,一个操作方法处理所有这些按钮。

让我们先看看包含这些按钮的 Razor 视图的简化版本

...
@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>                
    }
}
...

这四个按钮位于嵌套循环中。因此,按钮名称包含两个索引。渲染的 HTML 将类似于(对于第四个部门的第一个员工)

<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>

处理 UpdateBonus 按钮的控制器操作方法是

[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);
}

我们的按钮处理器接受两个索引参数和一个值参数(以及用于保存表单回发的模型参数)。

ButtonHandler 参考

ButtonHandler 属性具有以下属性

ActionName

MVC 操作的名称。默认情况下,不检查操作名称,只检查按钮名称。或者,您可以使用 [ActionName] 属性。

ButtonName

要处理的按钮的名称。默认情况下,这是操作方法的名称。

ArgumentNames

与操作方法参数匹配的逗号分隔的有序参数名称列表。

ValueArgumentName

用于绑定按钮值的操作方法参数的名称。

AllowGetRequests

按钮处理器是否接受 Http GET 请求。默认情况下,不接受 GET 请求。

ButtonHandler 代码

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;
        }
    }
}

示例

本文附带的示例包含一个简单的 MVC 应用程序,用于在员工奖金之间分配预算。当您启动应用程序时,公司有 1000 美元的股东收益和 500 美元保留用于员工奖金。当您增加预算时,股东收益会降低。当您为部门增加预算时,该预算将从公司预算中扣除。当您增加员工奖金时,该金额将从部门预算中扣除。

这只是一个用于尝试在表单上使用多个功能性按钮的示例应用程序。

没有后端数据库。

历史

  • 2012 年 9 月 24 日:初始版本
© . All rights reserved.