This article is part of the Maintainable MVC Series.
It keeps amazing me that every time I see some example MVC code from Scott Guthrie, Phil Haack or one of our other MVC heroes, it keeps looking like this:
[AcceptVerbs(HttpVerbs.Post)] public ActionResult Create(BlogItem blogItem) { if (ModelState.IsValid) { ... return RedirectToAction("OtherAction"); } return View(blogItem); }
The big problem with this code is that it returns a view (on the highlighted line), while the method handles a POST. It seems like a very bad practice, because it disrupts the natural flow of your website. It makes it impossible to make use of your browser history (the back button) without running into a ‘this page is expired’ warning. Or your user could post the same data multiple times.
It is an annoyance which will definitely cost your website some visitors! So no more return View in a post method!
The simple pattern that makes these warnings something of the past is the Post-Redirect-Get pattern (PRG); it just states that a post should always be followed by a browser redirect.
[AcceptVerbs(HttpVerbs.Post)] public ActionResult Create(BlogItem blogItem) { if (ModelState.IsValid) { ... return RedirectToAction("OtherAction"); } return RedirectToAction("Create"); }
If it is this simple, why do the MVC gurus return a view in their example? Well, to keep your form filled with the input the user just entered and to show the validation errors, after a redirect, takes a little extra effort. By default, MVC doesn’t remember your ModelState from one request to the next.
To make sure the ModelState is ported to the next request we use the ModelStateToTempDataAttribute from the MvcContrib project. If a controller is decorated with this attribute you no longer have to worry about showing validation errors in a form when using the PRG pattern.
ModelStateToTempDataAttribute
public class ModelStateToTempDataAttribute : ActionFilterAttribute { public const string TempDataKey = "__MvcContrib_ValidationFailures__"; public override void OnActionExecuted(ActionExecutedContext filterContext) { ModelStateDictionary modelState = filterContext.Controller.ViewData.ModelState; ControllerBase controller = filterContext.Controller; if(filterContext.Result is ViewResult) { // If there are failures in tempdata, copy them to the modelstate CopyTempDataToModelState(controller.ViewData.ModelState, controller.TempData); return; } // If we're redirecting and there are errors, put them in tempdata instead // (so they can later be copied back to modelstate) if((filterContext.Result is RedirectToRouteResult || filterContext.Result is RedirectResult) && !modelState.IsValid) { CopyModelStateToTempData(controller.ViewData.ModelState, controller.TempData); } } private void CopyTempDataToModelState(ModelStateDictionary modelState, TempDataDictionary tempData) { if(!tempData.ContainsKey(TempDataKey)) { return; } ModelStateDictionary fromTempData = tempData[TempDataKey] as ModelStateDictionary; if(fromTempData == null) { return; } foreach(KeyValuePair<string,ModelState> pair in fromTempData) { if (modelState.ContainsKey(pair.Key)) { modelState[pair.Key].Value = pair.Value.Value; foreach(ModelError error in pair.Value.Errors) { modelState[pair.Key].Errors.Add(error); } } else { modelState.Add(pair.Key, pair.Value); } } } private static void CopyModelStateToTempData(ModelStateDictionary modelState, TempDataDictionary tempData) { tempData[TempDataKey] = modelState; } }
The attribute only saves ModelState to TempData if there are validation errors. And if the next action returns a view, the ModelState is retrieved from TempData. TempData itself is a wrapper around Session-State in which objects are only present until the next request.
All we have to do now is to make use of this attribute is to apply it to each controller where needed:
[ModelStateToTempData] public HomeController : Controller {
I’ve read some comments from people who want to have more control and split the attribute in one writing to TempData and the other retrieving it. But this leads to a proliferation of attributes throughout your code. I can’t think of single moment I ever had need for finer-grained control than just applying the one attribute to the controller as a whole.
The drawback
Of course there is one caveat for the use of this attribute. It saves data in TempData, which is saved in Session-State. If the sites you build run on multiple machines behind a load balancer you can’t handle subsequent requests by different machines, unless they share their Session-State.
For the smaller sites we choose to set the load balancer in sticky mode, so a user will be coupled to the same server during his/her session. In this case, having multiple servers doesn’t really increase availability, in that a user will still hit the same server after it has crashed.
For storing Session-State MSDN shows us we have the following options:
- InProc mode, which stores session state in memory on the Web server. This is the default.
- StateServer mode, which stores session state in a separate process called the ASP.NET state service. This ensures that session state is preserved if the Web application is restarted and also makes session state available to multiple Web servers in a Web farm.
- SQLServer mode stores session state in a SQL Server database. This ensures that session state is preserved if the Web application is restarted and also makes session state available to multiple Web servers in a Web farm.
- Custom mode, which enables you to specify a custom storage provider.
- Off mode, which disables session state.
If we don’t take the sticky road and need to share Session-State between multiple servers we’ll have to go for the StateServer or SQLServer option. Both necessitate that the items stored in session are serializable.
More on how we encapsulate Session-State in a later part.