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






4.45/5 (7投票s)
轻松处理 ASP.NET MVC 中的按钮
引言
ASP.NET MVC 中一个常见的问题是在表单上处理按钮,当存在多个“提交”按钮时。例如,考虑以下演示表单(来自附带的示例应用程序)
每个按钮都会提交表单,这样输入的信息就不会丢失。但每个按钮应该有不同的效果。此外,一些按钮会为集合中的每个元素重复出现。例如,每个部门都有一个“添加员工”按钮,每个员工都有一个“+50”按钮。
有几种解决方案可以处理表单上的多个按钮,请参阅
- 如何在 ASP.NET MVC 框架中处理多个提交按钮?
- ASP.NET MVC – 同一个表单中的多个按钮
- ASP.NET MVC 的多个提交按钮:最终解决方案
- 支持 ASP.NET MVC 视图上的多个提交按钮
然而,我发现现有的解决方案都不够完善,无法以干净的方式处理我想处理的情况。
我在此介绍的按钮处理器的功能包括
- 视图中的简单直观的 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
属性,或通过设置 ButtonHandler
的 ActionName
属性来指定操作名称。您也可以显式设置 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 日:初始版本