Providing design time support for the Wizard Framework
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: Adding Named Panel Navigation to the WizardController
Article 4 in this series: You're looking at it!
Article 5 in this series: Adding a design time dialog and creating a VS project sample
Abstract:
The only code feature I've been asked to add to the wizard framework I've developed is designer support. Honestly, I hate the designer and so support for this feature definitely wouldn't come about without a request. This article will discuss the minimal changes to the wizard controller (we now support Control instead of Panel when adding panels), and the addition of two new designable base classes for inheriting your wizards. Note, these changes aren't ground-breaking and they don't displace any of the old logic so that is still available for you notepad code-warriors that are near and dear to my heart. Note that we aren't adding support for designing the actual wizard process and adding items to the controller. We probably could, but I'll let you write the code instead.
TOC:
- The minimal WizardController changes.
- Creating a DesignablePanel that supports basic navigation.
- Inheriting a DesignablePanelWithNavgation for named navigation support.
- Tips on using VS to get things up and running.
- Refactoring our code panels to use the new inheritance model.
- Conclusion
1. The minimal WizardController changes
Do you want to know why I love frameworks? Well, because they allow enhancements to be made to the system with minimal changes. I've talked about this before, but it really is cool how easily things come together. In the case of the WizardController, we had a number of places where we were casting to Panel in order to do some additional work. This constraint was not needed since the feature set we needed existed on the Control class. Hell, we could probably get away with removing this restriction altogether and moving all of the logic into an interface method, but that would require more work. Just note that the WizardController now uses a Control instead of a Panel whenever you navigate between wizard displays.
public void AddPanel(Control pnl) {
....
}
2. Creating a DesignablePanel that supports basic navigation
The DesignablePanel requires a few changes to the old design we had for WizardPanel. You can still use WizardPanel, but it isn't really designable. You can't set any properties on the WizardPanel and it doesn't have a default constructor. Our new DesignablePanel inherits from UserControl and implements the IWizardPanel interface. All of the member variables are also being made protected so that we can support a full inheritance chain and allow property overrides in derived classes.
public class DesignablePanel : UserControl, IWizardPanel {
protected bool prev = false;
protected bool next = false;
protected bool finish = false;
public DesignablePanel() {
}
....
}
The default values are important here, since we aren't initializing the control during the constructor phase. Next we'll add support for the interface, just like we did in WizardPanel. The only change is the addition of the [Browsable(false)] attribute. This buys us the ability to hide our public get only properties. Since they are get only, they can't be designed anyway. We'll make some new properties later so the user can select which buttons to show. In addition we'll add the virtual keyword to our properties so they can be overriden in our derived classes later.
[Browsable(false)] public virtual bool ShowNavigatePrevious { get { return this.prev; } }
[Browsable(false)] public virtual bool ShowNavigateNext { get { return this.next; } }
[Browsable(false)] public virtual bool ShowNavigateFinish { get { return this.finish; } }
Next we'll add the publicly visible properties that let you choose which buttons are active in the wizard dialog. These are basic get/set properties with the [Browsable(true)] attribute set. We'll also mark them virtual so they can be overriden later (this lets us hide members later that we don't want to be designed). Just for giggles we'll also add the Category attribute set to Wizard so our properties are categorized in the property pane and the Description attribute so you aren't confused as to what each property does.
[Browsable(true)]
[Category("Wizard")]
[Description("Determines if the Previous button is activated when this panel is displayed")]
public virtual bool NavigatePrevious {
get {
return this.prev;
}
set {
this.prev = value;
}
}
[Browsable(true)]
[Category("Wizard")]
[Description("Determines if the Next button is activated when this panel is displayed")]
public virtual bool NavigateNext {
get {
return this.next;
}
set {
this.next = value;
}
}
[Browsable(true)]
[Category("Wizard")]
[Description("Determines if the Finish button is activated when this panel is displayed")]
public virtual bool NavigateFinish {
get {
return this.finish;
}
set {
this.finish = value;
}
}
That is a wrap my friend. The rest you'll do in the designer later on by setting properties and building the code required to initialize the WizardController with your newly built dialogs. On to the named navigation designable dialog.
3. Inheriting a DesignablePanelWithNavgation for named navigation support
I made sure that this dialog was going to be super easy to implement through a solid inheritance model. You could consider this a real upgrade to the original panels, so make sure to take a solid look at the code. I've gone the extra distance of inheriting the old wizard panel classes from these new designable versions adding the old constructor syntax and updating the controls collections as appropriate. I definitely don't want to leave anyone out in the cold with this update ;-)
Since we are using a full inheritance chain, we'll start by inheriting from DesignablePanel and getting free IWizardPanel features. We'll also be implementing IWizardNavigation. To get this working we'll add some protected fields to back our properties and create the same empty constructor set we did earlier so that we get full design time support.
public class DesignablePanelWithNavigation : DesignablePanel, IWizardNavigation {
protected string name = null;
protected string nextPanel = null;
protected string prevPanel = null;
public DesignablePanelWithNavigation() {
}
....
}
IWizardNavigation has 4 properties we have to support, again, we'll be making them non browsable and virtual so that functionality can change later as we add more features on top of this control. In addition, we are going to reimplement ShowNavigatePrevious and ShowNavigateNext so they are based on whether or not panel names for navigation are present. We don't want the user to have to enable buttons, instead they'll just set the panel names and that will control the buttons. This is similar to the code approach we followed in WizardPanelWithNavigation.
[Browsable(false)] public virtual bool SupportNamedNavigation { get { return (this.nextPanel != null || this.prevPanel != null); } }
[Browsable(false)] public virtual string NavigateName { get { return this.name; } }
[Browsable(false)] public virtual string NavigatePreviousTarget { get { return this.prevPanel; } }
[Browsable(false)] public virtual string NavigateNextTarget { get { return this.nextPanel; } }
[Browsable(false)] public override bool ShowNavigatePrevious { get { return this.prevPanel != null; } }
[Browsable(false)] public override bool ShowNavigateNext { get { return this.nextPanel != null; } }
Next we'll add a new feature we haven't used before. We need to override our previous designable property implementations so that they don't show up in the designer. We'll go ahead and redefine the getter/setter functions and specify that the properties are no longer browsable. I could probably test this step a bit more, but I'm pretty sure you have to provide full implementations of both the getter/setter functions in order to disable the functionality of the properties, so that is what I'm doing. The getter now uses the Show functions in order to provide return values and the setter just falls through without changing any values. If people use call instead of callvirt to access some of the properties (which they shouldn't), then some of the values will be out of whack. However, we shouldn't be responsible for updating multiple data fields to support this scenario, nor is it needed by the WizardController.
[Browsable(false)]
public override bool NavigatePrevious {
get {
return ShowNavigatePrevious;
}
set { }
}
[Browsable(false)]
public override bool NavigateNext {
get {
return ShowNavigateNext;
}
set { }
}
We'll end by providing designable properties to set navigation targets and the name of this particular dialog. SupportNamedNavigation, as you can see from previously defined code, makes use of other properties to return its value, so we don't have to worry about designer support for it. We are going the full gambit again, setting virtual properties with Category and Description attributes.
[Browsable(true)]
[Category("Wizard")]
[Description("Sets the name of this wizard panel for navigation purposes. Each panel" +
"within a WizardController must have a unique name.")]
public virtual string PanelName {
get {
return this.name;
}
set {
this.name = value;
}
}
[Browsable(true)]
[Category("Wizard")]
[Description("Sets the name of the next panel in the series. This property, if set, enables" +
"the SupportNamedNavigation feature and the display of the Next button within" +
"a WizardDialog")]
public virtual string NextTargetName {
get {
return this.nextPanel;
}
set {
this.nextPanel = value;
}
}
[Browsable(true)]
[Category("Wizard")]
[Description("Sets the name of the previous panel in the series. This property, if set, enables" +
"the SupportNamedNavigation feature and the display of the Previous button within" +
"a WizardDialog")]
public virtual string PreviousTargetName {
get {
return this.prevPanel;
}
set {
this.prevPanel = value;
}
}
4. Tips on using VS to get things up and running
To use these new updates within VS, you have several options. The easiest would be to include a source file for each class. Some of the files will show up as class files and others will appear to be designable controls. You don't want to design them in place, and instead create an InheritedControl from them. Then you can design this new control which derives from the one of the base designable controls.
The second option you'll have is to compile these into a library and include them in your project. Unfortunately InheritedControl browsing doesn't take into account your referenced libraries, and each time you derive a new control, you'll need to navigate to the library on disk before it will give you the option of using the designable panels for new controls. This is a real pain in the butt.
Thankfully there is one more option. You'll use a mixture of option 1 and 2 for this. Simply include the library, browse once, and create a derived control. Don't do anything to this new control, and simply use it as the base class for all of your panels. This works out great and you can add common helper functions to your new base class that can be used in all of your wizard dialogs. Since we have implemented several common wizard features yet, you can implement them yourself in the short term.
When creating your WizardController, simply add a new source file to your project. This is where you'll add the Main method, and the code logic for running your wizard (this is assuming you don't already have an application you are launching the wizard from). When building the wizard, simply add the dialogs by calling AddPanel with the default constructor for each of your derived panels. If you are using navigation panels you won't be able to add two of the same panels without first changing the name (remember, our designable panels don't have constructors for changing common properties).
That should be all you need to create a wizard fully in the designer. You'll probably find it a lot easier to design your panels now that you can use the Windows Forms designer. You'll also be able to use the designer for your wizard dialog. Since the interface for that is so easy to implement, I won't even bother covering it here. If enough people are interested in the steps for designing the actual dialog and maybe a DesignableWizardDialog class, then I'll do it.
5. Refactoring our code panels to use the new inheritance model
I didn't really care much for refactoring the controls, but since we get the opportunity to shrink the code files a bit, I figured why not. The new refactored controls simply inherit from the designable versions (just as a custom designed panel would), add the old constructors in so we can quickly construct them, and add any visual controls that'll be needed to show the panel. The shrink in code size is actually quite pleasant.
public class WizardPanel : DesignablePanel {
protected string description = null;
protected Label lblDescription = new Label();
public WizardPanel(string description, bool prev, bool next, bool finish) {
this.prev = prev;
this.next = next;
this.finish = finish;
this.description = description;
InitializeComponent();
}
private void InitializeComponent() {
this.lblDescription.Dock = DockStyle.Fill;
this.lblDescription.Text = this.description;
this.Controls.Add(this.lblDescription);
}
}
public class WizardPanelWithNavigation : DesignablePanelWithNavigation {
protected string description = null;
protected Label lblDescription = new Label();
public WizardPanelWithNavigation(string name, string description, string prev, string next, bool finish) {
this.name = name;
this.nextPanel = next;
this.prevPanel = prev;
this.finish = finish;
this.description = description;
InitializeComponent();
}
private void InitializeComponent() {
this.lblDescription.Dock = DockStyle.Fill;
this.lblDescription.Text = this.description;
this.Controls.Add(this.lblDescription);
}
}
6. Conclusion
If you haven't used the wizard framework up to now because it was still too much work to create one, then you'll have to rethink that decision. You can quickly and easily create wizards in only a few minutes now using the Windows Forms designers. I was working on a proof of the feature earlier last week and found that laying out the panels was harder than I had originally thought, so maybe now I'll go back and use the designer (ick, can't believe I said that) in order to quickly push out that proof of concept. I definitely think it will be easier now to quickly create cool wizards without a bunch of work writing custom layout code.
Full Source (C#): Code-Only: Winforms Wizard Series Article 4 (C#)
Full Source (VB .NET): Code-Only: Winforms Wizard Series Article 4 (VB .NET)