ASP.NET MVC - How to Preserve ModelState Errors Across RedirectToAction?
Solution 1
You need to have the same instance of Review
on your HttpGet
action.
To do that you should save an object Review review
in temp variable on your HttpPost
action and then restore it on HttpGet
action.
[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"]});
}
}
If you want this to work even if the browser is refreshed after the first execution of the HttpGet
action, you could do this:
Review review = TempData["Review"] as Review;
TempData["Review"] = review;
Otherwise on refresh button object review
will be empty because there wouldn't be any data in TempData["Review"]
.
Solution 2
I had to solve this problem today myself, and came across this question.
Some of the answers are useful (using TempData), but don't really answer the question at hand.
The best advice I found was on this blog post:
http://www.jefclaes.be/2012/06/persisting-model-state-when-using-prg.html
Basically, use TempData to save and restore the ModelState object. However, it's a lot cleaner if you abstract this away into attributes.
E.g.
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"]);
}
}
}
Then as per your example, you could save / restore the ModelState like so:
[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"]});
}
}
If you also want to pass the model along in TempData (as bigb suggested) then you can still do that too.
Solution 3
Why not create a private function with the logic in the "Create" method and calling this method from both the Get and the Post method and just do return View().
Solution 4
I could use TempData["Errors"]
TempData are passed accross actions preserving data 1 time.
Solution 5
I suggest you return the view, and avoid duplication via an attribute on the action. Here is an example of populating to view data. You could do something similar with your create method logic.
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
}
Here is an example:
[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);
}
Related videos on Youtube
RPM1984
~ Past ~: Mainframes (Model 204, JCL) Java (J2SE, J2EE) Oracle VB.NET ASP.NET Web Forms/MVC ~ Present ~ .NET Core TDD, DDD (all the DDs!) Microservices Containers
Updated on July 30, 2021Comments
-
RPM1984 over 2 years
I have the following two action methods (simplified for question):
[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"]}); } }
So, if the validation passes, i redirect to another page (confirmation).
If an error occurs, i need to display the same page with the error.
If i do
return View()
, the error is displayed, but if i doreturn RedirectToAction
(as above), it loses the Model errors.I'm not surprised by the issue, just wondering how you guys handle this?
I could of course just return the same View instead of the redirect, but i have logic in the "Create" method which populates the view data, which i'd have to duplicate.
Any suggestions?
-
oblivignesh about 11 yearsI solve this problem by not using the Post-Redirect-Get pattern for validation errors. I just use View(). It's perfectly valid to do that instead of jumping through a bunch of hoops - and redirect messes with your browser history.
-
Russ Cam about 11 yearsAnd in addition to what @JimmyBogard has said, extract out the logic in the
Create
method which populates ViewData and call it in theCreate
GET method and also in the failed validation branch in theCreate
POST method. -
Francois Joly about 11 yearsAgreed, avoiding the problem is one way of solving it. I have some logic to populate stuff in my
Create
view, I just put it in some methodpopulateStuff
that I call in both theGET
and the failPOST
. -
The Muffin Man over 10 years@JimmyBogard I disagree, if you post to an action and then return the view you run into the issue where if the user hits refresh they get the warning about wanting to initiate that post again.
-
-
CRice over 13 yearsHow is this a bad idea? I think the attribute avoids the need to use another action because both actions can use the attribute to load to ViewData.
-
DreamSonic over 13 yearsPlease take a look at Post/Redirect/Get pattern: en.wikipedia.org/wiki/Post/Redirect/Get
-
CRice over 13 yearsThat is normally used after model validation is satisfied, to prevent further posts to the same form on refresh. But if the form has issues, then it needs to be corrected and reposted anyway. This question deals with handling model errors.
-
RPM1984 over 13 yearsExcellent. And a big +1 for mentioning the refresh issue. This is the most complete answer so i'll accept it, thanks a bunch. :)
-
John Farrell over 13 yearsThis doesn't really answer the question in the title. ModelState isn't preserved and that has ramifications such as input HtmlHelpers not preserving user entry. This is almost a workaround.
-
RPM1984 over 13 yearsI ended up doing what @Wim suggested in his answer.
-
CRice over 13 yearsFilters are for reusable code on actions, especially useful for putting things in ViewData. TempData is just a workaround.
-
Daniel Liuzzi over 13 yearsNo need to cast. TempData["Review"] = TempData["Review"]; works just as well (redundant as it may look :)
-
Daniel Liuzzi almost 12 yearsThis is what I do too, only instead of having a private function, I simply have my POST method call the GET method on error (i.e.
return Create(new { uniqueUri = ... });
. Your logic stays DRY (much like callingRedirectToAction
), but without the issues carried by redirecting, such as losing your ModelState. -
Skorunka František almost 12 years@DanielLiuzzi: doing it that way will not change the URL. So you end with url something like "/controller/create/".
-
Daniel Liuzzi almost 12 years@SkorunkaFrantišek And that's exactly the point. The question states If an error occurs, I need to display the same page with the error. In this context, it is perfectly acceptable (and preferable IMO) that the URL does NOT change if the same page is displayed. Also, one advantage this approach has is that if the error in question is not a validation error but a system error (DB timeout for example) it allows the user to simply refresh the page to resubmit the form.
-
asgeo1 over 11 years@jfar, I agree, this answer doesn't work and does not persist the ModelState. However, if you modify it so it does something like
TempData["ModelState"] = ModelState;
and restore withModelState.Merge((ModelStateDictionary)TempData["ModelState"]);
, then it would work -
ferventcoder about 11 yearsThank you. We implemented something similar to your approach. gist.github.com/ferventcoder/4735084
-
ajbeaven almost 11 yearsCould you not just
return Create(uniqueUri)
when validation fails on the POST? As ModelState values take precedence over the ViewModel passed in to the view, the posted data should still remain. -
Josh about 9 years@asgeo1 - great solution, but I ran into a problem using it in combination with repeating Partial Views, I posted the question here: stackoverflow.com/questions/28372330/…
-
Piotr Kula about 9 yearsThis is great for simple pages and views +1. The post/redirect/get comment is just a pattern, not a golden rule of life, so I choose to ignore that wiki link. The problem is when the view uses one ViewModel but posts to an action with separate models. The issue now is to display the page as it was... but how?
-
CRice about 9 years@ppumkin maybe try posting with ajax so that you don't have a hard time rebuilding your view server side.
-
Piotr Kula about 9 yearsYea. That's what I did in the end. Solved everything. Good to know I chose a good solution. Thanks for the comment. +1
-
Tom Pažourek almost 9 yearsWhat will happen if the user opens the form twice (one in each tab), breaks validation in first form and refreshes the second form. Won't the second form get temp data from the first one?
-
AaronLS over 5 yearsIn the case of an error you're not redirecting, so it is not PRG pattern, and will suffer from the problems of not using PRG. The question stated "across redirecttoaction"
-
CRice over 5 years@AaronLS what is PRG?
-
AaronLS over 5 years@CRice Post Redirect Get. DreamSonic linked the wiki definition, and this is a standard in most web applications to avoid a POST from being part of the browser history such that a user is confused or makes a mistake by refreshing or going back and resubmitting the POST unintentionally. The code in the question implements the PRG pattern, with the POST always returning a redirect.
-
CRice over 5 years@AaronLS Ah yes I recognise post redirect get just wasn't used to it being used as an acronym. Thanks for letting me know.
-
AaronLS over 5 years@CRise And FYI I didn't downvote. I think for some they may find it acceptable to diverge from PRG in the case of an error, so I think your answer may be a reasonable compromise for some. I just personally would avoid it or be mindful of the cons.
-
Marie over 5 yearsI dont know if it existed when this answer was given but rather than pulling the item out of tempdata and re-adding it you should use
var review = TempData.Peek("Review") as Review
-
PJ7 almost 5 yearsWarning - if the page is served all in one request (and not broken up via AJAX), you are asking for trouble using this solution since TempData is preserved through to the next request. For example: you enter search criteria into one page, then PRG to search results, then click a link to directly navigate back to the search page, the original search values will be repopulated. Other weird and sometimes hard to reproduce behavior pops up, too.
-
Rudey almost 4 yearsI wasn't able to make this work until I realized the session ID kept changing. This helped me solve that: stackoverflow.com/a/5835631/1185136
-
Rudey almost 4 yearsI wasn't able to make this work until I realized the session ID kept changing. This helped me solve that: stackoverflow.com/a/5835631/1185136
-
VDWWD over 3 yearsWorks perfectly!. Edited the answer to fix a small bracket error when pasting the code.
-
dan over 3 yearsQ: what is
NextRequest
andTempData
behavior when there are multiple browser-tabs making (multiple/simultaneous) requests? -
Robbie Chiha over 3 yearsThis is the only answer here that works in .net core 2.1.