Storing ModelState in TempData with ASP.NET MVC

MVC Preview 5 introduced the contept of ModelState for storing validation failures.

As the ModelState is part of the ViewData, it is only available for the current request. This means that you cannot redirect to another action in order to show the error summary, instead you have to render a view directly from your Save action. Personally I don’t think this is a good idea – the Save action should just try and save changes, and then redirect to another action to display validation errors.

One solution is to store the ModelState inside the TempData (as anything added to the TempData will be made available to the next HTTP request). It is possible to use an ActionFilterAttribute to make this happen automatically.

Under the covers, TempData makes use of ASP.NET Session State which means that every object that is stored in TempData needs to be marked as Serializable if you are using an out of process session store. Unfortunately, the ModelStateDictionary is not marked as serializable, so in order for this approach to work it is necessary to copy the contents of the ModelState into a temporary serializable dictionary.

I’ve made the code available in MvcContrib. You can use it by decorating your controllers with the ModelStateToTempData attribute:

using MvcContrib.Filters;
 
[ModelStateToTempData]
public class MyController : Controller {
  //...
}

The filter has the following features:

  • If your controller action returns a RedirectToRouteResult, anything in the ModelState dictionary will be wrapped in a ModelStateSerializable class and stored in TempData.
  • If your action returns a ViewResult, then any ModelState objects that were previously copied to TempData will be re-added to the ModelState dictionary.

HandleErrorAttribute and Remote Connections

Last week I upgraded my main application to MVC Preview 4. This morning I noticed that a number of users were receiving HTTP 500 error messages instead of being presented with a friendly error page.

It turns out the HandleErrorAttribute in Preview 4 is the culprit. Previously I’d been using MvcContrib’s RescueAttribute.

It appears that the HandleError attribute only works locally – when using a remote connection the server just displays the standard HTTP 500 message (this is under IIS7 using Integrated Pipeline).

I fired up the MVC source code and removed this line from HandleErrorAttribute.cs (line 85):

filterContext.HttpContext.Response.StatusCode = 500;

After doing this, the error pages are displayed correctly.

ASP.NET MVC Preview 4

I spent yesterday updating our largest application to MVC Preview 4. As part of the upgrade, I also made some tweaks to MvcContrib’s ConventionController (documentation not yet updated).

Here’s a summary of some of the things that have changed:

  • TempData finally works with out of process session state
  • Filters that implement IExceptionFilter can easily catch exceptions thrown by other filters
  • The RescueAttribute in MvcContrib is now an IExceptionFilter, so it can be used with classes that don’t inherit from ConventionController
  • Multiple filters can now have the same sort order. This makes it easy to define a ‘default’ order, and then have other filters execute before/after
  • TempData is now testable without needing to mock anything
  • All the ComponentController hackery is gone – RenderAction is a much nicer alternative
  • The bug that broke URL generation is fixed

Here’s what sucks with Preview 4:

  • SubDataItems still doesn’t seem to work properly
  • AuthorizeAttribute, OutputCacheAttribute and HandleError attribute are sealed and have no virtual methods
  • The new built in Ajax support doesn’t seem to be easily replaceable with alternative libraries

ASP.NET MVC – Intercepting the RouteValueDictionary

ASP.NET MVC Preview 3 suffers from a bug where the controller name is omitted from the RouteValueDictionary when you call Html.ActionLink or Url.Action unless you explicitly specify it.

Imagine you have the following routes defined:

routes.Add(new Route("Other/List",
	new RouteValueDictionary(new {
		controller = "Other",
		action = "List"
	}),
	new MvcRouteHandler())
);
 
routes.Add(new Route("{controller}/{action}/{id}",
	new RouteValueDictionary(new {
		action = "Index",
		id = (string)null
	}),
	new MvcRouteHandler())
);

..and you have a HomeController with two actions: Index and List

public class HomeController : Controller {
	public ActionResult Index() {
		return View();
	}
 
	public ActionResult List() {
		return View();
	}
}

And imagine the following code in the Index view:

<%= Url.Action("List") %>

This should generate MyApp/Home/List but instead it generates MyApp/Other/List.

To work around this, you can intercept the RouteValueDictionary before it is passed to your routes by adding a route ‘pre-parser’. In here, you can copy the controller name from the routedata into the RouteValuesDictionary.

public class RouteValuePreParser : RouteBase {
	public override RouteData GetRouteData(HttpContextBase httpContext) {
		return null;
	}
 
	public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values) {
		if(!values.ContainsKey("controller") && requestContext.RouteData.Values.ContainsKey("controller")) {
			values.Add("controller", requestContext.RouteData.Values["controller"]);
		}
		return null;
	}
}

You can then add this ‘fake’ route to your RouteTable, but it must be the first route:

RouteTable.Routes.Add(new RouteValuePreParser());

MvcContrib upgraded to ASP.NET MVC Preview 3

I’ve just finished upgrading MvcContrib to work with ASP.NET MVC Preview 3.

The source can be downloaded from http://mvccontrib.googlecode.com/svn/trunk/ using your favourite Subversion client.