Monday, November 7, 2011

PRG Pattern - How to Keep MVC ModelState

According to the definition of the HTTP POST and GET verbs:

  1. HTTP GET is used for non-changing (idempotent) data to your model.

  2. HTTP POST is used for changing data to your model.


Given this clear delineation, when receiving form data in your post back action method, return RedirectToAction(), which will result in a HTTP 302 (temporary redirect) and will generate a GET on the. This results in Post-Redirect-Get pattern.

One of the issue with this pattern, when using Asp .Net MVC, is that when validation fails or any exception occurs you have to copy the ModelState into TempData. Kazi Mansur has a great solution posted here under the sub section "13. Use PRG Pattern for Data Modification".

The examples he shows is:

[sourcecode language="csharp"]
public abstract class ModelStateTempDataTransfer : ActionFilterAttribute
{
protected static readonly string Key = typeof(ModelStateTempDataTransfer).FullName;
}

[AcceptVerbs(HttpVerbs.Get), OutputCache(CacheProfile = "Dashboard"), StoryListFilter, ImportModelStateFromTempData]
public ActionResult Dashboard(string userName, StoryListTab tab, OrderBy orderBy, int? page)
{
//Other Codes
return View();
}

[AcceptVerbs(HttpVerbs.Post), ExportModelStateToTempData]
public ActionResult Submit(string userName, string url)
{
if (ValidateSubmit(url))
{
try
{
_storyService.Submit(userName, url);
}
catch (Exception e)
{
ModelState.AddModelError(ModelStateException, e);
}
}

return Redirect(Url.Dashboard());
}
[/sourcecode]

and for the Action Filter itself

[sourcecode language="csharp"]
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);
}
}

public class ImportModelStateFromTempData : ModelStateTempDataTransfer
{
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
var 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);
}
}
[/sourcecode]


I like the way this has been handled but because I like to learn from other people's ideas I also like to use the workflow for HTTP POSTs as described by Jimmy Bogard in his post, Cleaning up POSTs in ASP.NET MVC.

In short each action that requires a POST using the following ActionResult:

[sourcecode language="csharp"]
public class FormActionResult<T> : ActionResult
{
public ViewResult Failure { get; private set; }
public ActionResult Success { get; private set; }
public T Form { get; private set; }

public FormActionResult(T form, ActionResult success, ViewResult failure)
{
Form = form;
Success = success;
Failure = failure;
}

public override void ExecuteResult(ControllerContext context)
{
if (!context.Controller.ViewData.ModelState.IsValid)
{
Failure.ExecuteResult(context);

return;
}

var handler = ObjectFactory.GetInstance<IFormHandler<T>>();

handler.Handle(Form);

Success.ExecuteResult(context);
}
}[/sourcecode]

Which not only cleans up the POST workflow and makes testing so much easier but it also helps separate application concerns. In the original Action Filter there is a guard clause to ensure that the Filter Context result is either a RedirectResult or a RedirectToRouteResult

[sourcecode language="csharp"]
if ((filterContext.Result is RedirectResult) || (filterContext.Result is RedirectToRouteResult))
[/sourcecode]

So how do we add the clause so that the result can be of the Generic type of FormActionResult? By using reflection.
[sourcecode language="csharp"]
if ((filterContext.Result is RedirectResult) || (filterContext.Result is RedirectToRouteResult) || (filterContext.Result.IsTypeOfGeneric(typeof(FormActionResult<>))))
[/sourcecode]

where the signature IsTypeOfGeneric is an extension method which looks like the following:
[sourcecode language="csharp"]
public static bool IsTypeOfGeneric<T>(this T source, Type genericTypeToCompare)
{
if (!genericTypeToCompare.IsGenericType)
throw new ApplicationException(@"Compare type needs to be generic");

if (!source.GetType().IsGenericType)
throw new ApplicationException(@"Base type needs to be generic");

return source.GetType().GetGenericTypeDefinition()==genericTypeToCompare;
}
[/sourcecode]

The msdn definition of the method GetGenericTypeDefinition is here

Now we can use FormActionResult within our code and the ModelState will be saved from the POST to the GET.