A RESTful Wizard Using ASP.Net MVC… Perhaps?

September 16, 2009 00:56 by admin

Over the last few weeks I have been creating a new web site using ASP.Net MVC. One area I was unsure as to how to approach was account registration. The default project creates a simple account registration page, but I wanted something more complex… more like a wizard.

Back in the Web Forms world of ASP.Net I could just use the CreateUserWizard control. in this post, Scott Hanselman shows how it is simple to combine Web Forms and MVC pages in a single web site. However, I’m in the world of MVC with this project and wanted to implement this as Restfully as I can… and the challenge is half the fun.

On the face of it, a wizard doesn’t seem a very Restful concept as it requires temporary state to be stored somewhere… within the browser, in hidden form fields within the page contents (e.g. view state), in session or a custom database table. This temporary state has to be held until the wizard is completed, at which point it is either committed or discarded.

However, you could think of the registration process as a resource in and of itself (albeit one that you will probably delete after a short time). Then you could POST to a resource to create a new registration process and PUT updates to that resource as the user moved through a “wizard.” Above all, we need to be pragmatic… a wizard is a great user interface concept, that is well understood by users and I wanted to support it within the ASP.Net MVC world.

Looking at examples on the web, the one thing that struck me was that generally the controllers people wrote seemed to be tightly coupled to the steps the wizard was going through. Some solutions instantiated a different view for each step, others told a view which step it should be displaying at any point in time. To my mind that ties the Controller and the View(s) too tightly. The controller should own the registration process, i.e. manage the build up of data, validate the data and ultimately commit that data. I don’t think it should care about the details of the user interface used to capture the data. For instance, a pure HTML view may submit the data in a number of discrete post backs, one per step. A view making heavy use of AJAX may use JavaScript to gather everything client side, then transmit all the data in a single post. One view may capture the data in 10 steps because it is rendering to a Smartphone, another may only take three steps because it targets a desktop browser.

So I set about implementing a wizard in which the controller knew as little as possible about the rendering.

The Controller

My starting point was a standard ASP.Net MVC web application. in the AccountController class, I changed the register methods. For my example, I cheated and stored the registration data in session (but the data could be given a unique key and stored in a database). The default Register method sets up a blank set of registration data, stores it in session and passes the data to the view:

    const string REGISTRATION_SESSION_KEY = "InprogressRegistration";
    public ActionResult Register()
    {
        Session[REGISTRATION_SESSION_KEY] = CreateBlankUser();
        return View(Session[REGISTRATION_SESSION_KEY]);
    }

CreateBlankUser creates a UserDetails object, containing three further objects; one to contain security information, one holding personal information and one for “Exciting” data. The contents of these classes doesn’t really matter, this is just an example, the main point is that we are not just going to capture the minimum account data.

The second Register method (that accepts post data) is more complex:
 

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Register(string CANCEL_BUTTON, string FINISH_BUTTON)
{
    // If the user cancels, drop out altogether
    if (!string.IsNullOrEmpty(CANCEL_BUTTON))
    {
        Session.Remove(REGISTRATION_SESSION_KEY);
        return this.RedirectToAction("Index", "Home");
    }

    UserDetails inProgressRegistration = Session[REGISTRATION_SESSION_KEY] as UserDetails;
    if (inProgressRegistration == null)
    {
        inProgressRegistration = CreateBlankUser();
    }

    TryUpdateModel<UserDetails>(inProgressRegistration);

    if (ValidateRegistration(inProgressRegistration))
    {
        if (!string.IsNullOrEmpty(FINISH_BUTTON))
        {
            // SAVE NEW USER DETAILS HERE

            Session.Remove(REGISTRATION_SESSION_KEY);
            return this.RedirectToAction("Index", "Home");
        }
    }

    return View(inProgressRegistration);
}

The above code:

  • Checks if the CANCEL_BUTTON FORM variable is populated. If it is, the controller deletes the registration data and redirects the user back to the homepage;
  • Retrieves the registration data accumulated so far (or creates a new set);
  • Updates the registration data with any values POSTed to the controller;
  • Validates the entire registration data model;
  • If the registration data is valid and the FINISH_BUTTON variable is populated, the controller creates the new user and redirects to the homepage;
  • If the registration data is not yet valid, all the registration data is passed to the view.

The first thing to note is that there are only two hardcoded parameters the controller is expecting. These indicate if the registration process is to be abandoned “CANCEL_BUTTON” or completed “FINISH_BUTTON.” The controller has no other expectations about the data being POSTed to it… it has no concept of which step the user is on or how many steps there are.

What about the data that we are trying to capture in the view? What choices do we have for mapping FORM variables sent by the browser to our existing registration data held in session? We could add UserDetails, to the Register method’s signature, allowing the framework to use Data Binding to automatically populate new objects with data found in the FORM variables:

public ActionResult Register(string CANCEL_BUTTON, string FINISH_BUTTON,
                             UserDetails userDetails)

However, we would have to merge the content of the new object passed into method into the existing object retrieved from session… how would we know which properties were valid (e.g. an int could be 0 because that is what was entered by the user or it could be 0 because that is its default value when there is no matching FORM variable)? My original code used this approach and had to make a judgement call (basically check if all values in an object are null) as to whether to overwrite the entire object in session or not. This then bakes into the controller an assumption that the view will return all data related to a particular object or nothing.

Alternatively, the controller could have looked at the actual FORM variables in the request and mapped them to the existing object. This is effectively what Data Binding does for us… so we would be writing lots of code to do something for which the framework has all the functionality already built in.

Instead, I choose TryUpdateModel() as this allows us to update an existing object. The controller still uses the default Data Binding mechanism for matching FORM variables to object properties… but since only properties that are actually present in the FORM data are updated, the controller does not need to know which values the view has populated on each POST back. A view could return one value at a time or all the values in one go… if a value is present, the existing registration data is updated, if not, the corresponding item of registration data is left alone.

Note: The default binder can bind complex objects to FORM variables so long as those FORM variables are named using the convention PROPERTYNAME .CHILDPROPERTYNAME e.g. if the object being updated has a property AccountDetails which is a complex object containing a property of its own called UserName, to bind to that UserName property the View must POST back a variable named “AccountDetails.UserName”.

Now if I changed this to be more Restful, and had the initial post create an addresable resource (i.e. a resource with some unique identifier) the user could pick up where they left off. Also, because the controller has no knowledge of the view’s progress, the user could start the process using a desktop browser displaying 3 steps and complete the process on a Smartphone with 10 steps. The “rehydration” of the process would repopulate the wizard screens with the data already entered so far.

A View

How a view provides a Wizard interface will probably vary from implementation to implementation. I present here the approach I took for my situation. Register.aspx makes use of three partial views, one for each step of the wizard:

  • RegisterAccount.ascx
  • Personal.ascx
  • Exciting.ascx

Personal.ascx and Exciting.ascx are located in the Shared directory as they could be used by other screens.

The view is in charge of deciding which of its wizard steps should be displayed at the moment. It is free to use whatever means it wants to do this. My implementation uses the following information:

  • Which was the last screen displayed
  • What button did the user press
  • The current Model.State

When loaded, Register.aspx looks for a hidden FORM variable “CURRENT_STEP” to decide which was the last step displayed. This variable is not used by the controller at all. This is the only piece of state information used by the wizard, and being a FORM variable, it is “managed” by the client.

Register.aspx then looks to see which button the user pressed:

  • BACK
  • NEXT
  • CANCEL – if this has been pressed, the Controller should detect it, delete the registration process resource and redirect to the home page. The View should never have to handle this situation.
  • FINISH – the View should only see this button has been pressed if there was a validation error that meant the registration process could not be completed yet.

The BACK and NEXT buttons are completely ignored by the Controller and are View specific. If a View chooses to implement a LAST button, a JUMP_THREE_STEPS or a RANDOM button, that is its own choice… the Controller does not care!

Next, the view sorts all the ModelState entries into new ModelStateDictionary objects, one per step. At the same time it removes the entries from its own Model State. My implementation has a step based on each property of the UserDetails class. This makes the mapping between ModelState and each step’s ModelStateDictionary simple. If your steps and objects don’t have a one to one mapping, this process would be a little more involved, but not difficult.

Once the basic “state” information has been gathered, the view decides which step to display. Again this is custom to the wizard, but my simple wizard performs the following checks in order:

  • If the Finish button had been pressed, there most be an error preventing registration. Therefore, loop through each step from the first to last, looking at each step’s ModelStateDictionary. The first step with an invalid ModelStateDictionary is displayed;
  • If the previous step has errors and the user hasn’t pressed the BACK button, redisplay the previous step. Pressing the BACK button is allowed because a user friendly wizard should always allow the user to go backwards… otherwise the user could make a choice on one step, move forward, then be forced to enter incorrect information on the next step even though they want to go back and change their earlier choice;
  • Move forward or backwards as requested (limited by the length of the wizard of course!).

Finally, the view is ready to display the current step:

  • If Finish was pressed or the user has been forced to stay on the same step because of an error, the View adds the errors related to just the current step back into its own Model State;
  • The view loads the correct partial view control.

The partial view control is either passed the entire UserDetails object or just the child object relevant to it. I used both techniques just to check it works. When passing in only part of the data the control still has to name the INPUT element using the PROPERTYNAME .CHILDPROPERTYNAME convention… which means, for a control that is really reusable, the “PROPERTYNAME.” would probably have to be passed into the control.

As I said above, the logic in the View can be as complex or simple as you like. The version I came up with should allow new steps to be added quite easily… create a new Partial View Control, add to the Step enum then customise the sorting of ModelStates into steps.

Validation

So we have a controller that is fairly separated from the Wizard View… only relying on an understanding of Cancel and Finish. However, I have skipped one important piece of functionality, Validation.

The DefaultModelBinder that comes with ASP.Net MVC 1.0 does perform some basic validation as it binds during the TryUpdateModel() method. For instance if the user enters a text string where a number is expected, the binder will add an error to the ModelState.

For a wizard though, we need to validate the across the entire Model, not just those values being updated from FORM variables. The AccountController performs some Model wide validation e.g. checking the Password and ConfirmPassword values match and that a PersonalDetails.Height of greater than 50 has been entered. For each validation error found, AddModelError() is called passing in the PROPERTYNAME .CHILDPROPERTYNAME of the incorrect value and an error message. A bug in the ASP.Net MVC HtmlHelper.GetModelStateValue() method causes an exception to be thrown if a ValueProviderResult isn’t supplied as well. This is corrected in ASP.Net MVC 2 Preview. For now though you have two options for a workaround:

  • Download a copy of the MVC 1.0 source code, make the fix and use your own build;
  • Supply a ValueProviderResult object.

For this version I choose the latter.

In my next blog entry I’ll look at making the Controller even more independent of the Model and the View by performing some more of the validation using DataAnnotaions. This also requires the former workaround.

The source code for my example can be downloaded: MvcWizard01.zip (239.59 kb)

kick it on DotNetKicks.com


Comments (4) -

October 30. 2009 02:12

achevin

I'm loving your attention to detail, very DT. I bet you wish you could replace DT's MVC over WebForms with Asp.Net MVC. I'm developing a MVC2 site and am devouring blogs like yours, been following since I left. Your blog is great, deserves more attention. Have you seen Steve Sanderson's blog/book? Similar attention to detail, recommend you buy the book, you WILL learn things. You should be getting similar hits. Pass on my regards to everybody. PS, as you've got my email addy, could you send me Will's? Cheers, Andy.

PS. Neil is ALWAYS right!

achevin

February 2. 2010 02:49

Tester

Great code...How would you modify your wizard to allow dynamically generated wizard steps ? In many cases wizards require user interaction yer/no, and then based on that display certain steps.

I can see that you use Enum for wizard steps. So this will not be possible with current implementations as Enums are compile time structures.

Tester

February 2. 2010 21:27

Piers

You're right... my view is pretty hardcoded, but there is nothing stopping you making it more dynamic. So long as the you can pass the current step Id back to the view as the user moves backwards and forwards there is nothing to say the next step isn't worked out from a dynamic list.

Piers

April 5. 2010 14:18

Morris

Great post, I like these posts most of all.
Practical information that everyone can use

Morris

Add comment

  Country flag

biuquote
  • Comment
  • Preview
Loading