I recommend briefly examining attached source code before reading the post because the code is not short enough to be pasted on a blog. Still reading "pure" text without the code is far from being nice :)
As you probably know, WPF defines so called look less controls. This means the look and feel of the user controls is completely separated from its behavior. So firstly, I'll go through the wizard's behavior, and at the end I'll explain wizard's style in a few sentences. For now let's only assume that the wizard has navigation buttons such as Next, Previous, Finish, etc., and three places for content: wizard's header, left (side) header and of course a place for displaying main content. Note that I'm not defining yet where these pieces are going to be displayed, for now I only assume they exist somewhere.
The first decision I had to take was the class I needed to inherit from. The options were UserControl or a Control. Because I wanted to give the wizard some styling capabilities (via ComponentResourceKey) and also I wanted it to look more "professional" I decided to inherit from a Control class. There's yet another factor that actually convinced me no to using UserControl. UserControl directly inherits from a ContentControl, and the wizard itself does not have content of any kind! Wizard's content is provided by means of wizard pages and the wizard should display one page at a time. Precisely, wizard will "know" which page it should display (i.e. which page is the current one) and the wizard's template will contain ContentControl that will be databound to the main content of the current page. Of course the same approach will be used for the headers.
As you may have already guessed, single wizard page is represented by WizardPage class. Because it indeed has a content, it directly inherits from ContentControl. And because single WizardPage may provide optional header and side header, it has corresponding dependency properties of type object. Besides, this class defines some other properties like CanXXX which indicate if XXX navigation button is enabled for the page, and PageClose along with PageShow events that are raised whenever a page is closed or shown. As outlined above, the Wizard class contains a collection of WizardPage class.
The Wizard class contains two important dependency properties - ActivePage and ActivePageIndex. I hope their names are self descriptive. Not surprisingly, these two properties depend on each other, i.e. if I change ActivePageIndex, I expect ActivePage will automatically get updated, and vice versa. Moreover, I don't want to receive an error if I accidentally set inappropriate value for these properties. All this can be achieved using change and coerce callbacks. Here's the code:
225
226 private static void OnActivePageIndexChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
227 {
228 Wizard wizard = (Wizard)d;
229 int index = (int)e.NewValue;
230 int oldIndex = (int)e.OldValue;
231
232 if (index != -1 && index != oldIndex)
233 wizard.ActivePage = wizard.WizardPages[index];
234 else if (index == -1)
235 wizard.ActivePage = null;
236 }
237
238 private static object CoerceActivePageIndex(DependencyObject d, object value)
239 {
240 Wizard wizard = (Wizard)d;
241 int index = (int)value;
242
243 if (index >= wizard.WizardPages.Count)
244 return wizard.WizardPages.Count - 1;
245
246 if (index >= 0 && index < wizard.WizardPages.Count)
247 return index;
248
249 if (index < 0 && wizard.WizardPages.Count > 0)
250 return 0;
251
252 return -1;
253 }
254
255 private static void OnActivePageChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
256 {
257 Wizard wizard = (Wizard)d;
258 WizardPage page = (WizardPage)e.NewValue;
259 WizardPage oldPage = (WizardPage)e.OldValue;
260
261 if (page != null && oldPage != page)
262 {
263 // Raise event
264 if (oldPage != null)
265 oldPage.OnPageClose();
266
267 // update the index
268 int index = wizard.WizardPages.IndexOf(page);
269 wizard.ActivePageIndex = index;
270
271 // Set boundary values for navigation buttons
272 if (index == 0)
273 {
274 wizard.ActivePage.CanNavigatePrevious = false;
275 if (wizard.WizardPages.Count == 1)
276 wizard.ActivePage.CanNavigateNext = false;
277 }
278 else if (index == wizard.WizardPages.Count - 1)
279 wizard.ActivePage.CanNavigateNext = false;
280
281 // After page is up and runnig, rais event
282 page.OnPageShow();
283 }
284 else if (page == null)
285 {
286 // Raise event
287 if (oldPage != null)
288 oldPage.OnPageClose();
289
290 wizard.ActivePageIndex = -1;
291 }
292 }
293
294 private static object CoerceActivePage(DependencyObject d, object value)
295 {
296 Wizard wizard = (Wizard)d;
297 WizardPage page = (WizardPage)value;
298
299 int index = wizard.WizardPages.IndexOf(page);
300
301 // Given page does not exist in the internal collection
302 if (index == -1)
303 {
304 if (wizard.WizardPages.Count > 0)
305 return wizard.WizardPages[0];
306
307 return null;
308 }
309
310 return page;
311 }
OnActivePageChanged callback is especially important, because beside updating ActivePageIndex, it performs two other things. Firstly, it raises two events on a WizardPage class - it raises PageClose event on a page that is about to be replaced, then the page gets replaced and PageShow event is raised on a new page. And secondly, it checks if the current page is first or last in the wizard and enables or disables Next/Previous buttons accordingly.
Wizard's look & feel is defined in Themes\Generic.xaml using simple grid layout. The most important part of the control's template is how actually content of wizard's active page gets displayed in the wizard. This is accomplished using content placeholders in form of ContentControl. There is one problem with this design, however. All wizard's pages except the active one are NOT part of the logical tree as they are simply not displayed. And if the active page gets replaced, it is automatically removed from the visual tree. This implies two things. The first one is that the page's DataContext propertyis not propagated to the parent, i.e. if you put an instance of some class in the window's DataContext and you bind controls inside pages to this instance, this won't work (yes, I'm talking about PresentationModel pattern). The second thing is that you cannot bind controls with each other. Hopefully, there is an easy solution to overcome the first problem:
44
45 ///
46 /// Returns a collection of wizard's pages.
47 ///
48 public WizardPagesCollection WizardPages
49 {
50 get { return m_WizardPages; }
51 set
52 {
53 m_WizardPages = value;
54 m_WizardPages.CollectionChanged += OnWizardPagesChanged;
55 }
56 }
142
143 private void OnWizardPagesChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
144 {
145 // This code glues all wizard's pages to wizard's DataContext. This is done due to the fact that when pages are switched, the
146 // page that is hidden looses its data context.
147 foreach (var page in WizardPages)
148 {
149 var binding = new Binding("DataContext") { Source = this };
150 BindingOperations.SetBinding(page, DataContextProperty, binding);
151 }
152 }
The second one is still unsolved, but because PresentationModel does work with the wizard, it's not a big deal (Update: to see how to solve these problems, see the second post).
You can download full sample from my Code Gallery.
Have fun!
8 comments:
It's much easier to implement such stuff in Eclipse RCP.
I would be surprised if it was harder. However, bear in mind that WPF is not a Rich Client Platform but rather a gui toolkit and as such is not directly comparable with Eclipse Platform ;)
Reuse, reuse, reuse ... Why for God's sake you want to develop another framework for Wizards ?!
hi Can I have complete code for Navigation Wizard in wpf, if you dont mind.
Can you supply the styling code necessary to produce the cool blend like gray look and feel?
This does the job so nicely for me. It works as expected (not always the case wiht code found on the Internet!) Thank you so much. I can concentrate on other things now.
"Welcome to Data Analysis Wizard" - snap shot displayed...but could you please let me know where in the solution the look and feel is present. It appears that there are 3 projects of which the second and the third are replicas of the first type.
Very interesting, giving much insights to my WinForms Wizard control problem.
Post a Comment