Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

TSWizard - A Wizard Framework for .NET

0.00/5 (No votes)
26 May 2003 3  
Provides a framework for creating wizards for use in your .NET applications

Table of Contents

Introduction

I started work on the wizard framework at the end of July 2002 when I started a project that needed to be easy to use and that demanded that I hold the hands of the user as they proceeded to do their work.

Unfortunately, the .NET Framework lacks anything for quick wizard creation; thus I started my work in designing a reusable framework. What you see before you is the second revision of the original framework. The changes were minor and were only done to emulate the .NET framework's design standards.

At the time, I hadn't yet known that Chris Sells' Genghis project[^] was going to include a wizard framework. I will let you decide which is more suited for what you want to do. If this design doesn't suit you well, psdavis has created one based on the Tab control (Wizard Tab Control).

I've had the code sitting around on my hard drive for a while now; and in a break from writing, I wrote this article.

The Basics

A wizard is fairly basic, it consists of a window (BaseWizard) that contains individual steps (BaseStep) that can be navigated to based on the state of other steps. The framework I present here has two main parts to it, the BaseWizard which will contain the individual steps and the BaseStep which represents a single step of the wizard.

Page Layouts

Pages in wizards typically have two different layouts, one for exterior pages -- that is the first and last pages in the wizard -- and one for interior pages -- that is the pages in the middle of the wizard. TSWizard implements this by letting the wizard itself decide the best way to format itself, and having each individual step tell the wizard how it should be formatted. The difference between the two is drastic, but can be visually appealing. Here is a summary of what Microsoft advises in its Wizard97 specification.

Exterior Pages

Exterior pages have a sidebar graphic with a boxed logo. It should also have text in a larger font containing the title. If the page is the welcome page, then it should have a paragraph or two describing what the wizard will be doing. If it is the finish page, then it should have a list of things the wizard has done or will do, plus any instructions for the user to do after the wizard closes.

Interior Pages

Interior pages are simpler in the step design but more complex in the wizard design. At the top, it should have the step's title in bold, followed by a short description of that step, which is indented. A small logo is also provided on the right hand side. In the step itself you can place your controls in a layered manner. The labels should be on the second line, followed by the control they label on the third line. If you need more area, you can move out a line (label on the first, control on the second).

PageLayout enum

Name Description
InteriorPage The wizard and page should be formatted as an interior page.
ExteriorPage The wizard and page should be formatted as an exterior page.
None The wizard and page should use the last format used by the wizard.

BaseWizard

The wizard being designed in interior page mode

The wizard being designed in exterior page mode

Visually, the wizard consists of several different parts:

  • The wizard's caption
  • An info bar containing
    • The step title
    • The step description
    • A logo
  • A side panel containing
    • A filler graphic
    • Another logo
  • The step container
  • Navigation buttons

BaseWizard Members

Name Description
AllowClose AllowClose Gets/Sets how the wizard should react to being closed.
bool BackEnabled Gets/Sets whether the back button should be enabled.
string FirstStepName Gets/Sets the name of the first step the wizard should show.
Image Logo Gets/Sets the image that should appear in the upper-right corner (appears only for Interior pages).
Image SideBarLogo Gets/Sets the image that should appear in the upper-right corner of the sidebar (appears only for Exterior pages).
Image SideBarImage Gets/Sets the image that should appear in the sidebar (appears only for Exterior pages).
bool NextEnabled Gets/Sets whether the next button should be enabled.
string Title Gets/Sets the step title.
WizardStepDictionary Steps Gets the name/BaseStep pair dictionary class for this wizard.
bool IsFinish Gets whether the current step's NextStep property is equal to the const value BaseStep.FinishStep.
void MoveBack() Moves to the previous step
void MoveNext() Moves to the next step
void MoveTo(string name) Moves to the step with the specified name
void AddStep(string name, BaseStep step) Adds a new step to the wizard with the specified name.
BaseStep GetStep(string name) Returns a step, given the name.
BaseStep RemoveStep(string name) Removes a step from the wizard, given the name; it returns a reference to that step.
void ResetSteps() Fires the ResetStep event on each of the BaseSteps added to the wizard.
void SetCurrentStep(string name) Sets the step shown in the wizard to the one with the given name.
void SetFinish(bool bFinish) Changes the text of the Next button to reflect the state passed in.
void OnLoadSteps(EventArgs e) Raises the LoadSteps event.
LoadSteps Fired when the steps should be added to the wizard. Steps can be added before and after this event is fired; but the step specified by the FirstStepName property should be added at this point. This has also been made the default event.

BaseWizard.StepDirection Members

Member Description
InitialStep This step is being displayed because it is the first step in the wizard
NextStep This step is being displayed because it was the next step in the wizard
PreviousStep This step is being displayed because it was the previous step in the wizard
Jump This step is being displayed because BaseWizard.SetCurrentStep was called directly

As always, what little documentation is here is only the intended behavior; actual behavior is dictated by the code.

Before I explain typical usage of the BaseWizard class, I first want to show the properties, methods, and events exposed by the BaseStep class.

BaseStep

An interior step being designed

An exterior step being designed

The BaseStep object is implemented as a UserControl. This has two advantages, first it allows the user to visually design the step and it makes it easier to display it in the wizard without resorting to 'trickery' in making a Form appear as a Control.

BaseStep Members

Name Description
bool IsFinished Gets/Sets whether clicking next will end the wizard. This value will also change the text displayed in the next button.
string NextStep Gets/Sets the name of the next step in the wizard. Set this to "FINISHED" or BaseStep.FinishStep if clicking next should close the wizard.
string PreviousStep Gets/Sets the name of the previous step in the wizard.
string StepTitle Gets/Sets the text that will be displayed next to the logo in the wizard.
string StepDescription Gets/Sets a short description of this step. It will be displayed underneath the title for Interior pages and in a label at the top of the step for Exterior pages.
BaseWizard Wizard Gets/Sets the instance of the wizard that this step belongs to. The wizard framework will take care of setting this property, so user code should only use the accessor (get) method.
Image Logo Gets/Sets an Image which will override the Logo used by the Wizard, if this is non-null.
Image SideBarLogo Gets/Sets an Image which will override the SideBarLogo used by the Wizard, if this is non-null.
Image SideBarImage Gets/Sets an Image which will override the SideBarImage used by the Wizard, if this is non-null.
PageLayout PageLayout Gets/Sets the wizard layout that should be used for this page. Can be set directly or you can derive from BaseInteriorStep or BaseExteriorStep which does this, plus sets the default size.
int NumLinesToDraw A design-time property to adjust the number of helper lines to draw on the step.
void OnNext() Called when the next button has been clicked and this step is the current one. By default, this method tells the Wizard to move to the next step.
void OnBack() Called when the back button has been clicked and this step is the current one. By default, this method tells the Wizard to move to the previous step.
void OnFinish() Called when the finish button has been clicked and this step is the current one. By default, this method tells the Wizard to close.
void OnNextStepChanged(EventArgs e) Fires the NextStepChanged event.
void OnPreviousStepChanged(EventArgs e) Fires the PreviousStepChanged event.
void OnStepTitleChanged(EventArgs e) Fires the StepTitleChanged event.
void OnShowStep(EventArgs e) Fires the ShowStep event.
void OnResetStep(EventArgs e) Fires the ResetStep event.
NextStepChanged Occurs when the NextStep property has changed.
PreviousStepChanged Occurs when the PreviousStep property has changed.
StepTitleChanged Occurs when the StepTitle property has changed.
StepDescriptionChanged Occurs when the StepDescription property has changed.
ShowStep Occurs when the step is shown in the wizard; this event will occur each time the step is shown, not just the first time. Now provides information about what direction this step is shown by.
ResetStep Occurs when the step should reset its fields, this event will fire after steps are loaded by LoadSteps in the Wizard and whenever ResetSteps is called in the Wizard.
LogoChanged Occurs when the Logo property has changed.
SideBarLogoChanged Occurs when the SideBarLogo property has changed.
SideBarImageChanged Occurs when the SideBarImage property has changed.
PageLayoutChanged Occurs when the PageLayout property has changed.
ValidateStep Occurs after the Next button has been clicked but before the wizard moves to the next step. Now fixed to fire when the Next button is now the Finish.

Demo Walkthrough

You have a couple different ways of using the wizard framework as far as how you design the wizard and its steps. My preferred way of creating a wizard is to use the Inherited Form and UserControl features of VS.NET which requires that the form and UserControl you wish to inherit from be in the current solution or for you to select the assembly they are located in. To accomplish this, you can add the TSWizard project to your solution or choose to browse for the assembly each time you wish to add a new wizard or step.

The rest of this article will focus on the creation of the simple wizard found in the demo.

Wizard Design

The wizard will consist of five steps:

  1. Introductory step; tells the user what the wizard will accomplish
  2. Information step; allows the user to enter in any information needed by the wizard
  3. Confirm step; recaps the information entered, then prompts for the user to click Next to do the work
  4. Work step; here the work will actually be done; once it is done, the wizard will automatically proceed to step 5
  5. Finish step; recap the work that was done and ask the user if they would like to run the wizard again

This should show off all the features I made available in the framework; plus give another demo on threading with windows forms. Off to start the work fun!

First, add a new Form and have it inherit from TSWizards.BaseWizard (don't forget to reference either the TSWizards project or the compiled DLL), I named it DemoWizard. Then prior to starting work on the actual content of the steps, I added 5 new UserControls to the project. The first one inherits from TSWizards.BaseExteriorPage, the next three inherit from TSWizards.BaseInteriorPage, and the last inherits from TSWizards.BaseExteriorPage. I named them Step1, Step2, Step3, Step4, and Step5.

The Wizard

In the wizard's LoadSteps event, I added code to add each of the steps to the wizard's step list.

AddStep("Step1", new Step1());
AddStep("Step2", new Step2());
AddStep("Step3", new Step3());
AddStep("Step4", new Step4());
AddStep("Step5", new Step5());
DemoWizard Properties
AllowClose AskIfNotFinish
FirstStepName Step1

Step 1

Pretty simple first screen, we don't want to scare the user away after all. Aside from the design of the step; the only code I added was a property exposing the checked state of the checkbox. In production code, you would also have it save and load this value so that the first step could be skipped. This should all be done within the LoadSteps event, because the FirstStepName property is used after it has completed.

Step1 Properties
StepTitle Welcome to the Wait On Us ordering wizard!
StepDescription In this wizard I will take your order, then fetch it in 30 seconds or less; or else its free!
NextStep Step2

Step 2

A bit more work here; the two combo boxes contain the values "Rare", "Medium Rare", "Medium Well", "Well Done". A lot more code is in this one as well. First, handle the ResetStep event and add the following line of code.

ResetCheckBoxes(this);

Now add the following method to the Step2 class:

private void ResetCheckBoxes(Control parent)
{
    CheckBox chk = null;
    ComboBox cbo = null;
    
    foreach(Control c in parent.Controls)
    {
        chk = c as CheckBox;
        cbo = c as ComboBox;
        
        if( chk != null )
        {
            chk.Checked = false;
        }
        else if( cbo != null )
        {
            cbo.SelectedIndex = -1;
        }
        else
        {
            ResetCheckBoxes(c);
        }
    }
}

This code takes in a Control as a parameter, then it loops through all child controls of the parent and then checks to see if the child control is a CheckBox or a ComboBox. If it is, it resets the values to the default. If it isn't either of those two controls, then it will recursively call itself using the child control as the new parent. The call starts by passing in the form instance. I could have done the same by calling it three times, once for each GroupBox on the UserControl.

Next, we need to validate the order; ensuring that we have that particular item in the kitchen to prepare. This is done by handling the ValidateStep event on the step object. Taking a look at the inventory in the kitchen, it looks like the restaurant ran out of tofu; so no more tofu burgers can be ordered. In the ValidateStep event handler, add the following code to make sure the customer didn't order any tofu burgers.

if( tofu.Checked )
{
    MessageBox.Show(
        "We are out of tofu burgers please reselect", 
        "Reselect items");
        
    e.Cancel = true;
}

We also need a way to get the data back out of the UserControl; I chose to do this by creating a StringCollection and adding the food name if the appropriate check box is checked. I put this into a public method called GetOrder. I'll only include a snippet of it as the code is fairly lengthy, but very redundant.

public StringCollection GetOrder()
{
    StringCollection sCol = new StringCollection();
    
    if( caesarSalad.Checked )
    {
        sCol.Add("Caesar Salad");
    }
    
    if( tossedSalad.Checked )
    {
        sCol.Add("Tossed Salad");
    }
    
    // ...
    
    return sCol;
}
Step2 Properties
StepTitle Place order
StepDescription What would you like to order?
NextStep Step3
PreviousStep Step1

With that code out of the way, we're ready to move on to Step 3!

Step 3

Another simple step to design, a simple mutli-line TextBox with the ReadOnly property set to true. In the ShowStep event, add the following bit of code to have the order reflect the latest selection.

System.Text.StringBuilder sb = new System.Text.StringBuilder();
Step2 step2 = Wizard.GetStep("Step2") as Step2;

if( step2 == null )
{
    throw new ApplicationException(
        "Step2 of the wizard wasn't really step2");
}

StringCollection order = step2.GetOrder();

foreach(string item in order)
{
    sb.AppendFormat("{0},\r\n", item);
}

orderConfirm.Text = sb.ToString();

Pretty simple really, it gets the instance of Step2 from the wizard, then uses the GetOrder method to retrieve the order that was placed. Now that this step is done, it's on to Step 4.

Step3 Properties
StepTitle Order confirmation
StepDescription Please confirm your order. If you are satisfied with your choices click next to have our talented chef prepare your order before your very eyes!
NextStep Step4
PreviousStep Step2

Step 4

Ahhhh, the fun part! This step is what actually does the 'work' of preparing the order.

It is important to note that all work has to be done in another thread; or else there won't be any visual confirmation of what is happening. This is because the ShowStep event runs in the context of either the Load event for the Wizard form or the Click event when Back or Next/Finish is clicked.

"But James," you may say; "I thought we couldn't do anything to the GUI while operating in another thread?" And that itself is true, but that is what the BeginInvoke, EndInvoke, and Invoke methods on the Control class are for! They take a delegate and run it on the thread that owns the handle to the underlying Win32 window. With that said, let's get into how we will go about preparing this order.

  • First, we begin by getting the list of items that were ordered
  • For each item ordered, we will:
    • Begin to prepare the item (put its name in the Now preparing label)
    • Make the item (sleep on the thread for 1 second)
    • Finish preparing the item (increase the progress bar to account for the item being done)
  • Once that is done, we will move to the next step of the wizard (the summary step)

Let's get to some code now.

private void DoWork()
{
    Step2 step2 = Wizard.GetStep("Step2") as Step2;
    
    if( step2 == null )
    {
        throw new ApplicationException(
            "Step2 of the wizard wasn't really step2");
    }
    
    StringCollection order = step2.GetOrder();
    
    if( order.Count > 0 )
    {
        BeginPreparingOrder(order.Count);
        
        foreach(string item in order)
        {
            Preparing(item);
            Thread.Sleep(1000);
            Prepared();
        }
    }
}

The method above will perform the steps I laid out above; doing nothing but moving to the next step if there were no items ordered. I've placed copies of the helper methods used in the method below.

First, I will discuss the technique that I use for ensuring that GUI matters take place on the correct thread.

private void BeginPreparingOrder(int items)
{
    if( InvokeRequired )
    {
        this.Invoke( new IntDelegate(BeginPreparingOrder), 
            new object [] { items } );
        return ;
    }
    
    progress.Maximum = items * 10;
    progress.Value = 0;
}

The Control class defines a property called InvokeRequired which will return whether Invoke (or its kin) needs to be called to place itself in the correct thread. I use that property to my advantage by calling Invoke on itself should it be required; if it isn't required, then continues to do its GUI work uninhibited. The downside to this practice is that you need to find or create delegates to match the signature of the method. I have done so for the two methods that I call with parameters, IntDelegate and StringDelegate.

private void Preparing(string item)
{
    if( InvokeRequired )
    {
        this.Invoke( new StringDelegate(Preparing), 
            new object [] { item } );
        return ;
    }
    
    preparing.Text = item;
}

private void Prepared()
{
    if( InvokeRequired )
    {
        this.Invoke( new MethodInvoker(Prepared), 
            new object [] { } );
        return ;
    }
    
    progress.PerformStep();
}

private void DonePreparing(IAsyncResult result)
{
    if( InvokeRequired )
    {
        Invoke(new AsyncCallback(DonePreparing), 
            new Object [] { result } );
        return ;
    }
    
    NextStep = "Step5";
    Wizard.MoveNext();
}

private delegate void IntDelegate(int num);
private delegate void StringDelegate(string str);

By itself, this code does nothing because nothing is ever calling the DoWork method. I do this by placing the following code in the ShowStep event.

MethodInvoker mi = new MethodInvoker( this.DoWork );

mi.BeginInvoke(new AsyncCallback(DonePreparing), null);

I suppose I'll give the property listing for this step now.

Step4 Properties
StepTitle Now Preparing
StepDescription Our world class chef is now preparing your order, please wait.
NextStep String.Empty
PreviousStep String.Empty

Step 5

This step changed a bit more than the others since the last version. The bulleted list is created by using another control that is part of the framework, BulletList, it is really easy to use. Set the Text property to the text to display above the list, and use the StringCollection provided by the Items property to add each item to the list. The order was retrieved the same way as it was in Step 3, so refer to that if you need a refresher. At the moment, the default behavior of OnFinish is to close the wizard; we will change this behavior to move to the first or second step if the CheckBox is checked.

Here it is folks, the last bit of code for this article:

protected override void OnFinish()
{
    if( runAgain.Checked )
    {
        string moveTo;
        
        Step1 step1 = Wizard.GetStep("Step1") as Step1;
        
        if( !step1.NoShowWelcomeAgain )
            moveTo = "Step1";
        else
            moveTo = "Step2";
        
        Wizard.ResetSteps();
        
        Wizard.MoveTo(moveTo);
        
        IsFinished = true;
    }
    else
    {
        base.OnFinish();
    }
}
Step5 Properties
StepTitle Your order is complete!
StepDescription Our world class chef is now preparing your order, please wait.
NextStep String.Empty
PreviousStep String.Empty

Conclusion

There isn't much here to learn from; it just took some ambition to create the classes in the first place.

Whew, this is by far my longest article so far; and it has been a while since I've written one. I hope all of you at least enjoyed reading the article and maybe you'll find the framework useful.

Acknowledgments

  • Bug findings/fixes
    • Chris Dufour
    • Steaven Woyan
    • Ryan LaNeve
    • Urs Enzler
  • Suggestions
    • Ken Ostrin
  • Suggestions without knowing it
    • Alex Kucherenko - for his Wizard article which inspired me to add the design-time guides to the steps

History

September 19, 2002

  • Initial posting

September 30, 2002

  • Added ValidateStep event, suggested and implemented by Chris Dufour. Thanks Chris! :-)

October 13, 2002

  • Fixed a bug where the wizard wouldn't close if it wasn't shown modally. (Bug found by Steaven Woyan; fix suggested by Ryan LaNeve)

May 24, 2003 - Version 1.1 - Happy 23rd Anniversary Mom & Dad!

  • Added the WizardLayout property as suggested by Ken Ostrin
  • Added the StepDirection enum and modified the ShowStep event to use it, also suggested by Ken Ostrin
  • Added properties to the BaseStep to override the graphics selected by the Wizard.
  • Added two new BaseSteps, one for Exterior pages (first and last steps) and one for Interior pages (all the ones in the middle)
  • Integrated fixes from
    • Chris Storha - ValidateStep event wasn't fired if the Finish button was clicked
    • Urs Enzler - The AskIfNotFinish member of the AllowClose enum is no longer needed and is now marked obsolete
    • Sorry to anyone if I added your fix/bug report but forgot to mention you.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here