ASP.NET MVC - 如何在RedirectToAction之间保留ModelState错误?

107

我有以下两个动作方法(为方便起见进行了简化):

[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(如上所示),它就会丢失模型错误信息。

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

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

有什么建议吗?


10
我通过不使用“后重定向-再获取”模式来解决验证错误的问题,而是仅使用View()。这样做完全有效,而且比跳过一堆障碍更为简单,重定向会影响您的浏览器历史记录。 - Jimmy Bogard
2
除了@JimmyBogard所说的之外,还需要从“Create”方法中提取出填充ViewData的逻辑,并在“Create”GET方法和“Create”POST方法中的验证失败分支中调用它。 - Russ Cam
1
同意,避免问题是解决问题的一种方式。我有一些逻辑来填充我的“Create”视图中的内容,我只是将其放在一些叫做“populateStuff”的方法中,在“GET”和失败的“POST”中都调用它。 - Francois Joly
13
我不同意@JimmyBogard的看法。如果您将文章发布到一个动作中,然后返回视图,那么遇到的问题是,如果用户单击“刷新”按钮,则会收到有关是否要再次启动该文章的警告。 - The Muffin Man
12个回答

87
我今天自己解决了这个问题,然后遇到了这个问题。其中一些答案很有用(使用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"]});
    }   
}

如果您也想像bigb建议的那样将模型传递给TempData,那么您仍然可以这样做。

谢谢。我们实现了类似于你的方法。https://gist.github.com/ferventcoder/4735084 - ferventcoder
@asgeo1 - 很棒的解决方案,但是我在与重复的部分视图组合使用时遇到了问题,我在这里发布了问题:https://dev59.com/WIfca4cB1Zd3GeqPlKD8 - Josh
警告 - 如果页面通过一个请求全部提供(而不是通过AJAX分解),那么使用此解决方案会引起麻烦,因为TempData会保留到下一个请求。例如:您在一个页面中输入搜索条件,然后PRG到搜索结果,然后单击链接直接导航回搜索页面,原始搜索值将被重新填充。其他奇怪的行为有时也会出现,这些行为很难复制。 - PJ7
直到我意识到会话ID一直在变化,我才能使其正常工作。这篇文章帮助了我解决问题:https://dev59.com/d3VC5IYBdhLWcg3weBA-#5835631 - Rudey
当有多个浏览器标签页同时发出请求时,NextRequestTempData的行为是什么? - dan

53

你需要在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"] 中没有任何数据。

2
太好了。并且因为提到刷新问题而加上一个大加1。这是最完整的答案,我会接受它的,非常感谢。 :) - RPM1984
8
这并没有真正回答标题中的问题。ModelState没有被保留,这会导致输入的HtmlHelpers不会保留用户输入的内容。这几乎是一个解决方法。 - John Farrell
1
我最终采用了@Wim在他的答案中提出的建议。 - RPM1984
19
@jfar,我同意,这个答案行不通并且不会保留ModelState。但是,如果你将它修改成像这样做一样 TempData["ModelState"] = ModelState; 然后通过 ModelState.Merge((ModelStateDictionary)TempData["ModelState"]); 进行还原,那么它就可以工作了。 - asgeo1
1
当POST操作验证失败时,您不能仅仅返回Create(uniqueUri)吗?因为ModelState值优先于传递给视图的ViewModel,所以已发布的数据应该仍然保留。 - ajbeaven
显示剩余4条评论

7
为什么不在“Create”方法中创建一个私有函数来处理逻辑,并从Get和Post方法中调用该方法,然后只需返回View()。

1
这也是我的做法,只不过我没有使用私有函数,而是在出现错误时让我的POST方法调用GET方法(即return Create(new { uniqueUri = ... });)。你的逻辑保持DRY(就像调用RedirectToAction一样),但避免了重定向带来的问题,例如丢失ModelState。 - Daniel Liuzzi
1
@DanielLiuzzi:用那种方式做不会改变URL。所以你最终得到的URL会像“/controller/create/”这样。 - Skorunka František
@SkorunkaFrantišek 这正是重点所在。问题声明了“如果出现错误,我需要显示相同的页面并显示错误”。在这种情况下,如果显示相同的页面,则URL不发生变化是完全可以接受的(而且我认为更可取)。此外,这种方法的一个优点是,如果遇到的错误不是验证错误,而是系统错误(例如DB超时),它允许用户简单地刷新页面以重新提交表单。 - Daniel Liuzzi

5

我可以使用TempData["Errors"]

TempData会在多个动作之间传递数据,但只保留一次。


4

我建议你返回视图,并通过操作上的属性避免重复。这里有一个填充视图数据的例子。你可以使用类似的逻辑来处理你的创建方法。

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

1
请查看Post/Redirect/Get模式:http://en.wikipedia.org/wiki/Post/Redirect/Get - DreamSonic
2
通常在模型验证满足后使用它,以防止在刷新时向同一表单提交更多的帖子。但是,如果表单存在问题,则仍需要进行更正和重新发布。这个问题涉及处理模型错误。 - CRice
这对于简单的页面和视图非常好 +1。Post/Redirect/Get注释只是一种模式,而不是生命中的黄金法则,因此我选择忽略那个维基链接。问题在于视图使用一个ViewModel,但发布到具有单独模型的操作。现在的问题是如何显示页面...但是怎么做呢? - Piotr Kula
1
@ppumkin 也许可以尝试使用ajax发布,这样你就不必费力重建你的视图服务器端了。 - CRice
是的。这就是我最终做的事情。解决了所有问题。很高兴知道我选择了一个好的解决方案。感谢您的评论。+1 - Piotr Kula
显示剩余7条评论

4

微软删除了在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");
}

完美运行!编辑了答案以修复粘贴代码时的小括号错误。 - VDWWD
这是唯一一个在 .net core 2.1 中有效的答案。 - Robbie Chiha

2

我有一个方法可以将模型状态添加到临时数据中。接着,在我的基本控制器中,我有一个方法来检查临时数据是否存在任何错误。如果有错误,它会将它们重新添加到ModelState中。


1
我的情景有点复杂,因为我正在使用PRG模式,所以我的ViewModel(“SummaryVM”)在TempData中,并且我的摘要屏幕显示它。此页面上有一个小表单,用于将一些信息POST到另一个操作。这种情况的复杂性来自用户需要在此页面上编辑SummaryVM中的某些字段的要求。
Summary.cshtml具有验证摘要,该摘要将捕获我们创建的ModelState错误。
@Html.ValidationSummary()

现在我的表单需要向HttpPost操作的Summary()提交。我有另一个非常小的ViewModel来表示编辑过的字段,模型绑定将把这些字段传递给我。

新表单:

@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控制器操作是整个过程的起点,它查找tempdata中的任何错误并将其添加到modelstate中。

[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);
            }
        }

1
    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);
        }
    }

当我解决问题时,遇到了很多不明显的障碍。我会逐步指出每一个步骤。我的评论将部分重复当前分支的答案。
  1. 实现两个属性。必须显式地为控制器(filterContext.Controller as Controller)指定类型,因为默认是对象类型。
  2. 明确从这篇文章中序列化ModelState https://andrewlock.net/post-redirect-get-using-tempdata-in-asp-net-core/
  3. 如果目标操作中的TempData为空,则在startup.cs中检查实现缓存。您需要添加memoryCache或SqlServerCache或另一个https://dev59.com/HFgR5IYBdhLWcg3wT7xL#41500275

1

我这里只提供示例代码 在您的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);
    }
}

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接