
Introduction
This beautiful screenshot doesn't really do justice to what's going on here, but this article is written in response to some fatally tragic code I've seen floating around the net to deal with the ASP.NET Wizard
control.
The Wizard
control is actually a pretty sweet tool.. it captures some of the predictable logic in a step by step process pretty nicely... but...
...one of the inflexible pieces of its design is actually critically deficient in my opinion, and that is the SideBar
. If you've ever looked through the Wizard
members looking for a different placement of the SideBar
, you will be as disappointed as I was. I decided to write this article when I noticed some people going way overboard with defeating this impediment in design. I want to present the solution I developed so that other people in this situation do not end up scrapping the use of the Wizard
control, or end up writing a more complex solution than what is necessary.
Using the code
This control can be used in several ways right out of the box. One of the preferred ways I like to use it is actually in the <HeaderTemplate>
of the Wizard
:
<asp:Wizard ID="Wizard1" runat="server" DisplaySideBar="False">
<HeaderTemplate>
<kferron:WizNav EnableViewState="true" ID="WizNav1" runat="server" />
</HeaderTemplate>
<WizardSteps>
<asp:WizardStep ID="Step1" runat="server" Title="Step 1">
Wow.
</asp:WizardStep>
<asp:WizardStep ID="Step2" runat="server" Title="Step 2">
Amazing.
</asp:WizardStep>
</WizardSteps>
</asp:Wizard>
Another usage, which is also a viable option because of the flexibility of placement, is to "attach" this control to a wizard, using the WizardToNavigate
property.
<kferron:WizNav WizardToNavigate="Wizard2"
EnableViewState="true" ID="WizNav1" runat="server" />
<asp:Wizard ID="Wizard2" runat="server">
<WizardSteps>
<asp:WizardStep ID="Wiz2Step1" runat="server" Title="Step 1">
</asp:WizardStep>
<asp:WizardStep ID="Wiz2Step2" runat="server" Title="Step 2">
</asp:WizardStep>
</WizardSteps>
</asp:Wizard>
So anyway, you can see that you can move the control around in the designer and it will remain coupled with the Wizard
.
About the code
I've stripped down my solution to the base elements for this article. There are many improvements that could and probably should be made before you just grab this control and run with it, but I wanted to focus on the fundamental topics here. I do, however, use a shell of a type that I make use of in the management of the navigation items. NavBarItem
is its name, and it is as exciting as it sounds:
public class NavBarItem
{
private WizardStep _step;
private string _id;
public NavBarItem(string id, WizardStep step)
{
_id = id;
_step = step;
}
public WizardStep Step
{
get { return _step; }
}
public string Id
{
get { return _id; }
}
As you can see, I actually retain a direct reference to the WizardStep
I am symbolizing with the NavBarItem
. This is a design decision that you could alter quite easily if you feel more comfortable working directly with string identifiers.
Now, let's go over how NavBarItem
is used. The following is in the Page_Load
in WizNav.ascx.cs:
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
Collection <NavBarItem> navs = new Collection <NavBarItem>();
foreach (WizardStep step in TargetWizard.WizardSteps)
{
navs.Add(new NavBarItem(step.Name,step));
}
NavBar.DataSource = navs;
NavBar.DataBind();
}
}
So if you're following along, this code accomplishes a couple of things for us. First of all, it uses TargetWizard
to locate the actual Wizard
control you are attempting to add navigation to. This allows us to iterate through the WizardSteps
collection and keep our nav bar nice and dynamic. Secondly, we bind our collection of NavBarItem
s to a Repeater
.
A Repeater
is nice here because now we have a very customizable presentation of our navigation items in the wizard. The basic HTML in WizNav.ascx is like:
<asp:Repeater ID="NavBar" runat="server"
OnItemCommand="NavBar_ItemCommand"
OnItemDataBound="NavBar_ItemDataBound"><ItemTemplate>
<asp:LinkButton ID="LinkButton1"
runat="server">LinkButton</asp:LinkButton>
</ItemTemplate>
</asp:Repeater>
Notice that we are attaching to both OnItemCommand
and OnItemDataBound
. First, let's look at what we accomplish by encapsulating logic in these events, starting with OnItemDataBound
.
protected void NavBar_ItemDataBound(object sender, RepeaterItemEventArgs e)
{
NavBarItem it = e.Item.DataItem as NavBarItem;
LinkButton lb = e.Item.FindControl("LinkButton1") as LinkButton;
lb.Text = it.Id;
lb.CommandArgument = it.Step.ID;
lb.CommandName = switchStepsCommandName;
}
OK, this can be cleaned up a bit, but let's focus on the core logic. This event fires when an item is bound to the Repeater
, and we actually have a reference to e.Item.DataItem
here, which is convenient, because we can just cast to our NavBarItem
.
It should be noted here, that in this event, all of our rendering properties of our navigation menu are accessible, including the current state of the wizard. For example, you could modify the logic here to make the current step the user is on not available in the nav bar, by doing:
protected void NavBar_ItemDataBound(object sender, RepeaterItemEventArgs e)
{
NavBarItem it = e.Item.DataItem as NavBarItem;
LinkButton lb = e.Item.FindControl("LinkButton1") as LinkButton;
if(it.Step == TargetWizard.ActiveStep) {
lb.Visible = false;
}
else {
lb.Text = it.Id;
lb.CommandArgument = it.Step.ID;
lb.CommandName = switchStepsCommandName;
}
}
Maybe someday if people are interested I can use all of the options that such a design allows for, but what I've given you is a paradigm that allows for all of the hooks you need.
We can also get a handle of any control we define in the Repeater
. In this example, LinkButton1
. LinkButton
can be a powerful mechanism for dispatching commands. Notice that we are setting a commandName
and a commandArgument
, and our argument is the programmatic ID of the WizardStep
that we have referenced in NavBarItem
.
OnItemCommand
of the Repeater
fires when the LinkButton
is clicked; the reasons why are outside the scope of this article, but if you're interested, this is a built-in bubbled event.
public void NavBar_ItemCommand(object source, RepeaterCommandEventArgs e)
{
if (e.CommandName == switchStepsCommandName)
{
TargetWizard.MoveTo((WizardStepBase)
TargetWizard.FindControl((string)e.CommandArgument));
}
}
So when this event fires, we check what command is attempting to run, and therefore know how to use the commandArgument
, which as I stated above, is the identifier of the WizardStep
.
Simple, easy to manage stuff. This should be one of the primary goals in your solutions.
Conclusion
I hope this helps some of you. This example shows how, with a properly constructed UserControl, taking advantage of what is already available to you is much better than reinventing the wheel. You can expand this UserControl quite easily.. perhaps you would want to use images in your navigation and have multistate images to highlight the user's progress. All you would need to do is modify the ItemTemplate
in the Repeater
. This is pretty simple stuff, but like I said, I was inspired to write this control when I saw numerous posts, some of which (in my opinion) are much uglier solutions to the same problem.
History
I wrote and added this article on 11/15/06.