首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >条件ModelState合并

条件ModelState合并
EN

Stack Overflow用户
提问于 2015-02-06 18:24:43
回答 1查看 2.1K关注 0票数 4

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

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

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

代码语言:javascript
复制
@for (int i = 0; i < Model.Addresses.Count; i++)
{
        address = (Address)Model.Addresses[i];
        @Html.Partial("_AddressModal", address);
}

部分视图"_AddressModal":

代码语言:javascript
复制
@model Acme.Domain.Models.Address
[...]
@Html.TextBoxFor(model => model.Address1, new { @class = "form-control" } )
[...]

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

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

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

下面是来自另一个问题的自定义ActionFilterAttributes代码:

代码语言:javascript
复制
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"]);
        }
    }
}
EN

回答 1

Stack Overflow用户

回答已采纳

发布于 2015-02-06 19:48:08

看看这一实现 (本福斯特)是否起作用:我大量使用它,从来没有问题。

您是否正确地设置了属性?‘RestoreModelStateFromTempDataAttributeget操作上,SetTempDataModelState在您的post操作上?

下面是所需的4个类(导出、导入、传输和验证)ModelState

代码语言:javascript
复制
 [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模式:

代码语言:javascript
复制
    [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检查,所以(使用操作过滤器)也是一样的:

代码语言:javascript
复制
    [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操作:

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

代码语言:javascript
复制
    [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,这是因为ValidateModelState运行在ActionExecuting上,因此它在进入方法主体之前进行验证,如果绑定有一些验证错误,它将自动重定向。然后,我有另一个不能在数据注释中进行的检查,因为它处理的是加载一个实体并查看它是否具有正确的需求(我知道这不是最好的例子,请将它想象为在注册时是否提供了用户名,我知道远程数据注释,但并不涵盖所有情况),然后我只需根据绑定以外的外部因素更新ModelState。由于ExportModelState运行在ActionExecuted上,所以我对ModelState的所有修改都在TempData上持久化,所以我将在HttpGet编辑操作中对它们进行修改。

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

我希望至少我清除了几个要点后得到的工作流程。如果这件事让人费解,请告诉我。很抱歉寄了这么长的邮筒。

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

票数 3
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/28372330

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档