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
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 BaseStep s 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
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 LoadStep s 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:
- Introductory step; tells the user what the wizard will accomplish
- Information step; allows the user to enter in any information needed by the wizard
- Confirm step; recaps the information entered, then prompts for the user to click Next to do the work
- Work step; here the work will actually be done; once it is done, the wizard will automatically proceed to step 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 UserControl
s 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
- 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
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
BaseStep
s, 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.