Click here to Skip to main content
Click here to Skip to main content

UserControl: ASP.NET 2.0 Wizard SideBar Replacement Navigation & How to

By , 15 Nov 2006
 

Sample Image - CodeProjectKferronWizardTopNav.gif

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 NavBarItems 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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Kevin C Ferron
Web Developer
United States United States
Member
No Biography provided

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
QuestionGetting links to change on Next and Previousmemberrhuttenh16 Nov '09 - 11:17 
Control works great when clicking on the steps with the navbar but I can not seem to get it to change the when I click the next or previous buttons in the wizard that uses this control. I think it just needs to be databound again but where do I do it?
Questionhow i can put the new side bar vertical?membereyalro6 Nov '07 - 23:13 

I want that the new side bar will be like the wizard:
 
step 1
step 2
step 3
 
and not like this: step 1 step 2 step 3
 
how i do this?
GeneralNavBarItem not requiredmemberinstitute6 Nov '07 - 0:45 
Thanks for the useful article.
 
I found that there is no need for the additional NavBarItem class as you can bind directly to the wizard step collection:
 
<code>
NavBar.DataSource = TargetWizard.WizardSteps;
NavBar.DataBind();
</code>
 
I also changed it to use the inline data binding syntax on the repeater:
<code>
<asp:Repeater ID="NavBar" runat="server" OnItemCommand="NavBar_ItemCommand" OnItemDataBound="NavBar_ItemDataBound"   >
<HeaderTemplate>
      <ul class='<%= CssClass %>'>
</HeaderTemplate>
<ItemTemplate>
      <li class='<%# GetCSSClass() %>'>
            <asp:LinkButton
            ID="lnkStep"
            runat="server"
            CommandName="NavigateToStep"
            CommandArgument='<%# Eval("ID") %>' >
                        &gt;&nbsp;<%# Eval("Name") %>
            </asp:LinkButton>
      </li>
</ItemTemplate>
<FooterTemplate></ul></FooterTemplate>
</asp:Repeater>
</code>
 
And changed the ItemDataBound event handler to disable the link for the current step and added a method to get the CSS class of the list item (class='selected' for the current step), and added a CSSClass property to use on the ul:
 
<code>
 
      protected void NavBar_ItemDataBound(object sender, RepeaterItemEventArgs e)
      {
            if (e.Item.ItemType == ListItemType.Item || e.Item.ItemType == ListItemType.AlternatingItem)
            {
                  WizardStep step = e.Item.DataItem as WizardStep;
                  LinkButton lb = null;
                  lb = e.Item.FindControl("lnkStep") as LinkButton;
 
                  lb.Enabled = true;
                  if (step.Visible || step.StepType == WizardStepType.Complete)
                  {
                        lb.Enabled = false;
                  }
 
            }
 
      }
 
      public string GetCSSClass()
      {
            bool IsVisible = (bool)Eval("Visible");
            if (IsVisible)
            {
                  return "selected";
            }
            return "";
      }
 
      public string CssClass
      {
            get { return _cssClass; }
            set { _cssClass = value; }
      }
 
</code>

Generalerror on TargetWizard. Change that property like thatmembermehmetfatih15 Oct '07 - 23:02 
protected Wizard TargetWizard
      {
            get
            {
 
                  if (!String.IsNullOrEmpty(wizardToNavigate))
                  {
                        parentWizard = this.Parent.FindControl(wizardToNavigate) as Wizard;
                  }
                  else
                  {//this is to support putting this control in the headertemplate of the wizard
                        Control _par = this.Parent;
                        while (_par != null)
                              if (_par is Wizard)
                              {
                                    parentWizard = _par as Wizard;
                                    break;
                              }
                              else
                                    _par = _par.Parent;
                  }
 
                  if (parentWizard != null)
                        return parentWizard;
                  else
                        throw new ArgumentOutOfRangeException(wizardToNavigate + " could not be found");
 

            }
 
      }
GeneralNice job.memberjlavigne16 May '07 - 12:52 
I was able to extent it for my needs just fine. here are the changes I made incase anyone else wants it.
 
in the Repeater I changed it to
 
<ItemTemplate>
<asp:Label ID="NavBarText" runat="server" Text=""></asp:Label>
<asp:LinkButton ID="NavBarLink" runat="server">LinkButton</asp:LinkButton>
<asp:Literal ID="NavBreak" runat="server"><br /></asp:Literal>
</ItemTemplate>

 
so I can have the option to have text, link or both along with the option to have a <br> so it can be on the left or on the top.
 
in the .cs file I added the following properties
 
public enum ebool { True, False };
 
private ebool showText = ebool.False;
private ebool showLink = ebool.True;
private ebool addBreak = ebool.True;
 
public ebool ShowText { get { return showText; } set { showText = value; } }
public ebool ShowLink { get { return showLink; } set { showLink = value; } }
public ebool AddBreak { get { return addBreak; } set { addBreak = value; } }

 
and in the NavBar_ItemDataBound method I changed it to the following
 
protected void NavBar_ItemDataBound(object sender, RepeaterItemEventArgs e)
{
NavBarItem it = e.Item.DataItem as NavBarItem;
LinkButton lb = e.Item.FindControl("NavBarLink") as LinkButton;
Label lbl = e.Item.FindControl("NavBarText") as Label;
Literal litBR = e.Item.FindControl("NavBreak") as Literal;
 
// setup label
lbl.Text = it.Id;
lbl.Visible = (showText == ebool.True) ? true : false;
 
// setup link
lb.Text = it.Id;
lb.CommandArgument = it.Step.ID;
lb.CommandName = switchStepsCommandName;
lb.Visible = (showLink == ebool.True) ? true : false;
 
// setup break
litBR.Visible = (addBreak == ebool.True) ? true : false;
}

 
great job with the info you provided and it worked great, cheers.
 

 
Have a good one.
 
Jay
http://www.bwlogic.com

GeneralItemDataBound fires for Header and Footer templatesmemberLee Ryman13 Dec '06 - 18:41 
The handler for ItemDataBound fires for a Header and Footer template as well, and in these instances the DataItem will be null/Nothing. Developers may want to check if the e.Item.ItemType is one of ListItemType.Item AlternatingItem, ListItemType.SelectedItem or ListItemType.EditItem before attempting to use the DataItem and assign stuff to the link button.
 
Regards,
 
Lee
QuestionHow to change border style of LinkButton for active step only? [modified]memberMollyKo3 Dec '06 - 21:32 
I would like to set border style in "solid" for link button if step is active... Like this:
 
Step 1 Step 2 Step 3 Step 4
 
If it is possible, are any changes required?
 

Thanks.

GeneralRe: How to change border style of LinkButton for active step only? [modified]memberMollyKo4 Dec '06 - 20:28 
Here is your code you wrote to verinda_bindra (http://www.codeproject.com/useritems/cool_tabstrip_cool_Wizard.asp?df=100&forumid=358957&exp=0&select=1759340#xx1759340xx[^])
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) {
//this is where logic for current step would be
//ie.
lb.Enabled = false;
lb.Text = it.Id; // maybe you would want to set the text to say something different?
 

//of course you could control colors/styles etc. here
 
}
else {
 
lb.Text = it.Id;
lb.CommandArgument = it.Step.ID;
lb.CommandName = switchStepsCommandName;
}
 

}

 
I used this code to change a style of link button, but style is changed only for the 1st button. If I click on the 2nd link button (to go to the 2nd step), the style is not changed for the 2nd button, and the 1st button always has unchangeable style...
 
I added the following code:

//of course you could control colors/styles etc. here
lb.BorderStyle = BorderStyle.Solid;

 

GeneralRe: How to change border style of LinkButton for active step only?memberKevin C Ferron9 Dec '06 - 0:11 
yeah i've actually changed my approach to this.. NavBarItem now inherits from DataListItem, and acts as a template container.. the usercontrol now overrides CreateControlHierarchy() and chooses which exposed template to instantiate the container. Basically this allows on the aspx to make a <selectednavitemtemplate> in the usercontrol declaration, and the logic in CreateControlHierarchy() is just which conditions determine which template instantiates in the NavBarItem.
 

I haven't decided if i'm going to update the article or not. If you're interested in the code, email me at kevin_ferron@hotmail.com
Generalentry level articlememberrajantawate1(www.jhatak.com)21 Nov '06 - 6:31 
absolutely entry-level stuff.
 
does not seem to require such a long article.
GeneralRe: entry level articlememberKevin C Ferron9 Dec '06 - 0:12 
i agree
GeneralRe: entry level articlememberLee Ryman13 Dec '06 - 18:49 
For someone who has never used validators before, its not verbose at all. You have to remember that developers with all levels of experience will be looking up these articles.
 
Regards,
 
Lee
Generalproblem with control validationmember@mstrad17 Oct '07 - 23:10 
Hello everybody.
I'm using this amazing control's to put the navbar in the header of the page.
I've put control validation on each step of my wizard, when i try to change step from navigation button inside the wizard if this control failed the active step doesn't change, but if i try to go in the step 2 or 3 using link generated by user control my wizard goes to the selected step ignoring the control validation.
now i'm tryng to resolve this.
is this appened only at me?
 
sorry for my english i'm a poor italian developer Poke tongue | ;-P
Roberto
GeneralRe: problem with control validationmemberzeeb719 Nov '07 - 1:33 
Hi Roberto,
 
In NavBar_ItemCommand you should check Page.IsValid before using the MoveTo method on the wizard
 
Hope this helps
Phil
 
Creative Ministries

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Permalink | Advertise | Privacy | Mobile
Web04 | 2.6.130516.1 | Last Updated 15 Nov 2006
Article Copyright 2006 by Kevin C Ferron
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid