The directory /var/www/sites/differentpla.net/files/images is not writable

Implementing a Wizard in C#, Part 2

roger's picture

In this installment (see here for the previous installment), we'll be fixing a few things and making the whole thing prettier.

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

Implementing the Cancel Button

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);
}

Implementing the Banner

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.

Fixing the Etched Line

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.

Banner Properties

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.

The Sidebar

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.

Update: Source code now lives at CodePlex.

Comments

awesome... very gr8 idea...

awesome... very gr8 idea... thanks...

License for Wizard

I would like to use your wizard in an commercial app. Under what license are you distributing this wizard?

Re: License for Wizard

Totally free for both commercial and non-commercial use. No warranty. I'd appreciate attribution in the source code (i.e. say where you got it from), but this is not necessary.

Thank you so much for this.

Thank you so much for this. I appreciate the hard work you've put into this. I love it! Thank you :)

Very useful Thanks

I found it very useful, I am going to use it. Thanks.

really cool

i find your wizard really cool, i have just one request. the way i understand it, the cancel button must enabled in any case if we want the form to get closed. What if we want the cancel button to be disabled at the last page for example and only want the finish button to implement the closing event (because cancel is not really the same as finish)? thanks in advance Volnay

Disabling Cancel button, but still allowing the wizard to close

You probably want to rearrange the code in wizardSheet_Closing, so that it checks finishButton.Enabled first.

thanks

Thank you for your answer Roger. I was afraid to break something if i did that! Cool

Thanks!

Very nice and very useful :) I'm going to use it in my code :) Thanks 1m.

Missing sidebar

Hi,

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

Hi Roger,
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.

thanks for the sample demo. very cool indeed.

Thumbs Up

Nicely done.

cool

sweet! nice job

Hi, How can switch from one

Hi, How can switch from one to another usercontrol occurs when a condition ?!?!?! if (radiobutton1.checked) { usercontrol1; } else { usercontrol2 }

Adding Pages

How would I go about adding multiple "Middle Pages"? I tried duplicating the wizard.pages.add to add another middle page. I also tried copying MiddlePage.cs to MiddlePage2.cs, etc. and adding them. When I do this, clicking Next does not ever go past the first Middle Page... How do I go about it? :) Chris

It should just work

Alternatively, set .NextPage in the event args.

Make sure that you set the

Make sure that you set the SetWizardButtons(WizardButtons.Back | WizardButtons.Next) correctly in the SetActive event of the page (the one previous before calling the next). It should work perfectly. Great job BTW.

Re: Adding Pages

The problem is, that the 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!

Thanks for that, it works! However I'm still having some problems. How do I populate the various middlepages with different buttons and controlls? If I use the designer for MiddlePage.cs, all the middlepages are the same. Any help will be greatly appreciated! Francis

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

Thanks a lot for this evaluation. I was searching for suitable code examples on wizards in C# and this is the only and fortunately best one I could find. We'll be using this stuff for commercial use and love you for having your licence terms as loose as that! Will mention you in a hint!

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

Hi Roger, What would be the most elegant way to make the Wizard dynamic? By dynamic I mean that maybe the number of pages in the Wizard depends on a previous Wizard step. Normal progression of Wizard is 'page1->page2->page3->page4'. But input in page1 might allow a progression 'page1->page3->page4'. How would you go about doing this? Cheers Isaac

Adding/removing pages

Up to now, I've generally just put all of the pages in the sequence, and then set e.NextPage in the WizardNext event to skip over any pages I don't want to show. I agree that it'd be useful if you could update the Pages collection dynamically, though...

How exactly do you do this?

How exactly do you do this? I would like to hop from MiddlePage2 --> MiddlePage4 if a certain combobox setting is chosen on MiddlePage2. Thanks!

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

I'm sorry, but I still can't manage to make it work. I have written this in my middlepage2 (sorry for the formatting, I don't know how to get it beter):
  • public event WizardPageEventHandler WizardNext;
  • public virtual void onWizardNext(WizardPageEventArgs e)
  • {
  • if (pageComboBox.SelectedIndex == 1)
  • {
  • new page has to be loaded here, but how?
  • }
  • }
  • Adding an (optional) page to the wizard

    See this.

    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!!

    very good work!! i really like it...

    Buttons on Wizard

    I am trying to add two more buttons in wizard under [Flags] public enum WizardButtons { None = 0x0000, Back = 0x0001, Next = 0x0002, Finish = 0x0004, } but it is not taking it correctly can you help me in like what value to set for new buttons.as for others buttons you have done.like Next = 0x0002 as so on....please help me.... thanks.

    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 have added another button to Wizardsheet. I have only made the button visible on the last page, because the purpose of the button is to return to the second page in the wizard. How do I go about getting this to work? I assume one has to make an event similar to "WizardBack", however I seem to be missing where the button is linked to the event. Thanks!

    I got it working just after

    I got it working just after I postet my question:
      private void confAnotherMachineButton_Click(object sender, EventArgs e)
      {
      WizardPageEventArgs wpea = PreChangePage(-3);
      PostChangePage(wpea);
      }

    Jumping to a different page

    You should be able to get away with using SetActivePage.

    Change Visible after setActive

    Hi, thanks, very nice and useful wizard. I just have a scenario, when I connect to a USB device in the second page. The problem was, where to put a code to connect to the device? When I put it to the VisibleChange event, or SetActive event, or whereever possible, the message box has popped up when the first page was visible yet. So, I have changed WizardSheet.cs a little bit:
    		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 into
            private void MyWizardPage_VisibleChanged(object sender, EventArgs e)
            {
                if (this.Visible == true)
                {
                    // my code, MessageBox in case of problems
                }
            }
    

    Finish disabled

    Hi Roger,
    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

    No, I mean I want the "Next" button invisible, the "Finish" button visible, but disabled. When the user makes a certain action (ticks the check box, etc.), then the "Finish" button will be enabled. Yes, I can modify the SetWizardButtons easily, but I though more people may need this feature, so that's why I posted message.

    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

    Hi, accroding to your suggestion I have modified WizardSheet.cz. First, I have added a flag:
    	[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;
                    }
                else
    

    Re: Finish disabled

    That change looks pretty good to me. If I get time, I'll integrate it into the code here.

    Multiple middle pages

    I have followed the steps below to make several middlepages, and it works. However I am having some trouble populating the pages. If I use the MiddlePage designer in Visual Studio 2K5, all the middle pages get the same buttons and controls. Can enybody help me populate the middlepages with independant features? Any help will be greatly appreciated! Francis

    Re: Multiple middle pages

    So you make one middle page and populate it more times? You have to make one class for every middle page. Every such a class will subclass the middle page. Do I understand your problem right?

    Comment viewing options

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