tags:

views:

86

answers:

3

I'm developing a wizard type solution in MVC2 and I'd like to prevent users from going to step 2 directly, however, I'd still like it to show up in the URL.

Also, because progress can be saved at any time, I'd still like the ability to programmatically go to Step2. How can I do this?

[HttpGet]
        public ActionResult Step1() {

            return View("Step1View");
        }

        [HttpPost]
        public ActionResult Step1(Stuff s) {

            return RedirectToAction("Step2", new { S = s });
        }

        [HttpGet] //<-- how do I stop users going directly here
        public ActionResult Step2(Stuff s) {


            return View();
        }

        [HttpPost]
        public ActionResult Step2(Stuff2 s) {


            return RedirectToAction("Step3");
        }
+2  A: 

I haven't tried this myself but if I were to I'd give some consideration to ActionFilters. I'd create some context object that describes the wizard data and the steps through that wizard (maybe wrapping a model or two in some fashion).

This wizard context would be self 'validating' in the sense that I can ask what the next valid step is.

Then, with that, I load it up from the action filter and then if the current action is not valid for that step I redirect.

Of course, I can do that without the action filter and just have that as pre-amble to the method I'm looking at. Personally, I'd do this first, then play with action filters to try and make it look a little neater if I had time.

Simon
Yeah, action filters are a nice way to achieve this kind of flow. Check out: http://msdn.microsoft.com/en-us/library/dd410209.aspx
Russ C
+1  A: 

Action filters are the way to go

First of all this will heavily depend on the storage system of your temporary saved data. Action filter should check this store and see whether step 1 data exists. If it doesn't you can always add an error to ModelState thus make it invalid. Your Step 2 code would therefore look like this:

[HttpGet]
[CheckExistingData]
public ActionResult Step2(Stuff s)
{
    if (!this.ModelState.IsValid)
    {
        return RedirectToAction("Step1");
    }
    return View(s);
}

So. Your filter should check for existing data and either:

  • fill up Stuff parameter or
  • add model state error and keep parameter null

Data consolidation

The way that you've written redirect to action (in step 1 POST) to just provide a complex object is not a good way of doing it. You should consolidate data storage, so no matter where your user came to step 2, filter would always work the same. In your Step 1 POST you should save data to this particular storage and just redirect to Step 2.

You have two scenarios of getting to Step 2:

  1. From step 1
  2. From anywhere after data has been saved

This way, your step 2 would work the same for both scenarios. And you could create an arbitrary number of steps in your wizard, and the same process would still work the same.

Robert Koritnik
+1  A: 

I have finished developing a highly reusable wizard that by just doing:

return Navigate();

from the actions, the wizard knows what to do (this is possible if you implement a wizard pattern). Navigate() being a method defined on a base WizardController class.

The reason this works is that, in essence, step info gets serialized to the page with each request (AJAX or not), and is deserialized when the controller reads the response in the OnActionExecuting method.

The framework uses WizardStep attributes to know which action corresponds to which wizard step, and the controller is decorated with a WizardOptions attribute that dictates how the Wizard will allow itself to be navigated. EG:

    [WizardStepOptions(WizardNavigatorRules.LeapBackOnly, WizardButtonRules.Both, WizardCompleteRules.DisableNavigation)]
    public class MembershipFormController : WizardController<ESregister.Models.TheSociety.RegistrationData>
    {
        [WizardStep(1, "Start")]
        public override ActionResult Start()
        {
            return Navigate();
        }

It works a dream. If in the course of your wizard's use, you need to prune or add steps, you just define which steps are to be displayed using the Range property, also defined on the base class WizardController:

        [WizardStep(2, "Category")]
        public ActionResult Category()
        {
            return Navigate();            
        }

        [HttpPost]
        public ActionResult Category(int ? Category)
        {
            if (Category == null)
            {
                ModelState.AddModelError("Category", "You must fill in a Category!");
                return Navigate();
            }
            if (Category == 3)
            {
                Range = new List<int> { 1, 2, 7, 8 };
            }
            else
            {
                Range = DefaultRange();
            }
            return Navigate();
        }

The wizard framework implements PRG automatically. You only need to provide HttpPost in a case like the above where you need to, for example, prune the steps range depending on user input.

It also provides navigation controls as follows:

<% StepManager stepManager = (StepManager)TempData["stepManager"];
   Html.WizardNavigator(stepManager);  %>
   Html.WizardButtons(stepManager, WizardButtonLocation.Top); %>

Where the WizardNavigator shows / provides links to the different steps (links if allowed) and WizardButtons are the Start, Next, Continue, Previous and Confirm buttons.

It is working in production.

I have included all this detail to show what is possible and that the suggested solution does work.

awrigley