Introduction

I need to add some wizard capability to an application that I'm developing. One of the things I noted about the requirements is that there are going to be potentially many wizards and that there will be pages in common between each wizard. Another requirement is to be able to chain each wizard "page" to create a complete wizard and then process all the user selections at the end.

I decided to put together a prototype wizard that supported plug-ins for each page. I also wanted to achieve maximum flexibility with the least amount of work, so that I could take the prototype and then tailor it to my specific application requirements (heavily declarative). The current prototype:

Allows you to create any wizard form in the Visual Studio form designer, the only requirement is that it has a container for each page of the wizard. The container can be any container control, such as a Panel.

Allows you to create the wizard pages in the Visual Studio form designer. There are no restrictions--you can use third party controls, etc.

The whole framework should be very easy to modify to support a WPF-based wizard.

The wizard framework manages:

Loading the assemblies that contain the wizard pages.

Instantiating the specified classes.

Managing back, next, cancel, and finish states.

Provides a callback mechanism that the plug-in can use to notify the framework that a state change has happened.

Allows the plug-in to:

implement functionality before the page is exited,

inform the framework that the data on the page is validated,

implement its own help.

The state of each page is preserved, so if the user clicks on "back", his/her previous selections are there (same with next).

What the framework does not do is:

It does not provide data management for the data in each page.

It does not provide a mechanism for modifying the wizard workflow.

It does not adjust for different page sizes (for example, it might want to set itself to the maximum extents of the plug-in).

This is functionality that is potentially application specific. I'll update this article if I come up with some good general solutions for these two omissions.

I had also hoped to put the assemblies into a separate AppDomain. Unfortunately, this is not possible because the controls that a plug-in define are copied over to the application's wizard form. The cross-boundary management of objects (the controls) that are not set up for marshalling ended up beyond the scope of what I wanted to work on. See the section "What Else" for some alternatives.

Setup

To use the wizard, create your own wizard wrapping form. For example, I created this form:

Note: Underneath the "Finish" button is the "Next" button.

So, the setup for a wizard requires:

Specify "using Clifton.Wizard" (rename the namespace if you wish).

Instantiating the container form.

Creating a ContainerInfo class in which you tell the wizard framework about the button instances and page container instance.

The Plug-in Interface

Each plug-in must implement the IPlugin interface. For convenience, a base class (described next) is provided that defines the default behavior and helps manage the control state of the plug-in.

using System;
using System.Collections.Generic;
using System.Windows.Forms;
namespace Clifton.Wizard.Interfaces
{
publicinterface IPlugin
{
///<summary>/// The plugin should return true if the current wizard page data is valid.
///</summary>bool IsValid { get; }
///<summary>/// The plugin should return true if there is help available.
///</summary>bool HasHelp { get; }
///<summary>/// The plugin can implement this method if it needs to do special processing
/// before the wizard proceeds to the next page.
///</summary>void OnNext();
///<summary>/// The plugin can implement this method to display help.
///</summary>void OnHelp();
///<summary>/// The plugin should return the controls that the wizard will place in the
/// container area.
///</summary>///<returns></returns> List<Control> GetControls();
///<summary>/// The plugin needs to implement this event container so that the wizard can
/// be notified of state changes, which the plugin will call itself.
///</summary>event EventHandler UpdateStateEvent;
}
}

The WizardBase Class

As mentioned, the WizardBase class provides some default implementations for the IPlugin interface, as should be self-evident by reading the comments.

using System;
using System.Collections.Generic;
using System.Windows.Forms;
namespace Clifton.Wizard.Interfaces
{
///<summary>/// This abstract class defines common fields, properties, and certain
/// default behavior that each plugin can leverage.
///</summary>publicabstractclass WizardBase : IPlugin
{
///<summary>/// The event that the plugin can use to notify the wizard of a state change.
///</summary>publicevent EventHandler UpdateStateEvent;
///<summary>/// The control list is preserved so that the control's state is maintained
/// as the user navigates backwards and forwards through the wizard.
///</summary>protected List<Control> ctrlList;
protected Form form;
///<summary>/// True if the plugin's data is validated and the user can proceed with
/// the next wizard page. True is the default.
///</summary>publicvirtualbool IsValid
{
get { returntrue; }
}
///<summary>/// True if the plugin is going to provide help. The default is false.
///</summary>publicvirtualbool HasHelp
{
get { returnfalse; }
}
///<summary>/// Constructor.
///</summary>public WizardBase()
{
ctrlList = new List<Control>();
}
///<summary>/// The plugin can override this method if it needs to do
/// something before the wizard proceeds to the next page.
///</summary>publicvirtualvoid OnNext()
{
// Do nothing.
}
///<summary>/// The plugin can override this method if it wants to display
/// some help.
///</summary>publicvirtualvoid OnHelp()
{
// Do nothing.
}
///<summary>/// Returns the controls from the form that the plugin assigned
/// in the class. The plugin can override this method to return
/// a custom control list.
///</summary>///<returns></returns>publicvirtual List<Control> GetControls()
{
// If this is the first time we're calling this method,
// load the controls from the plugin form.
if (ctrlList.Count == 0)
{
// Once loaded, we reuse the same control instances
// which as the advantage of preserving state if the
// user goes back to a previous page (and forward again.)
GetFormControls();
}
// Otherwise, return the control list that we acquired from
// the form.
return ctrlList;
}
///<summary>/// Iterates through the form's top level controls to construct
/// a list of form controls.
///</summary>protectedvirtualvoid GetFormControls()
{
foreach (Control c in form.Controls)
{
ctrlList.Add(c);
}
}
///<summary>/// The plugin can call this method to raise the UpdateStateEvent,
/// which informs the wizard that the button states need to be updated.
///</summary>protectedvoid RaiseUpdateState()
{
if (UpdateStateEvent != null)
{
UpdateStateEvent(this, EventArgs.Empty);
}
}
}
}

What Does a Plug-in Look Like?

Let's take a look at two plug-ins: the welcome page and the ingredients page. Remember that each plug-in is a separate assembly.

The Welcome Page

The welcome page can be found in the Welcome project. It defines a form that looks like this:

It also defines a single WizardPlugin class that relies heavily on the base class default behavior:

Note how the plug-in type name Welcome.WizardPlugin correlates to the namespace and the class name in the code above. Obviously, the assembly name corresponds to the project name. The wizard framework, therefore, displays:

The Ingredients Page

This is a little more interesting because this page requires validation before the "Next" button is enabled.

The last point is the most critical. The form on which the controls were defined no longer has those controls! They now belong to the container panel defined in the container wizard form. So, I am taking advantage of the fact that the button is a child of this container, getting the button's parent, and then setting the check state of all CheckBox controls on the container. Yes, I could also have done something like:

ckIngr1.Checked=true;
ckIngr2.Checked=true;

Whatever.

The WizardPlugin class (you can name the class anything you want, I just used this class name for all the wizard pages in the demo) for this page now raises the state change event that the wizard framework uses to be notified of a state change. It also implements the IsValid property.

The RaiseUpdateState method is implemented by the base class (see above).

The Bake Page

As a final page (I'm not going to walk through the Prepare Dough and Roll Dough pages, you can review the code for those yourself), I'll illustrate the Bake page.

For this wizard page, I've implemented a progress bar (the baking time, relativistic). When the baker clicks on Start, the timer begins, and the Finish button does not become enabled until the timer completes. And lastly, when the baker clicks on "Finish", he is instructed as to what to do with his cookies.

The page's form defines the controls and the Start button's event handler. Note the use of the DoneEvent.

Not exactly rocket science. Since you may not want all these buttons in your wizard, you are allowed to specify null for buttons that aren't desired, and therefore the SetButtonState method checks to make sure this button really exists.

So, I think that's taken up enough space describing what's under the hood. The code is commented so it should be easy to change the framework as needed.

What Else

I'm not particularly enamored with Windows Forms--instead, I would like to use MyXaml and declarative markup to instantiate the wizard pages. I will be extending the wizard to support this, and if anyone is interested in this implementation, post a comment, and I'll most likely put together a whole separate article on declarative-based wizard pages. One thing I'd like to investigate is the advantage that a declarative-based solution gives me in creating an AppDomain, so that when the wizard is finished, I can unload all the plug-ins used in the wizard. It would also be interesting to see how this works with WPF.

Enjoy!

Revision History

5/23/2008: Update zip file to include the missing Clifton.Wizard and Clifton.Wizard.Interfaces projects.

License

This article, along with any associated source code and files, is licensed under The BSD License