条件模型状态合并 [英] Conditional ModelState Merge

查看:22
本文介绍了条件模型状态合并的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我实现了对Preserve 的第二个响应跨 RedirectToAction 的 ModelState 错误?"问题,涉及使用两个自定义 ActionFilterAttributes.我喜欢这个解决方案,它通过向需要该功能的方法添加一个属性来保持代码干净.

I implemented the 2nd response to the "Preserve ModelState Errors Across RedirectToAction?" question which involves using two custom ActionFilterAttributes. I like the solution, it keeps the code clean by just adding an attribute to methods that need the functionality.

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

The solution works well in most cases, but I've run across an issue with a repeating Partial View. Basically I have Partial View that uses it's own Model, separate from the Model that parent view uses.

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

The simplified version of my code from the main View:

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

部分视图_AddressModal":

The Partial View "_AddressModal":

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

当不使用自定义 ActionFilterAttributes 时,一切都按预期工作.随着 Partial View 的每次执行,lamba 表达式(例如model => model.Address1")从 ModelState 中提取正确的值.

When not using the custom ActionFilterAttributes everything works as expected. With each execution of the Partial View, the lamba expressions such "model => model.Address1" pull the correct value from ModelState.

问题是当我重定向并使用自定义 ActionFilterAttributes 时.核心问题是不仅更新了一个 Address 实例的 ModelState,而且 Partial View 构建的所有 Addresses 的 ModelState 都被覆盖,因此它们包含相同的值,而不是正确的实例值.

The problem is when I get redirect and have used the custom ActionFilterAttributes. The core problem is that not only is the ModelState for the one instance of Address updated, but the ModelState of all the Addresses built by the Partial View get overwritten so they contain the same values, instead of the correct instance values.

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

My question is how do I modify the custom ActionFilterAttributes so that it only updates the ModelState of the one affected Address instance, not all the ModelStates? I want to avoid adding anything to the methods that use the attribute, to keep the clean implementation.

这是另一个问题中的自定义 ActionFilterAttributes 代码:

Here is the custom ActionFilterAttributes code from the other question:

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"]);
        }
    }
}

推荐答案

检查是否 这个实现(本福斯特)确实有效:我经常使用它,从来没有遇到过问题.

Check out if this implementation (ben foster) does work : I used it heavily and never had a problem.

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

Are you setting the attributes correctly ? ' RestoreModelStateFromTempDataAttribute on the get action and SetTempDataModelState on your post action ?

这里是需要的 4 个类(Export、Import、Transfer 和 Validate)ModelState

Here are the 4 classes needed (Export, Import, Transfer and 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 模式:

This is a normal (non action filter) PRG pattern :

    [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 检查,因此(使用操作过滤器)相同:

What do you want to avoid with action filters (or their purpose) is to remove the ModelState.IsValid check on every post Action, so the same (with action filters) would be :

    [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 动作:

there's no much more happening here. So, if you only use ExportModelState action filter, you will end up with a post action like this :

    [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 模式(很多人不喜欢),但如果你在控制器中重定向,我真的看不到任何好处,除了一种场景,你有额外的模型状态错误,为了完整性,让我给你一个例子:

Which makes me ask you, why you even bother with ActionFilters in the first place ? While I do like the ValidateModelState pattern, (lots of people doesn't), I don't really see any benefit if you are redirecting in your controller except for one scenario, where you have additional modelstate errors, for completeness let me give you an example:

    [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 编辑动作.

In the last example, I used both ValidateModelState and ExportModelState, this is because ValidateModelState runs on ActionExecuting so it validates before entering the body of the method, if the binding have some validation error it will redirect automatically. Then I have another check that can't be in the data annotations because it deals with loading an entity and seeing if it has the correct requirements (I know is not the best example, think of it as looking if provided username is available when registration, I know about Remote data annotation but doesn't cover all cases) then I just update the ModelState with my own errors depending on external factors others than the binding. Since ExportModelState runs on ActionExecuted, all my modifications to ModelState are persisted on TempData so I will have them on HttpGet Edit action.

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

I know all this can confuse some of us, there are no really good indications on how to do MVC on the Controller / PRG side. I was thinking hard in making a blog post to cover all the scenarios and solutions. This is only the 1% of it.

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

I hope at least I cleared few key points of the POST - GET workflow. If this confuses more than it helps, please let me know. Sorry for the long post.

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

I wanted to note also that there is ONE subtle difference in the PRG returning an ActionResult that the ones returning a RedirectToRouteResult. If you refresh the page (F5) after having a ValidationError, with the RedirectToRouteResult, the errors will NOT be persisted and you get a clean view as if you entered for the first time. With the ActionResult ones, you refresh and see the exact same page including the errors. This has nothing to do with the ActionResult or RedirectToRouteResult return types, its because in one scenario you redirect always on POST while the other you redirect only on success POST. PRG does not suggest to blinding redirect on unsucessfully POST, yet some people prefer to do redirect on every post, which requires TempData transfer.

这篇关于条件模型状态合并的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆