What is an efficient and maintainable method for creating a wizard-style navigation system with complex rules in an ASP.NET application?

StackOverflow https://stackoverflow.com/questions/9456536

Question

I'm working on team, updating a commercial web app in C#. We're using Telerik's RadControls. The application guides the user through a set of tasks in a wizard-like fashion, but also allows the user to jump back to previous steps and make modifications. Additionally, some steps have complex validation methods. For example, once you complete a task on step 5, steps 7, 8, and 9 become available. Also, if you change any settings on step 2, everything after that point has to be re-done, so all steps after step 3 must be disabled.

  • There is a navigation bar on the left side of the screen (a Telerik RadPanel) that lists all of the steps in sequential order. Depending on where you are at in the process, some steps are available to the user, and some are disabled.
  • There is a drop-down box at the top of the page (a Telerik SplitButton on a RadToolBar) that also contains all of the steps
  • There are forward and backward buttons that let you move to the next or previous step in the process.

Each of the "steps" have their own Page, so clicking on a link to "Step 1" will take you to step1.aspx

The navigation code is strewn all over the master page. There are methods that handle the step-enabling logic that are basically massive switch statements on the navigation panel's selected index. I want to re-write all of this functionality and put it into one "NavigationManager" class that has a reference to all of the necessary controls (although I'm open to suggestions on that). I'm looking for a solution that will:

  • Know where the user is in the process, and where the user is allowed to go so that it can enable and disable navigation items on every page load. Each steps should have a kind of validation process that enables it to decide where to send the user.
  • Know how to enable and disable navigation items for each control. How do I link this up? The button indexes are unpredictably different for each navigation control.
  • Know how the steps are related so that the user can click the "next" button and be taken to the next sequential step.
  • Be maintainable - The current system is so convoluted that fixing one bug causes others.
  • Be efficient - It shouldn't take much time to process on each page load

I suppose that what I'm really having difficulty with is how to define all of these steps somewhere once, so that the system can make use of them. I thought about defining all of the steps in an enum, but I'm foreseeing lot's of switch statements for enabling the step buttons on the navigation controls.

I've Googled every related keyword I could think of, and I can't find anything helpful. I know that this not a unique issue. I see lots of examples of web apps with similar navigation systems, such as TurboTax.

So, my questions are:

  • How do I define all of my steps in one spot so that the entire program knows how to deal with them?
  • How do I use those definitions to determine which steps the user can access, and then enable the appropriate navigation items?
Was it helpful?

Solution

It looks like you need to know several things in your system.

  • First, you need a list of steps and tasks for each step.
  • Second, you need to know the state of each task of each step. This could be simple such as a bit flag or could be an enum with more complex statuses, such as Not Started, Incomplete, and Complete. Each task should know its own status and it's own criteria for determining that status. Once all tasks in a step are complete, the step is automatically considered complete. You can choose to associate a page URL to each step, and even a control ID to each task.
  • Finally, you need a global context which is aware of the status of all tasks at page load, and retains the complex rules you mentioned. One way of doing this is to define a list of tasks and/or steps that must be complete and used to set a property CanBeVisible based on this List.All(x x=> true); this can be done in a database, XML, whichever, so long as the mapping is loaded into the context with updated state information for each task. Then, any navigation control can tap into this global context and know exactly what options to render visible.

The idea is to encapsulate the mapping of visibility dependencies to the status of each task in a central location, and then any other UI element such as navigation panel can use this info to render only those controls that satisfy criteria for visibility for any given step and set of tasks.

Reply from CKirb250: Sorry, had to place my comment here because the formatting in the comment box sucks.

  • Yes, I do need a list of steps. Where should I put them? Should they be a simple enum? Should they be laid out in XML format somwhere so that I can reference them with a key value, their name that should be shown to user, and what aspx page corresponds to that step?

  • This is tracked in the database. In order to know the state of each step, the database is queried.

  • How could the complex rules be defined? I'm imagining a giant switch statement that says if (currentStep == steps.Step1) { if (page.IsFilledOut) { enableSteps(1, 2, 3, 4, 5); } }.

Here is a way to set this up in XML - each element should be defined as a class, and if tracked in a database (as is recommended) then you have some tables (Step, Task, VisibilityDependency, at least.) You can generate XML from DB (or just populate the classes directly from DB). I used XML as an easy example to just visualize what I am thinking of:

<WizardSchema>
  <Steps>
    <Step>
      <StepID>1</StepID>
      <StepOrder>1</StepOrder>
      <StepTitle>First Step</StepTitle>
      <StepUrl>~/step1.aspx</StepUrl>
      <Tasks>
        <Task>
          <TaskID>1</TaskID>
          <TaskOrder>1</TaskOrder>
          <TaskPrompt>Enter your first name:</TaskPrompt>
          <TaskControlID>FirstNameTextBox</TaskControlID>
          <VisibilityDependencyList></VisibilityDependencyList>
          <IsCompleted>True</IsCompleted>
        </Task>
        <Task>
          <TaskID>2</TaskID>
          <TaskOrder>2</TaskOrder>
          <TaskPrompt>Enter your last name:</TaskPrompt>
          <TaskControlID>LastNameTextBox</TaskControlID>
          <VisibilityDependencyList>
            <VisibilityDependency StepID="1" TaskID="1" />
          </VisibilityDependencyList>
          <IsCompleted>False</IsCompleted>
        </Task>
      </Tasks>
    </Step>
    <Step>
      <StepID>2</StepID>
      <StepOrder>2</StepOrder>
      <StepTitle>Second Step</StepTitle>
      <StepUrl>~/step2.aspx</StepUrl>
      <Tasks>
        <Task>
          <TaskID>3</TaskID>
          <TaskOrder>1</TaskOrder>
          <TaskPrompt>Enter your phone number type:</TaskPrompt>
          <TaskControlID>PhoneNumberTypeDropDown</TaskControlID>
          <VisibilityDependencyList>
            <VisibilityDependency StepID="1" /> 
            <!-- Not setting a TaskID attribute here means ALL tasks should be complete in Step 1 for this dependency to return true -->
          </VisibilityDependencyList>
          <IsCompleted>False</IsCompleted>
        </Task>
        <Task>
          <TaskID>4</TaskID>
          <TaskOrder>2</TaskOrder>
          <TaskPrompt>Enter your phone number:</TaskPrompt>
          <TaskControlID>PhoneNumberTextBox</TaskControlID>
          <VisibilityDependencyList>
            <VisibilityDependency StepID="1" />
            <VisibilityDependency StepID="2" TaskID="1" />
          </VisibilityDependencyList>
          <IsCompleted>False</IsCompleted>
        </Task>
      </Tasks>
    </Step>
  </Steps>
</WizardSchema>

Now what I am thinking is your navigation control would poll your global context, writing some code for this now just to give you an idea of how I would do this.

Here is some code to represent this schema and a method to return all tasks that can be displayed on a page; no switch statements!

using System;
using System.Linq;
using System.Collections.Generic;

namespace Stackoverflow.Answers.WizardSchema
{

    // Classes to represent your schema
    public class VisibilityDependency
    {
        public int StepID { get; set; }
        public int? TaskID { get; set; } // nullable to denote lack of presense
    }

    public class Task
    {
        public int TaskID { get; set; }
        public int TaskOrder { get; set; }
        public string TaskControlID { get; set; }
        public bool IsComplete { get; set; }
        public List<VisibilityDependency> VisibilityDependencyList { get; set; }
    }

    public class Step
    {
        // properties in XML

        public int StepID { get; set; }
        public string StepTitle { get; set; }
        public List<Task> Tasks { get; set; }
    }

    // Class to act as a global context
    public class WizardSchemaProvider
    {
        /// <summary>
        /// Global variable to keep state of all steps (which contani all tasks)
        /// </summary>
        public List<Step> Steps { get; set; }

        /// <summary>
        /// Current step, determined by URL or some other means
        /// </summary>
        public Step CurrentStep { get { return null; /* add some logic to determine current step from URL */ } }

        /// <summary>
        /// Default Constructor; can get data here to populate Steps property
        /// </summary>
        public WizardSchemaProvider()
        {
            // Init; get your data from DB
        }

        /// <summary>
        /// Utility method - returns all tasks that match visibility dependency for the current page;
        /// Designed to be called from a navigation control;
        /// </summary>
        /// <param name="step"></param>
        /// <returns></returns>
        private IEnumerable<Task> GetAllTasksToDisplay(Step step)
        {

            // Let's break down the visibility dependency for each one by encapsulating into a delegate;
            Func<VisibilityDependency, bool> isVisibilityDependencyMet = v =>
            {
                // Get the step in the visibility dependency
                var stepToCheck = Steps.First(s => s.StepID == v.StepID);

                if (null == v.TaskID)
                {
                    // If the task is null, we want all tasks for the step to be completed
                    return stepToCheck
                        .Tasks // Look at the List<Task> for the step in question
                        .All(t => t.IsComplete); // make sure all steps are complete

                    // if the above is all true, then the current task being checked can be displayed
                }

                // If the task ID is not null, we only want the specific task (not the whole step)
                return stepToCheck
                    .Tasks
                    .First(t => t.TaskID == v.TaskID) // get the task to check
                    .IsComplete;
            };

            // This Func just runs throgh the list of dependencies for each task to return whether they are met or not; all must be met
            var tasksThatCanBeVisible = step
                .Tasks
                .Where(t => t.VisibilityDependencyList
                    .All(v => isVisibilityDependencyMet(v)
                ));

            return tasksThatCanBeVisible;    
        }

        public List<string> GetControlIDListForTasksToDisplay(Step step)
        {
            return this.GetAllTasksToDisplay(this.CurrentStep).Select(t => t.TaskControlID).ToList();
        }

    }

}

Let me know if this is enough to spin your own ideas for a clean way to refactor your code. I've developed and worked on many wizard style systems and I have seen first hand what you describe; namely, that it gets to be a mess real fast if not architected well from the onset. Good luck!

OTHER TIPS

Have you looked at the asp:Wizard control? There is some example of usage here: The ASP.NET 2.0 Wizard Control and the doc msdn.microsoft

A strategy could be to create a custom control for each step, and then add them into the wizard. In this way your code for each step can be isolated, and the single-page containing the wizard, can use the interface to these controls to handle changes at each step!

I recently designed my own "Wizard" control; granted it was for WinForms, but I think that my approach has some merit as it differed from a number of other wizards which seem so generically common.

I inverted the process from a "step-based" approach to a "task-based" approach. That is, none of the steps know about any other steps or how they relate, and the task knows about all steps. When a step was "complete" the control is returned to the task which then collects any OUTPUT state and feeds it as the INPUT state to the next step(s). Because the task knows about each individual kind of step (through static typing) it can perform any appropriate operations in a type-safe manner. (The task and the task UI should be separate, but related components: the task does the work, and works in conjunction with the UI to handle user navigation.)

Consistent helper methods/classes are then used to advance along steps in the trivial cases only requiring minimal wiring to do "custom work". (I prefer to keep logic "in code" because so often it can quickly escape the bounds of a markup language / "rule object".)

Now, where it really differs is how the UI is handled ;-) I would recommend keeping the UI explicit (in either an ASCX or template or whatnot) and not try to generate it dynamically... unless there really is a good reason.

Hope this gives some insights for your design!

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top