条件ModelState合并

我实现了第二个响应“ 保留模型状态错误跨RedirectToAction? ”问题,涉及使用两个自定义ActionFilterAttributes。 我喜欢这个解决方案,它通过向需要该function的方法添加属性来保持代码清洁。

解决方案在大多数情况下运行良好,但我遇到了重复的部分视图问题。 基本上我有部分视图使用它自己的模型,与父视图使用的模型分开。

主视图中我的代码的简化版本:

@for (int i = 0; i < Model.Addresses.Count; i++) { address = (Address)Model.Addresses[i]; @Html.Partial("_AddressModal", address); } 

部分视图“_AddressModal”:

 @model Acme.Domain.Models.Address [...] @Html.TextBoxFor(model => model.Address1, new { @class = "form-control" } ) [...] 

不使用自定义ActionFilterAttributes时,一切都按预期工作。 每次执行部分视图时,lamba表达式如“model => model.Address1”从ModelState中提取正确的值。

问题是当我获得重定向并使用自定义ActionFilterAttributes时。 核心问题是,不仅更新了一个Address实例的ModelState,而且部分视图构建的所有地址的ModelState都被覆盖,因此它们包含相同的值,而不是正确的实例值。

我的问题是如何修改自定义ActionFilterAttributes,以便它只更新受影响的一个Address实例的ModelState,而不是所有ModelStates? 我想避免向使用该属性的方法添加任何内容,以保持干净的实现。

以下是来自其他问题的自定义ActionFilterAttributes代码:

 public class SetTempDataModelStateAttribute : ActionFilterAttribute { public override void OnActionExecuted(ActionExecutedContext filterContext) { base.OnActionExecuted(filterContext); filterContext.Controller.TempData["ModelState"] = filterContext.Controller.ViewData.ModelState; } } public class RestoreModelStateFromTempDataAttribute : ActionFilterAttribute { public override void OnActionExecuting(ActionExecutingContext filterContext) { base.OnActionExecuting(filterContext); if (filterContext.Controller.TempData.ContainsKey("ModelState")) { filterContext.Controller.ViewData.ModelState.Merge( (ModelStateDictionary)filterContext.Controller.TempData["ModelState"]); } } } 

看看这个实现 (ben foster)是否有效:我大量使用它并且从未遇到过问题。

你正确设置属性? ‘动作上的SetTempDataModelStatepost动作的SetTempDataModelState

以下是需要的4个类(导出,导入,传输和validation)ModelState

  [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] public class ExportModelStateToTempDataAttribute : ModelStateTempDataTransfer { public override void OnActionExecuted(ActionExecutedContext filterContext) { // Only copy when ModelState is invalid and we're performing a Redirect (ie PRG) if (!filterContext.Controller.ViewData.ModelState.IsValid && (filterContext.Result is RedirectResult || filterContext.Result is RedirectToRouteResult)) { ExportModelStateToTempData(filterContext); } base.OnActionExecuted(filterContext); } } ///  /// An Action Filter for importing ModelState from TempData. /// You need to decorate your GET actions with this when using the . ///  ///  /// Useful when following the PRG (Post, Redirect, Get) pattern. ///  [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] public class ImportModelStateFromTempDataAttribute : ModelStateTempDataTransfer { public override void OnActionExecuted(ActionExecutedContext filterContext) { // Only copy from TempData if we are rendering a View/Partial if (filterContext.Result is ViewResult) { ImportModelStateFromTempData(filterContext); } else { // remove it RemoveModelStateFromTempData(filterContext); } base.OnActionExecuted(filterContext); } } [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] public abstract class ModelStateTempDataTransfer : ActionFilterAttribute { protected static readonly string Key = typeof(ModelStateTempDataTransfer).FullName; ///  /// Exports the current ModelState to TempData (available on the next request). ///  protected static void ExportModelStateToTempData(ControllerContext context) { context.Controller.TempData[Key] = context.Controller.ViewData.ModelState; } ///  /// Populates the current ModelState with the values in TempData ///  protected static void ImportModelStateFromTempData(ControllerContext context) { var prevModelState = context.Controller.TempData[Key] as ModelStateDictionary; context.Controller.ViewData.ModelState.Merge(prevModelState); } ///  /// Removes ModelState from TempData ///  protected static void RemoveModelStateFromTempData(ControllerContext context) { context.Controller.TempData[Key] = null; } } ///  /// An ActionFilter for automatically validating ModelState before a controller action is executed. /// Performs a Redirect if ModelState is invalid. Assumes the  is used on the GET action. ///  [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] public class ValidateModelStateAttribute : ModelStateTempDataTransfer { public override void OnActionExecuting(ActionExecutingContext filterContext) { if (!filterContext.Controller.ViewData.ModelState.IsValid) { if (filterContext.HttpContext.Request.IsAjaxRequest()) { ProcessAjax(filterContext); } else { ProcessNormal(filterContext); } } base.OnActionExecuting(filterContext); } protected virtual void ProcessNormal(ActionExecutingContext filterContext) { // Export ModelState to TempData so it's available on next request ExportModelStateToTempData(filterContext); // redirect back to GET action filterContext.Result = new RedirectToRouteResult(filterContext.RouteData.Values); } protected virtual void ProcessAjax(ActionExecutingContext filterContext) { var errors = filterContext.Controller.ViewData.ModelState.ToSerializableDictionary(); var json = new JavaScriptSerializer().Serialize(errors); // send 400 status code (Bad Request) filterContext.Result = new HttpStatusCodeResult((int)HttpStatusCode.BadRequest, json); } } 

编辑

这是一个普通(非动作filter)PRG模式:

  [HttpGet] public async Task Edit(Guid id) { var calendarEvent = await calendarService.FindByIdAsync(id); if (calendarEvent == null) return this.RedirectToAction(c => c.Index()); var model = new CalendarEditViewModel(calendarEvent); ViewData.Model = model; return View(); } [HttpPost] public async Task Edit(Guid id, CalendarEventBindingModel binding) { if (!ModelState.IsValid) return await Edit(id); var calendarEvent = await calendarService.FindByIdAsync(id); if (calendarEvent != null) { CalendarEvent model = calendarService.Update(calendarEvent, binding); await context.SaveChangesAsync(); } return this.RedirectToAction(c => c.Index()); } 

您希望通过动作filter(或其目的)避免的是删除每个postAction上的ModelState.IsValid检查,因此相同(使用动作filter)将是:

  [HttpGet, ImportModelStateFromTempData] public async Task Edit(Guid id) { var calendarEvent = await calendarService.FindByIdAsync(id); if (calendarEvent == null) return this.RedirectToAction(c => c.Index()); var model = new CalendarEditViewModel(calendarEvent); ViewData.Model = model; return View(); } // ActionResult changed to RedirectToRouteResult [HttpPost, ValidateModelState] public async Task Edit(Guid id, CalendarEventBindingModel binding) { // removed ModelState.IsValid check var calendarEvent = await calendarService.FindByIdAsync(id); if (calendarEvent != null) { CalendarEvent model = calendarService.Update(calendarEvent, binding); await context.SaveChangesAsync(); } return this.RedirectToAction(c => c.Index()); } 

这里没有更多的事情发生。 因此,如果您只使用ExportModelState操作filter,您最终会得到一个像这样的post操作:

  [HttpPost, ExportModelStateToTempData] public async Task Edit(Guid id, CalendarEventBindingModel binding) { if (!ModelState.IsValid) return RedirectToAction("Edit", new { id }); var calendarEvent = await calendarService.FindByIdAsync(id); if (calendarEvent != null) { CalendarEvent model = calendarService.Update(calendarEvent, binding); await context.SaveChangesAsync(); } return this.RedirectToAction(c => c.Index()); } 

这让我问你,为什么你首先要打扰ActionFilters ? 虽然我确实喜欢ValidateModelState模式(很多人没有),但是如果你在控制器中重定向除了一个场景,你有额外的模型状态错误,我真的没有看到任何好处,为了完整性让我给你一个例子:

  [HttpPost, ValidateModelState, ExportModelStateToTempData] public async Task Edit(Guid id, CalendarEventBindingModel binding) { var calendarEvent = await calendarService.FindByIdAsync(id); if (!(calendarEvent.DateStart > DateTime.UtcNow.AddDays(7)) && binding.DateStart != calendarEvent.DateStart) { ModelState.AddModelError("id", "Sorry, Date start cannot be updated with less than 7 days of event."); return RedirectToAction("Edit", new { id }); } if (calendarEvent != null) { CalendarEvent model = calendarService.Update(calendarEvent, binding); await context.SaveChangesAsync(); } return this.RedirectToAction(c => c.Index()); } 

在最后一个示例中,我使用了ValidateModelStateExportModelState ,这是因为ValidateModelStateActionExecuting运行,因此它在进入方法体之前进行validation,如果绑定有一些validation错误,它将自动重定向。 然后我有一个不能在数据注释中的另一个检查,因为它处理加载实体并查看它是否具有正确的要求(我知道这不是最好的例子,将其视为在注册时提供的用户名是否可用) ,我知道远程数据注释,但没有涵盖所有情况)然后我只是根据除绑定之外的外部因素更新ModelState我自己的错误。 由于ExportModelStateExportModelState运行, ActionExecuted我对ModelState所有修改都TempDataTempData因此我将在HttpGet Edit操作上使用它们。

我知道所有这些都会使我们中的一些人感到困惑,关于如何在Controller / PRG端进行MVC没有很好的指示。 我正在努力制作博客文章以涵盖所有场景和解决方案。 这只是它的1%。

我希望至少我清除了POST-GET工作流程的几个关键点。 如果这有点困扰,请告诉我。 对不起,很长的post。

我还要注意,返回ActionResult的PRG中有一个微妙的区别,即返回RedirectToRouteResult的那个。 如果在具有ValidationError之后使用RedirectToRouteResult刷新页面(F5),则错误将不会保留,并且您将获得一个干净的视图,就像您第一次输入一样。 使用ActionResult,您可以刷新并查看包含错误的完全相同的页面。 这与ActionResult或RedirectToRouteResult返回类型无关,因为在一种情况下,您始终在POST上重定向,而另一种情况仅在成功POST时重定向。 PRG并不建议在不成功的POST上盲目重定向,但有些人更喜欢在每个post上进行重定向,这需要TempData传输。