Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

SimpleWizardUpdate

5.00/5 (7 votes)
27 Aug 2023CPOL5 min read 8.5K   152  
An update of a CodeProject article SimpleWizard
SimpleWizardUpdate is a rewritten version of the original SimpleWizard project posted on CodeProject in 2010, offering a flexible and user-friendly wizard control for creating installation wizards, leveraging a dynamic navigation system and user-defined UserControl pages.

Update 8/27/2023

  • Removed the PreviousPageId and am now using a Stack for navigation, makes it a lot cleaner.
  • Added an Action page, where in the example it just calls a MessageBox, but it could be anything.
  • Cleaned up code

Introduction

The SimpleWizardUpdate is a complete rewrite of the SimpleWizard project that was first posted on CodeProject in 2010 but later moved to Github. The last activity was 5 years ago and the owner has shut the project down.

I was looking for a simple wizard type control that I could modify to use as an install wizard for an application that I've had available on my site for some time. I thought it would be about time that I provide some kind of installation wizard that would be more user friendly and look a little more professional than what I had. After doing some download and try attempts, I finally found an article on CodeProject by Matt Gordon. I downloaded it and attempted to modify the existing code to meet my requirements but soon found that it wouldn't work the way I needed it too. I ended up modifying the code to the point where it no longer resembles the original so I'm releasing it as my code but giving credit to Matt Gordon for getting me started on the right track.

The Basics

The WizardHost design window, shown in the image below is the window where the pages, which are UserControls are loaded into the User area when the Navigation buttons are clicked. The layout shown is for the demo application, the window can be configured in any way desired. The WizardHost window can be thought of as a master page where the window can be divided into dynamic areas, where the pages can be viewed, or static areas that will be displayed regardless of the page shown. For example, in the Demo window, the Title, Sidebar and Navigation Button areas are static areas that are shown with every page and the User area is a dynamic area where the pages are viewed.

The IWizardPage interface is the page template that contains a link to the content, navigation information, page validation control methods and methods that are invoked when the pages are loaded, unloaded and at some point to save the page information.

The PreviousPageId and NextPageIds are of type string because the master page information in WizardHost is stored as a Dictionary where the key is a string and the value is a WizardPage. As such, the PageIds are used as the key to access a reference to the page in the WizardPages Dictionary.

C#
public interface IWizardPage
{
    public enum PageType
    {
        Normal,
        Action
    }

    UserControl Content { get; }

    PageType GetPageType { get; }

    string? PageId { get; set; }
    List<string> NextPageIds { get; set; }
    string? NextButtonText { get; }
    // This is to determine which Next page to load
    public string? PageToLoad() { return null; }

    string? Title { get; set; }

    void OnLoad();
    void OnUnload();
    void InstallInfo(ref InstallInfo info);
    void PerformAction(ref InstallInfo Info);

    bool PageValid { get; }
    string ValidationMessage { get; }
}

The Wizard initialization is reminiscent of an article I wrote some time ago, Finite State Machine for embedded systems. In the Finite State Machine, I transitioned between states based on some event and in the SimpleWizard, I transition between pages based on the button click event.

Page Loading

The heart of the application is the LoadPage method where UserControls are removed from and added to, in this case, the Content panel. In the Wizard window, the image shown above the Content panel is the User area. As stated previously, the Content area can be any part of the window or the whole window, depending on your requirements.

I won't devote a lot of time discussing the code, it is well documented and should be fairly easy to follow.

C#
public void LoadPage(IWizardPage? page)
{
    if (CurrentPage != null)
    {
        // Make InstallationInfo available on OnUnLoad
        //  and PerformAction
        CurrentPage.InstallInfo(ref info);

        // If this is an action page, perform the passing info
        //  If just a normal page, do Unload
        // Need to test if the page's PageId is on the NextPage list
        //  which indicates that the user clicked next instead of previous.
        if (CurrentPage.GetPageType == IWizardPage.PageType.Action &&
             CurrentPage.NextPageIds.Contains(page.PageId))
            CurrentPage?.PerformAction(ref info);
        else
            CurrentPage?.OnUnload();
    }

    if (page != null)
    {
        // Make InstallationInfo available on OnLoad
        page.InstallInfo(ref info);

        CurrentPage = page;

        lblTitle.Text = page?.Title;

        // Remove the old panel and add the new one
        contentPanel.Controls.Clear();
        contentPanel?.Controls.Add(CurrentPage?.Content);

        UpdateNavigation();

        CurrentPage?.OnLoad();
    }
    else
        throw new Exception("Error: No page to load");
}

Page Navigation

The image below illustrates the page layout for the example SimpleWizardUpdate application, provided in the download and as can be seen, the page navigation works pretty much like a doubly linked list in that the WizardPage contains a link to the previous page and a list of links to the next page(s). There can be only one previous page but there can be multiple next pages.

When the Previous navigation button is clicked, determining the page to load is easy because there is only a single page that can be configured, therefore the code is fairly simple as shown in listing below. But when the Next button is clicked, figuring which page to load is a bit more complicated. I thought about this problem for a while and finally decided to just ask the page itself which page to load. So, the solution was to keep a list of NextPageIds in the page and in the case of the demo, I determine which page to load based on which button was clicked in the InstallBase page. Then, in the btnNext_Click handler shown below, I make a call to the CurrentPage's PageToLoad method and a string id to the appropriate page is returned and a lookup in the WizardPages dictionary is made and the page loaded.

C#
private void btnPrevious_Click(object sender, EventArgs e)
{
    string? page = WizardPageStack.Pop();
    if (page != null)
        LoadPage(WizardPages[page]);
}

private void btnNext_Click(object sender, EventArgs e)
{
    if (!CheckPageIsValid())
        return;

    string? page = CurrentPage?.PageToLoad();
    if (page != null)
    {
        WizardPageStack.Push(CurrentPage.PageId);
        CurrentNextPage = WizardPages[page];
        LoadPage(CurrentNextPage);
    }
    else
        PerformCleanup();
}

Page Example

I chose the InstallBase page as an example because it demonstrates most of the functionality provided by the page.

The design page for InstallBase is shown in the following image and contains a couple of custom controls that I need to give the creators credit.

The first is the RJButton control by RJCodeAdvance, I got the control from Github. It is a very nice control with a nice set of features and is a very easy to configure drop in control.

The second control is the RoundedPanelPanel Control that was a copy and paste from Stackoverflow and provided by Ege Ayden. It too was a very nice control that surprisingly worked with little modification.
Note: Modified to draw border, code was there. I just had to uncomment and add a property to allow border to be drawn or not. (Easy Peasy)

The page contains a link to two next pages and the next page is determined by which button is clicked. Looking at the code below, the rjButton[n]_Click handlers get the Page Id string from the buttons Tag property and from this sets the page property which is used by the PageToLoad method to determine the page that will be loaded next.

In the OnLoad method, I highlight the appropriate button panel depending on whether there is a registry entry defined.

C#
public partial class InstallBase : UserControl, IWizardPage
{
    RegistryKey? key = null;
    string? page = null;

    public UserControl Content { get { return this; } }
    public PageType GetPageType { get { return PageType.Normal; } }

    public string? PageId { get; set; }
    public List<string>? NextPageIds { get; set; } = new List<string>();
    public string? NextButtonText { get { return null; } }
    public string? PageToLoad() { return page == null ? NextPageIds?[0] : page; }
    public string? Title { get; set; }

    public bool PageValid { get { return true; } }
    public string ValidationMessage { get { return "What could go wrong"; } }

    public InstallBase(string id)
    {
        InitializeComponent();

        PageId = id;
    }

    public void OnLoad()
    {
        if (RegistryHelper.DoesKeyExist() == null)
            rjButton1.PerformClick();
        else
            rjButton2.PerformClick();
    }

    public void OnUnload() { }

    public void PerformAction(ref InstallInfo Info) { }
    public void InstallInfo(ref InstallInfo info) { }

    private void rjButton1_Click(object sender, EventArgs e)
    {
        page = rjButton1.Tag.ToString();

        pnlUpdate.BackColor = Color.Transparent;
        pnlUpdate.IsDrawBorder = false;

        pnlNew.BackColor = Color.Azure;
        pnlNew.IsDrawBorder = true;
    }

    private void rjButton2_Click(object sender, EventArgs e)
    {
        page = rjButton2.Tag.ToString();

        pnlNew.BackColor = Color.Transparent;
        pnlNew.IsDrawBorder = false;

        pnlUpdate.BackColor = Color.Azure;
        pnlUpdate.IsDrawBorder = true;
    }
}

Conclusion

The demo application shows what the application is capable of and demonstrates much of the functionality that the control provides.

I believe the code is set up in such a way that the wizard can be used for a variety of uses. I've tried to make it as simple as possible but with the idea that it could be extended.

History

  • 25th August, 2023: Initial version

License

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