条件模型状态合并
Conditional ModelState Merge
我实现了对“Preserve ModelState Errors Across RedirectToAction?”问题的第二个响应,该问题涉及使用两个自定义 ActionFilterAttributes。我喜欢这个解决方案,它通过向需要该功能的方法添加一个属性来保持代码整洁。
该解决方案在大多数情况下都运行良好,但我 运行 遇到了重复局部视图的问题。基本上我有部分视图使用它自己的模型,与父视图使用的模型分开。
主视图中我的代码的简化版本:
@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 时,一切正常。每次执行分部视图时,"model => model.Address1" 等兰巴表达式都会从 ModelState 中提取正确的值。
问题是当我获得重定向并使用自定义 ActionFilterAttributes 时。核心问题是,不仅 Address 的一个实例的 ModelState 更新了,而且由 Partial View 构建的所有 Addresses 的 ModelState 都被覆盖,因此它们包含相同的值,而不是正确的实例值。
我的问题是如何修改自定义 ActionFilterAttributes 以便它只更新一个受影响的 Address 实例的 ModelState,而不是所有 ModelState?我想避免向使用属性的方法添加任何内容,以保持干净的实现。
这是来自另一个问题的自定义 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"]);
}
}
}
检查 this implementation (ben foster) 是否有效:
我经常使用它,从来没有遇到过问题。
您是否正确设置了属性? ' RestoreModelStateFromTempDataAttribute
在 get
操作上,SetTempDataModelState
在你的 post
操作上?
这是需要的 4 个 类(导出、导入、传输和验证)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 (i.e. PRG)
if (!filterContext.Controller.ViewData.ModelState.IsValid &&
(filterContext.Result is RedirectResult || filterContext.Result is RedirectToRouteResult))
{
ExportModelStateToTempData(filterContext);
}
base.OnActionExecuted(filterContext);
}
}
/// <summary>
/// An Action Filter for importing ModelState from TempData.
/// You need to decorate your GET actions with this when using the <see cref="ValidateModelStateAttribute"/>.
/// </summary>
/// <remarks>
/// Useful when following the PRG (Post, Redirect, Get) pattern.
/// </remarks>
[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;
/// <summary>
/// Exports the current ModelState to TempData (available on the next request).
/// </summary>
protected static void ExportModelStateToTempData(ControllerContext context)
{
context.Controller.TempData[Key] = context.Controller.ViewData.ModelState;
}
/// <summary>
/// Populates the current ModelState with the values in TempData
/// </summary>
protected static void ImportModelStateFromTempData(ControllerContext context)
{
var prevModelState = context.Controller.TempData[Key] as ModelStateDictionary;
context.Controller.ViewData.ModelState.Merge(prevModelState);
}
/// <summary>
/// Removes ModelState from TempData
/// </summary>
protected static void RemoveModelStateFromTempData(ControllerContext context)
{
context.Controller.TempData[Key] = null;
}
}
/// <summary>
/// An ActionFilter for automatically validating ModelState before a controller action is executed.
/// Performs a Redirect if ModelState is invalid. Assumes the <see cref="ImportModelStateFromTempDataAttribute"/> is used on the GET action.
/// </summary>
[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);
}
}
编辑
这是一个正常的(非动作过滤器)PRG 模式:
[HttpGet]
public async Task<ActionResult> Edit(Guid id)
{
var calendarEvent = await calendarService.FindByIdAsync(id);
if (calendarEvent == null) return this.RedirectToAction<CalendarController>(c => c.Index());
var model = new CalendarEditViewModel(calendarEvent);
ViewData.Model = model;
return View();
}
[HttpPost]
public async Task<ActionResult> 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<CalendarController>(c => c.Index());
}
你想用动作过滤器(或它们的目的)避免什么是删除每个 post 动作的 ModelState.IsValid 检查,所以相同的(动作过滤器)将是:
[HttpGet, ImportModelStateFromTempData]
public async Task<ActionResult> Edit(Guid id)
{
var calendarEvent = await calendarService.FindByIdAsync(id);
if (calendarEvent == null) return this.RedirectToAction<CalendarController>(c => c.Index());
var model = new CalendarEditViewModel(calendarEvent);
ViewData.Model = model;
return View();
}
// ActionResult changed to RedirectToRouteResult
[HttpPost, ValidateModelState]
public async Task<RedirectToRouteResult> 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<CalendarController>(c => c.Index());
}
这里没有更多的事情发生。所以,如果你只使用 ExportModelState 动作过滤器,你最终会得到一个像这样的 post 动作:
[HttpPost, ExportModelStateToTempData]
public async Task<RedirectToRouteResult> 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<CalendarController>(c => c.Index());
}
这让我问你,为什么你一开始还要为 ActionFilters
烦恼?
虽然我确实喜欢 ValidateModelState 模式(很多人不喜欢),但如果您在控制器中重定向,我真的看不到任何好处,除了一种情况,您有其他模型状态错误,为了完整性让我给你一个例子:
[HttpPost, ValidateModelState, ExportModelStateToTempData]
public async Task<RedirectToRouteResult> 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<CalendarController>(c => c.Index());
}
在最后一个例子中,我同时使用了ValidateModelState
和ExportModelState
,这是因为ValidateModelState
在ActionExecuting
上运行,所以它在进入方法主体之前进行验证,如果绑定有一些验证错误,它将自动重定向。
然后我有另一个不能在数据注释中的检查,因为它处理加载实体并查看它是否具有正确的要求(我知道这不是最好的例子,将其视为在注册时查看提供的用户名是否可用,我知道远程数据注释但不涵盖所有情况)然后我只是根据绑定以外的外部因素用我自己的错误更新 ModelState
。由于 ExportModelState
在 ActionExecuted
上运行,我对 ModelState
的所有修改都保留在 TempData
上,因此我将在 HttpGet
编辑操作中保留它们。
我知道这一切会让我们中的一些人感到困惑,没有关于如何在控制器/PRG 端执行 MVC 的真正好的指示。我正在努力制作一个博客 post 来涵盖所有场景和解决方案。这只是其中的1%。
我希望至少我清除了 POST - GET 工作流的几个关键点。如果这混淆多于帮助,请告诉我。很抱歉 post.
我还想指出,在 PRG returning ActionResult 和 returning RedirectToRouteResult 中存在一个 微妙 差异。
如果您在出现 ValidationError 后刷新页面 (F5),使用 RedirectToRouteResult,错误将不会持续存在,您将获得一个清晰的视图,就像您第一次输入一样。使用 ActionResult,您刷新并看到完全相同的页面,包括错误。这与 ActionResult 或 RedirectToRouteResult return 类型无关,这是因为在一种情况下您总是在 POST 上重定向,而另一种情况下您仅在成功时重定向 POST。 PRG 不建议在不成功的 POST 上进行盲目重定向,但有些人更喜欢在每个 post 上进行重定向,这需要 TempData 传输。
我实现了对“Preserve ModelState Errors Across RedirectToAction?”问题的第二个响应,该问题涉及使用两个自定义 ActionFilterAttributes。我喜欢这个解决方案,它通过向需要该功能的方法添加一个属性来保持代码整洁。
该解决方案在大多数情况下都运行良好,但我 运行 遇到了重复局部视图的问题。基本上我有部分视图使用它自己的模型,与父视图使用的模型分开。
主视图中我的代码的简化版本:
@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 时,一切正常。每次执行分部视图时,"model => model.Address1" 等兰巴表达式都会从 ModelState 中提取正确的值。
问题是当我获得重定向并使用自定义 ActionFilterAttributes 时。核心问题是,不仅 Address 的一个实例的 ModelState 更新了,而且由 Partial View 构建的所有 Addresses 的 ModelState 都被覆盖,因此它们包含相同的值,而不是正确的实例值。
我的问题是如何修改自定义 ActionFilterAttributes 以便它只更新一个受影响的 Address 实例的 ModelState,而不是所有 ModelState?我想避免向使用属性的方法添加任何内容,以保持干净的实现。
这是来自另一个问题的自定义 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"]);
}
}
}
检查 this implementation (ben foster) 是否有效: 我经常使用它,从来没有遇到过问题。
您是否正确设置了属性? ' RestoreModelStateFromTempDataAttribute
在 get
操作上,SetTempDataModelState
在你的 post
操作上?
这是需要的 4 个 类(导出、导入、传输和验证)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 (i.e. PRG)
if (!filterContext.Controller.ViewData.ModelState.IsValid &&
(filterContext.Result is RedirectResult || filterContext.Result is RedirectToRouteResult))
{
ExportModelStateToTempData(filterContext);
}
base.OnActionExecuted(filterContext);
}
}
/// <summary>
/// An Action Filter for importing ModelState from TempData.
/// You need to decorate your GET actions with this when using the <see cref="ValidateModelStateAttribute"/>.
/// </summary>
/// <remarks>
/// Useful when following the PRG (Post, Redirect, Get) pattern.
/// </remarks>
[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;
/// <summary>
/// Exports the current ModelState to TempData (available on the next request).
/// </summary>
protected static void ExportModelStateToTempData(ControllerContext context)
{
context.Controller.TempData[Key] = context.Controller.ViewData.ModelState;
}
/// <summary>
/// Populates the current ModelState with the values in TempData
/// </summary>
protected static void ImportModelStateFromTempData(ControllerContext context)
{
var prevModelState = context.Controller.TempData[Key] as ModelStateDictionary;
context.Controller.ViewData.ModelState.Merge(prevModelState);
}
/// <summary>
/// Removes ModelState from TempData
/// </summary>
protected static void RemoveModelStateFromTempData(ControllerContext context)
{
context.Controller.TempData[Key] = null;
}
}
/// <summary>
/// An ActionFilter for automatically validating ModelState before a controller action is executed.
/// Performs a Redirect if ModelState is invalid. Assumes the <see cref="ImportModelStateFromTempDataAttribute"/> is used on the GET action.
/// </summary>
[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);
}
}
编辑
这是一个正常的(非动作过滤器)PRG 模式:
[HttpGet]
public async Task<ActionResult> Edit(Guid id)
{
var calendarEvent = await calendarService.FindByIdAsync(id);
if (calendarEvent == null) return this.RedirectToAction<CalendarController>(c => c.Index());
var model = new CalendarEditViewModel(calendarEvent);
ViewData.Model = model;
return View();
}
[HttpPost]
public async Task<ActionResult> 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<CalendarController>(c => c.Index());
}
你想用动作过滤器(或它们的目的)避免什么是删除每个 post 动作的 ModelState.IsValid 检查,所以相同的(动作过滤器)将是:
[HttpGet, ImportModelStateFromTempData]
public async Task<ActionResult> Edit(Guid id)
{
var calendarEvent = await calendarService.FindByIdAsync(id);
if (calendarEvent == null) return this.RedirectToAction<CalendarController>(c => c.Index());
var model = new CalendarEditViewModel(calendarEvent);
ViewData.Model = model;
return View();
}
// ActionResult changed to RedirectToRouteResult
[HttpPost, ValidateModelState]
public async Task<RedirectToRouteResult> 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<CalendarController>(c => c.Index());
}
这里没有更多的事情发生。所以,如果你只使用 ExportModelState 动作过滤器,你最终会得到一个像这样的 post 动作:
[HttpPost, ExportModelStateToTempData]
public async Task<RedirectToRouteResult> 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<CalendarController>(c => c.Index());
}
这让我问你,为什么你一开始还要为 ActionFilters
烦恼?
虽然我确实喜欢 ValidateModelState 模式(很多人不喜欢),但如果您在控制器中重定向,我真的看不到任何好处,除了一种情况,您有其他模型状态错误,为了完整性让我给你一个例子:
[HttpPost, ValidateModelState, ExportModelStateToTempData]
public async Task<RedirectToRouteResult> 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<CalendarController>(c => c.Index());
}
在最后一个例子中,我同时使用了ValidateModelState
和ExportModelState
,这是因为ValidateModelState
在ActionExecuting
上运行,所以它在进入方法主体之前进行验证,如果绑定有一些验证错误,它将自动重定向。
然后我有另一个不能在数据注释中的检查,因为它处理加载实体并查看它是否具有正确的要求(我知道这不是最好的例子,将其视为在注册时查看提供的用户名是否可用,我知道远程数据注释但不涵盖所有情况)然后我只是根据绑定以外的外部因素用我自己的错误更新 ModelState
。由于 ExportModelState
在 ActionExecuted
上运行,我对 ModelState
的所有修改都保留在 TempData
上,因此我将在 HttpGet
编辑操作中保留它们。
我知道这一切会让我们中的一些人感到困惑,没有关于如何在控制器/PRG 端执行 MVC 的真正好的指示。我正在努力制作一个博客 post 来涵盖所有场景和解决方案。这只是其中的1%。
我希望至少我清除了 POST - GET 工作流的几个关键点。如果这混淆多于帮助,请告诉我。很抱歉 post.
我还想指出,在 PRG returning ActionResult 和 returning RedirectToRouteResult 中存在一个 微妙 差异。 如果您在出现 ValidationError 后刷新页面 (F5),使用 RedirectToRouteResult,错误将不会持续存在,您将获得一个清晰的视图,就像您第一次输入一样。使用 ActionResult,您刷新并看到完全相同的页面,包括错误。这与 ActionResult 或 RedirectToRouteResult return 类型无关,这是因为在一种情况下您总是在 POST 上重定向,而另一种情况下您仅在成功时重定向 POST。 PRG 不建议在不成功的 POST 上进行盲目重定向,但有些人更喜欢在每个 post 上进行重定向,这需要 TempData 传输。