Implementing a Wizard in C#

roger's picture

Oddly, the Windows Forms libraries don't provide any support for writing wizards. Here's one way to do it.

Update: Source code now lives at CodePlex; please post comments, issues, etc., there instead.

This is part one; part two (which includes source code) is here.

The design is based on the one I used for implementing a paged Options dialog. Essentially, each of the pages is implemented as a user control, and they live on a form.

First, we start with a new C# application; this is our test app:

The first thing I tend to do with C# apps these days is move Main somewhere else, the way that Visual Studio 2005 does, so we'll have a class called Program that looks like this:

using System;
using System.Windows.Forms;

namespace TestWizard
{
    public class Program
    {
        [STAThread]
        static void Main()
        {
            WizardSheet wizard = new WizardSheet();
            wizard.Pages.Add(new WelcomePage());
            wizard.Pages.Add(new MiddlePage());
            wizard.Pages.Add(new CompletePage());
            Application.Run(wizard);
        }
    }
}

Now we've got a Program.cs file, we don't need the Form1.cs file, so we'll delete it. Obviously, this won't compile, because we've not implemented the WizardSheet class nor any of the pages.

The Wizard.UI Project

Our next step is to create a library project which will implement the wizard classes. Right click on the solution in Solution Explorer and select Add / New Project:

We want a Windows Control Library, called "Wizard.UI":

By default, Visual Studio puts a custom user control into the project. We don't want it, so we'll delete it.

What we do want is a Windows Form, so we'll create that. It's called WizardSheet:

We need to fix up a few things before it'll compile. First we need to add a reference from our TestWizard project to our Wizard.UI project, and then we need to add a "using" statement.

We also need to implement our 3 page classes. Our next step is to create the WizardPage class that they'll be deriving from. More specifically, I'm going to be creating 3 new classes: the base WizardPage class, and ExternalWizardPage and InternalWizardPage . These two classes will be used for the first/last pages in a wizard and the middle pages respectively. By splitting it up like this, we'll make it easier to implement proper Wizard97-style wizards.

WizardPage is a User Control class:

We don't bother adding any UI elements to it, so we just end up with a boring grey square. The interesting UI will be added in the other two classes. We'll add these as "Inherited User Control" classes:

Again, for the moment, we'll leave these as boring grey squares. At this point, however, we can create our three missing WelcomePage, MiddlePage and CompletePage classes.

We add these to the "TestWizard" project as inherited user controls. WelcomePage and CompletePage inherit from ExternalWizardPage. MiddlePage inherits from InternalWizardPage.

Now we need to implement "Pages", as in "wizard.Pages.Add":

private IList _pages = new ArrayList();

public IList Pages
{
    get { return _pages; }
}

Hurrah. It compiles. Doesn't do anything yet, though.

Adding The Buttons

We need to put some buttons on the wizard. We need 4 buttons: Back, Next, Finish and Cancel. The Next and Finish buttons will be positioned on top of each other. To make them easier to manage, we'll put them in a panel. This panel will be called buttonPanel, and is docked to the bottom of the form. Make it 40 or so pixels high.

The 4 buttons should have their Anchor property set to "Bottom, Right". That should all look like this:

The Etched Line

It's still a bit boring looking. Let's spruce it up a bit by putting a nice etched line across the top of the button panel. We'll need a new User Control. You can try putting the control in the same project, but I've found that the designer support is a bit wonky if you do this. We'll create a new project, called Wizard.Controls. Again, it's a Windows Control Library project.

This time, rather than delete Visual Studio's new UserControl1.cs file, we'll just rename it. Call it EtchedLine.cs, and rename the class it contains to EtchedLine. Build the new project.

Now, if we add a reference from Wizard.UI to Wizard.Controls, we can drop this new class onto our WizardSheet form. Specifically, drop it onto the page panel, set its height to 8 pixels and set it to dock to the top of the panel.

We'll need to implement the drawing behaviour, which is easy:

Color _darkColor = SystemColors.ControlDark;
Color _lightColor = SystemColors.ControlLightLight;

protected override void OnPaint(PaintEventArgs e)
{
    base.OnPaint(e);

    Brush lightBrush = new SolidBrush(_lightColor);
    Brush darkBrush = new SolidBrush(_darkColor);
    Pen lightPen = new Pen(lightBrush, 1);
    Pen darkPen = new Pen(darkBrush, 1);

    e.Graphics.DrawLine(darkPen, 0, 0, this.Width, 0);
    e.Graphics.DrawLine(lightPen, 0, 1, this.Width, 1);
}

To get it to redraw properly when resized, we need to call Refresh:

protected override void OnResize(EventArgs e)
{
    base.OnResize (e);

    Refresh();
}

One small wrinkle is that we don't want our control to appear in the tab order for the dialog, so we need to add a little snippet to our constructor:

public EtchedLine()
{
    // This call is required by the Windows.Forms Form Designer.
    InitializeComponent();

    // Avoid receiving the focus.
    SetStyle(ControlStyles.Selectable, false);
}

We'll also make the colours editable in the designer. The defaults will generally be OK, though:

[Category("Appearance")]
Color DarkColor
{

    get { return _darkColor; }

    set
    {
        _darkColor = value;
        Refresh();
    }
}

[Category("Appearance")]
Color LightColor
{
    get { return _lightColor; }

    set
    {
        _lightColor = value;
        Refresh();
    }
}

Now, if we look at the form in the designer, it's got a natty etched line running across it above the buttons:

Displaying the Pages

Of course, it still doesn't display anything interesting. To display the pages, we'll need another panel. This one is called pagePanel. It lives on the WizardSheet form and fills all of the space not used by the button panel.

We'll use the same design as for the options dialogs linked above. The size of the form will be adjusted so that the page panel can fit the largest page.

Note that this means that your pages might be resized even if the wizard itself has a fixed border, so be sure to set the Anchor property on your controls properly.

This is implemented in the Load event:

private void WizardSheet_Load(object sender, System.EventArgs e)
{
    if (_pages.Count != 0)
    {
        ResizeToFit();
        SetActivePage(0);
    }
    else
        SetWizardButtons(WizardButtons.None);
}

private void ResizeToFit()
{
    Size maxPageSize = new Size(buttonPanel.Width, 0);

    foreach (WizardPage page in _pages)
    {
        if (page.Width > maxPageSize.Width)
            maxPageSize.Width = page.Width;
        if (page.Height > maxPageSize.Height)
            maxPageSize.Height = page.Height;
    }

    foreach (WizardPage page in _pages)
    {
        page.Size = maxPageSize;
    }

    Size extraSize = this.Size;
    extraSize -= pagePanel.Size;

    Size newSize = maxPageSize + extraSize;
    this.Size = newSize;
}

We'll stub out the two missing functions:

public void SetActivePage(int pageIndex)
{
}

public void SetWizardButtons(WizardButtons buttons)
{
}

WizardButtons is an enum. It lives at namespace level:

[Flags]
public enum WizardButtons
{
    None = 0x0000,
    Back = 0x0001,
    Next = 0x0002,
    Finish = 0x0004,
}

That compiles, so we can get on with implementing the SetActivePage method:

public void SetActivePage(int pageIndex)
{
    if (pageIndex < 0 || pageIndex >= _pages.Count)
        throw new ArgumentOutOfRangeException("pageIndex");

    WizardPage page = (WizardPage)_pages[pageIndex];
    SetActivePage(page);
}

private void SetActivePage(WizardPage newPage)
{
    // If this page isn't in the Controls collection, add it.
    // This is what causes the Load event, so we defer
    // it as late as possible.
    if (!pagePanel.Controls.Contains(newPage))
        pagePanel.Controls.Add(newPage);

    // Show this page.
    newPage.Visible = true;

    // Hide all of the other pages.
    foreach (WizardPage page in _pages)
    {
        if (page != newPage)
            page.Visible = false;
    }
}

To check that that's working properly, we need to throw a few controls on the three pages. A label control with the name of the page is fine.

OnSetActive

The next thing to do is to implement the Back, Next, Finish and Cancel buttons. We need to implement SetWizardButtons, and we probably ought to implement an OnSetActive method. Let's start with OnSetActive, which requires some changes (in bold) to the SetActivePage method as well:

private void SetActivePage(WizardPage newPage)
{
    WizardPage oldActivePage = _activePage;

    // If this page isn't in the Controls collection, add it.
    // This is what causes the Load event, so we defer
    // it as late as possible.
    if (!pagePanel.Controls.Contains(newPage))
        pagePanel.Controls.Add(newPage);

    // Show this page.
    newPage.Visible = true;

    _activePage = newPage;

    // Allow the page to cancel this.
    CancelEventArgs e = new CancelEventArgs();
    newPage.OnSetActive(e);

    if (e.Cancel)
    {
        newPage.Visible = false;
        _activePage = oldActivePage;
    }


    // Hide all of the other pages.
    foreach (WizardPage page in _pages)
    {
        if (page != _activePage)
            page.Visible = false;
    }
}

Note that we allow the page to cancel the event.

Now we need to implement the OnSetActive function:

[Category("Wizard")]
public event CancelEventHandler SetActive;

public virtual void OnSetActive(CancelEventArgs e)
{
    if (SetActive != null)
        SetActive(this, e);
}

This way, the derived classes can either override the virtual function, or they can handle the event. Note that the event is put into a "Wizard" category. We'll be adding other events to this category later.

The other thing we do at this point is add a [DefaultEvent("SetActive")] attribute to the class. This means that when you double-click on the page in the designer, it will automatically add a handler for the SetActive event and allow you to edit it.

Now we can test this by handling the event in our three page classes:

private void WelcomePage_SetActive(object sender,
    System.ComponentModel.CancelEventArgs e)
{
    SetWizardButtons(WizardButtons.Next);
}

The other pages are similar: MiddlePage has Back and Next buttons; CompletePage has Back and Finish buttons.

SetWizardButtons

We need to implement the WizardPage.SetWizardButtons helper function:

protected WizardSheet GetWizard()
{
    WizardSheet wizard = (WizardSheet)this.ParentForm;
    return wizard;
}

protected void SetWizardButtons(WizardButtons buttons)
{
    GetWizard().SetWizardButtons(buttons);
}

And we need to implement that WizardSheet.SetWizardButtons function:

internal void SetWizardButtons(WizardButtons buttons)
{
    // The Back button is simple.
    backButton.Enabled = ((buttons & WizardButtons.Back) != 0);

    // The Next button is a bit more complicated.
    // If we've got a Finish button, then it's disabled and hidden.
    if ((buttons & WizardButtons.Finish) != 0)
    {
        finishButton.Visible = true;
        finishButton.Enabled = true;

        nextButton.Visible = false;
        nextButton.Enabled = false;

        this.AcceptButton = finishButton;
    }
    else
    {
        finishButton.Visible = false;
        finishButton.Enabled = false;

        nextButton.Visible = true;
        nextButton.Enabled = ((buttons & WizardButtons.Next) != 0);

        this.AcceptButton = nextButton;
    }
}

Note that this code also sets the AcceptButton value, so that pressing Enter will press the right button.

Implementing The Next Button

When the user presses the Next button, we need to let the page know. By default, we'll skip to the next page. However, the page can choose to go to a different page. In MFC, this is done by having the wizard page return the dialog ID of the page. Since WinForms doesn't have dialog IDs, we'll use the page's name. We'll also dress it up in nice .NET-style events.

It looks like this:

private void nextButton_Click(object sender, System.EventArgs e)
{
    // Figure out which page is next.
    int activeIndex = GetActiveIndex();
    int nextIndex = activeIndex + 1;

    if (nextIndex < 0 || nextIndex >= _pages.Count)
        nextIndex = activeIndex;

    // Fill in the event args.
    WizardPage newPage = (WizardPage)_pages[nextIndex];

    WizardPageEventArgs wnea = new WizardPageEventArgs();
    wnea.NewPage = newPage.Name;
    wnea.Cancel = false;

    // Tell the current page. It's allowed to choose a different page.
    _activePage.OnWizardNext(wnea);

    // Are we cancelling the event?
    if (wnea.Cancel)
        return;

    // Go to the new page.
    SetActivePage(wnea.NewPage);
}

First we figure out which page is next by default. Then we ask the current page to handle the event. It can cancel the event, in which case we'll stay where we were, or it can nominate a different page to go to.

The WizardPageEventArgs.cs file looks like this:

using System;
using System.ComponentModel;

namespace Wizard.UI
{
    public class WizardPageEventArgs : CancelEventArgs
    {
        string _newPage = null;

        public string NewPage
        {
            get { return _newPage; }
            set { _newPage = value; }
        }
    }

    public delegate void WizardPageEventHandler(object sender, WizardPageEventArgs e);
}

We need to implement GetActiveIndex as well:

private int GetActiveIndex()
{
    WizardPage activePage = GetActivePage();

    for (int i = 0; i < _pages.Count; ++i)
    {
        if (activePage == _pages[i])
            return i;
    }

    return -1;
}

private WizardPage GetActivePage()
{
    return _activePage;
}

We need to implement the OnWizardNext function:

[Category("Wizard")]
public event WizardPageEventHandler WizardNext;

public virtual void OnWizardNext(WizardPageEventArgs e)
{
    if (WizardNext != null)
        WizardNext(this, e);
}

And we need to implement the overload of SetActivePage that takes a page name. It looks like this:

private WizardPage FindPage(string pageName)
{
    foreach (WizardPage page in _pages)
    {
        if (page.Name == pageName)
            return page;
    }

    return null;
}

private void SetActivePage(string newPageName)
{
    WizardPage newPage = FindPage(newPageName);

    if (newPage == null)
        throw new Exception(string.Format("Can't find page named {0}", newPageName));

    SetActivePage(newPage);
}

Note that sometimes, when you create a new wizard page, the Name property doesn't take immediately. This appears to be a (minor) bug in the designer. If you make a change to the page, it seems to work OK.

Implementing the Back Button

As you'd probably expect, the code for the Back button is almost identical to the code for the Next button. So much so that we can do a little refactoring and end up with this:

private WizardPageEventArgs PreChangePage(int delta)
{
    // Figure out which page is next.
    int activeIndex = GetActiveIndex();
    int nextIndex = activeIndex + delta;

    if (nextIndex < 0 || nextIndex >= _pages.Count)
        nextIndex = activeIndex;

    // Fill in the event args.
    WizardPage newPage = (WizardPage)_pages[nextIndex];

    WizardPageEventArgs e = new WizardPageEventArgs();
    e.NewPage = newPage.Name;
    e.Cancel = false;

    return e;
}

private void PostChangePage(WizardPageEventArgs e)
{
    if (!e.Cancel)
        SetActivePage(e.NewPage);
}

private void nextButton_Click(object sender, System.EventArgs e)
{
    WizardPageEventArgs wpea = PreChangePage(+1);
    _activePage.OnWizardNext(wpea);
    PostChangePage(wpea);
}

private void backButton_Click(object sender, System.EventArgs e)
{
    WizardPageEventArgs wpea = PreChangePage(-1);
    _activePage.OnWizardBack(wpea);
    PostChangePage(wpea);
}

Don't forget to implement OnWizardBack.

Implementing the Finish Button

Easy:

private void finishButton_Click(object sender, System.EventArgs e)
{
    CancelEventArgs cea = new CancelEventArgs();
    _activePage.OnWizardFinish(cea);
    if (cea.Cancel)
        return;

    this.DialogResult = DialogResult.OK;
    this.Close();
}

The OnWizardFinish method is implemented as you'd expect.

After all that, we end up with a working wizard:

Note that I've turned on visual styles for the application, and I've moved the Back button a bit closer to the Next button.

In the next installment, we'll fix a few things, and make it a lot prettier. Also, there's source attached to that page.

Comments

errors! errors! and lots of confusion

there are some instructions that are hard to understand!!! where to put some of the codes? where is _activePage declared??? it gives a lot of errors!!!

RE: errors! errors! and lots of confusion

Er, download the source code from the second installment of the series?

Great one!!!

Great sample of how to do generic code. one question: I've tried to relize how can i add more functionality to the Next button per page and couldn't find out. thanks.

OnWizardNext

You can attach your code to the WizardNext event handler in your page. Or are you looking for more than that?

Awesome tool!

Roger, you just saved me a lot of hours of work. The design is great, really elegant and easy to follow, with great possibility for re-use. You really nailed this. Thanks so much. Best regards, JT

An error

an error occurred when pressing the Cancel button. the system warn: "Cannot access a disposed object"

"Cannot access a disposed object"

Did it say which object?

I am very sad to say that i

I am very sad to say that i came to know about this post recently i implemented similar scenario in my project with multiple forms.... which has a bunch of problems the main problem is flickering during transistion of the form is there any way to avoid it..?

Avoiding flicker

Yep. Change your code to use this instead.

Broken Url

Hello, At the start of the articule you mention a tabbed dialog. The link defaults back to your home page. Is this work still available as I have found the wizard and the options dialog very informative for study. Thanks. Gavin

Great work

Nice job. really informative and well designed.the follow up did fix a lot of problems too.

Pretty good. You might also

Pretty good. You might also consider a 3rd party solution like this one: http://community.devexpress.com/blogs/ctodx/archive/2008/06/09/gandalf-shows-his-vista.aspx

Fair point

...just that there weren't any C# wizard libraries around 3 years ago, when I wrote this post :)

The wizard throws unhandled exception.

If the code throws exception on clicking on next button I can't handle it in the main method that starts the wizard. If I have:
        [STAThread]
        static void Main()
        {
            try{
            WizardSheet wizard = new WizardSheet();
            wizard.Pages.Add(new WelcomePage());
            wizard.Pages.Add(new MiddlePage());
            wizard.Pages.Add(new CompletePage());
            Application.Run(wizard);
            }catch
           {
               //handle exception
           }
        }
and an exception is thrown on pressing next button the exception is not passed to the catch section. And as a result unhandled exception dialog is shown. It seems that there is some thread problem...

The problem is not in the

The problem is not in the wizard. The problem comes from the Application.Run().

When running the app in VS it catches the exception in the try-catch block to help the developer.

When the app is running out of the VS the try-catch block doesn't handle the exceptions that are thrown by the gui created by calling Application.Run(). To handle these exceptions you should use Application.ThreadException.

Hi, Awesome, thanks so much

Hi, Awesome, thanks so much for this.. It was so helpful!! -DG

Finish button event

Hi First of all I just wanted to thank for great and usefull tool. My question is in what event (where, which cls ) should I use to save all values from the MiddlePage (from the user input) to the textFile for example? Appreciate any help.

Saving information

It depends. Generally speaking, your wizard shouldn't do anything permanent until the user presses Finish. Some wizards have things happen earlier in the sequence, and then the Finish page is just a summary of what you just did.

If you opt to have it all happen at the end, you can put it in the handler for Finish. I think that this event gets sent to each page, giving them all a chance to commit their changes. It's been a while since I wrote the code, so you'll have to double-check. Alternatively, if you're using the WizardContext pattern, you could get it to save the data after the wizard is finished. Check the result from ShowDialog (it's a DialogResult.

this wizard really works

this wizard really works great!Thanks I would like to have a summary in finish page e.g. display input data from previous page. E.g. wizard step 1 asks to enter name in a textbox, then in wizard summary step, id like to display what was inputed in the textbox. How do i pass values from wizard step to another?

Pass a "context" object to all of the pages

See the other comment with the same title as this one.

Moving To and From Forms...which Event?

What do you think the best event of System.Windows.Forms.UserControl would be to piggyback on for "Enable Next Button" (after validiation). Let's say I have a simple textbox on my middle page. I do not want to enabled the Next button until the user types something (anything?) in the box. I have it working wired to the textbox/KeyUp event. HOWEVER. If I place a value in the textbox, "Next" is enabled, I hit Next and go to the next screen..THEN I hit "Back", the Next button is grayed out. I tried the System.Windows.Forms.UserControl.Load event, but that doesn't happen everytime. Aka, the form is loaded once I believe. Another place where this same event would be used is ... I enter some data on Page2 of the wizard, go to Page3 (and Page3 has a read only version of this data), I go back to Page2, change the data, then I go to Page3...I need to re-read the data (when Page3 is shown) to reflect the data (re-entered) from Page2. I LOVE this mini-framework, but have this issue. I think old VB6 had a Form_Activate event. (That's a major head strain to think back that far). Anyway, I wanted to ask the designer of the code what he thought about most correct event would be. Thanks for the source code and original project, btw. Here is my "enable Next" code. private void HandleNextButtonEnabled() { if (txtLastName.Text.Length > 0) //Example text box on a form { SetWizardButtons(WizardButtons.Back | WizardButtons.Next); } else { SetWizardButtons(WizardButtons.Back); } } I call it from the textbox/KeyUp event. And that works. I am just looking for the correct piggyback event when navigating the Next/Back buttons. Thanks!

Keeping data/controls updated

Use the SetActive event. It's fired whenever the page becomes active.

Thanks for the wizard control

I just want to say thanks for sharing your wizard. I customized it some and used it in my ID3AlbumArtFixer at http://dalepreston.com/Blog/2009/07/id3-album-art-fixer.html. Your wizard saved me a lot of work. It was easy to use and to customize; signs of a well designed application.

waw too much confusion

i spent all day long trying to do it but i had nothinn more than errors and ko of ERRORS !!! please can you make it easier ?and more explained !!! sorry for sayinn that but i'm a biginner in c# and i need to implement a wizard in my app !! please make a real Tutorial !! problems i've had : 1 "IError1The type or namespace name 'IList' could not be found (are you missing a using directive or an assembly reference?)" 2 internalWizardPage inherite from what ? 3 where do i put the code : private IList _pages = new ArrayList(); public IList Pages { get { return _pages; } } etc.... thanks a lot

Re: waw too much confusion

Download the source code attached to part 2.

Incorporating with an Add-In

Thanks a lot, this is by far the best example Ive seen even though it is almost 5 years old. Ive used almost all your code save for the fact that Ive integrated it into an Excel Add-Ins project. The wizard runs like a dream when its set as the default project but calling it from my click event from my menu item doesnt seem to trigger the application. I do apologize for my ignorance but I was hoping you might be able to assist me. All Im doing is calling the same block of code that is in the Program class when the user clicks the menu item. Am I missing something here? Perhaps my click event need to to return the class and not be void? Thanks much, Matt

Re: Incorporating with an Add-In

Wild guess here, but you probably want to replace Application.Run(wizard) with wizard.ShowDialog()

Disable a button

Hi Roger, thanks for the code. It's really helpfull. I just can't figure out how to disable the 'next' button if a certain textbox is empty. Can you help me with that? Regards, Wessel.

Excellent, but I have a question for you

Thank you very much for this work of yours. I am going to be implementing this solution in a few places at my work and thus, I've made a few tweaks to it to make the UI more informative. One of those changes is the addition of what I've called a "NavigationBar" to the left, which is part of WizardSheet. What I do is I figure out the number of pages added to the sheet and then take that count to create the same number of labels which are added to the NavigationBar...denoted as "Step X". What I need to do is bold and underline each step of the process, thus the sheet or the page or whatever needs to know which page has the active focus and then modify the respective label in the NavigationBar. All I want to do is Bold and Underline the current step based on whatever page I'm on. Where should I put the actual code though? I'd appreciate any feedback!

Nevermind! I figured it out...

Thanks again. You've saved me HOURS of research!

Almost done

Since finding your code about a week ago, I'm about 70% finished with my project. The problem I have now is being able to save data from page to page. I saw a few comments that you've made on here to that effect, but I'm still having trouble wrapping my head around this.

I have multiple internal pages and the sheet has no way of knowing what controls I have on each page. Thus the quagmire I'm in is I don't know WHERE to put my dictionary object to save, what I am calling "Properties", and I don't know WHEN to call the save function to store this data. You'd mentioned something about the Finish event handler, but in which project would I do that: your base Wizard.UI class or my Wizard driver class?

Once I get this info, my project will jump to 90% complete! Any help is greatly appreciated.

Thanks a lot!

I learned a lot and got 2 middle pages working, thanks to roger and Francis and Ondrej. I never would have figured out how to do this on my own. Now the guys downstairs will have a nice wizard just like downtown, and I feel a lot smarter!

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.