I recently ran across a scenario where I needed to have a form span several pages and build built-in MVC objects left me perplexed as to how to do this cleanly and easily.

The general use-case
A tabbed experience “wizard style” page with a big form that needs to be broken up to not overwhelm the user and the screen. Stackoverflow was not as helpful as usual, giving a lot of suggestions that appear great on the surface but do not work in reality. Storing state between pages is suprisingly complex in the stateless web if you stick to MVC priniciples and avoid session state. Read on to avoid making the same mistakes.

What doesn’t work:

Using Tempdata. This was my first choice, and one recommended by others on sites like StackOverflow. Unfortunately, I found that Tempdata is absolutely unusable. The vision behind it is solid, but the implementation is broken. Tempdata disappears on the next HTTP request, which could easily be on a different tab in today’s browsers. Nobody seems to acknowledge this point, but if there is any chance of the user accessing your site from more than one window, Tempdata is useless. If the next request is from another page OR EVEN AJAX on the same page, your tempdata evaporates: Never use Tempdata for anything. Ever. It does not work reliably.

Session storage. Okay, theoretically this would work, but why are we using MVC again? Session storage is evil and using it even a single time in your application makes load balancing between servers much more difficult. Suddenly you need some kind of shared session storage between servers or sticky load balancing. Both of which I would rather not deal with. I guess you could say the same for Tempdata even though it’s slightly cleaner because session storage doesn’t hang around forever.

Database storage between pages. This would work too, except it requires all of the fields in your model to be nullable if you’re using entity framework. Might as well throw all of your model validation out the window if you choose this option. It can (so of course, eventually would) also result in a ton of orphaned partially complete forms in the database. Not the best option unless you plan on allowing users to go back and finish uncompleted forms, in which case go ahead. Just make sure you store uncompleted objects in a table without constraints and move them to the completed(and validated) table on the last page and use something like AutoMapper to make things cleaner.

Storing temporary data in cookies. I don’t know why people mention this as a valid option. It’s not. Cookies are size limited and sites have a global cookie limit you don’t want to exceed unless you want cookie based authentication to unexplainably break someday. Also, if you forget to expire your cookies they hang around forever and are sent with every single HTTP request. HTML5 Local storage is a great option but support for it is relatively new at this point. As a consultant, you want to build something maintainable and clean that will still work on older versions of IE.
So what are the remaining options?

Not many. You have three, and two of them are different flavors of the same thing.

First option is Ajaxing out the whole form so that it’s one page. This is a great option for people using Angular or another SPA framework, maybe the ultimate option. Support for such frameworks is growing and they’re starting to mature nicely. I’m not going to go into this option since there’s a lot to it. In my case the existing application was 100% MVC and adding a big library for one page doesn’t seem like a wise choice. Another problem for some projects is that older browsers have poor javascript support. Microsoft knew this when designing MVC. Vanilla MVC will run without javascript support at all.

So what are my MVC options? Not that fancy. Basically you need to persist data between pages yourself. You need to have one huge model and pass between controller action filling small pieces at a time. The easiest and classic way of handling this is using hidden form fields. With a big form doing this is a pain and only gets worse whenever you add a field to one of the earlier pages.

The Best option I found in MVC is to use your own viewstate. Don’t be afraid, it’s not nearly as scary or opaque as WebForms viewstate. The easiest and cleanest way I found to persist data between pages is to use the great Newtonsoft JSON library to serialize viewmodel into a single hidden field, and deserialize on POST requests. Essentially replicating Viewstate without the baggage that comes from using webforms. You create individual partial models for each page with fields matching the fields on a model repesenting the whole form. The page then validates against the smaller partial model like normal, allowing you to use built in validation. Then you use AutoMapper to map the mini model back to the full size model in your controller action and stuff it back into a serialized JSON field that you send to the next page. At the last page re-validate the whole model to make sure the user didn’t alter it, then save to the database. Doing it this way doesn’t require javascript and leverages built-in form validation. It’s not the cleanest solution I’ve done, but it works well and doesn’t have a lot of moving parts.

A few gotchas with this this method. You need to make sure all controller actions return the view for the next page when finished. It requires moving exclusively to POST requests which seems a bit unusual compared to the typical GET POST REDIRECT chain in MVC forms.
Something like:

[HttpPost] action1 (PartialViewModel partialvm ) { //dostuff and check validation blablablabla //if we get here model successfully validated //watch for exceptions here if user changed hidden string or came to this page from somewhere else var wholemodel=Deserialize(partialvm.GiantViewModelJSONString); Mapper.Map(partialvm,wholemodel);//Automapper var model = new Action2ViewModel(); model.GiantViewModelJSONString=Serialize(wholemodel); return View("Action2",model) }

That way you can ensure the data gets passed along properly. Don’t try to use the query string for this, it can exceed the 4000 character limit easily, your requests should be POST only.