Wednesday, February 18, 2009

WPF Wizard Control - Part II

In my previous post about rolling your own WPF wizard control, I've described how one can easily create simple, styleable wizard in WPF. Generally, I blogged that the wizard consists of two things, namely
  • Wizard class - representing the wizard with its buttons (Next, Previous, Finish, etc.) and simple behavior concerned around managing which wizard page should be displayed,
  • WizardPage class - representing a container for a single wizard page.
The first class inherited from the Control class whereas the second inherited from the ContentControl class. Because the Wizard class wasn't derived from any control that can have content, it had to have a bindable collection of wizard's pages. Thus, I brought WizardPagesCollection class into play, which was defined as follows:

    1 
    2     /// 
    3     /// Wizard pages collection.
    4     /// 
    5     public class WizardPagesCollection : ObservableCollection<WizardPage>
    6     {
    7 
    8     }

Besides the collection of pages, the Wizard had two properties, namely
  • ActivePageIndex - the index of the current page in the collection being displayed,
  • ActivePage - the active page itself.
Of course, both properties depends on each other, i.e. if ActivePageIndex changes, ActivePage has to be updated accordingly. This was done using dependency property callbacks. Moreover, I used coerce callbacks to validate the values.
After coding this solution I realized there's a problem with it - the only wizard page that is the part of the wizard's logical tree is the page being displayed! This meant that DataContext property wasn't set correctly, and binding to controls across wizard pages was not possible.

A better Solution

Fortunately, it is very easy to overcome these problems, even without modifying wizard's template! My first assumption about wizard's base class was mistaken, because wizard is indeed a control that should have a content - its own pages :) And because there can be more than one page, the choice is obvious - ItemsControl.

Inheriting from ItemsControl reduced the following code from the Wizard class:

    1 
    2 private WizardPagesCollection m_WizardPages;
    3 
    4 /// 
    5 /// Returns a collection of wizard's pages.
    6 /// 
    7 public WizardPagesCollection WizardPages
    8 {
    9     get { return m_WizardPages; }
   10     set
   11     {
   12         m_WizardPages = value;
   13         m_WizardPages.CollectionChanged += OnWizardPagesChanged;
   14     }
   15 }
   16 
   17 private void OnWizardPagesChanged(object sender, NotifyCollectionChangedEventArgs e)
   18 {
   19     // This code glues all wizard's pages to wizard's DataContext.
   20     // This is done due to the fact that when pages are switched, the
   21     // page that is hidden looses its data context.
   22     foreach (var page in WizardPages)
   23     {
   24         var binding = new Binding("DataContext") { Source = this };
   25         BindingOperations.SetBinding(page, DataContextProperty, binding);
   26     }
   27 }

This was actually the ugly code that attached the value of Wizard's DataContext property to each wizard's page DataContext which eliminated the problem no. 1. And because now wizard contains all the pages within its Items collection (which can be databound via ItemsSource property), all pages appear in the wizard's logical tree. Thus, problems described earlier in this post no more exist :)

Because now I used ItemsControl, I could virtually put anything in the wizard and it will compile. But the wizard expects to contain instances of WizardPage class, so I needed to tell the wizard that it need to wrap any content that is not a WizardPage into an instance of WizardPage. This is done using the following code:

    1 protected override DependencyObject GetContainerForItemOverride()
    2 {
    3     return new WizardPage();
    4 }
    5 
    6 protected override bool IsItemItsOwnContainerOverride(object item)
    7 {
    8     return item is WizardPage;
    9 }

Note, however, that in this solution, both ActivePageIndex and ActivePage properties still play vital role. Also note that there is not a single change in the Wizard's template, which is very simple. The one problem with it is that it defines the "view" for the wizard and for the wizard's page. This will be fixed in the third release :)

I will blog about better approach to implementing custom WPF wizard which fixes the complexity of using the two mentioned properties and uses separate templates for the wizard and its pages in my next post, so stay tuned ;)

The code for this post can be downloaded from here. Note that it actually contains three Wizard's implementations. The one described in this post is contained within WpfWizard2 project. WpfWizard3 will be subject of my next post.

11 comments:

Mladen said...

Thanks for this. It's a problem I'm currently trying to solve and your solution seems pretty elegant to me. Not sure why you had Java people commenting on your previous Wizard post - I like it and please keep it up!

Steve Skarupa said...

In the way of a disclaimer: I'm a WPF newbie and still learning the ropes...

I really like the simplicity of your wizard control and have decided to use it for a screen in my application. I tried to incorporate your wizard (wpfwizard2) into my application but I am seem to have developed a strange problem that is keeping any of my XAML designers from loading.

In the Generic.xaml- WIZARD NAVIGATION section, you bind the Button.IsEnabled to a MultiBinding using the navigationMultiConverer. In your sample project there are no errors, but when I brought in this code (and merged it with my other resourceDictionaries) the designer is about a "Problem Loading" and when I inspect the errors window it says "specified cast is not valid".

The code compiles and runs correctly (wizard displays and functions fine), but the designer does not load. Because it works at runtime, I normally wouldn't let it bother me but until I get my head completely around the new mark up and libraries, I rely heavily on the designer to show me a preview of my layout...

Do you know what the issue might be? Where I should start digging?

Thanks for your time.

-Steve

Steve Skarupa said...

I solved the problem... The "Ah-ha" moment came while I was reading an article on troubleshooting WPF designer load failures (http://msdn.microsoft.com/en-us/library/bb546934.aspx). It became clear that some code is actually executing at design time. In this case the Navigation MultiConverter was firing but the values parameter is null at design time. So putting in a bit of defensive code to check for null and default to false fixed the problem in the designer.

I added the following lines of code to the top of the Convert method in the NavigationMultiConverter class:

if (values == null || values.Length < 2) return false;
if (!(values[0] is bool)) return false;
if (!(values[1] is bool)) return false;

Thanks again for your efforts!

-Steve

Christian Hagelid said...

Hi. Thank you sharing this wizard solution.

Disclaimer: I am very much a WPF noob!

I liked your approach to implementing the wizard control so I decided to use it in an application I am building. My question is around design time support. At the moment I can't seem to figure out how to get design time support beyond WizardPage one. Is there a way I can configure the application in order to get design time support for the rest of the wizard as well? Maybe put each WizardPage in a separate XAML some how?

thanks again for sharing

Timothy Parez said...

Works great with one exception, whenever you go back the sidebar dissapears...

patanpatan said...

Will you post about your third implementation of the wizard?

Anonymous said...

how can i add vertical scroll in the content??

Anonymous said...

Hehe, that code looks familiar :-)

mb said...

Piotr, thanks for your control. Really nice work.
However i'm having problems with dynamically added Wizard Pages and setting correct DataContext for them. Is there some preferrable way to add in runtime pages to Wizard and set DataContext for them?

I'm trying to perform following steps:

1) create a set of pages in design time - these would be pages, that i can use in future

2) define desired set of pages and add them to collection

WizardPagesCollection oldSetOfPages = restoreWizard.WizardPages; // oldSetOfPages - these are the Pages, that I created in design time

restoreWizard.WizardPages = new WizardPagesCollection(); // create a new collection

foreach (IConfigurationWizardTabBase pageViewModel in WizardPagesViewModels)
{
switch (pageViewModel.PageTitle)
{
case "StartPage":
WizardPage startPage = oldSetOfPages.SingleOrDefault(wp => wp.Name == "StartPage");
restoreWizard.WizardPages.Add(startPage);
break;

}

..............
}

3) Set DataContext for all pages from Set 2 in a separate cycle

The problem is that for some pages Navigating to next page works only after second click on "Next" button.

So, again, my question is is there a preferrable way to add pages for wizard and set DataContext for them in run time? And if my approach is correct, where can the probelm with "Next" button be?

Uchiha Itachi said...

Very interesting, giving much insights to my WinForms Wizard control problem.

Anonymous said...

KetticWizard Control for Windows Forms