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


91

我有以下两种操作方法(简化了问题):

[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(如上),它将丢失模型错误。

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

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

有什么建议?


10
我通过不使用Post-Redirect-Get模式解决验证错误来解决此问题。我只是使用View()。这样做是完全有效的,而不是跳过一堆圈-并将混乱的信息重定向到您的浏览器历史记录。
Jimmy Bogard

2
除了@JimmyBogard所说的以外,还要提取Create填充ViewData 的方法中的逻辑,并在CreateGET方法中以及在CreatePOST方法的失败验证分支中对其进行调用。
Russ Cam

1
同意,避免问题是解决问题的一种方法。我有一些逻辑可以填充Create视图中的内容,我只是将其放入了populateStuff我在the GET和fail中调用的某种方法POST
弗朗索瓦·乔利

12
@JimmyBogard我不同意,如果您发布操作,然后返回视图,则会遇到问题,如果用户单击刷新,他们会收到有关要再次发起该发布的警告。
松饼人

Answers:


50

你需要有相同的实例Review上的HttpGet动作。为此,您应该Review reviewHttpPost操作中将对象保存在temp变量中,然后在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'1

8
这并不能真正回答标题中的问题。不保留ModelState,并且具有分支,例如输入HtmlHelpers不保留用户条目。这几乎是一种解决方法。
约翰·法雷尔

我最终做了@Wim在他的回答中建议的事情。
RPM1984 2011年

17
@jfar,我同意,此答案不起作用,并且不保留ModelState。但是,如果您对其进行修改,使其执行类似的操作TempData["ModelState"] = ModelState; 并使用恢复ModelState.Merge((ModelStateDictionary)TempData["ModelState"]);,那么它将起作用
asgeo1 2012年

1
您能否不只是return Create(uniqueUri)在POST上验证失败时?由于ModelState值优先于传递给视图的ViewModel,因此发布的数据仍应保留。
2013年

83

我今天必须自己解决这个问题,并且遇到了这个问题。

一些答案是有用的(使用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
ferventcoder

好答案。谢谢。
Mark Vickery 2014年

3
该解决方案是我使用stackoverflow的原因。谢啦!
jugg1es 2014年

@ asgeo1 -伟大的解决方案,但我用它结合重复的部分景色碰到一个问题,我在这里张贴的问题:stackoverflow.com/questions/28372330/...
乔什-

秉承MVC的精神,采用简单解决方案并使之变得非常优雅的可爱示例。非常好!
AHowgego

7

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


这实际上是我最终要做的-您读了我的想法。+1 :)
RPM1984 2011年

1
这也是我要做的,只是没有错误,我只是让POST方法在发生错误时调用GET方法(即return Create(new { uniqueUri = ... });,您的逻辑保持DRY(非常类似于call RedirectToAction),但没有重定向所带来的问题,例如失去你的ModelState。
丹尼尔·里尤兹

1
@DanielLiuzzi:这样做不会更改URL。因此,您以url结尾,例如“ / controller / create /”。
SkorunkaFrantišek2012年

@SkorunkaFrantišek这就是重点。问题指出,如果发生错误,我需要显示与错误相同的页面。在这种情况下,如果显示相同的页面,则URL不更改是完全可以接受的(并且最好是IMO)。同样,此方法的一个优点是,如果所讨论的错误不是验证错误,而是系统错误(例如,数据库超时),则它允许用户简单地刷新页面以重新提交表单。
Daniel Liuzzi 2012年


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

这是一个坏主意吗?我认为该属性避免了使用其他操作的需要,因为这两个操作都可以使用该属性加载到ViewData。
CRice 2011年

1
请看一下Post / Redirect / Get模式:en.wikipedia.org/wiki/Post/Redirect/Get
DreamSonic 2011年

2
通常在满足模型验证后使用该功能,以防止刷新后再发布到同一表单。但是,如果表单存在问题,则无论如何都需要对其进行更正和重新发布。这个问题处理模型错误。
CRice 2011年

过滤器用于操作中的可重用代码,对于将内容放入ViewData尤其有用。TempData只是一种解决方法。
CRice 2011年

1
@ppumkin也许尝试使用ajax进行发布,这样您就不会在重建视图服务器方面遇到麻烦。
CRice

2

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


1

当我使用PRG模式时,我的场景会稍微复杂一些,因此我的ViewModel(“ SummaryVM”)在TempData中,并且“摘要”屏幕会显示它。此页面上有一个小表格,可将一些信息发布到另一个Action。复杂性来自于要求用户在此页面上编辑SummaryVM中的某些字段。

Summary.cshtml具有验证摘要,该摘要将捕获我们将创建的ModelState错误。

@Html.ValidationSummary()

我的表单现在需要发布到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控制器操作将查找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

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

完美的作品!编辑了答案,以修复粘贴代码时出现的小括号错误。
VDWWD

0

我更喜欢在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!");
    }

0

我在这里仅提供示例代码,您可以在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);
    }
}

而已。您不必为此编写动作过滤器。如果要将模型状态错误发送到另一个控制器的另一个视图,则与上面的代码一样简单。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.