如何不提交完整ViewModel实现POST请求且错误时保留视图现有ViewModel数据
最优方案:同域AJAX调用MVC本地Action(无需修改Identity Server配置)
你之前对身份校验的顾虑是多余的:
- 同域下AJAX请求会自动携带MVC的认证Cookie,服务端可以正常识别用户身份
- 服务端调用后端API的逻辑和普通页面请求完全一致,
DelegatingHandler获取AccessToken的流程不受影响,不会出现未授权问题 - 全程不需要JS接触AccessToken,也不需要开启Implicit流,完全符合Code流的安全设计
实现代码
- 详情页添加全局防伪标记和错误展示区域
<!-- 页面任意位置加一次即可 --> @Html.AntiForgeryToken() <!-- 错误提示区域 --> <div class="alert alert-danger d-none" id="errorBox"></div>
- 晋升按钮绑定数据属性
<button class="btn btn-promote" data-organization-id="@Model.Id" data-member-id="@member.Id"> 晋升 </button>
- JS请求逻辑
$('.btn-promote').click(async function() { const orgId = $(this).data('organization-id'); const memberId = $(this).data('member-id'); const token = $('input[name="__RequestVerificationToken"]').val(); try { const res = await fetch('/Member/Promote', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'RequestVerificationToken': token }, body: `organizationId=${orgId}&memberId=${memberId}` }); const result = await res.json(); if (result.success) { // 成功直接跳转到详情页,自动重置筛选状态 window.location.href = `/Organization/Details?organizationId=${orgId}`; } else { // 失败直接展示错误,页面分页、筛选状态完全保留 $('#errorBox').text(result.errors.join(';')).removeClass('d-none'); } } catch (e) { $('#errorBox').text('操作失败,请重试').removeClass('d-none'); } });
- 后端Promote方法调整
[HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> Promote(Guid organizationId, Guid memberId) { var result = await _memberService.Promote(organizationId, memberId); if (result.IsFailure) { ModelState.AddModelError(string.Empty, result.Error); return Json(new { success = false, errors = ModelState.Values.SelectMany(v => v.Errors.Select(e => e.ErrorMessage)).ToList() }); } return Json(new { success = true }); }
备选方案:纯服务端PRG模式(无需写JS)
你之前对TempData的顾虑是误解:ASP.NET Core默认的TempData使用CookieTempDataProvider,不需要开启会话,数据加密后存在客户端Cookie中,仅存储搜索参数和错误信息的数据量极小,完全符合规范。
实现代码
- 晋升按钮表单仅提交必要参数
<form method="post" asp-action="Promote" asp-controller="Member"> <input type="hidden" name="organizationId" value="@Model.Id" /> <input type="hidden" name="memberId" value="@member.Id" /> <!-- 仅序列化SearchMetadata字段为隐藏域 --> @Html.HiddenFor(m => m.SearchMetadata.PageIndex) @Html.HiddenFor(m => m.SearchMetadata.PageSize) @Html.HiddenFor(m => m.SearchMetadata.SearchKeyword) <!-- 其他筛选字段同理 --> <button type="submit" class="btn btn-promote">晋升</button> </form>
- 后端Promote方法逻辑
[HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> Promote(Guid organizationId, Guid memberId, MembersResourceParameters searchMetadata) { var result = await _memberService.Promote(organizationId, memberId); if (result.IsFailure) { // 存错误和搜索参数到TempData TempData["OperationError"] = result.Error; TempData["LastSearchMetadata"] = JsonSerializer.Serialize(searchMetadata); return RedirectToAction("Details", "Organization", new { organizationId }); } // 成功直接跳转,不传递搜索参数,自动重置状态 return RedirectToAction("Details", "Organization", new { organizationId }); }
- 调整OrganizationController的Details方法
public async Task<IActionResult> Details(Guid organizationId) { MembersResourceParameters searchParam = new(); // 读取TempData中的历史搜索参数 if (TempData["LastSearchMetadata"] is string json) { searchParam = JsonSerializer.Deserialize<MembersResourceParameters>(json); // 注入错误信息到模型状态 if (TempData["OperationError"] is string error) { ModelState.AddModelError(string.Empty, error); } // 可选:保留TempData防止刷新丢失错误 TempData.Keep(); } // 用搜索参数请求API获取视图模型 var orgData = await _organizationService.GetDetails(organizationId, searchParam); var model = _mapper.Map<OrganizationDetailsViewModel>(orgData); return View(model); }
该方案URL永远为详情页地址,完全符合你的需求。
内容的提问来源于stack exchange,提问作者RocH
相关产品推荐
相关产品推荐

