ASP.NET MVC Core/6: 多个提交按钮

48

我需要多个提交按钮来执行控制器中的不同操作。

我在这里看到了一个优雅的解决方案:How do you handle multiple submit buttons in ASP.NET MVC Framework? 使用这种解决方案,可以为动作方法添加自定义属性。当处理路由时,该自定义属性的方法会检查属性的值是否与点击的提交按钮的名称匹配。

但是,在MVC Core(RC2版本)中,我没有找到 ActionNameSelectorAttribute(我还搜索了 Github 存储库)。我发现了一种类似的解决方案,它使用 ActionMethodSelectorAttribute (http://www.dotnetcurry.com/aspnet-mvc/724/handle-multiple-submit-buttons-aspnet-mvc-action-methods)。

ActionMethodSelectorAttribute 可用,但是方法 IsValidForRequest 具有不同的签名。有一个类型为 RouteContext 的参数。但是我无法在其中找到与我的自定义属性值进行比较的 POST 数据。

在MVC Core中是否有类似以前MVC版本中的优雅解决方案?


1
根据您的喜好,可能会起作用的方法是使用客户端(即:JQuery)更改<form>元素的实际URL。在单击事件中,找出哪个按钮被点击,更改<form>元素的[action]属性并提交<form>。 - Vlince
4个回答

84

您可以使用HTML5 formaction 属性来实现这一点,而不是通过服务器端路由。

<form action="" method="post">
    <input type="submit" value="Option 1" formaction="DoWorkOne" />
    <input type="submit" value="Option 2" formaction="DoWorkTwo"/>
</form>

然后只需编写如下控制器操作:

[HttpPost]
public IActionResult DoWorkOne(TheModel model) { ... }

[HttpPost]
public IActionResult DoWorkTwo(TheModel model) { ... }

一个适用于旧版浏览器的好的polyfill可以在这里找到。

请记住...

  1. 当用户按下回车键时,将始终选择第一个提交按钮。
  2. 如果出现错误(包括ModelState或其他错误),则需要将用户发送回正确的视图。(如果通过AJAX进行提交,则不会出现此问题。)

3
啊,好的发现!在这种情况下,您需要使用 Url.Action(actionName, controllerName) 辅助方法来生成适当的目标,以用于 formaction 属性。 - Will Ray
1
完美的解决方案,但有一个陷阱,如果我们使用您的方法,并且第一个操作是/post/add,第二个操作是/post/addcontinue。如果用户单击addcontinue按钮并且页面存在验证问题,则我们将表单发送回用户并要求他更正错误。但由于我们返回了addcontinue操作,页面URL会更改为/post/addcontinue,因此如果用户刷新页面,则浏览器会向/post/addcontinue发出get请求,而我们没有任何关于post/addcontinue的get操作。如何在两个按钮中保持页面URL与/post/add一致,这是最好的解决方案? - Mohammad Akbari
2
@MohammadAkbari 这是拥有多个提交按钮固有的问题,不幸的是,在MVC4和5的先前答案中也发生了这种情况。你可以通过几种不同的方式来实现你想要的效果,但这取决于使用情况。例如,AJAX表单提交就是一种选择。 - Will Ray
您可以通过在帖子操作中添加一个名为“提交”的字符串参数或与按钮名称相同来轻松地使用一种操作并切换多个帖子。请参阅http://www.binaryintellect.net/articles/c69d78a3-21d7-416b-9d10-6b812a862778.aspx。 - Damitha
1
这在ASP.NET Core 2.0 MVC中有效。它使使用多个表单部分变得更加容易。它还简化了用于在调用之间使用的ModelView数据的隐藏字段。干得好。我不知道按钮的formaction字段存在。我遇到了一个问题,即模型字段在模型的不同部分的表单中被清除,因为只有单个表单数据在提交后保留。 - Peter Suwara
显示剩余2条评论

51

ASP.NET Core 1.1.0 拥有FormActionTagHelper, 它可以创建一个formaction属性。

<form>
    <button asp-action="Login" asp-controller="Account">log in</button>
    <button asp-action="Register" asp-controller="Account">sign up</button>
</form>

那会呈现出这样的效果:

<button formaction="/Account/Login">log in</button>
<button formaction="/Account/Register">sign up</button>
它还适用于 type="image"type="submit"input 标签。

2
嗨Shuan,我注意到一个问题,如果我验证模型并且它不正确,URL会更改为操作名称。你有什么想法如何解决这个问题吗?类似于: { ModelState.AddModelError(string.Empty, "我们在表单数据方面遇到了问题"); } return View("Edit", editModel); - nologo
也许@nologo的答案就是重定向?我不确定。 - Jess

6

我以前做过这个,过去我会将表单发布到不同的控制器操作中。问题在于,在服务器端验证错误时,你只能选择以下两种情况之一:

  1. return View(vm) 会在 URL 中保留 post 操作名称...呃。
  2. return Redirect(...) 需要使用 TempData 来保存 ModelState。同样很让人不爽。

这就是我选择的方案。

  1. 使用按钮的 name 属性绑定到 POST 请求中的一个变量。
  2. 按钮的 value 是一个枚举类型,用于区分提交的操作。 枚举类型是类型安全的,并且在 switch 语句中的效果更好。;)
  3. POST 到与 GET 相同的操作名称。这样,在服务器端验证错误时,URL 中不会出现 POST 操作名称。
  4. 如果有验证错误,则重建你的视图模型并使用 return View(viewModel),遵循适当的 PGR 模式。

使用这种技术,无需使用 TempData

在我的用例中,我有一个用户/详细信息页面,其中包含“添加角色”和“删除角色”操作。

这里是这些按钮。它们可以是 button 标签而不是 input 标签...;)

<button type="submit" class="btn btn-primary" name="SubmitAction" value="@UserDetailsSubmitAction.RemoveRole">Remove Role</button>
<button type="submit" class="btn btn-primary" name="SubmitAction" value="@UserDetailsSubmitAction.AddRole">Add Users to Role</button>

这是控制器动作,我已经将switch代码块重构成它们自己的函数以使其更易于阅读。我必须发布到2个不同的视图模型,所以一个不会被填充,但是模型绑定程序不会关心!

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Details(
    SelectedUserRoleViewModel removeRoleViewModel, 
    SelectedRoleViewModel addRoleViewModel,
    UserDetailsSubmitAction submitAction)
{
    switch (submitAction)
    {
        case UserDetailsSubmitAction.AddRole:
        {
            return await AddRole(addRoleViewModel);
        }
        case UserDetailsSubmitAction.RemoveRole:
        {
            return await RemoveRole(removeRoleViewModel);
        }
        default:
            throw new ArgumentOutOfRangeException(nameof(submitAction), submitAction, null);
    }
}

private async Task<IActionResult> RemoveRole(SelectedUserRoleViewModel removeRoleViewModel)
{
    if (!ModelState.IsValid)
    {
        var viewModel = await _userService.GetDetailsViewModel(removeRoleViewModel.UserId);
        return View(viewModel);
    }

    await _userRoleService.Remove(removeRoleViewModel.SelectedUserRoleId);

    return Redirect(Request.Headers["Referer"].ToString());
}

private async Task<IActionResult> AddRole(SelectedRoleViewModel addRoleViewModel)
{
    if (!ModelState.IsValid)
    {
        var viewModel = await _userService.GetDetailsViewModel(addRoleViewModel.UserId);
        return View(viewModel);
    }

    await _userRoleService.Add(addRoleViewModel);

    return Redirect(Request.Headers["Referer"].ToString());
}

作为替代方案,您可以使用AJAX提交表单

我发现在 .Net 6 的 ASP 中这个不再起作用了。在 5 中可以工作,但当我升级项目后就停止了。只有一个命名参数传递了 null。 - iCollect.it Ltd
嗨,@GoneCoding,.NET 6 对我来说是可行的。 - Jess
我在.NET 6中再次使用了这种技术。我遇到的一个问题是,您不能为您要发布的按钮值使用隐藏输入。ModelState会妨碍您的进展。 - Jess

0

更好的答案是使用jQuery Unobtrusive AJAX,忘记所有混乱。

  1. 您可以为控制器操作指定语义名称。
  2. 您不必重定向或使用tempdata。
  3. 服务器端验证错误时,URL上没有提交操作名称。
  4. 在服务器端验证错误时,您可以返回表单或仅返回错误消息。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接