Patrick Desjardins Blog
Patrick Desjardins picture from a conference

How to save the ModelState into session following the good practice

Posted on: 2012-04-21

Tim Barcz, Matt Hawley, Stephen Walther and Scott Guthrie (VP at Microsoft and lead for many project like Entity Framework, Asp.Net, etc) have already discussed about this problematic and created the PRG pattern to solve this problem. In fact, to solve this problem you should not handle manually the ModelState but simply use Import and Export attribute like the following example.

[AcceptVerbs(HttpVerbs.Get), ImportModelStateFromTempData] 
public ActionResult MyAction(ModelObject myObject) {
   return View(); 
}

[AcceptVerbs(HttpVerbs.Post), ExportModelStateToTempData] 
public ActionResult MyActionSubmit(ModelObject myObject) { 
  return View(); 
} 

These attributes are not from the .Net framework and you need to have them inside your project by creating them. Once it's done once, it's done for the life time of your project.

First, you need to create the attributes. To do, you need to create a class that inherit the class ActionFilterAttribute. Since we are using 2 attributes that share the same information, we will create 3 classes. The first one will contain the sharing key for the session and the two others will be for the Import and Export.

public abstract class ModelStateTempDataTransfer : ActionFilterAttribute { 
  protected static readonly string Key = typeof(ModelStateTempDataTransfer).FullName; 
} 

Then, the class to export. Here you can add more custom code for your project. This version will all the ModelState only if this one contain errors.

 public class ExportModelStateToTempData : ModelStateTempDataTransfer { 
  public override void OnActionExecuted(ActionExecutedContext filterContext) { 
    //Only export when ModelState is not valid 
    if (!filterContext.Controller.ViewData.ModelState.IsValid) { 
      //Export if we are redirecting 
      if ((filterContext.Result is RedirectResult) || (filterContext.Result is RedirectToRouteResult)) { 
        filterContext.Controller.TempData[Key] = filterContext.Controller.ViewData.ModelState; 
      } 
    }

    base.OnActionExecuted(filterContext); 
  } 
} 

The last class will import the ModelState. In fact, it will merge the new one with the old one in the session.

public class ImportModelStateFromTempData : ModelStateTempDataTransfer { 
  public override void OnActionExecuted(ActionExecutedContext filterContext) { 
    ModelStateDictionary modelState = filterContext.Controller.TempData[Key] as ModelStateDictionary;

  if (modelState != null) { 
    //Only Import if we are viewing 
    if (filterContext.Result is ViewResult) { 
      filterContext.Controller.ViewData.ModelState.Merge(modelState); 
    } else { 
      //Otherwise remove it. 
      filterContext.Controller.TempData.Remove(Key);
    } 
  }

  base.OnActionExecuted(filterContext); 
  }
} 

As you can see, we do not use the session directly but we store everything into the TempData which use the session but handle the life cycle for us. This mean that it won't stay for 20 minutes (default value of a session life time). It will stay until the next post back and be there if the request is redirected.

You can see it in the MVC open source project called MVCContrib (slightly modified version of this one). You can also find the source of the code in this blog post at this website.