To persist the model state across redirects, we can put TempData to work. The sole purpose of TempData is exactly this; persisting state until the next request.
public ActionResult Index() { ViewData.Model = ... if (TempData.ContainsKey("ModelState")) ModelState.Merge((ModelStateDictionary)TempData["ModelState"]); return View(); } [HttpPost] public ActionResult Update(AddModel inputModel) { if (ModelState.IsValid) ... TempData["ModelState"] = ModelState; return RedirectToAction("Index"); }So this works, but I found it to be a bit too cumbersome. And so did Davy Brion, he introduced a clean abstraction into the project, smoothing out some of the friction: making use of action filter attributes, we were able to eliminate duplication across controllers, leaving behind an AOP-ish taste.
The SetTempDataModelStateAttribute stores the model state in the TempData dictionary.
public class SetTempDataModelStateAttribute : ActionFilterAttribute { public override void OnActionExecuted(ActionExecutedContext filterContext) { base.OnActionExecuted(filterContext); filterContext.Controller.TempData["ModelState"] = filterContext.Controller.ViewData.ModelState; } }While the RestoreModelStateFromTempDataAttribute restores it by pulling it out of TempData again, when it exists.
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"]); } } }So when we apply these attributes to the example at the beginning of this post, we end up with something like this.
[RestoreModelStateFromTempData] public ActionResult Index() { ViewData.Model = ... return View(); } [HttpPost] [SetTempDataModelState] public ActionResult Update(AddModel inputModel) { if (ModelState.IsValid) ... return RedirectToAction("Index"); }Very clean. I'm interested to hear how you handle these concerns when using the PRG pattern.
Great Article :)
ReplyDeleteWe can also submit our .net related links on http://www.dotnettechy.com to improve traffic.
The dotnettechy.com is a community of .Net developers joined together to learn, to teach, to find solutions, to find interview questions and answers, to find .net website / blog collection and to have fun programming.
I use the ModelStateToTempDataAttribute in MvcContrib https://github.com/mvccontrib/MvcContrib/blob/master/src/MVCContrib/Filters/ModelStateToTempDataAttribute.cs (Jeremy Skinner, 2008)
ReplyDeleteWorks in the exact way.
I use the ModelStateToTempDataAttribute in MvcContrib https://github.com/mvccontrib/MvcContrib/blob/master/src/MVCContrib/Filters/ModelStateToTempDataAttribute.cs (Jeremy Skinner, 2008)
ReplyDeleteWorks in the same way.
Why not just return the View if the model fails validation.
ReplyDeleteI don't want a request to be resubmitted by accident on a refresh.
Deleteaherrick is right, you shouldn't be using PRG when you want to persist posted data i.e. remember user values on a form that has been posted. You should return the view.
ReplyDeleteThe logic goes like this in the controller action (for the post)
if (ModelState.IsValid){
tempdata("Confirmation") = New Confirmation("Save OK", "Record saved")
return RedirectToAction("x", "y")
} else {
return View(Model)
}
If the request is "accidentally resubmitted" it will just fail validation again. If it pases validation, PRG kicks in and you won't get a double post.
The form on the Index view posts to the Update action. If I return a view in my Update action, I need to create another view, or if I want to use the Index view there, I'm messing up my url's.
DeleteThere is a better way - give your GET and POST actions the same name. The GET takes an ID or similar as a parameter, and the POST takes the model.
ReplyDeleteThen in your view you can use Html.BeginForm() without any parameters, and it will POST to the correct location.
It's less work, easier and works well :)
I took that approach before, but you still have the double form submission problem when the client refreshes, and it's not very 'correct' url-wise. Also, if you post to multiple actions in one page, it gets messy real fast.
Delete