Article 1 in a series about WinForms Wizards: The fastest Wizard in the West.
See Also:
Article 1 in this series: You're looking at it!
Article 2 in this series: A slightly better WinForms wizard, and slightly more work.
Article 3 in this series: Adding Named Panel Navigation to the WizardController
Article 4 in this series: Providing design time support for the Wizard Framework
Article 5 in this series: Adding a design time dialog and creating a VS project sample
I've often looked at the process of creating a wizard in Windows Forms. The need for a wizard type progression between various UI dialogs is always an issue with the primary functionality working as follows.
- The wizard must allow navigation from the first dialog to the last dialog. Shortcutting past optional dialogs is a plus.
- The wizard must allow any custom UI buttons to control the progression of the wizard.
- If the programmer makes a mistake the wizard should probably still finish somehow (no cancel button for instance).
- All data collected should be readily accessible at the end of the wizard.
Well, that isn't too hard to accomplish, and so I've coded up a series of classes that I call the WizardController in order to quickly generate new wizards as needed, generally as custom actions in install processes. Note the WizardController can be used in any existing application with few changes to any existing wizard design.
We'll start by examining how the wizard controls progression through the dialogs. I thought an interface added to any Form class, since the Form is mostly used for dialogs would be the best way to go. What this does is ensure that I can use a single Form for each step in the wizard rather than having common navigation logic and adding/removing controls at run-time. We'll call this interface IWizardDialog and it appears as below.
public interface IWizardDialog {
Control NavigatePrevious { get; }
Control NavigateNext { get; }
Control NavigateFinish { get; }
}
You can see that the interface defines each dialog should return some controls back to the WizardController in order to handle navigation. These could be PictureBox controls, Button controls, or any custom control that gets the job done and registers a click event. That is the key to the IWizardDialog interface, is that it is used to collect these controls so the WizardController can hook various events.
Next the WizardController should be defined. Each controller created is going to store the current dialog offset in the sequence, whether or not the wizard has completed, and an array of dialogs that can be accessed during wizard execution and after the wizard is complete (this is the key to getting at the data, the forms never actually disappear!).
public class WizardController {
private bool complete = false;
private int wizardIndex = -1;
private ArrayList wizardDialogs = new ArrayList();
public WizardController() {
}
}
Forms are added to the controller as dialogs and are inserted into the sequence as added. Forget all that InsertAt functionality, this guy is quick and dirty. The AddDialog method takes a simple Form type, but some extra checks are thrown in to make sure the dialog also supports IWizardDialog. If everything is cool, then the interface is queried for the necessary controls and the events are hooked.
public void AddDialog(Form dialog) {
IWizardDialog wDialog = dialog as IWizardDialog;
if ( wDialog != null ) {
wizardDialogs.Add(wDialog);
if ( wDialog.NavigatePrevious != null ) {
wDialog.NavigatePrevious.Click += new EventHandler(Wizard_NavigatePrevious);
}
if ( wDialog.NavigateNext != null ) {
wDialog.NavigateNext.Click += new EventHandler(Wizard_NavigateNext);
}
if ( wDialog.NavigateFinish != null ) {
wDialog.NavigateFinish.Click += new EventHandler(Wizard_NavigateFinish);
}
} else {
throw new Exception("Wizard dialogs must support IWizardDialog");
}
}
After all of your dialogs are added you get a chance to start the wizard using the StartWizard method. Some basic checks to make sure you have some dialogs and that the wizard hasn't already been started without finishing and off you go. Various flags are set and the first dialog is shown to the user. Hopefully the dialog has either a Next or Finish button set so things actually work. If not the user might not ever find out what the end results of your wizard might be since they'll be stuck looking at the first form.
public void StartWizard() {
if ( wizardDialogs.Count == 0 ) {
throw new Exception("Must add dialogs to the wizard");
}
if ( wizardIndex != -1 && !complete ) {
throw new Exception("Wizard has already been started");
}
complete = false;
wizardIndex = 0;
Form startForm = wizardDialogs[wizardIndex] as Form;
if ( startForm != null ) {
startForm.Show();
}
}
The Wizard_Navigate* series of methods is where all of the action happens. You'll notice that these event handlers are hooked up during the AddDialog process and so when the user actually clicks on an action item in your dialog to proceed to the next step. All of the code in these methods is pretty standard for iterating over a collection of dialogs and displaying them in order. The internal index counter is continuously updated based on the step, special checks are made to make sure the event handler being called is the appropriate one (check the code comments, I think they are pretty good), and finally the Wizard_NavigateFinish method closes down all the open dialogs and sets the complete flag so you can start working with any data you've collected.
public void Wizard_NavigateNext(object sender, EventArgs e) {
wizardIndex++;
if ( wizardIndex == wizardDialogs.Count ) {
Wizard_NavigateFinish(sender, e); // This shouldn't happen if your dialogs are correct
return;
}
Form currentForm = wizardDialogs[wizardIndex-1] as Form;
Form newForm = wizardDialogs[wizardIndex] as Form;
if ( currentForm != null && newForm != null ) {
currentForm.Hide();
newForm.Show();
}
}
public void Wizard_NavigatePrevious(object sender, EventArgs e) {
if ( wizardIndex > 0 ) {
Form currentForm = wizardDialogs[wizardIndex] as Form;
Form newForm = wizardDialogs[wizardIndex-1] as Form;
if ( currentForm != null && newForm != null ) {
currentForm.Hide();
newForm.Show();
wizardIndex--;
}
}
}
public void Wizard_NavigateFinish(object sender, EventArgs e) {
// this could be fired anywhere in case you have an opt out
// early button on any of your forms.
complete = true;
for(int i = 0; i < wizardDialogs.Count; i++) {
Form currentForm = wizardDialogs[i] as Form;
if ( currentForm != null ) {
currentForm.Hide();
}
}
}
I made this wizard so it could be run on a separate thread or at least not monitored by the main application unless it needed to be. While the wizard is running you can poll the Complete property to wait until the user is done. The Dialogs property returns all of your dialogs so you can investigate their controls for values. Note that currently the values could be empty and the wizard might still progress. Rather than include validation as part of the wizard, I think it would be best left to each individual dialog. Since the WizardController hooks directly into the click events for the navigation controls, you'd have to do a little bit of work to make sure the validation logic on the dialog gets run before the event on the controller is fired. That is a bit more complex and doesn't comprise a basic wizard.
public bool Complete {
get {
return complete;
}
}
public ArrayList Dialogs {
get {
return wizardDialogs;
}
}
So to test our little wizard we'll also need some test dialogs. I've coded up a basic dialog that does nothing more than display previous, next, finish buttons to allow the wizard to navigate. I use several instances of this form with different display parameters in order to construct a fully featured wizard that tests all of the navigation features in the controller. Note that the dialog is derived from Form and implements IWizardDialog. This is a requirement to be accepted by the WizardController class.
public class WizardTestDialog : Form, IWizardDialog {
private Button previousButton;
private Button nextButton;
private Button finishButton;
private bool previous = false;
private bool next = false;
private bool finish = false;
private string title;
public WizardTestDialog(string title, bool previous, bool next, bool finish) {
this.previous = previous;
this.next = next;
this.finish = finish;
this.title = title;
InitializeComponent();
}
public Control NavigatePrevious {
get {
return (previous) ? previousButton : null;
}
}
public Control NavigateNext {
get {
return (next) ? nextButton : null;
}
}
public Control NavigateFinish {
get {
return (finish) ? finishButton : null;
}
}
private void InitializeComponent() {
this.Text = title;
this.previousButton = new Button();
this.previousButton.Text = "Previous";
this.previousButton.Enabled = previous;
this.nextButton = new Button();
this.nextButton.Text = "Next";
this.nextButton.Enabled = next;
this.nextButton.Left = this.previousButton.Right;
this.finishButton = new Button();
this.finishButton.Text = "Finish";
this.finishButton.Enabled = finish;
this.finishButton.Left = this.nextButton.Right;
this.Controls.AddRange(new Control[] { previousButton, nextButton, finishButton });
}
}
Finally we have to test the wizard as an application. The following code adds 5 dialogs with varying buttons displayed to allow the user to test all of the wizard navigation and functionality. Notice that Step4 will allow the user to short-circuit out of the controller and end the wizard, while normally Step5 would be the end. This shows Step5 as an optional dialog, a situation that often comes up in wizard design. The last bit of code starts the wizard and does a polling loop waiting for it to complete. There is probably a better way to do the polling in an application where only the wizard is being displayed (as a main form), and I think deriving WizardController from ApplicationContext might be a key. I'll leave that up to the user as an experiment.
public class WizardTester {
[STAThread()]
private static void Main(string[] args) {
WizardController controller = new WizardController();
controller.AddDialog(new WizardTestDialog("Step1", false, true, false));
controller.AddDialog(new WizardTestDialog("Step2", true, true, false));
controller.AddDialog(new WizardTestDialog("Step3", true, true, false));
controller.AddDialog(new WizardTestDialog("Step4", true, true, true));
controller.AddDialog(new WizardTestDialog("Step5", true, true, true));
controller.StartWizard();
while(!controller.Complete) {
Application.DoEvents();
}
}
}
So our wizard didn't turn out all that simple in the long run, but it does show promise as a nice framework for more complex wizards. The next step would be adding in some basic validation logic to each dialog, maybe working on that ApplicationContext code so our application doesn't have to create it's own message pump, and probably providing some cool events. I'll certainly be using this as a base in many of the applications that I write since it demonstrates how easy wizards can be, as well as providing some unique ways to allow truly customizable wizard UI to drive the wizard process.