【问题标题】:Conditional ModelState Merge条件模型状态合并
【发布时间】:2015-04-06 23:34:51
【问题描述】:

我实现了对“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”这样的lamba表达式从ModelState中提取正确的值。

问题是当我获得重定向并使用了自定义 ActionFilterAttributes 时。核心问题是,不仅地址的一个实例的 ModelState 被更新,而且由 Partial View 构建的所有地址的 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"]);
        }
    }
}

【问题讨论】:

  • 基于your previous question 我认为您误解了EditorTemplate 和部分的使用。您不能以这种方式使用局部视图来呈现集合 - 检查 html,您将看到重复的名称和 id 属性,因此无论如何无法在回发时正确绑定集合。
  • @StephenMuecke - 如果您有更好的方法来实现相同的用户体验目标,我会全力以赴。我已将问题发布在:stackoverflow.com/questions/28386959/…

标签: c# asp.net-mvc razor asp.net-mvc-5


【解决方案1】:

检查this implementation(本福斯特)是否有效: 我经常使用它,从来没有遇到过问题。

您是否正确设置了属性? ' RestoreModelStateFromTempDataAttributeget 操作上和SetTempDataModelState 在您的post 操作上?

这里是需要的 4 个类(Export、Import、Transfer 和 Validate)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());
    }

您希望使用操作过滤器(或它们的目的)避免什么是删除对每个帖子操作的 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());
    }

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

我知道所有这一切都会让我们中的一些人感到困惑,没有关于如何在 Controller / PRG 端执行 MVC 的真正好的指示。我在努力写一篇博客文章来涵盖所有场景和解决方案。这只是其中的 1%。

我希望至少我清除了 POST - GET 工作流程的几个关键点。如果这令人困惑多于帮助,请告诉我。抱歉,帖子太长了。

我还想指出,PRG 返回的 ActionResult 与返回 RedirectToRouteResult 的 PRG 之间存在一个细微的差异。 如果您在出现 ValidationError 后刷新页面 (F5),并使用 RedirectToRouteResult,则错误将不会持续存在,并且您会获得一个干净的视图,就像您第一次进入一样。使用 ActionResult ,您刷新并看到完全相同的页面,包括错误。这与 ActionResult 或 RedirectToRouteResult 返回类型无关,因为在一种情况下您总是在 POST 上重定向,而另一种情况下您只在成功 POST 上重定向。 PRG 不建议对不成功的 POST 进行盲目重定向,但有些人更喜欢在每个帖子上都进行重定向,这需要 TempData 传输。

【讨论】:

  • 是的,我在 get 操作上正确设置了 RestoreModelStateFromTempDataAttribute,在您的 post 操作上正确设置了 SetTempDataModelState。我现在就试试这个。
  • 最终遇到了同样的问题,它基本上更新了所有地址模型状态,因此它们最终都得到相同的数据。所以我坚持清除 ModelState(在我上面的解决方法中),除非我能弄清楚如何定位正确的 ModelState。
  • 我找到了问题的根源,为什么即使它应该是有效的,所有的 Address 对象都包含相同的数据。我有一个名为 Address.AddressType 的子对象,它有一个名为 Name 的必需属性。它不是经过编辑的,所以它没有作为帖子的一部分填充。这导致了验证错误,因为它是一个必填字段,所以我最终将它作为隐藏值传递,以便父 Address 对象可以通过验证.
  • 我发现了这一点,因为我注意到在您附加的代码中,它仅在对象无效时才写入 TempData,但由于每次调用都无效,它会将所有内容都写入 TempData。因此,我已将您发布的代码与我的代码合并,一旦我通过了必填字段,它就解决了问题。由于我使用的是您发送的代码,它有助于找出问题,我认为这是一个可以接受的答案。我不喜欢的唯一部分代码(我没有使用)是重定向逻辑。我更喜欢在控制器中使用它。
  • 如果您不想要重定向,您可以在发布操作中使用 ExportModelState 而不是使用 ValidateModelState,然后您可以根据需要进行重定向。请记住,如果您不使用重定向,您还应该检查 if (Model.IsValid)。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-12-04
  • 2012-07-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多