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 SolutionFortunately, 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.