Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WinForms

Composite UI Application Block (CAB) Wizard

4.33/5 (2 votes)
13 Mar 2009CPOL4 min read 30.9K   280  
Shows a very simple way to create a wizard in CAB

Introduction

In this article, I show a fairly simple implementation of a Wizard in CAB. I did something similar at the company I work for and decided that it might be useful to someone else out there with similar needs.

I'm assuming that you know the basics of CAB, this is a really elementary implementation of a Wizard and I won't be delving into massively complicated CAB capabilities. I'm also certain that this wizard could be done better using some of the more advanced features of CAB, but it suffices, and can be useful in a production environment.

Background 

One of the things I tried hard to do with this wizard was to make it easy to implement, whilst at the same time staying true to the basic tenets of CAB. I didn't want to have to write an entire guidance package just so the wizard could be used. I'll be discussing why I did things the way I did as I go through the code.  

Code 

I'm using VS 2005, the CAB framework and the Patterns and Practices Guidance Packages.

I started with a basic CAB shell, happily generated for me using the guidance packages. Good times.
I added 3 CAB Modules, and a Class Library project. The first two modules (cunningly named ModuleA and ModuleB) contain a single view each to illustrate that views from different modules can be shown in the wizard. The third module contains the view which we will be using to host the views in the wizard. 

The class library simply contains two interfaces, one which goes on the UserControl of any view you wish to go into the wizard, and another which goes onto the presenter of the same. These interfaces allow us to treat any view, no matter which module it is in, the same way, allowing us to keep the mantra of the CAB project (No Module will reference any other module).

SolutionExplorer.JPG

The interfaces:

C#
namespace WizardInterface
{
    public interface IWizardView
    {
        IWizardPresenter Presenter { get; }
        int Priority { get; set; }
    }
} 
C#
 public interface IWizardPresenter
{
    bool Startup();
    bool Shutdown();
} 

The implementation of the interfaces in WizardView1:

C#
namespace CABWizard.ModuleA
{
    public class WizardView1Presenter : Presenter<IWizardView1>, IWizardPresenter
    {
        /// <summary>
        /// This method is a placeholder that will be called by 
        /// the view when it's been loaded <see cref="System.Winforms.Control.OnLoad"/>
        /// </summary>
        public override void OnViewReady()
        {
            base.OnViewReady();
        }

        /// <summary>
        /// Close the view
        /// </summary>
        public void OnCloseView()
        {
            base.CloseView();
        }

        #region IWizardPresenter Members
        public bool Startup()
        {
            return true;
        }

        public bool Shutdown()
        {
            return true;
        }
        #endregion
    }
}
C#
namespace CABWizard.ModuleA
{
    [SmartPart]
    public partial class WizardView1 : UserControl, IWizardView1, IWizardView
    {
        public WizardView1()
        {
            InitializeComponent();
        }

        /// <summary>
        /// Sets the presenter. The dependency injection system will automatically
        /// create a new presenter for you.
        /// </summary>
        [CreateNew]
        public WizardView1Presenter Presenter
        {
            set
            {
                _presenter = value;
                _presenter.View = this;
            }
        }

        protected override void OnLoad(EventArgs e)
        {
            _presenter.OnViewReady();
        }

        #region IWizardView Members

        IWizardPresenter IWizardView.Presenter
        {
            get { return _presenter; }
        }

        private int priorityField;
        public int Priority
        {
            get { return priorityField; }
            set { priorityField = value; }
        }

        #endregion
    }
} 

You will note that I have to expose the Presenter as a public property on the view. I really didn't want to have to do this, instead I wanted to communicate with just the presenters, and allow the Presenters to control the Views. Unfortunately, the dependency injection on the Views which instantiates the presenter is only performed with the View added to a WorkItem. Unfortunately, if I'm passing around Presenters instead of Views, I patently need them beforehand. Creating a Presenter by hand is also messy, because you have to manually set up the WorkItem and other base properties on the presenter. It's just a giant pain.

In terms of getting the list of views for display we throw events around. One event, to which all modules must subscribe, asking for a list of views, and more events to pass the instantiated views back to the WizardModule.

The RequestForView event in the WizardModule, and the subscription in ModuleA:

C#
[EventPublication(EventTopicNames.RequestForViews, PublicationScope.Global)]
public event EventHandler<EventArgs<List<string>>> RequestForViews;
protected virtual void OnRequestForViews(EventArgs<List<string>> eventArgs)
{
    if (RequestForViews != null)
    {
        RequestForViews(this, eventArgs);
    }
} 
C#
[EventSubscription(EventTopicNames.RequestForViews, ThreadOption.UserInterface)]
public void OnRequestForViews(object sender, EventArgs<List<string>> eventArgs)
{
    List<IWizardView> views = new List<IWizardView>();
    foreach (string s in eventArgs.Data)
    {
        switch (s)
        {
            case "WizardView1":
                {
                    WizardView1 view = new WizardView1();
                    view.Priority = eventArgs.Data.IndexOf(s) + 1;
                    views.Add(view);
                    break;
                }
            case "WizardView3":
                {
                    WizardView1 view = new WizardView1();
                    view.Priority = eventArgs.Data.IndexOf(s) + 1;
                    views.Add(view);
                    break;
                }
        }
    }
     OnReturnRequestedViews(views);
} 

Now the event used by ModuleA to pass the list of views back to the WizardModule:

C#
[EventPublication(EventTopicNames.ReturnRequestedViews, PublicationScope.Global)]
public event EventHandler<EventArgs<List<IWizardView>>> ReturnRequestedViews;
protected virtual void OnReturnRequestedViews(List<IWizardView> eventArgs)
{
    if (ReturnRequestedViews != null)
    {
        ReturnRequestedViews(this, new EventArgs<List<IWizardView>>(eventArgs));
    }
} 
C#
[EventSubscription(EventTopicNames.ReturnRequestedViews, ThreadOption.UserInterface)]
public void OnReturnRequestedViews(object sender, EventArgs<List<IWizardView>> eventArgs)
{
    viewList.AddRange(eventArgs.Data);
     //All the views that we asked for, have arrived. 
     //If they never arrive (i.e., a module didn't load) at least nothing *breaks*.
    if (viewList.Count == viewNames.Count)
    {
        viewList.Sort(delegate(IWizardView i, IWizardView j)
                    {
                        return i.Priority.CompareTo(j.Priority);
                    });
        viewList.Add((IWizardView)new EndOfWizard());
         ShowWizardHost();
    }
}
 private void ShowWizardHost()
{
    WizardHost wizardHost = new WizardHost(viewList);
     WorkItem.SmartParts.Add(wizardHost, ViewNames.WizardHost);
    WorkItem.Workspaces[WorkspaceNames.RightWorkspace].Show(wizardHost);
}

Note that here is where I've taken a shortcut, and left the rest to you. The list of view names (or whatever other method you decide you want to use to get the views) is hardcoded, you could use a service, a wizard builder or whatever other mechanism you want here.

We create a new Workspace on the WizardHost View, and then add the Workspace to the WorkItem. We then show the first wizard view in the WizardHost's DeckWorkspace. There is some simple logic for the next and previous buttons, and bob is your father's brother.  

The most important thing on the presenter is your Startup method. If this returns false, the Wizard Host will skip over this view and move on. You can still execute code here before returning the boolean. So if you need an 'invisible' view as a save point, you can have it. The startup method is also used to set up the data in the view, call a database, whatever the view needs.

You will also note that the WizardView1 comes up twice. This is just to illustrate that you can have more than one of the same type of view in the wizard, useful if your view performs more than one role. To check the behaviour of the wizard, set the return true; value on wizardview1 to return false;. Watch as the wizard carries on without any problems, skipping both instances of WizardView1.

Points of Interest

This is a *very* basic wizard. There are a lot more features you can add such as:

  • Adding properties to adjust the behaviour of the Wizard to ensure you cannot continue past a View until that View is satisfied that all criteria are met on the View(Validations for example). 
  • With a fair amount of cunning you could make a 'dynamic' Wizard which allows the list of Views to be changed 'on the fly'. 
  • You could add a progress bar to the Wizard host showing which view the user is currently seeing, and how many steps are left.
  • You could certainly make it a lot prettier :)

History

  • 13th March, 2009: Initial post

License

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