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

Wizard Form Implementation

, 24 Jun 2010 CPOL
Rate this:
Please Sign up or sign in to vote.
Create a dynamically resizable wizard form

Introduction

In my current job, I'll soon be working on a task where we're going to convert hundreds forms in an Oracle Forms application to a WinForms .Net application. There are a couple of "wizards" in the existing app, but when I went looking in the templates for a Wizard form, imagine my dismay when I didn't find one. Despite my best efforts, I also couldn't find anything that was a) lightweight, b) contained "enough" features, or c) worked all the time. There are a couple of articles here on CP, but none of them felt right for me. Since I'm still learning .Net, I figured this would be a good opportunity to create my own wizard code. Along the way, I learned a few new niggles about WinForms.

Code Formatting Disclaimer: In an attempt to keep the width of the <pre> blocks to something reasonable, I changed the formatting you see in this article and in some cases, removed several "this." qualifiers. The actual code in the source files will look different.

Usage Disclaimer: This code is designed to be used within the context of a non-resizable stand-alone Form environment.

The Concept

Essentially, this code doesn't establish any innovative methods or new ideas where your garden variety wizard form is concerned, there's no clever designer integration, and you still have to actually write a bit of code to "make it go". Here's a feature list:

  • A graphic panel that allows you to provide some eye-candy via a (optional) gradient background, and support for an (optional) image (nothing new here)
  • Specify text for a title and/or subtitle, as well as the ability to configure the font and color of the text (nothing new here)
  • Support for multiple possible paths through the wizard (nothing new here)
  • Efficient tracking of visited pages (nothing new here)
  • Re-sizes the wizard form to accommodate the largest wizard page

Implementation is about as simple as I could make it, but there's at least one thing I did that I'd wished I could have done a different way. I'll point it out in the ensuing discussions regarding the base classes.

Techniques Use

The following techniques were utilized in order to implement this code:

  • Inheritance (deriving Form and UserControl classes)
  • Polymorphism (multiple wizard pages deriving from the same base class)
  • Encapsulation (utilizing public, protected, and private data members and methods)
  • Custom events
  • In-designer detection
  • Custom drawing and image display
  • Programmatic form resizing

The WizardFormLib Assembly

Keeping things compartmentalized is always a good idea, so I put all of the base code into this assembly. It's comprised of a few support classes and the two primary classes, WizardFormBase, and WizardPage

The Page Chain

One of the aspects of a wizard is the desire to maintain some sort of list of pages that have been visited. This becomes even more important if the wizard presents multiple data-driven paths from the start page to the stop page. It's pretty obvious that you have to maintain a list of the pages, but it would be highly inefficient to maintain a list of ALL of the pages in the wizard when you 're not going to necessarily visit all of them. So, the WizardPageChain class is what I came up with to resolve the issues.

This is a fairly simple class, and simply manages the list of pages as they are visited. When a page is visited (when the user starts the wizard or clicks the Next button), it is added to the end of the list, and it is considered to be the "current page". If the user clicks the Back button, the page is removed from the end of the list, and the new last page in the list is considered the "current page". It's really nothing more than a fancy queue. The code is as follows (comments were omitted in the interest of brevity), and can be found in the file WizardFormLib.WizardPageChain.cs:

public class WizardPageChain
{
    private List<object> m_pageChain = new List<object>();
    private WizardFormBase m_parent = null;

    
//--------------------------------------------------------------------------------
    public int Count
    {
        get { return m_pageChain.Count; }
    }

    //--------------------------------------------------------------------------------
    public WizardPage CurrentPage
    {
        get 
        { 
            if (this.Count > 0)
            {
                return (WizardPage)this.m_pageChain[this.Count-1]; 
            }
            else
            {
                throw new Exception("No pages in page chain list.");
            }
        }
    }

    //--------------------------------------------------------------------------------
    public WizardPageChain(WizardFormBase parent)
    {
        m_parent = parent;
        this.m_pageChain.Clear();
    }

    //--------------------------------------------------------------------------------
    public WizardPage GoBack()
    {
        if (this.Count > 1)
        {
            this.CurrentPage.Visible = false;
            this.m_pageChain.RemoveAt(this.Count - 1);
        }
        else
        {
            throw new Exception("No pages in page chain list.");
        }
        WizardPage currentPage = this.CurrentPage;
        currentPage.Visible = true;
        return currentPage;
    }

	//--------------------------------------------------------------------------------
    public WizardPage GoNext(WizardPage nextPage)
    {
        m_pageChain.Add(nextPage);
        WizardPage currentPage = this.CurrentPage;
        if (this.Count > 1)
        {
            ((WizardPage)(m_pageChain[this.Count-2])).Visible = false;
        }
        currentPage.Visible = true;
        return currentPage;
    }

    //--------------------------------------------------------------------------------
    public WizardPage SaveData()
    {
        WizardPage invalidPage = null;
        foreach (WizardPage page in m_pageChain)
        {
            if (!page.SaveData())
            {
                invalidPage = page;
                break;
            }
        }
        return invalidPage;
        }
    }

Events, Handlers, and Arguments

To facilitate page/form interaction, I implemented a few custom events. These elements aren't at all remarkable, and are only mentioned here in the interest of completeness. Their names should be reasonably descriptive of their reason for being.

public delegate void WizardPageActivateHandler(object sender, WizardPageActivateArgs e);
public delegate void WizardPageChangeHandler(object sender, WizardPageChangeArgs e);
public delegate void WizardPageCreatedHandler(object sender, WizardPageCreatedArgs e);

public class WizardPageActivateArgs : EventArgs
{
    private WizardPage     m_activePage = null;
    private WizardStepType m_stepType   = WizardStepType.None;

    public WizardPage ActivatedPage
    {
        get { return m_activePage; }
    }
    public WizardStepType StepType
    {
        get { return m_stepType; }
    }

    public WizardPageActivateArgs(WizardPage page, WizardStepType step)
    {
        m_activePage = page;
        m_stepType   = step;
    }
}

public class WizardPageChangeArgs : EventArgs
{
    private WizardPage     m_activePage = null;
    private WizardStepType m_stepType   = WizardStepType.None;

    public WizardStepType StepType
    {
        get { return m_stepType; }
    }
    public WizardPage ActivatedPage
    {
        get { return m_activePage; }
    }

    public WizardPageChangeArgs(WizardPage page, WizardStepType step)
    {
        m_activePage = page;
        m_stepType   = step;
    }
}

public class WizardPageCreatedArgs : EventArgs
{
    private Size m_size;

    public Size Size
    {
        get { return m_size; }
    }

    public WizardPageCreatedArgs(Size size)
    {
        m_size = size;
    }
}

WizardFormBase - The Base Form Class

WizardFormDesigner.png

The form template (seen above) is just wide enough to contain the four buttons on the form. The reason is that the wizard pages will ultimately define the size of the form. As you can see, there are two Panel containers, two separators, and a pair Label controls, as well as the four required wizard buttons. The top panel is docked at the top of the form, and the top separator line is docked under the top panel. The pagePanel container and the lower separator line are anchored to the right, and bottom edges of the form, as are the buttons. The form itself is not re-sizable by the user, but since our form changes size so that it can conform to the largest axis of all of the combined pages, we need to set these anchors. Before we talk about the code itself, lets talk about the top panel container.

The top panel is where we put our eye candy. You have the option of using a solid background, or a gradient, along with an optional image that can be displayed in one of three positions - the left side, the right side, or the center. The position of the image dictates which edge the gradient starts on. Assuming white as the background color, and dark slate blue as the gradient color, here's the way the gradient will be painted.

GradientExample.png

Notice also that the title/subtitle text automatically positions itself depending on the position of the bitmap. In the event that you use a centered image, the title/subtitle text components aren't painted at all. Finally, the image itself is re-size to fit the height of the panel. In the event that the re-sized bitmap is too long to fit within the width of the form, it will simply be clipped at the edges of the form.

The actual code that comprises the WizardFormBase class is mostly comprised of properties, data members, and methods to show/hide and enable/disable the buttons. The largest single chunk of code deals exclusively with the painting of the top graphic panel and the title/subtitle text for a given wizard page. Those are the two methods we'll talk about first.

To draw the panel itself, we intercept the Paint event. The first thing we do is gather our components around us so they're within easy reach:

private void graphicPanelTop_Paint(object sender, PaintEventArgs e)
{
    // define all of our structures
    // to ease typing, let's graph the graphics object from the arguments
    Graphics  g         = e.Graphics;

    // our graphic panel image - set to null because we don't know yet if we even 
    // need it.
    Bitmap    image     = null;

    // The rectangle of the image - initialize to "nothing" because we don't have 
    // the image yet
    Rectangle imageRect = new Rectangle(0, 0, 0, 0);

    // the rectangle of the container panel itself
    Rectangle panelRect	= new Rectangle(0, 0, 
                                        this.graphicPanelTop.Width, 
                                        this.graphicPanelTop.Height);

    // the rectange to be used for the gradient - by default, it's the same as 
    // the panel
    Rectangle gradientRect = new Rectangle(0, 0, 
                                           panelRect.Width, 
                                           panelRect.Height);

    // the brush used to paint the gradient - set to null because we don't know 
    // yet if we need it
    Brush     gradientBrush     = null;

    // the gradient direction 0 = left-to-right, 180=right-to-left
    int       gradientDirection = 0;

    // shorter way to know if we need to paint the gradient if the two colors 
    // don't match, this variable will be true.
    bool      needGradient      = (this.GraphicPanelGradientColor != 
                                   this.GraphicPanelBackgroundColor);

There is support in the enum for a left or right-positioned graphic panel, but I didn't really need it, so that's where support for that positioning ends. For the eternal tinkerers out there, it would require a completely new/additional method just for painting such a panel, so have at it if you're so inclined. All of this means that I need to put in a sanity check to make sure the programmer didn't use any of the side-oriented positioning for this version of the library. The breadth of the check includes translating the selected position to its appropriate top-oriented equivalent.

    // Sanity check for the image position - since this panel is at the top of 
    // the form, we automatically adjust the setting to it's closest equivalent 
    // value.
    switch (this.GraphicPanelImagePosition)
    {
        case WizardImagePosition.Top :
            this.GraphicPanelImagePosition = WizardImagePosition.Right;
            break;

        case WizardImagePosition.Bottom :
            this.GraphicPanelImagePosition = WizardImagePosition.Center;
            break;

        case WizardImagePosition.Middle :
            this.GraphicPanelImagePosition = WizardImagePosition.Left;
            break;
    }

Next we load the image from the resources, and calculating the image position.

    try
    {
        // retrieve the image if necessary, resize it if necessary, and 
        // position it in the panel
        if (this.GraphicPanelImageResource != "")
        {
            // since this code is in a DLL, and since the bitmap is located in the 
            // exe's resources, we need to get the *entry* assembly in orer to load 
            // the appropriate resource stream.
            Assembly assembly = Assembly.GetEntryAssembly();

            // if the GraphicPanelImageResource string is incorrect, an exception 
            // will be thrown at the next line of code (saying the stream is null)
            Stream stream = assembly.GetManifestResourceStream(this.GraphicPanelImageResource);

            // create the bitmap from the stream
            image = new Bitmap(Bitmap.FromStream(stream));

            // establish the image rectangle size
            imageRect.Size = new Size(image.Width, image.Height);

            // if the image isn't at least as tall as the panel, we need to 
            // resize it
            if (imageRect.Size.Height != panelRect.Size.Height)
            {
                // find out how much shorter/taller it is than the panel
                float resizePercent = (float)panelRect.Height / (float)imageRect.Height;

                // and then adjust the width so that the aspect ratio remains intact
                imageRect.Size = new Size((int)((float)imageRect.Width * resizePercent), 
                                          panelRect.Height);
            }

            // Establish the position of the image within the container panel. 
            // Since we earlier performed a sanity check to ensure a valid 
            // position, we can assume that all is well at this point.
            switch (this.GraphicPanelImagePosition)
            {
                case WizardImagePosition.Right :
                    imageRect.Location = new Point(panelRect.Width - imageRect.Width, 0);
                    break;
                case WizardImagePosition.Left :
                    imageRect.Location = new Point(0, 0);
                    break;
                case WizardImagePosition.Center :
                    imageRect.Location = new Point((int)(((float)panelRect.Width - 
                                                  (float)imageRect.Width) * 0.5), 0);
                    break;
            }
        } // if (this.GraphicPanelImageResource != "")

Next, we calculate our gradient rectangle, and its position. One point to notice is that we start out assuming the gradient rectangle consumes the entire graphic panel. Whether it does or not is based on whether or not the programmer has specified that the image has a transparent background. If it doesn't, the gradient rectangle only consumes the space not intended for use by the image.

        // The direction of the gradient is determined by the location of the image. 
        // If the image is in the center, two gradients are painted - one from each 
        // outside edge of the panel.

        // Assume the image is at one side or the other (as opposed to the center).
        bool needOppositeGradient = false;
        if (needGradient)
        {
            switch (this.GraphicPanelImagePosition)
            {
                case WizardImagePosition.Left :
                    if (!m_graphicPanelImageIsTransparent)
                    {
                        gradientRect.Location = new Point(imageRect.Width-1, 0);
                        gradientRect.Size = new Size(gradientRect.Width - imageRect.Width, 
                                                     gradientRect.Height);
                        gradientDirection = 180;
                    }
                    break;

                case WizardImagePosition.Right :
                    if (!m_graphicPanelImageIsTransparent)
                    {
                        gradientRect.Location = new Point(0, 0);
                        gradientRect.Size = new Size(gradientRect.Width - imageRect.Width, 
                                                     gradientRect.Height);
                        gradientDirection = 0;
                    }
                    break;

                case WizardImagePosition.Center :
                    {
                        needOppositeGradient = true;
                        gradientRect.Location = new Point(0, 0);
                        gradientRect.Size = new Size((int)(((float)gradientRect.Width - 
                                                     (float)imageRect.Width) * 0.5), 
                                                     gradientRect.Height);
                        // initially create the brush for the left-right gradient
                        gradientDirection = 0;
                    }
                    break;
            }

            // we can now create our gradient brush
            gradientBrush = new LinearGradientBrush(gradientRect, 
                                                    GraphicPanelGradientColor, 
                                                    GraphicPanelBackgroundColor,
                                                    gradientDirection);
        }

Finally we can start painting. We start with the gradient rectangle, and then lay on the image. This allows us to make a 1-pixel mistake without overwriting the image. (Oh c'mon - we all make mistakes every now and then.) We also need to be prepare to draw the opposite gradient rectangle in the even the image is centered in the panel.

        // clear our panel with the background color
        g.Clear(this.GraphicPanelBackgroundColor);

        // if we're going to paint a gradient, paint it
        if (needGradient && gradientBrush != null)
        {
            g.FillRectangle(gradientBrush, gradientRect);
            if (needOppositeGradient)
            {
                // clean up the brush
                gradientBrush.Dispose();
                // reverse the direction of the gradient
                gradientDirection = (gradientDirection == 180) ? 0 : 180;
                // move the rectangle to the right side of the bitmap
                gradientRect.Location = new Point(gradientRect.Width + 
                                                  imageRect.Width, 0);
                // create a new gradient brush for the right side 
                gradientBrush = new LinearGradientBrush(gradientRect, 
                                                        GraphicPanelGradientColor, 
                                                        GraphicPanelBackgroundColor,
                                                        gradientDirection);
                // paint!
                g.FillRectangle(gradientBrush, gradientRect);
            }
            // and clean up the brush
            gradientBrush.Dispose();
        }

        // if we have an image to display, paint it
        if (image != null)
        {
            g.DrawImage(image, imageRect);
            // and don't forget to clean up our image resource
            image.Dispose();
        }

    }
    catch (Exception ex)
    {
        throw ex;
    }
}

That was a HUGE method. You may be wondering why we didn't paint the title/subtitle in that method. Well, that's because only the wizard pages know when the title/subtitle need to be changed, and when a new page is activated, they call the following method in order to facilitate this.

protected void PaintTitle(string title, string subtitle)
{
    // Set our text, color and initial location - if the image is placed in 
    // the center, we don't draw the title/subtitle.
    if (this.GraphicPanelImagePosition == WizardImagePosition.Center)
    {
        this.labelTitle.Visible = false;
        this.labelSubtitle.Visible = false;
        return;
    }
    else
    {
        this.labelTitle.Visible = true;
        this.labelSubtitle.Visible = true;
    }

    // configure the title label
    this.labelTitle.AutoSize	= true;
    this.labelTitle.Text	= m_pageChain.CurrentPage.Title;
    this.labelTitle.Font	= m_graphicPanelTitleFont;
    this.labelTitle.ForeColor	= m_graphicPanelTitleColor;

    // configure the subtitle label
    this.labelSubtitle.AutoSize	= true;
    this.labelSubtitle.Text	= m_pageChain.CurrentPage.Subtitle;
    this.labelSubtitle.Font	= m_graphicPanelSubtitleFont;
    this.labelSubtitle.ForeColor= m_graphicPanelSubtitleColor;

    // if the image is on the left, we have to also move the title and 
    // subtitle to the other side of the form.
    if (this.GraphicPanelImagePosition == WizardImagePosition.Left)
    {
        this.labelTitle.Location = new Point(this.graphicPanelTop.Width - 10 - 
                                             this.labelTitle.Size.Width, 
                                             this.labelTitle.Location.Y);
        this.labelSubtitle.Location = new Point(this.graphicPanelTop.Width - 10 - 
                                                this.labelSubtitle.Size.Width, 
                                                this.labelSubtitle.Location.Y);
    }

    // we need the panel rect so we can correctly position the text in 
    // the center of the panel
    Rectangle panelRect = new Rectangle(0, 0, 
                                        this.graphicPanelTop.Width, 
                                        this.graphicPanelTop.Height);

    try
    {
        using (Graphics g = Graphics.FromHwndInternal(this.Handle))
        {
            // combine the heights so we can determine first y-position.
            int textHeight = (this.labelTitle.Height + this.labelSubtitle.Height);
            int y = (int)(((float)panelRect.Height - (float)textHeight) * 0.5f);
            // position the title
            this.labelTitle.Location = new Point(this.labelTitle.Location.X, y);
            // calculate the y-position of the subtitle
            y += this.labelTitle.Size.Height; 
            // and then position it
            this.labelSubtitle.Location = new Point(this.labelSubtitle.Location.X, 
                                                    this.labelSubtitle.Location.Y);
            // make them paint
            this.labelTitle.Invalidate();
            this.labelSubtitle.Invalidate();
        }
    }
    catch (Exception ex)
    {
        throw ex;
    }
}

When a wizard page is created, it calls the PageCreated method. This method takes care of adding the page (remember, it's a UserControl) to the pagePanel's child controls list. While we're here, it also sets the hide/show state of all the buttons. This is a form-global setting, but it can be modfieid with the wizard pages if needed. The way I have it figured, when a button is physically hidden from view, it's probably going to be hidden for the life of the form. Finally, the method performs some sanity checks on the page that was created to make sure it's not appearing in the wrong order, and then saves the page as a start page or stop page, if applicable.

public void PageCreated(WizardPage page)
{
    m_pageCount++;
    pagePanel.Controls.Add(page);

    // hide the appropriate buttons (this should be specified wherever you 
    // instantiate your wizard object)
    if (ButtonBackHide)
    {
        page.ButtonStateBack &= ~WizardButtonState.Visible;
    }
    if (ButtonNextHide)
    {
        page.ButtonStateNext &= ~WizardButtonState.Visible;
    }
    if (ButtonCancelHide)
    {
        page.ButtonStateCancel &= ~WizardButtonState.Visible;
    }
    if (ButtonHelpHide)
    {
        page.ButtonStateHelp &= ~WizardButtonState.Visible;
    }

    // I realize some of the exceptions seem redundant, but it helps to better 
    // diagnose a programming error.
    switch (page.WizardPageType)
    {
        case WizardPageType.Start :
            {
                if (m_startPage != null)
                {
                    throw new Exception("A start page has already been specified.");
                }
                if (m_stopPage != null)
                {
                    throw new Exception("A start page cannot be specified after a stop page has been specified.");
                }
                if (this.PageCount > 0)
                {
                    throw new Exception("A start page cannot be specified after other pages have been specified.");
                }
                m_startPage = page;
            }
            break;

        case WizardPageType.Stop :
            {
                if (m_stopPage != null)
                {
                    throw new Exception("A stop page has already been specified.");
                }
                if (m_startPage == null)
                {
                    throw new Exception("A stop page cannot be specified until a start page has been specified.");
                }
                m_stopPage = page;
            }
            break;

        case WizardPageType.Intermediate :
            {
                if (m_startPage == null)
                {
                    throw new Exception("Intermediate pages cannot be specified until a start page has been specified.");
                }
                if (m_stopPage != null)
                {
                    throw new Exception("Intermediate pages cannot be specified after a stop page has been specified.");
                }
            }
            break;
    }
}

Another method that's called during wizard page creation is DiscoverPagePanelSize. This method is responsible for tracking the largest necessary page size so that we can make the form large enough to display each page in its entirety.

public void DiscoverPagePanelSize(Size pageSize)
{
    if (pageSize.Width > m_desiredPagePanelSize.Width)
    {
        m_desiredPagePanelSize.Width = pageSize.Width;
    }
    if (pageSize.Height > m_desiredPagePanelSize.Height)
    {
        m_desiredPagePanelSize.Height = pageSize.Height;
    }
}

Finally, we get to the method that kicks everything off. After you've created your pages and configured the wizard form itself, you simply call StartWizard. This method performs some sanity checks to make sure we a) have wizard pages, b) at least one start page, and c) at least one stop page.

public void StartWizard()
{
    if (m_pageCount == 0)
    {
        throw new Exception("There are no pages in the wizard.");
    }
    if (m_startPage == null)
    {
        throw new Exception("A start page has not been added to the wizard.");
    }
    if (m_stopPage == null)
    {
        throw new Exception("A stop page has not been added to the wizard.");
    }

    this.Width += (m_desiredPagePanelSize.Width  - this.pagePanel.Size.Width);
    this.Height += (m_desiredPagePanelSize.Height - this.pagePanel.Size.Height);

    // seed the chain
    m_pageChain.GoNext(m_startPage);
    UpdateButtonsState(m_startPage);
}

When the wizard is started, or a new page is activated, we have to update the graphic panel, the visibility and enabled state of the buttons on the wizard form, and the text on the Next button. The following method handles those chores. The button state is maintained as flags so that a single variable can be used to maintain the button's state.

public void UpdateWizardForm(WizardPage page)
{
    PaintTitle(page.Title, page.Subtitle);

    // take care of changing the buttons to their appropriate state for the activated page
    this.buttonBack.Visible   = (page.ButtonStateBack & WizardButtonState.Visible) 
                                 == WizardButtonState.Visible;
    this.buttonBack.Enabled   = (page.ButtonStateBack & WizardButtonState.Enabled) 
                                 == WizardButtonState.Enabled;
    this.buttonNext.Visible   = (page.ButtonStateNext & WizardButtonState.Visible) 
                                 == WizardButtonState.Visible;
    this.buttonNext.Enabled   = (page.ButtonStateNext & WizardButtonState.Enabled) 
                                 == WizardButtonState.Enabled;
    this.buttonCancel.Visible = (page.ButtonStateCancel & WizardButtonState.Visible) 
                                 == WizardButtonState.Visible;
    this.buttonCancel.Enabled = (page.ButtonStateCancel & WizardButtonState.Enabled) 
                                 == WizardButtonState.Enabled;
    this.buttonHelp.Visible   = (page.ButtonStateHelp & WizardButtonState.Visible) 
                                 == WizardButtonState.Visible;
    this.buttonHelp.Enabled   = (page.ButtonStateHelp & WizardButtonState.Enabled) 
                                 == WizardButtonState.Enabled;
    // see if we need to change the text of the Next button
    if (page.WizardPageType == WizardPageType.Stop)
    {
        this.buttonNext.Text = "Finish";
    }
    else
    {
        this.buttonNext.Text = "Next >";
    }
}

When you get right down to it, the WizardFormBase class is comprised mostly of drawing code to support the graphic panel container, yet it hides a lot of the mechanics of wizard forms from the programmer. I'm not saying that the class allows you to create a wizard in less than three lines of code, but it's reasonably low-impact enough to help keep your mind on the derived wizard pages themselves. Speaking of the wizard pages, that's where we're going next.

WizardPage - The Base Page Class

First, there are three constructor overloads. The first one is the default constructor, and was retained to support the designer in our derived forms - it's not really intended to be used for real pages. The other two overloads provide support for specifying the parent wizard form, and the page type. Both of the "real" constructor overloads call the Init method. This method is responsible for some minor configuration (docking style, initial visibility, and page type), notifying the parent form of its existence, and adding a message handler for the WizardPageChangedEvent event.

private void Init(WizardFormBase parent, WizardPageType pageType)
{
    InitializeComponent();

    m_parentWizardForm = parent;
    this.Visible       = false;
    this.Dock          = DockStyle.Fill;
    m_pageType         = pageType;
    // if this is the start page, disable the Back button
    if (WizardPageType == WizardPageType.Start)
    {
        ButtonStateBack &= ~WizardButtonState.Enabled;
    }
    m_parentWizardForm.PageCreated(this);
    m_parentWizardForm.WizardPageChangeEvent += new WizardPageChangeHandler(parentForm_WizardPageChange);
}

The remaining methods are fairly minor as far as functionality goes. they're included with their associate comments so we can same time and a little space.

//--------------------------------------------------------------------------------
/// <summary>
/// Adds a "next page" item to the list of possible next pages. The derived 
/// Wizard page can then decide on its own which page is next based on the 
/// values of one/more controls in the derived page.
/// </summary>
/// <param name="nextPage">The page to add as a possible "next" page</param>
public void AddNextPage(WizardPage nextPage)
{
    m_nextPages.Add(nextPage);
}

//--------------------------------------------------------------------------------
/// <summary>
/// Allows the derived Wizard form to raise the WizardPageActivated event.
/// </summary>
/// <param name="e"></param>
protected void Raise_WizardPageActivated(WizardPageActivateArgs e)
{
    WizardPageActivated(this, e);
}

//--------------------------------------------------------------------------------
/// <summary>
/// Base method used to save data for all visited wizard pages. This copy of 
/// the method always returns true.
/// </summary>
/// <returns>True if the data was succesfully saved</returns>
public virtual bool SaveData()
{
    return true;
}

//--------------------------------------------------------------------------------
/// <summary>
/// Get the next page to be shown. This is virtual so that you can override it 
/// in order to provide a programmatically determined "next" page.
/// </summary>
/// <returns>The page that will be displayed next</returns>
public virtual WizardPage GetNextPage()
{
    // sanity check to make sure we have a page to return
    if (m_nextPages.Count == 0)
    {
        throw new Exception("No pages have been specified as a \"next\" page.");
    }
    // return the first page in the list of "next" pages
    return m_nextPages[0];
}

//--------------------------------------------------------------------------------
/// <summary>
/// Allows the base class to handle a page change event. Right now, there's 
/// nothing to do, but you could add some apppropriate functionalty that 
/// suits your application.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void parentForm_WizardPageChange(object sender, WizardPageChangeArgs e)
{
}

//--------------------------------------------------------------------------------
/// <summary>
/// Fired when a page is made visible.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void WizardPage_VisibleChanged(object sender, EventArgs e)
{
    // The designer will crash the IDE if this code is executed *in the designer.  
    // To avoid this pain in the *ass* issue, we have to check to see if the 
    // designer is active before executing the code, and no - there is no built-in 
    // method in the UseControl class to provide this status, so I wrote a small 
    // function that can be called from within this class whenever necessary. The 
    // only problem is that ANY derived control (or form) may need this method.
    if (!WizardUtility.IsDesignTime())
    {
        if (this.Visible)
        {
            WizardPageActivated(this, new WizardPageActivateArgs(this, m_stepType));
        }
    }
}

The Sample Application

The sample application is a simple affair that initially presents a form with a single button. Clicking that button will display the wizard form (the reason we're all here).

The WizardExample Form

Before beginning, make sure you've added the WizardFormLib project to your solution, and compiled the solution (don't forget to add a reference to the assembly in your application project). This will "prime the pump" as it were, and the IDE will be able to help us out a little.

To create a wizard form, you need to add a new item to the app. In the templates dialog, you want to select Inherited Form, as shown below:

AddNewItemForm.png

After you click okay, you'll be prompted to select a base class. The WizardFormBase should be one of your selections. Select it, and click OK. At this point, the IDE will show you the new form. Just go ahead and close that window, and view the code for the new form class. Go ahead and add using WizardFormLib;, and change the base class to WizardFormBase.

Now, create some WizardPage-derived objects. This process is similar to creatingthe wizard form - add a new item to the application project, and select "Inherited UserControl" from the available templates. Don't worry about populating the pages with controls just yet because you need to add some code that will make the pages work. For each one, you need to add some overloaded constructors and an Init function, like so:

//--------------------------------------------------------------------------------
public WizardPage1(WizardFormBase parent) : base(parent)
{
    InitPage();
}

//--------------------------------------------------------------------------------
public WizardPage1(WizardFormBase parent, WizardPageType pageType) : base(parent, pageType)
{
    InitPage();
}

//--------------------------------------------------------------------------------
public void InitPage()
{
    ButtonStateNext &= ~WizardButtonState.Enabled;
    InitializeComponent();
    base.Size = this.Size;
    this.ParentWizardForm.DiscoverPagePanelSize(this.Size);
    this.ParentWizardForm.EnableNextButton(false);
}

In our eaxmple app, the first page allows the user to take a different path through the wizard depending on which radio button is clicked. If you need the same functionality, you need to override the following function in your derived class:

public override WizardPage GetNextPage()
{
    // some volutary sanity checking
    if (m_nextPages.Count != 2)
    {
        throw new Exception("Page 1 expects two \"next\" pages to be specified.");
    }
    // make a choice
    if (this.radioButton1.Checked)
    {
        return m_nextPages[0];
    }
    else
    {
        return m_nextPages[1];
    }
}

After you've created all of your wizard pages, go ahead and populate the pages with controls. (The pages in the sample app are understandably useless, having the primary purpose of simply providing pages that are of different size.) After you've populated your pages, return to your wizard form code so you can instantiate the pages and start the wizard. Go ahead and add an event handler for the Load event, and make it look something like this:

private void WizardExample_Load(object sender, EventArgs e)
{
    // configure the wizard form itself
    this.GraphicPanelImagePosition = WizardImagePosition.Left;
    this.GraphicPanelImageResource = "WizardDemo.udplogo.png";
    this.GraphicPanelGradientColor = Color.DarkSlateBlue;

    // add handlers for the buttons
    this.buttonBack.Click	+= new System.EventHandler(this.buttonBack_Click);
    this.buttonNext.Click	+= new System.EventHandler(this.buttonNext_Click);
    this.buttonCancel.Click	+= new System.EventHandler(this.buttonCancel_Click);
    this.buttonHelp.Click	+= new System.EventHandler(this.buttonHelp_Click);

    // create the wizard pages we need
    page1	= new WizardPage1(this, WizardPageType.Start);
    page2a	= new WizardPage2a(this);
    page2b	= new WizardPage2b(this);
    page3	= new WizardPage3(this);
    page4	= new WizardPage4(this, WizardPageType.Stop);

    // add a handler that lets us know when a page has been activated (notice that
    // in our sample app, all of the handlers point to the same function - you may 
    // need different functionality)
    page1.WizardPageActivated	+= new WizardPageActivateHandler(WizardPageActivated);
    page2a.WizardPageActivated	+= new WizardPageActivateHandler(WizardPageActivated);
    page2b.WizardPageActivated	+= new WizardPageActivateHandler(WizardPageActivated);
    page3.WizardPageActivated	+= new WizardPageActivateHandler(WizardPageActivated);
    page4.WizardPageActivated	+= new WizardPageActivateHandler(WizardPageActivated);

    // make sure all of the necessary pages have a "next" page so they know where 
    // to steer the user when he clicks the Next button
    page1.AddNextPage(page2a);
    page1.AddNextPage(page2b);
    page2a.AddNextPage(page3);
    page2b.AddNextPage(page3);
    page3.AddNextPage(page4);

    // start the wizard
    StartWizard();
}

The last thing you have to do is to make the event handlers do something. The sample application doesn't actually have anything to do when WizardPageActivated event is raised (thanks Steven Nichiolas!), but I established a handler for future needs.

//--------------------------------------------------------------------------------
/// <summary>
/// Fired when a wizard page is activated (made visible)
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void WizardPageActivated(object sender, WizardPageActivateArgs e)
{
}

//--------------------------------------------------------------------------------
/// <summary>
/// Fired when the back button is clicked
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void buttonBack_Click(object sender, EventArgs e)
{
    // tell the page chain to go to the previous page
    WizardPage currentPage = m_pageChain.GoBack();
    // raise the page change event (this currently does nothing but lets the 
    // base class know when the active page has changed
    Raise_WizardPageChangeEvent(new WizardPageChangeArgs(currentPage, WizardStepType.Previous));
}

//--------------------------------------------------------------------------------
/// <summary>
/// Fired when the Next button is clicked
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void buttonNext_Click(object sender, EventArgs e)
{
    // if the current page (before changing) is the last page in the wizard, 
    // take steps to close the wizard
    if (m_pageChain.CurrentPage.WizardPageType == WizardPageType.Stop)
    {
        // call the central SaveData method (which calls the SaveData 
        // method in each page in the chain
        if (m_pageChain.SaveData() == null)
        {
            // and if everything is okay, close the wizard form
            this.Close();
        }
    }
    // otherwise, move to the next page in the chain, and let the base class know
    else
    {
        WizardPage currentPage = m_pageChain.GoNext(m_pageChain.CurrentPage.GetNextPage());
        Raise_WizardPageChangeEvent(new WizardPageChangeArgs(currentPage, WizardStepType.Next));
    }
}

//--------------------------------------------------------------------------------
/// <summary>
/// Fired when the user clicks the Cancel button
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void buttonCancel_Click(object sender, EventArgs e)
{
    this.Close();
}

//--------------------------------------------------------------------------------
/// <summary>
/// Fired when the user clicks the Help button
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void buttonHelp_Click(object sender, EventArgs e)
{
    MessageBox.Show("Not implemented yet.");
}

Niggles and Minutia

In the process of writing this code, I learned some valuable information about WinForms programming.

Designer Issues

When you are deriving from a Form or UserControl class, you have to be very careful about the code you put in your base classes. The reason is that it's very easy to freak the designer out. In my case, I added an event handler for the VisibleChanging event.

Right after I added that event handler, attempting to load one of the derived wizard pages in the designer resulted in a complete IDE crash. It has something to do with the this pointer not being set to the instance of an object. When I ran the code as an application, it was fine, but trying to load it in the designer in the IDE resulted in tragedy.

The solution is to determine whehther or not the code is running in the IDE, and if not, executing the offending code. I found several methods for accomplishing this, but the only thing that worked was the following code. I created a utility class so I could use the code in the future (if necessary).

using System.Diagnostics;

//--------------------------------------------------------------------------------
public static bool IsDesignTime()
{
    // finally - one that worked as desired. I see a potential problem, though, 
    // if Micrsoft decides to change the IDEs ProcessName property. I can verify 
    // that this will at least work in VS2005 and VS2008.
    return (Process.GetCurrentProcess().ProcessName.ToLower() == "devenv");
}

Inheritance Not

In order to make use of fairly generic functions without having to worry about types, I pass WizardPage objects around - a lot. It simply makes life easier. Generally speaking, this makes life easier on you, but in this case, the effect was exactly the opposite. In my case, everything was fine until I decided I wanted the form to automatically resize itself based on the largest combine dimensions of all of the wizard pages.

When you inherit from a Form or UserControl, and you refuse to use the derived class type as a reference, I've found that properties in the derived class do NOT overwrite the same properties in the base class. You actually have to transfer the desired properties to the base class so that things happen the way you expect (or, at least the way you'd expect in a C++ application). Of course, this discovery was the direct of result of my apparently unnatural desire to pass around the base class object. I discovered this while trying to dynamically resize the form, and if I was using the actual class type, this wouldn't have been a problem.

The Top Panel Bitmap (Section added on 12/15/2008)

The image used in the top graphic panel must be added as a resource, and must be configured as an EMBEDDED resource. If you don't do this, you'll get an exception and the top panel will contain nothing but a white background with a big red X painted through it.

Common Gotcha's (Section added on 12/15/2008)

If you fail to specify the correct assembly name when you initialize your wizard form, the library will not be able to locate your image resource.

private void WizardExample_Load(object sender, EventArgs e)
{
    // configure the wizard form itself
    this.GraphicPanelImagePosition = WizardImagePosition.Left;

    /// THIS LINE MUST SPECIFY THE CORRECT ASSEMBLY
    this.GraphicPanelImageResource = "WizardDemo.udplogo.png";

    this.GraphicPanelGradientColor = Color.DarkSlateBlue;

Addendum (01/20/2009) - WizardFormStartedEvent Event

Some time after posting this article, I decided it might be nice if you could optionally center a given group of controls on the wizard page. I figured that the best way to apporoach this was to create a container control on the page, place your controls inside that container, and then simply center the container. Of course, "simply" never happens when you're on a schedule. Here's how I approached it.

Knowing that the form is automatically made large enough to contain the largest page, I knew that I had to wait until after all of the pages had been added, and then fire an event indicating that the form was ready to go. Fortunately, we already have the StartWizard() method, which is called immediately after addng the pages. All I had to do was create a suitable (empty) EventArgs class, and an event.

In EventArgs.cs:

public delegate void WizardFormStartedHandler(object sender, WizardFormStartedArgs e);

public class WizardFormStartedArgs : EventArgs
{
    public WizardFormStartedArgs()
    {
    }
}

In WizardFormBase.cs

namespace WizardFormLib
{
    public partial class WizardFormBase : Form
    {
        public event WizardFormStartedHandler WizardFormStartedEvent;

        public void Raise_WizardFormStartedEvent(WizardFormStartedArgs e)
        {
            WizardFormStartedEvent(this, e);
        }

        public void StartWizard()
        {
            //... code

            // broadcast the "wizard started" event
            Raise_WizardFormStartedEvent(new WizardFormStartedArgs());
        }

    }
}

Finally, in any wizard page that you wish to handle the event in, simply add a handler for it. I added a WizardPage5 object to the demo, and put a handler in it:

public partial class WizardPage5 : WizardFormLib.WizardPage
{

    public void InitPage()
    {
        InitializeComponent();
        base.Size = this.Size;
        this.ParentWizardForm.DiscoverPagePanelSize(this.Size);

        // add a handler to let us know when the wizard form has been "started"
        this.ParentWizardForm.WizardFormStartedEvent += 
              new WizardFormStartedHandler(ParentWizardForm_WizardFormStartedEvent);
    }

    void ParentWizardForm_WizardFormStartedEvent(object sender, WizardFormStartedArgs e)
    {
        // center the groupbox container in the page. This should always work 
        // because the form is large enough to accomodate this wizard page.

        // get the size of the page panel
        Size parentPanel = this.ParentWizardForm.PagePanelSize;

        // calculate our x/y centers
        int x = (int)((parentPanel.Width - this.groupBox1.Width) * 0.5);
        int y = (int)((parentPanel.Height - this.groupBox1.Height) * 0.5);

        // move the container to its new location
        this.groupBox1.Location = new Point(x, y);
    }

}

When the wizard page was designed it looked like this:

page5_designer.png

When the page is displayed, it looks like this:

centeredpage.png

If you're going to center controls on a wizard page (like I've done above), they must be enclosed by some kind of container control (a Panel, GroupBox, etc), and you must remember to set the Dock property to None for that container.

As a side note, some of you might be wondering how I can be sure that the centering code will always work. Don't forget - the form is resized to accomodate the largest wizard page that you add to it. Therefore, the centering code I've demonstrated above will always work.

In Closing

Like I said, this may not be the most innovative piece of work, but the point is that it *does* work. If you spot something that looks sideways, let me know and if I deem it within the context of the article, I'll update it at my earliest convenience.

Cautionary Note: It is virtually impossible to write an article for CodeProject when Top Gear is on television. Smile | :)

History

  • 06/24/2010: Fixed a couple of phraseology issues.
  • 02/17/2009: Added a "Start" button that allows the user to go all the way back to the first wizard page with one mouse click, fixed a bug that prevented the Help button from being displayed, and streamlined some of the code.
  • 01/21/2009: Added functionality to allow the programmer to center a container control within the wizard page. See article addendum for more info.
  • 01/17/2009: Implemented bug fix described by Liesbet. Many thanks!
  • 12/18/2008: Implemented recommendation by Steven Nicholas regarding setting the state of the wizard form buttons (I did it a little different than he described, but the general idea is the same). Thanks Steven!.
  • 12/15/2008: Fixed a couple of spelling errors and descriptive text errors.
  • 12/14/2008: Original article posted.

License

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

Share

About the Author

John Simmons / outlaw programmer
Software Developer (Senior)
United States United States
I've been paid as a programmer since 1982 with experience in Pascal, and C++ (both self-taught), and began writing Windows programs in 1991 using Visual C++ and MFC. In the 2nd half of 2007, I started writing C# Windows Forms and ASP.Net applications, and have since done WPF, Silverlight, WCF, web services, and Windows services.
 
My weakest point is that my moments of clarity are too brief to hold a meaningful conversation that requires more than 30 seconds to complete. Thankfully, grunts of agreement are all that is required to conduct most discussions without committing to any particular belief system.

Comments and Discussions

 
GeneralThank you very much Pinmembercdm27112-Oct-09 0:39 
GeneralRe: Thank you very much PinmvpJohn Simmons / outlaw programmer2-Oct-09 2:22 
QuestionThanks your article!!..AND I have a question...Please Answer!..;) PinmemberJoininline28-Sep-09 22:59 
AnswerRe: Thanks your article!!..AND I have a question...Please Answer!..;) PinmvpJohn Simmons / outlaw programmer1-Oct-09 0:28 
GeneralGreat Job on the article PinmemberHanksComputer21-Mar-09 17:32 
GeneralRe: Great Job on the article PinmvpJohn Simmons / outlaw programmer22-Mar-09 4:18 
GeneralChanges PinmvpJohn Simmons / outlaw programmer17-Feb-09 5:53 
I submitted a new zip file that contain the changes described in the 2/17/2009 update. I didn't bother changing the article text since it was just adding another button and deleting some code that kept the help button from displaying.
 

"Why don't you tie a kerosene-soaked rag around your ankles so the ants won't climb up and eat your candy ass..." - Dale Earnhardt, 1997
-----
"...the staggering layers of obscenity in your statement make it a work of art on so many levels." - Jason Jystad, 10/26/2001

GeneralNice work, John PinmemberMike Ellison21-Jan-09 8:03 
GeneralIsDesignTime = Component.DesignMode PinmemberCharl Pohlmann21-Jan-09 4:59 
GeneralRe: IsDesignTime = Component.DesignMode PinmvpJohn Simmons / outlaw programmer21-Jan-09 5:18 
GeneralThat's weird [modified] PinmvpJohn Simmons / outlaw programmer21-Jan-09 1:10 
GeneralSmall code correction PinmemberLiesbet14-Jan-09 10:21 
GeneralRe: Small code correction PinmvpJohn Simmons / outlaw programmer15-Jan-09 0:13 
GeneralRe: Small code correction PinmvpJohn Simmons / outlaw programmer21-Jan-09 1:08 
GeneralFxCop Usage PinmvpJohn Simmons / outlaw programmer19-Dec-08 7:45 
GeneralLittle problem in PageChain PinmemberGerhardKreuzer18-Dec-08 13:34 
GeneralRe: Little problem in PageChain PinmvpJohn Simmons / outlaw programmer19-Dec-08 0:23 
GeneralExtended... PinmemberSteven Nicholas17-Dec-08 9:56 
GeneralRe: Extended... PinmvpJohn Simmons / outlaw programmer17-Dec-08 11:40 
GeneralRe: Extended... PinmemberSteven Nicholas17-Dec-08 19:41 
GeneralRe: Extended... PinmvpJohn Simmons / outlaw programmer18-Dec-08 5:18 
GeneralRe: Extended... PinmemberSteven Nicholas22-Dec-08 6:38 
QuestionGood work but ... PinmemberNacereddine17-Dec-08 4:23 
AnswerRe: Good work but ... PinmvpJohn Simmons / outlaw programmer17-Dec-08 4:50 
GeneralRe: Good work but ... Pinmemberjohannesnestler17-Feb-09 5:53 
GeneralThank you. PinmemberJamal Alqabandi15-Dec-08 19:36 
GeneralNice work, John. PinmemberRob Graham15-Dec-08 10:04 
GeneralRe: Nice work, John. PinmemberRasqual Twilight15-Dec-08 13:43 
GeneralWell Done... PinmemberGary Noble15-Dec-08 4:16 
GeneralMessage Automatically Removed Pinmemberpaulb14-Dec-08 19:57 
GeneralRe: My vote of 1 [modified] PinmvpJohn Simmons / outlaw programmer15-Dec-08 0:27 
GeneralRe: My vote of 1 PinmemberWes Aday17-Dec-08 5:29 
GeneralRe: My vote of 1 PinmemberPaul Conrad15-Dec-08 11:35 
GeneralRe: My vote of 1 PinmvpJohn Simmons / outlaw programmer15-Dec-08 12:30 
GeneralRe: My vote of 1 PinmvpJohn Simmons / outlaw programmer16-Dec-08 6:03 
GeneralNice PinmemberMark Nischalke14-Dec-08 16:55 
GeneralRe: Nice PinmvpJohn Simmons / outlaw programmer15-Dec-08 1:39 
GeneralAwesome! PinmemberPaul Conrad14-Dec-08 16:10 

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

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web03 | 2.8.141223.1 | Last Updated 24 Jun 2010
Article Copyright 2008 by John Simmons / outlaw programmer
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid