Adding Named Panel Navigation to the WizardController

See also:
Article 1 in this series: Article 1 in a series about WinForms Wizards: The fastest Wizard in the West.
Article 2 in this series: A slightly better WinForms wizard, and slightly more work.
Article 3 in this series: You're looking at it!
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

Abstract:
You can't fight MSDN and the power of visibility.  With the added visibility on this series I'm going to concentrate on implementing small features quite frequently and eventually devise a small game that takes advantage of the library.  Today I'll be adding named navigation support.  You can think of this as conditional navigation support, since generally the panels decide which panel should be navigated to based on some selection the user makes.  In my example, I'm still assuming a single path through the wizard, but you can easily change things around to support different types of navigation.  For those that have been following the series up to now, I'm keeping all of the old support.  The new interface allows the wizard to support, conditionally, whether or not the navigation will be done sequentially or by using named navigation.  You can even mix and match named and sequential navigation if you wish, but ensuring you add the panels in the right order could prove to be a real pain so I don't recommend it.

TOC:

  1. Examples of some non sequential or named navigation wizards.
  2. Creating a simple named navigation interface.
  3. Plugging the interface into the WizardController and maintain backwards compatibility.
  4. Creating our named navigation panels.  Oh, it's sooo easy!
  5. Making sure we support legacy wizards and the new syntax.
  6. Conclusion

1.  Examples of some non sequential or named navigation wizards.
Sequential wizards make a lot of sense in most cases since the process of collecting data is linear.  However, when the input by the user needs to change the UI shown or how the panels change, then you have to start making uses of named navigation where each panel gets to choose the next panel to display or the previous panel to show.  Generally when users need to make processes like these they don't even use a wizard, they simply hack together a bunch of forms.  The reason is they are thinking a linear process can't apply to a non linear or branched problem.

Installation wizards are probably the most obvious case for named navigation.  They require that panels change based on user decisions, and they often short-circuit complicated install decisions by providing selections for common installs.  Search wizards will often embed all of their controls in the same UI and conditionally show or hide various portions, when they could instead make use of multiple panels and some global navigation controls.  Data conversion wizards are another place where user selections change the UI.  And my favorite of all, GAMES, can make heavy use of nested wizards to let the user walk through complex decision trees.  As a matter of fact, I'm going to show in a future article how to code a dungeon simulation game using nothing but a basic wizard framework, so stay tuned.

2.  Create a simple named navigation interface.
The first step is to define an additional interface that our WizardController can digest.  We'll call this IWizardNavigation.  Since moving forward we kind of expect users to implement an interface even if they don't use it, we'll let them implement IWizardNavigation but turn the feature off through a boolean property.  SupportNamedNavigation allows the framework to determine what kind of wizard panel a given UI element really is.  If it supports named navigation then we'll use the remainder of the properties.  If not, we'll simply ignore them (except for NavigateName, since other navigation panels might want to navigate to this panel).  A great example for this would be the final panel in a run.  Most likely it won't want to allow you to navigate backwards, but instead just look at the results of your actions.

For any named navigation we need to be able to identify panels.  Since jumping to panels with the same name would cause a problem, we'll also need to make sure they are unique, but that comes later.  Each panel will support a NavigateName property allowing us to identify the panel so we can pick it out of the collection.  NavigatePreviousTarget will return the name of the panel we should move to if the previous button is pushed, and NavigateNextTarget will return the name of the panel to jump to when the next button is pushed.  As far as these properties stand, the NavigateName property should be fixed and always return the same thing (boy could you mess the wizard up returning a different name every time ;-), but the other two can be set based on user input allowing you to conditionally change panels.

public interface IWizardNavigation {
    bool SupportNamedNavigation { get; }
    string NavigateName { get; }
    string NavigatePreviousTarget { get; }
    string NavigateNextTarget { get; }
}

3.  Plugging the interface into the WizardController and maintain backwards compatibility.
The WizardController class is where all of the important stuff happens.  Things only marginally change from the previous two examples, and you'll be surprised how easy things are to implement both named and sequential panels all within the same code.  First, we'll need a mapping table to select the panels when their names come up.  We'll add a simple hashtable for this (case sensitive, so be careful), and we'll be maintaining both a Hashtable and an ArrayList to hold our panels now.  The only time we modify these collections are in the AddPanel calls, so we aren't privy to making mistakes that get the two out of sync.  AddPanel looks a little different now.  We have to check for two interfaces on entry IWizardPanel (the required interface), and IWizardNavigation (in case named navigation is supported).  We'll throw a new exception now, if the user has already added a navigation panel with an identical name.  Beyond that the only change is updating our nameMap to reference our newly added named navigation panel.

public void AddPanel(Panel pnl) {
    IWizardPanel wPanel = pnl as IWizardPanel;
    IWizardNavigation wNav = pnl as IWizardNavigation;

    if ( wPanel == null ) {
        throw new Exception("Wizard panels must support IWizardPanel");
    }

    if ( wNav != null ) {
        if ( nameMap.ContainsKey(wNav.NavigateName) ) {
            throw new Exception("Wizard Navigation Panels can't share names");
        }

        nameMap[wNav.NavigateName] = wNav;
    }

    wizardPanels.Add(wPanel);
}

The remainder of the changes are in the navigation code (note all of the code will be included at the end of the article as usual, so if you want to play, just jump on day, copy and compile).  Wizard_NavigatePrevious has some code to grab the current panel and check for the IWizardNavigate interface.  Once we get the interface, we either decide to do sequential navigation or named navigation based on support for the interface and the value of the SupportNamedNavigation parameter.  The easiest way to provide backwards compatibility is to leave all of the index based code in place, and simply have the named navigation code convert the target panel down to an index.  Turns out ArrayList has a great way to do this.  We can call IndexOf passing in the panel we stored in the name map, then get the actual index.  Things just fall into place when you think through the algorithm ahead of time.  During a normal retro-fit we might have overlooked these simple collection features and would have needed a bunch of extra code to implement backwards compatibility.

Wizard_NavigateNext uses the same feature set and we again just set the index.  If the panel doesn't exist in the case of NavigateNext, we set the panel 1 past the final panel.  Basically equivalent to calling Finish.  In the case of NavigatePrevious we just return out of the loop and don't do anything.  Can you mess yourself and create non-completable wizards?  Yeah, but the WizardController can't do everything for you can it?  (We'll talk about this later, since it is possible for the WizardController to detect possible issues with your control flow).

public void Wizard_NavigateNext(object sender, EventArgs e) {
    IWizardNavigation wizardNav = wizardPanels[wizardIndex] as IWizardNavigation;

    if ( wizardNav != null && wizardNav.SupportNamedNavigation ) {
        if ( nameMap.ContainsKey(wizardNav.NavigateNextTarget) ) {
            wizardIndex = wizardPanels.IndexOf(nameMap[wizardNav.NavigateNextTarget]);
        } else {
            // Since we can't find the next panel, we pop out
            // and finish.  Same as if we walked off the end
            // of the array before.
            wizardIndex = wizardPanels.Count;
        }
    } else {
        wizardIndex++;
    }

    if ( wizardIndex == wizardPanels.Count ) {
        Wizard_NavigateFinish(sender, e); // This shouldn't happen if your dialogs are correct
        return;
    }

    Panel newPanel = wizardPanels[wizardIndex] as Panel;

    if ( newPanel != null ) {
        InitPanel(newPanel);
    }
}

public void Wizard_NavigatePrevious(object sender, EventArgs e) {
    IWizardNavigation wizardNav = wizardPanels[wizardIndex] as IWizardNavigation;
    if ( wizardNav != null && wizardNav.SupportNamedNavigation ) {
        if ( nameMap.ContainsKey(wizardNav.NavigatePreviousTarget) ) {
            wizardIndex = wizardPanels.IndexOf(nameMap[wizardNav.NavigatePreviousTarget]);
        } else {
            // Can't go back from here I guess
            // This actually isn't bad, but the user should
            // have specified that no previous button be available.
            return;
        }
    } else {
        if ( wizardIndex > 0 ) {
            wizardIndex--;
        } else {
            return;
        }
    }

    Panel newPanel = wizardPanels[wizardIndex] as Panel;

    if ( newPanel != null ) {
        InitPanel(newPanel);
    }
}

4. Creating our named navigation panels.  Oh, it's sooo easy!
I won't lie to you, it really is easy.  Remember, these wizard panels do nothing more than implement the minimum interface to actually create a workable wizard at the end of the article.  They don't do anything cool, and I'm not even using conditional branching in the navigation.  The new WizardPanelWithNavigation uses all of the old code of the WizardPanel, but also implements IWizardNavigation.  We could have even simplified the code by inheriting from WizardPanel (icky because we forgot to make all of the member fields over there protected so we could change them ;-), but 30 lines of code is only 30 lines of code.  The new panel requires that you pass in a name parameter, and that the previous and next button parameters become strings.  Since we are targeting panels now, we'll let the presence or absence of a target determine whether or not to show a previous or next button.  I'm spending more characters on the explanation than the code, just look below!

public class WizardPanelWithNavigation : Panel, IWizardPanel, IWizardNavigation {
    private bool prev;
    private bool next;
    private bool finish;
    private string name;
    private string nextPanel;
    private string prevPanel;
   
    private Label description = new Label();
   
    public WizardPanelWithNavigation(string name, string description, string prev, string next, bool finish) {
        this.prev = (prev != null);
        this.next = (next != null);
        this.finish = finish;
        this.description.Text = description;
        this.name = name;
        this.nextPanel = next;
        this.prevPanel = prev;
       
        InitializeComponent();
    }
   
    private void InitializeComponent() {
        this.description.Dock = DockStyle.Fill;
        this.Controls.Add(this.description);
    }
   
    public bool ShowNavigatePrevious { get { return this.prev; } }
    public bool ShowNavigateNext { get { return this.next; } }
    public bool ShowNavigateFinish { get { return this.finish; } }
   
    public bool SupportNamedNavigation { get { return true; } }
    public string NavigateName { get { return this.name; } }
    public string NavigatePreviousTarget { get { return this.prevPanel; } }
    public string NavigateNextTarget { get { return this.nextPanel; } }
}

5.  Making sure we support legacy wizards and the new syntax.
We'll be using the same code as the second article in this series to make sure we support legacy wizards.  We'll also be creating a new named navigation version.  You can see the code for this jumps us all around the panels rather than running them in a sequential order.  I've also demonstrated how you can continue making use of Application.Run to run multiple dialogs from your applications Main method.  I think lots of tool developers could really benefit from this format and use of Application.Run, because they no longer need a main or primary form to run multiple pieces of UI.  I cover the esoterics of Application.Run and the Windows Forms message pump in another posting People are confused by ApplicationContext in Windows Forms, but there really isn't any magic happening.

public class WizardTester {
    [STAThread()]
    private static void Main(string[] args) {
        WizardController controller = new WizardController();
        controller.SetDialog(new WizardTestDialog("What do you want?"));
        controller.AddPanel(new WizardPanel("foo 1", false, true, false));
        controller.AddPanel(new WizardPanel("foo 2", true, true, false));
        controller.AddPanel(new WizardPanel("foo 3", true, true, false));
        controller.AddPanel(new WizardPanel("foo 4", true, true, true));
        controller.AddPanel(new WizardPanel("foo 5", true, false, true));

        controller.StartWizard();
        Application.Run(controller);

        controller = new WizardController();
        controller.SetDialog(new WizardTestDialog("What do you want?"));
        controller.AddPanel(new WizardPanelWithNavigation("Panel 1", "Welcome Screen!",     null, "Panel 3", false));
        controller.AddPanel(new WizardPanelWithNavigation("Panel 2", "License Agreement!",  "Panel 3", "Panel 4", false));
        controller.AddPanel(new WizardPanelWithNavigation("Panel 3", "Install Options",     "Panel 1", "Panel 2", false));
        controller.AddPanel(new WizardPanelWithNavigation("Panel 4", "Confirmation Screen", "Panel 2", "Panel 5", false));
        controller.AddPanel(new WizardPanelWithNavigation("Panel 5", "Install Now!",        null, null, true));

        controller.StartWizard();
        Application.Run(controller);
    }
}

6.  Conclusion
Well, I said I'd continue updating this framework so here I am.  This named navigation support really makes sense at this time.  The validation logic that comes later will build on top of this for sure.  After all, if we have controls that can change the navigation of the UI, it makes sense to also have validation for those controls that helps to enable/disable/or pevent navigation of the UI.  Taking baby steps.

Full Source (C#): Code-Only: Winforms Wizard Series Article 3 (C#)
Full Source (VB .NET): Code-Only: Winforms Wizard Series Article 3 (VB .NET)

Published Monday, April 12, 2004 5:38 PM by Justin Rogers
Filed under:

Comments

No Comments