In this installment (see here for the previous installment), we'll be fixing a few things and making the whole thing prettier.
Actually implementing the cancel button is easy:
private void cancelButton_Click(object sender, System.EventArgs e)
{
this.Close();
}
But that's not all -- we need to check with the active page to see if it wants to close. This is as simple as handling the Closing event:
private void WizardSheet_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
if (!cancelButton.Enabled)
e.Cancel = true;
else if (!finishButton.Enabled)
OnQueryCancel(e);
}
protected virtual void OnQueryCancel(CancelEventArgs e)
{
_activePage.OnQueryCancel(e);
}
We could just call OnQueryCancel from the cancelButton_Click handler, except that this doesn't handle the user clicking on the close box. So we have to handle the Closing event. However, this is fired whenever the form is closing, so we need to be a bit smarter.
This code checks to see if the cancel button is disabled. If it is, we don't need to tell the page -- because the user could only be closing the wizard with the close box. It could be the Finish button, in which case we don't need to tell the page either.
If we decide to tell the page, we call OnQueryCancel, which is implemented pretty much as you'd expect:
[Category("Wizard")]
public event CancelEventHandler QueryCancel;
public virtual void OnQueryCancel(CancelEventArgs e)
{
if (QueryCancel != null)
QueryCancel(this, e);
}
First, making it prettier. We'll implement a banner for the middle page.
In the Wizard.UI project, create a new user control. The class is called WizardBanner. Its BackColor property is set to "Window", and it has two label controls and an etched line on it:

The Anchor and Dock properties for the labels and line are set appropriately.
One thing we need to fix is that the etched line isn't drawn in the correct place. This requires a simple change to the EtchedLine.OnPaint method, and a new property:
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);
if (this.Edge == EtchEdge.Top)
{
e.Graphics.DrawLine(darkPen, 0, 0, this.Width, 0);
e.Graphics.DrawLine(lightPen, 0, 1, this.Width, 1);
}
else if (this.Edge == EtchEdge.Bottom)
{
e.Graphics.DrawLine(darkPen, 0, this.Height - 2,
this.Width, this.Height - 2);
e.Graphics.DrawLine(lightPen, 0, this.Height - 1,
this.Width, this.Height - 1);
}
}
EtchEdge _edge = EtchEdge.Top;
[Category("Appearance")]
public EtchEdge Edge
{
get
{
return _edge;
}
set
{
_edge = value;
Refresh();
}
}
EtchEdge is a public enum at namespace level, with two members: Top and Bottom.
We need to add a couple of properties to the banner class before it's useful. These control the text displayed:
[Category("Appearance")]
public string Title
{
get { return titleLabel.Text; }
set { titleLabel.Text = value; }
}
[Category("Appearance")]
public string Subtitle
{
get { return subtitleLabel.Text; }
set { subtitleLabel.Text = value; }
}
Then we can drop it onto the Internal wizard page, dock it, give it a name and set it to public:

Making it public means that we can change the text in MiddlePage, which is derived from InternalWizardPage:

Next we need to implement the sidebar for the outer pages. This is done similarly.
We add a new user control to the Wizard.UI project, called WizardSidebar. It does nothing particularly interesting. Then we drop it onto ExternalWizardPage, and set its properties appropriately. The background colour of the ExternalWizardPage form is set to "Window". If we drop some controls onto the WelcomePage form and set the sidebar graphic, we'll end up with something that looks like this:

And that's much prettier.
And that's about it. Source code is attached.
| Attachment | Size |
|---|---|
| Wizard-in-CSharp.zip | 50.05 KB |
Comments
awesome... very gr8 idea...
License for Wizard
Re: License for Wizard
Very useful Thanks
really cool
Disabling Cancel button, but still allowing the wizard to close
thanks
Thanks!
Missing sidebar
Firstly, thanks for writing this great little tutorial and releasing the code with such a lax licence. :)
When I compile the sample "TestWizard" app and run it, I can't see the sidebar graphic. Have you seen this problem before? Unfortunately, I don't know enough C# to be able to debug it right now.
Cheers,
-A
PS. I'm using Visual Studio 2005 SP1 (8.0.50727.762) and .NET framework version 2.0.50727
RE: Missing sidebar graphic
It's a Visual Studio 2003 project, so I just downloaded it myself and converted it to a Visual Studio 2005 project.
If I press F5 to start the project, I see the sidebar graphic correctly on both the welcome and finish pages.
What operating system are you running it on? I'm using Windows 2003.
RE: Missing sidebar graphic
Thanks for your quick reply. I'm using Windows XP SP2. I just downloaded the code again and converted the VSDotNet 2003 project to VS2005, and pressed F5 to run the app. The sidebar on both, the welcome and complete pages of the wizard, is blank, missing the expected graphic. Did
Cheers,
-Adam
thanks for the sample demo.
Thumbs Up
cool
Hi, How can switch from one
Adding Pages
It should just work
Make sure that you set the
Re: Adding Pages
SetActivePage(string newPageName)searches for the NAME-Property of the WizardPage. So if you add 2 MiddlePages, which have the same Name, this method can't differ them.That means -> Instead of:
this.Pages.Add(new WelcomePage());this.Pages.Add(new MiddlePage());
this.Pages.Add(new MiddlePage());
this.Pages.Add(new CompletePage());
you can use:
MiddlePage mp = new MiddlePage();
mp.Name = "MidPage";
MiddlePage mp2 = new MiddlePage();
mp2.Name = "MidPage2";
this.Pages.Add(new WelcomePage());
this.Pages.Add(mp);
this.Pages.Add(mp2);
this.Pages.Add(new CompletePage());
Now this should work :D
Thanks for that, it works!
You need to copy MiddlePage.cs
You need a separate .cs file for each page. So you'll need to copy MiddlePage.cs to OtherPage.cs, AnotherPage.cs, YetAnotherPage.cs and then edit those separately.
Obviously, those are lousy names; you should name the pages based on what's on them. For example, you might have pages called UserDetailsPage.cs and DatabaseConnectionDetailsPage.cs, or whatever makes sense in your program.
Thanks
Sizing problem after resizing the sheet
If you change the size of the from WizardSheet derived form, the
ResizeToFit()method in WizardSheet does not seem to be calculating the correct size causing the Wizard to be the wrong size.I do not think it is necessary to change the size of the from WizardSheet derived form, because you should change the size of the from InternalWizardPage or ExternalWizardPage derived forms but it is tempting for someone who is new to the Wizard to change the size of the Sheet and consequently run into trouble (like my colleague did).
Does anyone know a solution to this problem?
Great Wizard, BTW!
Re: Sizing problem after resizing the sheet
Yep. In WizardSheet.ResizeToFit, change the second loop to look like this:
foreach (WizardPage page in _pages) { page.Size = maxPageSize; page.Dock = DockStyle.Fill; }(Add the page.Dock line).
I did this while fixing another bug that I found, and then couldn't reproduce your bug. I think they're the same thing.
Re: Sizing problem after resizing the sheet
Roger,
I have tried the fix but it did not solve the problem. It does make the wizard look better though. Before the fix the page would be smaller than the sheet which looked awful (the page was the right size, the sheet was the wrong size). Now the page fills the sheet so they are both the wrong size.
Here is some more detailed information about the test is performed:
My test project contains one page (derived from ExternalWizardPage) which has a size of 824; 322. If I keep the size of the WizardSheet derived form the same as the WizardSheet form it derives from, the WizardSheet derived form will have a size of 392; 175. If I run this wizard the wizard will have a size of 832; 396 which is what I expect (824; 322 plus some extra pixels for the window borders, title bar, etc.).
When I change the size of the WizardSheet derived form to 492; 275 and run the wizard, the wizard will have a size of 932; 496 instead of 832; 396. In other words, I have increased the size of the sheet by 100 pixels and the size of the wizard is also increased by 100 pixels making it larger than any form in the project.
Robert-Jan
Re: Sizing problem after resizing the sheet
Thanks for the repro recipe. I've got a lot on at the moment, but I'll try to get it reproduced and fixed in the next couple of days.
Dynamic Content
Adding/removing pages
How exactly do you do this?
Skipping pages
In MiddlePage2, add an event handler for the WizardNext event. In that handler, look at the combobox setting. Then, if it's set, change e.NextPage to "MiddlePage4", rather than "MiddlePage3".
I'm sorry, but I still can't
Adding an (optional) page to the wizard
Data access from withing the wizard
Hi Roger. Excellent tutorial, thank you.
I am having problems accessing a local database from within the application. When using bound controls they are populated if added to the base pages but empty when added to the middle pages. The controls are bound to a ce database created entirely within the c# environment. This is not the case when I use tablesadapters in code but only when I try dragging databound objects onto the "forms". I'm assuming that it's due to the timing of the GetData call. Can you let me know why this is the case or better again how to get around it? Thanks again.
Stephen
Re: Data access from withing the wizard
Can't really help you there, I'm afraid. I've never done any data binding in Windows Forms, let alone with this wizard code.
well done!!
Buttons on Wizard
Extra Buttons
I guess that it depends on what you want the buttons for. If you're simply looking to add a couple of buttons to all of the pages of the wizard (e.g. a Help button), you can simply bung it straight onto the class that you derived from WizardSheet.
If you're looking for a couple of buttons that behave like Back and Next, you'll (again) need to bung them on the WizardSheet (or the derived class), and you can add button-specific Enable methods to that class. For example, you might have an EnableHelpButton method.
Alternatively, add some extra bit flags to the enum, and change the implementation of SetWizardButtons. Note that the current implementation is similar to how MFC wizards work, in that you can't have both the Next and Finish buttons visible at the same time.
Whichever option you choose, you'll need to put button controls onto the form. Defining extra bit fields does nothing magic.
There's more than you ever wanted to know about bit fields in .NET on this CodeProject page, or there's this or the documentation for the [Flags] attribute.
I have added another button
I got it working just after
private void confAnotherMachineButton_Click(object sender, EventArgs e)
{
WizardPageEventArgs wpea = PreChangePage(-3);
PostChangePage(wpea);
}
Jumping to a different page
Change Visible after setActive
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); _activePage = newPage; CancelEventArgs e = new CancelEventArgs(); newPage.OnSetActive(e); if (e.Cancel) { _activePage = oldActivePage; } // Hide all of the other pages. foreach (WizardPage page in _pages) { if (page != _activePage) page.Visible = false; } // Show active this page. _activePage.Visible = true; }Now, I can put my code intoprivate void MyWizardPage_VisibleChanged(object sender, EventArgs e) { if (this.Visible == true) { // my code, MessageBox in case of problems } }Finish disabled
how to make the finish button visible, but disabled? Maybe one more flag in WizardButtons? In fact, we have 4 states: Next enabled, Next disabled, Finish enabled, Finish disabled. It is possible to code by 4 bits, but might be confusing.
Re: Finish disabled
This isn't how the Wizard97 spec calls for a wizard to be implemented -- it calls for the Finish button to replace the Next button.
If you want them both visible at the same time, remove the bit in SetWizardButtons where it hides the Finish button, and move the buttons around so that they don't overlap.
Re: Finish disabled
Re: Finish disabled
Ah. Gotcha. I thought you wanted it more like the wizard in RSS Bandit, where you can skip to the end by pressing Finish, and it uses defaults for all of the other pages.
OK. To do what you suggest, I'd probably add a WizardButtons.DisabledFinish flag that acts like WizardButtons.Finish, in that it hides the Next button, and shows the Finish button, but it also disables the Finish button.
Thanks for the suggestion. I'm not going to have time to make this change any time soon, so if you get it working, please (if you can) post another comment with the relevant changes in it.
Re: Finish disabled
[Flags] public enum WizardButtons { None = 0x0000, Back = 0x0001, Next = 0x0002, Finish = 0x0004, FinishDisabled = 0x0008 }and then changed SetWizardButtons method a little bit:// 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 | WizardButtons.FinishDisabled)) != 0) { // Hide Next, show Finish finishButton.Visible = true; finishButton.Enabled = ((buttons & WizardButtons.Finish) != 0); nextButton.Visible = false; nextButton.Enabled = false; this.AcceptButton = finishButton; } elseRe: Finish disabled
That change looks pretty good to me. If I get time, I'll integrate it into the code here.
Multiple middle pages
Re: Multiple middle pages
Ondrej is right