Recently I was presented with a website navigation problem. The problem presented itself as we drew up plans to enhance security on our site. In our security scheme, some pages are accessible to authenticated users, others are not, and others require an extra layer of security. This is pretty typical. The problem is deciding how design user navigation through the site that does not enforce a overly rigid path, that flows through existing pages before presenting the final result.
When you speak about a series of steps or events that are preliminary to a final step, you often speak in terms of work
flow, or wizards as a means to that end. Wizards are the generally the best workflow technique because they are easy to understand, use visual cues to indicate the progress, and because users are familiar with them. It’s a paradigm that universally relied upon by software developers. Wizards are standard interfaces for software installers. They are used heavily by web merchants. Even your mom knows what a wizard is. Why is that? Well, for one, there’s really no other way to do it. Anytime you can enumerate “In order to do A, you do first do X, Y, and Z”, then you are in wizard territory.
An installer or a wizard implemented in a desktop program can easily enforce the path of information gathering, through modal dialogs. There’s really nowhere else to go but forward or backward. Where web wizard interfaces are concerned, the code behind the wizard has to do extra verification to insure the wizard is followed correctly. The information is stateful, and must be persisted between steps; after all, the user can decide to browse off to facebook during the process, and may jump back into the wizard at any place in the series of steps.
This is why web wizards should be extra-friendly – they should outline the steps in advance, and they should show
breadcrumbs in the intermediate steps to make it obvious to the user where she is in the process. The Amazon Services wizard is a case in point – it’s very easy to use and understand. Before the wizard begins, a graphic illustrates the number of steps to completion. The point is to make it dead simple to sign up.
Wizards are great when you have control from start to finish or you are doing new development. But what about the cases where you have to wedge in some workflow onto a pre-existing site? If you don’t have the luxury of building a clean new page or series of pages, how do you control the navigational flow? I’ve been pondering this problem recently, and worked up a solution that satisfies my needs. First, this is not a traditional wizard application. I need to send the user through a series of pages, and the user won’t realize that she is using a wizard at all. That’s where the “invisible” part comes in. Second, I want the decision on navigation logic to be encapsulated apart from the page logic.
The NavigationPath Control
The NavigationPath Control uses a navigation score to determine if the user has followed all the steps in the navigation. The navigation score is calculated by examining an object called the NavigationState, which is stored in session, along with an optional user entity object. The navigation score insures that the user completes each page in the navigation path in the order in which it is defined. The score does not prohibit the user from leaving the application to go to another page, then returning to a page in the navigation path; however, there are rules to the navigation sequence, and if the rules are violated, the navigation state will be abandoned. The rules are:
- The user must complete the navigation pages in sequence.
- The user may go backward in the sequence of steps.
- The user may not skip a step in the sequence.
If the user goes backwards in the series of steps, the completion state of all subsequent steps is reset to incomplete. If the user skips a step, the navigation state is abandoned. When the navigation state is abandoned, the NavigationState object is removed from session, and the NavigationPath control no longer attempts to navigate through pages (steps) defined in the navigation path. In order to restart the navigation, the user has to return to the first page in the navigation path.
The NavigationPath control handles the navigation logic in coordination with the page with two events: OnNavPathPageInit and OnNavPathPagePreRender. The names of these two events indicate exactly where they are called in the page lifecycle. The page needs to implement at least one of these events.
A Sample Project
I put together a sample project to test the concept (and the code). The sample uses the familiar shopping cart application - a user selects an item, perhaps using a coupon code, and places it in the cart.
If the user is not logged in, she is routed through a login page. If a coupon is entered, she is taken to a coupon page to
confirm the coupon. Finally, she is directed to the cart page. NavigationPath events determine if the navigation condition is satisfied for the page. If the condition is satisfied, the event code sets a flag, and the user control handles the navigation to the next page in the navigation path.
After the initial item is added to the cart, the user can return to the inventory page. When the second item is selected, the
login page should be skipped. The page is loaded, but it is not displayed or returned to the user. The is because the event logic for the login page determines that the user has already logged in, so she is not stopped at the login page.
The third navigation path example shows the navigation path that applies when no coupon code is entered on the inventory
page. The navigation skips the login page, since the user is already logged in, and skips the coupon page, since no coupon was entered on the navigation page.
This final image shows the event handlers that are provided for each of the pages in the sample project.
The inventory page is the initial page that defines of navigation path. None of the other pages contain a navigation path definition, nor are the pages “aware” of the prior or next page in the navigation.
Let’s examine the layout and event handlers for each of the pages. First, the assembly is declared in web.config like so:
<pages>
<controls>
<add tagPrefix="nav" namespace="AsAbove.Web.Controls" assembly="AsAbove.Web.Controls.NavigationPath"/>
</controls>
</pages>
A NavigationPath control is declared in each of the pages. In the following listings, I show NavigationPath declaration. I am only showing the code for the Inventory page, because the logic in the event handlers is similar in each page: if the criteria of the page is complete, the property NavPathValidationArgs.Complete is set to true.
Inventory
<nav:NavigationPath id="ItemToCart" runat="server" OnNavPathPagePreRender="CheckSelection"
Text="Inventory,Login,Coupon,Cart" />
private ItemEntity Entity = new ItemEntity ();
private bool Selected;
protected void btnItem1_Click (object sender, EventArgs e)
{
Entity.Coupon = txtCoupon1.Text;
Entity.Item = imgItem1.AlternateText;
Entity.Price = decimal.Parse (lblPrice1.Text);
Selected = true;
}
protected void btnItem2_Click (object sender, EventArgs e)
{
Entity.Coupon = txtCoupon2.Text;
Entity.Item = imgItem2.AlternateText;
Entity.Price = decimal.Parse (lblPrice2.Text);
Selected = true;
}
protected void CheckSelection (object sender, NavPathValidationArgs e)
{
if (!Selected)
return;
e.Entity = Entity;
e.Complete = true;
}
Login
<nav:NavigationPath id="ItemToCart" runat="server" OnNavPathPageInit="CheckAuthStatus"
OnNavPathPagePreRender="CheckAuthStatus" />
Coupon
<nav:NavigationPath id="ItemToCart" runat="server" OnNavPathPageInit="CheckCouponStatus"
OnNavPathPagePreRender="CheckConfirmedStatus" />
Cart
<nav:NavigationPath id="ItemToCart" runat="server" />
There are some points to note about the control declarations:
- The Text property on the Inventory page is the only page that declares the navigation path. It provides a comma-delimited list of the pages in the path.
- Each page uses the same ID for the control, ItemToCart
- The OnNavPathPageInit event should only be provided if the page can complete the navigation criteria without displaying to the user.
In the codebehind, there are a couple of notable points:
- As the first page in the navigation path, the Inventory page creates a custom entity object ItemEntity and populates it with the input entries on the page. This object is used by the pages to store information particular to this application - such as the item description, coupon, and price.
- The Event handler CheckSelection notes when the button has been clicked by checking the Selected property. If the button was clicked, it assigns the ItemEntity object to the NavPathValidationArgs.Entity property.
Download NavigationPath.zip (49 kb)
Tags: wizard, navigation, asp.net, redirect