【问题标题】:ASP.NET MVC - How to Preserve ModelState Errors Across RedirectToAction?ASP.NET MVC - 如何跨 RedirectToAction 保留 ModelState 错误?
【发布时间】:2011-06-06 06:48:37
【问题描述】:

我有以下两种操作方法(为提问而简化):

[HttpGet]
public ActionResult Create(string uniqueUri)
{
   // get some stuff based on uniqueuri, set in ViewData.  
   return View();
}

[HttpPost]
public ActionResult Create(Review review)
{
   // validate review
   if (validatedOk)
   {
      return RedirectToAction("Details", new { postId = review.PostId});
   }  
   else
   {
      ModelState.AddModelError("ReviewErrors", "some error occured");
      return RedirectToAction("Create", new { uniqueUri = Request.RequestContext.RouteData.Values["uniqueUri"]});
   }   
}

所以,如果验证通过,我会重定向到另一个页面(确认)。

如果发生错误,我需要显示与错误相同的页面。

如果我执行return View(),则会显示错误,但如果我执行return RedirectToAction(如上),则会丢失模型错误。

我对这个问题并不感到惊讶,只是想知道你们如何处理这个问题?

我当然可以只返回相同的视图而不是重定向,但我在填充视图数据的“创建”方法中有逻辑,我必须复制它。

有什么建议吗?

【问题讨论】:

  • 我通过不对验证错误使用 Post-Redirect-Get 模式来解决这个问题。我只是使用视图()。这样做是完全有效的,而不是跳过一堆箍 - 并重定向您的浏览器历史记录。
  • 除了@JimmyBogard 所说的,提取出填充ViewData 的Create 方法中的逻辑,并在Create GET 方法和失败的验证分支中调用它CreatePOST 方法。
  • 同意,避免问题是解决问题的一种方法。我有一些逻辑可以在我的Create 视图中填充内容,我只是把它放在我在GET 和失败POST 中调用的一些方法populateStuff 中。
  • @JimmyBogard 我不同意,如果您发布到操作然后返回视图,您会遇到这样的问题:如果用户点击刷新,他们会收到关于想要再次启动该帖子的警告。

标签: asp.net-mvc error-handling modelstate redirecttoaction http-redirect


【解决方案1】:

我今天不得不自己解决这个问题,并遇到了这个问题。

有些答案很有用(使用 TempData),但并没有真正回答手头的问题。

我在这篇博文中找到了最好的建议:

http://www.jefclaes.be/2012/06/persisting-model-state-when-using-prg.html

基本上,使用 TempData 来保存和恢复 ModelState 对象。但是,如果您将其抽象为属性,它会干净得多。

例如

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

然后根据您的示例,您可以像这样保存/恢复 ModelState:

[HttpGet]
[RestoreModelStateFromTempData]
public ActionResult Create(string uniqueUri)
{
    // get some stuff based on uniqueuri, set in ViewData.  
    return View();
}

[HttpPost]
[SetTempDataModelState]
public ActionResult Create(Review review)
{
    // validate review
    if (validatedOk)
    {
        return RedirectToAction("Details", new { postId = review.PostId});
    }  
    else
    {
        ModelState.AddModelError("ReviewErrors", "some error occured");
        return RedirectToAction("Create", new { uniqueUri = Request.RequestContext.RouteData.Values["uniqueUri"]});
    }   
}

如果您还想在 TempData 中传递模型(如 bigb 建议的那样),那么您仍然可以这样做。

【讨论】:

  • 谢谢。我们实施了与您的方法类似的方法。 gist.github.com/ferventcoder/4735084
  • @asgeo1 - 很好的解决方案,但我在使用它和重复部分视图时遇到了问题,我在这里发布了问题:*.com/questions/28372330/…
  • 警告 - 如果页面全部在一个请求中提供(并且没有通过 AJAX 分解),那么您使用此解决方案会遇到麻烦,因为 TempData 会保留到 next 请求。例如:您在一个页面中输入搜索条件,然后 PRG 到搜索结果,然后单击链接直接导航回搜索页面,原始搜索值将被重新填充。还会出现其他奇怪且有时难以重现的行为。
  • 在我意识到会话 ID 不断变化之前,我无法完成这项工作。这帮助我解决了这个问题:*.com/a/5835631/1185136
  • 问:当有多个浏览器选项卡发出(多个/同时)请求时,NextRequestTempData 的行为是什么?
【解决方案2】:

您的HttpGet 操作需要具有相同的Review 实例。 为此,您应该在 HttpPost 操作的临时变量中保存对象 Review review,然后在 HttpGet 操作上恢复它。

[HttpGet]
public ActionResult Create(string uniqueUri)
{
   //Restore
   Review review = TempData["Review"] as Review;            

   // get some stuff based on uniqueuri, set in ViewData.  
   return View(review);
}
[HttpPost]
public ActionResult Create(Review review)
{
   //Save your object
   TempData["Review"] = review;

   // validate review
   if (validatedOk)
   {
      return RedirectToAction("Details", new { postId = review.PostId});
   }  
   else
   {
      ModelState.AddModelError("ReviewErrors", "some error occured");
      return RedirectToAction("Create", new { uniqueUri = Request.RequestContext.RouteData.Values["uniqueUri"]});
   }   
}

如果您希望即使在第一次执行 HttpGet 操作后刷新浏览器也能正常工作,您可以这样做:

  Review review = TempData["Review"] as Review;  
  TempData["Review"] = review;

否则刷新按钮对象review 将为空,因为TempData["Review"] 中没有任何数据。

【讨论】:

  • 优秀。并且提到刷新问题是一个很大的+1。这是最完整的答案,所以我会接受,谢谢。 :)
  • 这并不能真正回答标题中的问题。 ModelState 未保留,并且具有诸如输入 HtmlHelpers 未保留用户条目之类的后果。这几乎是一种解决方法。
  • 我最终做了@Wim 在他的回答中建议的事情。
  • @jfar,我同意,这个答案不起作用并且不会保留 ModelState。但是,如果您对其进行修改使其执行TempData["ModelState"] = ModelState; 之类的操作并使用ModelState.Merge((ModelStateDictionary)TempData["ModelState"]); 进行恢复,那么它将起作用
  • 当 POST 验证失败时,您能不能只 return Create(uniqueUri)?由于 ModelState 值优先于传入视图的 ViewModel,因此发布的数据仍应保留。
【解决方案3】:

为什么不使用“Create”方法中的逻辑创建一个私有函数,并从 Get 和 Post 方法调用此方法并返回 View()。

【讨论】:

  • 这也是我所做的,只是没有私有函数,我只是让我的 POST 方法在错误时调用 GET 方法(即return Create(new { uniqueUri = ... });。你的逻辑保持干燥(就像调用 @ 987654322@),但没有重定向带来的问题,例如丢失您的 ModelState。
  • @DanielLiuzzi:这样做不会改变 URL。所以你以 url 结尾,比如“/controller/create/”。
  • @SkorunkaFrantišek 这正是重点。问题指出 如果发生错误,我需要显示与错误相同的页面。 在这种情况下,如果同一页面出现,则 URL 不会更改是完全可以接受的(并且更可取的 IMO)显示。此外,这种方法的一个优点是,如果所讨论的错误不是验证错误而是系统错误(例如 DB 超时),它允许用户简单地刷新页面以重新提交表单。
【解决方案4】:

我可以使用TempData["Errors"]

TempData 在保存数据的操作中传递 1 次。

【讨论】:

    【解决方案5】:

    我建议您返回视图,并通过操作上的属性避免重复。这是一个填充以查看数据的示例。你可以用你的 create 方法逻辑做类似的事情。

    public class GetStuffBasedOnUniqueUriAttribute : ActionFilterAttribute
    {
        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            var filter = new GetStuffBasedOnUniqueUriFilter();
    
            filter.OnActionExecuting(filterContext);
        }
    }
    
    
    public class GetStuffBasedOnUniqueUriFilter : IActionFilter
    {
        #region IActionFilter Members
    
        public void OnActionExecuted(ActionExecutedContext filterContext)
        {
    
        }
    
        public void OnActionExecuting(ActionExecutingContext filterContext)
        {
            filterContext.Controller.ViewData["somekey"] = filterContext.RouteData.Values["uniqueUri"];
        }
    
        #endregion
    }
    

    这是一个例子:

    [HttpGet, GetStuffBasedOnUniqueUri]
    public ActionResult Create()
    {
        return View();
    }
    
    [HttpPost, GetStuffBasedOnUniqueUri]
    public ActionResult Create(Review review)
    {
        // validate review
        if (validatedOk)
        {
            return RedirectToAction("Details", new { postId = review.PostId });
        }
    
        ModelState.AddModelError("ReviewErrors", "some error occured");
        return View(review);
    }
    

    【讨论】:

    • 这是个坏主意吗?我认为该属性避免了使用另一个操作的需要,因为这两个操作都可以使用该属性加载到 ViewData。
    • 请查看 Post/Redirect/Get 模式:en.wikipedia.org/wiki/Post/Redirect/Get
    • 这通常在满足模型验证后使用,以防止在刷新时进一步发布到同一表单。但是如果表格有问题,那么无论如何都需要更正和重新发布。这个问题涉及处理模型错误。
    • 过滤器用于操作上的可重用代码,对于将内容放入 ViewData 中特别有用。 TempData 只是一种解决方法。
    • @ppumkin 可能会尝试使用 ajax 发布,这样您就不会很难重建视图服务器端。
    【解决方案6】:

    我有一种将模型状态添加到临时数据的方法。然后,我的基本控制器中有一个方法可以检查临时数据是否有任何错误。如果有,它会将它们添加回 ModelState。

    【讨论】:

      【解决方案7】:

      Microsoft 删除了在 TempData 中存储复杂数据类型的功能,因此之前的答案不再适用;你只能存储简单的类型,比如字符串。我已更改 @asgeo1 的答案以按预期工作。

      public class SetTempDataModelStateAttribute : ActionFilterAttribute
      {
          public override void OnActionExecuted(ActionExecutedContext filterContext)
          {
              base.OnActionExecuted(filterContext);
      
              var controller = filterContext.Controller as Controller;
              var modelState = controller?.ViewData.ModelState;
              if (modelState != null)
              {
                  var listError = modelState.Where(x => x.Value.Errors.Any())
                      .ToDictionary(m => m.Key, m => m.Value.Errors
                      .Select(s => s.ErrorMessage)
                      .FirstOrDefault(s => s != null));
                  controller.TempData["KEY HERE"] = JsonConvert.SerializeObject(listError);
              }
          }
      }
      
      
      public class RestoreModelStateFromTempDataAttribute : ActionFilterAttribute
      {
          public override void OnActionExecuting(ActionExecutingContext filterContext)
          {
              base.OnActionExecuting(filterContext);
      
              var controller = filterContext.Controller as Controller;
              var tempData = controller?.TempData?.Keys;
              if (controller != null && tempData != null)
              {
                  if (tempData.Contains("KEY HERE"))
                  {
                      var modelStateString = controller.TempData["KEY HERE"].ToString();
                      var listError = JsonConvert.DeserializeObject<Dictionary<string, string>>(modelStateString);
                      var modelState = new ModelStateDictionary();
                      foreach (var item in listError)
                      {
                          modelState.AddModelError(item.Key, item.Value ?? "");
                      }
      
                      controller.ViewData.ModelState.Merge(modelState);
                  }
              }
          }
      }
      

      从这里,您可以根据需要在控制器方法上简单地添加所需的数据注释。

      [RestoreModelStateFromTempDataAttribute]
      [HttpGet]
      public async Task<IActionResult> MethodName()
      {
      }
      
      
      [SetTempDataModelStateAttribute]
      [HttpPost]
      public async Task<IActionResult> MethodName()
      {
          ModelState.AddModelError("KEY HERE", "ERROR HERE");
      }
      

      【讨论】:

      • 完美运行!编辑了粘贴代码时修复小括号错误的答案。
      • 这是唯一适用于 .net core 2.1 的答案。
      【解决方案8】:

      我的场景有点复杂,因为我使用的是 PRG 模式,所以我的 ViewModel(“SummaryVM”)位于 TempData 中,而我的摘要屏幕会显示它。此页面上有一个小表单,用于将一些信息发布到另一个操作。 复杂性来自要求用户在此页面上编辑 SummaryVM 中的某些字段。

      Summary.cshtml 包含验证摘要,它将捕获我们将创建的 ModelState 错误。

      @Html.ValidationSummary()
      

      我的表单现在需要 POST 到 Summary() 的 HttpPost 操作。我有另一个非常小的 ViewModel 来表示已编辑的字段,modelbinding 会将这些交给我。

      新形式:

      @using (Html.BeginForm("Summary", "MyController", FormMethod.Post))
      {
          @Html.Hidden("TelNo") @* // Javascript to update this *@
      

      还有动作……

      [HttpPost]
      public ActionResult Summary(EditedItemsVM vm)
      

      在这里我做了一些验证,我检测到一些错误的输入,所以我需要返回到错误的摘要页面。为此,我使用 TempData,它可以在重定向后继续存在。 如果数据没有问题,我将 SummaryVM 对象替换为副本(但当然更改了已编辑的字段)然后执行 RedirectToAction("NextAction");

      // Telephone number wasn't in the right format
      List<string> listOfErrors = new List<string>();
      listOfErrors.Add("Telephone Number was not in the correct format. Value supplied was: " + vm.TelNo);
      TempData["SummaryEditedErrors"] = listOfErrors;
      return RedirectToAction("Summary");
      

      Summary 控制器动作,所有这一切都从这里开始,查找临时数据中的任何错误并将它们添加到模型状态。

      [HttpGet]
      [OutputCache(Duration = 0)]
      public ActionResult Summary()
      {
          // setup, including retrieval of the viewmodel from TempData...
      
      
          // And finally if we are coming back to this after a failed attempt to edit some of the fields on the page,
          // load the errors stored from TempData.
              List<string> editErrors = new List<string>();
              object errData = TempData["SummaryEditedErrors"];
              if (errData != null)
              {
                  editErrors = (List<string>)errData;
                  foreach(string err in editErrors)
                  {
                      // ValidationSummary() will see these
                      ModelState.AddModelError("", err);
                  }
              }
      

      【讨论】:

        【解决方案9】:
            public class RestoreModelStateFromTempDataAttribute : ActionFilterAttribute
            {
                public override void OnActionExecuting(ActionExecutingContext filterContext)
                {
                    var controller = filterContext.Controller as Controller;
                    if (controller.TempData.ContainsKey("ModelState"))
                    {
                        var modelState = ModelStateHelpers.DeserialiseModelState(controller.TempData["ModelState"].ToString());
                        controller.ViewData.ModelState.Merge(modelState);
                    }
                    base.OnActionExecuting(filterContext);
                }
            }
            public class SetTempDataModelStateAttribute : ActionFilterAttribute
            {
                public override void OnActionExecuted(ActionExecutedContext filterContext)
                {
                    var controller = filterContext.Controller as Controller;
                    controller.TempData["ModelState"] = ModelStateHelpers.SerialiseModelState(controller.ViewData.ModelState);
                    base.OnActionExecuted(filterContext);
                }
            }
        

        当我解决一些问题时,我遇到了很多不明显的障碍。我会一步一步说明一切。 我的 cmets 将部分复制当前分支的答案

        1. 实现两个属性。您必须明确指定控制器的类型(filterContext.Controller as Controller),因为默认是对象类型。
        2. 从本文https://andrewlock.net/post-redirect-get-using-tempdata-in-asp-net-core/显式实现序列化ModelState
        3. 如果在 startup.cs 中检查实现缓存时,目标操作中的 TempData 为空。您需要添加 memoryCache 或 SqlServerCache 或另一个 https://*.com/a/41500275/11903993

        【讨论】:

          【解决方案10】:

          我更喜欢向我的 ViewModel 添加一个填充默认值的方法:

          public class RegisterViewModel
          {
              public string FirstName { get; set; }
              public IList<Gender> Genders { get; set; }
              //Some other properties here ....
              //...
              //...
          
              ViewModelType PopulateDefaultViewData()
              {
                  this.FirstName = "No body";
                  this.Genders = new List<Gender>()
                  {
                      Gender.Male,
                      Gender.Female
                  };
          
                  //Maybe other assinments here for other properties...
              }
          }
          

          然后,当我需要这样的原始数据时,我会调用它:

              [HttpGet]
              public async Task<IActionResult> Register()
              {
                  var vm = new RegisterViewModel().PopulateDefaultViewValues();
                  return View(vm);
              }
          
              [HttpPost]
              public async Task<IActionResult> Register(RegisterViewModel vm)
              {
                  if (!ModelState.IsValid)
                  {
                      return View(vm.PopulateDefaultViewValues());
                  }
          
                  var user = await userService.RegisterAsync(
                      email: vm.Email,
                      password: vm.Password,
                      firstName: vm.FirstName,
                      lastName: vm.LastName,
                      gender: vm.Gender,
                      birthdate: vm.Birthdate);
          
                  return Json("Registered successfully!");
              }
          

          【讨论】:

            【解决方案11】:

            我在这里只给出示例代码 在您的 viewModel 中,您可以将“ModelStateDictionary”类型的一个属性添加为

            public ModelStateDictionary ModelStateErrors { get; set; }
            

            在您的 POST 操作方法中,您可以像这样直接编写代码

            model.ModelStateErrors = ModelState; 
            

            然后将此模型分配给 Tempdata,如下所示

            TempData["Model"] = model;
            

            当您重定向到其他控制器的操作方法时,您必须在控制器中读取 Tempdata 值

            if (TempData["Model"] != null)
            {
                viewModel = TempData["Model"] as ViewModel; //Your viewmodel class Type
                if(viewModel.ModelStateErrors != null && viewModel.ModelStateErrors.Count>0)
                {
                    this.ViewData.ModelState.Merge(viewModel.ModelStateErrors);
                }
            }
            

            就是这样。您不必为此编写操作过滤器。如果您想将模型状态错误发送到另一个控制器的另一个视图,这就像上面的代码一样简单。

            【讨论】: