Click here to Skip to main content
13,734,201 members
Click here to Skip to main content
Add your own
alternative version

Stats

12.5K views
465 downloads
18 bookmarked
Posted 13 Apr 2016
Licenced CPOL

Another Take on a WPF Wizard

Rate this:
Please Sign up or sign in to vote.
Any technology suitably advanced will generally be viewed as "magic".

Introduction

This article presents my rendition of a WPF Wizard control. I'm aware there are several other attempts to create (some right here on CodeProject), but the way I see it, there's nothing wrong with having more choices.

Background

This is another in my seemingly never-ending series of real-world programming examples, where, instead of theory and flowery praise for this new feature or that new paradigm, I present practical application of thread-worn knowledge. The only thing new about this code is that it's probably never been assembled in this form before. If fate really exists, Microsoft will soon completely adandon support for WPF. They always do that with technology I begrudgingly adopt, especially when I wait the requisite 5-8 years for said technology to mature.

There are several extension classes in the source code that have nothing to do with the wizard, but that were there in the actual app I was writing at work. I left them in there on the off chance that someone might find them useful.

Finally, I'm aware that the way I do things might not be anything like the way you do things. Commenting about style or moving things around in the code because "that's where or how you would do it", will be met with silence (or derision if I'm in a particularly foul mood when I read your comment) unless it would provide tangible improvement regarding code function.

Assumptions

This article is NOT written for rookies. It assumes that the reader has a thorough knowledge of .Net and C#, as well as a reasonably complete working knowledge of WPF. As stated above, I'm not here to discuss theory or best practice. I'm here to share some code I wrote for an actual application. If you want explanations of esoteric principles, Google is your friend.

The Code

Some friendly heads-up notes:

  • As I wrote this article, I decided I wanted to refactor some minor aspects of the code, and those refactorings may not have made it into the final edit of this article (but they had no tangible affect on the article itself).
     
  • Comments that exist in the actual source code were removed in the text of this article in the interest of clutter mitigation.
     
  • The discussion in this section does not include for the compiler directive that forces the code to support demo mode.
     

The wizard has the following major components:

  • CtrlWizard - the user control that represents the wizard
     
  • WizardConfig - a configuration object for the wizard
     
  • WizardPageBase - an abstract base class for all wizard pages
     
  • WizardWindowBase - an abstract base class for wizard forms (that contain the CtrlWizard object)
     
  • WizardSharedData/WizardSharedDataItem - an object that can be used to store data shared between wizard pages, the parent form, and the wizard control
     
  • WizardConfig - contains configuration options for the wizard
     

WizardConfig

In order to efficiently configure the wizard control, I felt that a configuration object that contains appropriate setting properties would be a good idea. This class inherits from another class that derives from INotifyPropertyChanged. I figured that since this is a WPF assembly, it might be a good idea, even though it's not really necessary when you get right down to it because the setup of the wizard only happens one time when the wizard form is created, so making it notifiable is actually kinda pointless (but look at the neat additional code you get to use). Since this class is nothing more than a property bag, I'll only list the properties, what they're for, and what the default values are.

Property Name Default Value Description
ShowNavPanel true bool - Gets/sets display of the navigation panel (that contains the list box) on/off.
ShowContentBanner true bool - Gets/sets display of the title banner on/off.
ShowReset true bool - Gets/sets display of the Reset button on/off.
ShowPrev true bool - Gets/sets display of the Prev button on/off.
ShowNext true bool - Gets/sets display of the Next button on/off.
ShowFinish true bool - Gets/sets display of the Finish button on/off.
ShowCancel true bool - Gets/sets display of the Cancel button on/off.
ShowBannerImage false bool - Gets/sets display of the banner image on/off.
ShowPage1OfN false bool - Gets/sets display of the "Page N of N" title text on/off.
NavListWidth 150 double - Gets/sets the width of the nasvigation ListBox.
ContentBannerHeight 40 double - Gets/sets the height of the title banner.
BannerBackgroundColor Colors.LightSteelBlue Color - Gets/sets the color for the banner/button panel background.
BannerBorderColor Colors.SteelBlue Color - Gets/sets the color for the banner/button panel border.
BannerTiitleColor Colors.Black Color - Gets/sets the color for the banner/button panel title text.
BannerSubtitleColor Colors.Black Color - Gets/sets the color for the banner/button panel subtitle text.
ColorBannerBackground N/A SolidColorBrush - Gets the brush (based on the BannerBackgroundColor) for the banner/button panel background.
ColorBannerBorder N.A SolidColorBrush - Gets the brush (based on the BannerBorderColor) for the banner/button panel border.
ColorBannerTitle N/A SolidColorBrush - Gets the brush (based on the BannerTitleColor) for the banner title.
ColorBannerSubtitle N/A SolidColorBrush - Gets the brush (based on the BannerSubtitleColor) for the banner subtitle.
BannerBorderThicknessValue 2 double - Sets the thichness of the banner/button panel border.
BannerTextAlignment HorizontalAlignment.Left HorizontalAlignment - Sets the alignment of the text in the banner.
BannerImageAlignment HorizontalAlignment.Right HorizontalAlignment - Sets the alignment of the image used in the banner.
Pages null ObservableCollection<WizardPageBase> - Contains the wizards pages.
BannerImageFilePath** string.Empty string - Sets the path to the image used in the banner. This is done in the code so that the image can be loaded from the desired assembly resource.
  • (** Not bound in the wizard control itself.)

WizardSharedData/SharedDataItem

When the wizard is instantiated, an object is created that allows the programmer to store data that can be accessed by other pages, the control itself, and the parent window. I added this because in all of my wizards to date, the pages are designed to modify different aspects of a single viewmodel item.

It's a simple ObservableCollection of items that the programmer can add objects to. It's really nothing special, and warrants no real discussion beyond metioning that you can look at it as a sort of Session variable, like you have in a web application.

Here's a picture of my car, because nobody wants to see an unending page of text.

WizardPageBase

This class is an abstract base class from which all of your wizard pages must be derived. WizardPageBase is derived from UserControl because it's always contained by a CtrlWizard object (which itself is a UserControl-derived object). It's also derived from INotityPropertyChanged (and implements the required interface components) so that binding will work on objects contained in the SharedData collection.

As you might guess, the constructor simply performs some essential initialization, along with some sanity checking. Since we may not yet have created the parent wizard control, we set that value to null. Then we create a handler for the controls IsVisibleChanged event. Because I'm manually creating this event handler, I use a dstructor to unhuck it when the page is disposed. I don't know if .Net has been updated to do this for me, but old habits die hard, and besides, you can never be too safe. Right?

public WizardPageBase(string pageName, string shortName, string subtitle)
{
    if (string.IsNullOrEmpty(pageName.Trim()))
    {
        throw new ArgumentNullException("The pageName must be specified.");
    }
    if (string.IsNullOrEmpty(shortName.Trim()))
    {
        throw new ArgumentNullException("The shortName must be specified.");
    }
    this.PageName  = pageName;
    this.ShortName = shortName;
    this.Subtitle  = subtitle;
    this.IsVisibleChanged += UserControl_IsVisibleChanged;
}

~WizardPageBase()
{
	this.IsVisibleChanged -= UserControl_IsVisibleChanged;
}

This brings us to the only abstract method in the class. The reason we have this method is that all of the pages are instantiated when the wizard control is created, and because all wizard pages are actually UserControl objects, there may be a reason not to perform some app-specific processing until the page is actually visible. This method allows that the programmer to perform said processing. Since the method is abstract, it MUST be implemented in the inheriting class.

protected abstract void OnVisibleChanged();

Other Methods

The following methods are all virtual because they're not required in your own wizard pages, but still can be called to perform processing at specific times.

  • UpdateButtons() - This method is meant to update the buttons in the parent wizard control based on some condition that happens while viewing the page which is usually caused by user interaction with controls in the wizard page. This is the most commonly overridden method in a wizard page.
     
  • Reset() - This method is called by the wizard control when the user clicks the Reset button.
public virtual void UpdateButtons()
{
	if (this.ParentWiz == null)
	{
		throw new Exception("Parent wizard control has not been specified.");
	}
}

public virtual void Reset()
{
}

There are also a couple of event handlers.

  • ButtonClick() - this event handler handles all button clicks made on the buttons in the control wizard controls button panel. The body of this method in the base class contains an example of usage within your own deirved class.
     
  • UserControl_IsVisibleChanged - this event handler simply calls the overriden OnVisiblityChange method in the derived class
     
public virtual void ButtonClick(object sender, ref RoutedEventArgs e)
{
}

protected virtual void UserControl_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
{
	this.OnVisibleChanged();
}

Properties

Property Name Description
ParentWiz CtrlWizard - the wizard control that contains this page
PageName string - the name of the page (shown in the wizard title banner)
ShortName string - the short name. This is intended to be a unique name (case-insensitive) to easily identify the page when using conditional processing with the next/prev buttons.
ParentWizardWindow Window - the parent window that cntains the parent wizard control.
Subtitle string - the (optional) subtitle displayed in the title banner. If subtitle text is not specified for a page, the UI element is collapsed on the wizard to allow for proper placement of the main title.
PrevPage virtual string - the ShortName of the page from which the user navigated to the current page. This is set by the wizard control as part of the normal processing of the Next button. The wizard page itself should NOT set this property.
NextPage virtual string - the ShortName of the page to which navigation will take the user when he clicks the Next button. This property only needs to be set in the event the programmer wants to navigate to a specific page based on some conition being met in the current page.
SharedData WizardSharedData - gets the current shared data object from the parent wizard control.

WizardWindowBase

This class is an abstract base class from which your wizard form must be derived. WizardWindowBase is derived from Window. Unless otherwise noted, all methods and event handlers in this class are virtual.

Initialization

The constructor for this class instantiates the collection of pages, as well as the WizardConfig object.

public WizardWindowBase()
{
	this.Pages     = new ObservableCollection<wizardpagebase>();
	this.WizConfig = new WizardConfig();
}

Methods

  • InitWizard() - the (abstract) mehtod where pages should be instantiated and wizard configuration occurs. This method MUST be overriden in the derived class.
     
  • ConfigureWizard() - this method instantiates the wizard, retrieves the buttons from the wizard (to ease typing later), sets up event handlers, and calls the method that adds the wizard control to the form.
     
  • AddToUI() - adds the specified pages to the wizard control, and adds the control to the form.
     
  • SetupEvents() - adds event handlers for the Finihs, Cancel, and Reset buttons.
     
protected abstract void InitWizard();

protected virtual void ConfigureWizard(Grid grid)
{
	this.WizConfig.Pages = this.Pages;

	this.Wizard          = new CtrlWizard(this.WizConfig);

	this.wizBtnFinish    = (Button)(this.Wizard.finishButtonPanel.Children[0]);
	this.wizBtnCancel    = (Button)(this.Wizard.cancelButtonPanel.Children[0]);
	this.wizBtnReset     = (Button)(this.Wizard.resetButtonPanel.Children[0]);
	this.wizBtnNext      = (Button)(this.Wizard.nextButtonPanel.Children[0]);
	this.wizBtnPrevious  = (Button)(this.Wizard.prevButtonPanel.Children[0]);

	this.SetupEvents();
	this.AddToUI(grid);

	this.isInitialized = true;
}

protected virtual void AddToUI(Grid grid)
{
	foreach(WizardPageBase page in this.Wizard.Pages)
	{
		page.ParentWiz = this.Wizard;
	}
	grid.Children.Add(this.Wizard);
}

protected virtual void SetupEvents()
{
	this.wizBtnFinish.Click += this.WizFinish_Click;
	this.wizBtnCancel.Click += this.WizCancel_Click;
	this.wizBtnReset.Click  += this.WizReset_Click;
}

Event Handlers

These event handlers are self-explanatory, but the Window_Closing handler deserves a brief discussion. I was originally going to put this code into the class destructor, but because the buttons were in a child control (the wizard) that had already been destroyed, I had to use the Window_Closing event instead.

protected virtual void WizReset_Click(object sender, RoutedEventArgs e)
{
}

protected virtual void WizFinish_Click(object sender, RoutedEventArgs e)
{
	this.DialogResult = true;			
}

protected virtual void WizCancel_Click(object sender, RoutedEventArgs e)
{
	this.DialogResult = false;
}

protected virtual void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
	this.wizBtnFinish.Click -= this.WizFinish_Click;
	this.wizBtnCancel.Click -= this.WizCancel_Click;
	this.wizBtnReset.Click  -= this.WizReset_Click;
}

Fields

Field Name Description
wizBtnFinish Button - the Finish button
wizBtnCancel Button - the Cancel button
wizBtnReset Button - the Reset button
wizBtnNext Button - the Next button
wizBtnPrevious Button - the Prev button

Properties

Property Name Description
IsValid bool - set by the wizard control after its been initialized. It simply lets the programmer know if everything is copestic, not specifically what is wrong. Typically, this property will be false if there are no pages specified, one or more pages has the same ShortName.
Pages ObservableCollection<wizardpagebase> - this is the list of pages created in the derived Window.
Wizard CtrlWizard - this is the control wizard, which is instantiated when the derived window calls ConfigureWizard method.
WizConfig WizardConfig - this is the wizard configuration object which is instantiated when the derived window calls ConfigureWizard method.
Here's another picture of my car.

CtrlWizard

This is the "big kahuna" in this article, and is the main reason we're here. During initial development of your wizard form, my advice is to put breakpoints in the exception if's so that the debugger doesn't stop at seemingly bizarre elsewhere in the code because this object didn't initialize correctly.

Initialization

  • Constructor - performs some sanity checks on the WizardConfig object and the page collection, creates the SharedData object, and initializes itself from the WizardConfig object.
     
  • InitWithConfig() - Sets the visibility of the specified wizard pages to Collapsed, adds them to the body of the wizard control, and activates the first page.
     
public CtrlWizard(WizardConfig config)
{
    if (config == null)
    {
        throw new ArgumentNullException("config");
    }
    if (config.Pages == null || config.Pages.Count == 0)
    {
        throw new InvalidOperationException("The WizardConfig.Pages collection cannot be null or empty.");
    }

    this.ActivePageIndex = -1;
    this.ActivePage      = null;

    this.WizConfig       = config;
    this.SharedData      = new WizardSharedData();

    this.InitializeComponent();
    this.DataContext     = this;

    this.InitWithConfig(config);
}

private void InitWithConfig(WizardConfig config)
{
    this.CanEnableFinishButton = config.ShowNavPanel;
    if (this.Pages == null)
    {
        this.Pages = new ObservableCollection<WizardPageBase>();
        foreach(WizardPageBase page in config.Pages)
        {
            this.Pages.Add(page);
            ((UserControl)(page)).Visibility = Visibility.Collapsed;
            page.ParentWiz = this;
            this.gridContentBody.Children.Add(((UserControl)(page)));
        }
        this.PageCount = this.Pages.Count;
        this.SharedData.AddUpdateItem("WizardPages", this.Pages);
    }
    this.UpdateForm(0);
}

Methods

  • UpdatedConfig() - updates the control with new config settings.
     
  • UpdateForm(int newPageIndex) - selects the page specified by the newPageIndex as the active page.
     
  • UpdateForm(string shortName) - selects the page specified by the shortName as the active page.
     
  • UpdateButtons() - standard button updates (button state can be overriden inside each page as necessary)
     
  • IsLastPage() - determines if the current page is the last page (controls state of the Finish and Next buttons).
     
  • IsFirstPage() - determines if the current page is the last page (controls state of the Prev button).
     
  • GetCurrentPage() - gets the currently active WizardPage object.
     
  • PagesAreDistinct() - performs sanity check for unique page names - case sensitivity is not a factor during this check because this code wants to promote the use of truly unique names.
     
public void UpdatedConfig()
{
    this.InitWithConfig(this.WizConfig);
    this.NotifyPropertyChanged("WizConfig");
}

public void UpdateForm(int newPageIndex)
{
    if (this.ActivePageIndex != newPageIndex)
    {
        if (this.ActivePage != null )
        {
            ((UserControl)(this.ActivePage)).Visibility = Visibility.Collapsed;
        }

        this.ActivePageIndex = newPageIndex;
        this.ActivePage      = this.GetCurrentPage();
        this.ActivePageName  = this.ActivePage.PageName;

        ((UserControl)(this.ActivePage)).Visibility = Visibility.Visible;
        this.lbNavigation.SelectedIndex             = this.ActivePageIndex;
        this.NotifyPropertyChanged("PageSubtitle");
        this.NotifyPropertyChanged("PageSubtitleVisibility");
        this.UpdateButtons();
    }
}

public void UpdateForm(string shortName)
{
    if (string.IsNullOrEmpty(shortName))
    {
        throw new ArgumentNullException("A shortName was not returned by the current page. Navidation aborted" );
    }

    WizardPageBase newPage = this.Pages.Where(x=>x.ShortName == shortName).FirstOrDefault();
    if (newPage == null)
    {
        throw new Exception(string.Format("Could not find page '{0}'. Navigation aborted."));
    }

    WizardPageBase currentPage = this.GetCurrentPage();
    int index = this.Pages.IndexOf(newPage);
    this.UpdateForm(index);
}

public void UpdateButtons()
{
    bool isFirstPage  = this.IsFirstPage(this.ActivePage);
    bool isLastPage   = this.IsLastPage(this.ActivePage);
    bool enableFinish = ((this.WizConfig.ShowFinishButton) && (isLastPage || this.CanEnableFinishButton));

    this.btnReset.IsEnabled  = this.WizConfig.ShowResetButton  ? true : false;
    this.btnPrev.IsEnabled   = isFirstPage      ? false : true;
    this.btnNext.IsEnabled   = isLastPage       ? false : true;
    this.btnFinish.IsEnabled = enableFinish;
    this.btnCancel.IsEnabled = this.WizConfig.ShowCancelButton ? true : false;

    this.ActivePage.UpdateButtons();
}

public bool IsLastPage(WizardPageBase page)
{
    bool result = false;
    WizardPageBase last = this.Pages.Last<wizardpagebase>();
    result = (last.PageName == page.PageName && last.GetType().Name == page.GetType().Name);
    return result;
}

public bool IsFirstPage(WizardPageBase page)
{
    bool result = false;
    WizardPageBase first = this.Pages.First<wizardpagebase>();
    result = (first.PageName == page.PageName);
    return result;
}

private WizardPageBase GetCurrentPage()
{
    WizardPageBase currentPage = null;
    if (this.ActivePageIndex >= 0)
    {
        currentPage = (WizardPageBase)(this.gridContentBody.Children[ActivePageIndex]);
    }
    return currentPage;
}

private bool PagesAreDistinct()
{
    int distinct = this.Pages.DistinctBy(x=>x.ShortName).Count();
    bool result = (this.Pages.Count > 0 && distinct == this.Pages.Count);
    return result;
}
Here's a picture of my car, because racecar.

Event Handlers

  • lbNavigation_SelectionChanged() - handles selection changed in the navigation panel lListBox.
     
  • btnReset_Click() - activates the first page of the wizard (the parent form is responsible for resetting application data associated with the wiard instance
     
  • btnPrev_Click() - activates the previous page in the wizard. If the current page was displayed as the result of a condition, the appropriate previous page will be selected.
     
  • btnNext_Click() - activates the next page in the wizard. The current page determines the next page in the sequence (via the page.ButtonClick method).
     
  • btnFinish_Click() - closes the wizard after the last page finishes its processing (ostensibly, saving the data via the page.ButtonClick method).
     
  • btnCancel_Click() - closes the wizard.
     
private void lbNavigation_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    if (!e.Handled)
    {
        ListBox listbox = sender as ListBox;
        this.UpdateForm(listbox.SelectedIndex);
        e.Handled = true;
    }
}

private void btnReset_Click(object sender, RoutedEventArgs e)
{
    this.UpdateForm(0);
}

private void btnPrev_Click(object sender, RoutedEventArgs e)
{
    if (this.HasChildren)
    {
        WizardPageBase currentPage = this.GetCurrentPage();
        currentPage.ButtonClick(sender, ref e); 

        if (!e.Handled)
        {
            string prevPage = currentPage.PrevPage;
            if (string.IsNullOrEmpty(prevPage))
            {
                this.UpdateForm(this.ActivePageIndex - 1);
            }
            else
            {
                this.UpdateForm(prevPage);
            }
            e.Handled = true;
        }
    }
    else
    {
        e.Handled = true;
    }
}

private void btnNext_Click(object sender, RoutedEventArgs e)
{
    if (this.HasChildren)
    {
        WizardPageBase currentPage = this.GetCurrentPage();
        currentPage.ButtonClick(sender, ref e);

        if (!e.Handled)
        {
            string nextPage = currentPage.NextPage;
            if (string.IsNullOrEmpty(nextPage))
            {
                this.UpdateForm(this.ActivePageIndex + 1);
                currentPage = this.GetCurrentPage();
                currentPage.PrevPage = string.Empty;
            }
            else
            {
                string prevPage = currentPage.ShortName;
                this.UpdateForm(nextPage);
                currentPage = this.GetCurrentPage();
                // so we can find our way back if they hit the "Prev" button
                currentPage.PrevPage = prevPage;
            }
            e.Handled = true;
        }
        else
        {
            // do we need to add any processing here?
        }
    }
    else
    {
        e.Handled = true;
    }
}

private void btnFinish_Click(object sender, RoutedEventArgs e)
{
    if (this.HasChildren)
    {
        this.GetCurrentPage().ButtonClick(sender, ref e);
        if (this.Parent is Window)
        {
            ((Window)(this.Parent)).Close();
            e.Handled = true;
        }
    }
    else
    {
        e.Handled = true;
    }
}

private void btnCancel_Click(object sender, RoutedEventArgs e)
{
    if (this.HasChildren)
    {
        this.GetCurrentPage().ButtonClick(sender, ref e);

        if (this.Parent is Window)
        {
            ((Window)(this.Parent)).Close();
            e.Handled = true;
        }
    }
    else
    {
        e.Handled = true;
    }
}

Properties

Property Name Description
IsValid bool - Gets the valid status of the wizard control. It will return false if any of the following conditions are true:
  • the SharedData object is null.
     
  • the WizardConfig is null.
     
  • the collection of wizard pages in the WzardConfig object is null or empty.
     
  • one or more of the pages has a similar ShortName.
PageCount int - Gets/sets the number of pages in the wizard page collection. (*)
ActivePageIndex int - Gets/sets the active wizard page index, and is used for both display and navigation.
ActivePageNumber int - Gets/sets the active page number, and is used for display.
ActivePageName string - Gets/sets the active pages title.
ActivePage WizardPageBase - Gets/sets the active wizard page object
CanEnableFinishButton bool - Gets/sets value that indicates whether or not the default enabled status of the Finish button is enabled (*).
HasChildren bool - Gets a value that indicates whether the wizard control has child pages (shot-hand for checking the count in the collection).
Pages ObservableCollection<WizardPageBase> - Gets/sets the wizard page collection.
SharedData WizardSharedData - Gets/sets the SharedData object.
WizConfig WizardConfig - Gets/sets the WizardConfig object.
PageSubtitle string - Gets/sets the page subtitle text.
PageSubtitleVisibility Visibility - Gets the visibility of the subtitle text, base on the value of the WizardConfig.ShowSubtitle flag AND whether or not the subtitle text is null/empty.
ParentWindow Window - Gets the controls parent window.

(*) Properties marked this way could benefit from some refactoring.

Using the Code - The Sample Application

Know ahead of time that for the purposes of creating a reasonable sample application, I made some changes to the actual wizard configuration object which resolved some binding issues that I was having with the WPF Colors object. I used the compiler directive __DEMO__ in the sample aplication and WpfControls projects to control whether the code is (or is not) in "demo" mode. When you actually use this code, simply deleting the compiler directive from WPFControls will change the way the bound brush properties instantiate the returned SolidColorBrush objects.

The Main Window

The main window is a simple WPF form that allows you to change visual aspects of the wizard. I recognize that it makes absolutely no sense to hide all of the buttons and the title banner, but in the interest of completeness, I allowed that to be done. I want to discuss a couple of things regarding this form, but only because they presented inetersting problems.

Binding to Enumerators

One of the things I ran into was the need to bind a couple of ComboBox controls to the HorizontalAlignment enumerator.

The first step was to define on ObjectDataProvider. If you're doing this for an enumerator that you've created, don't forget taht you have to add/use a namespace declaration that points to your project code.

<ObjectDataProvider MethodName="GetValues" ObjectType="{x:Type sys:Enum}" x:Key="HorzAlign">
    <ObjectDataProvider.MethodParameters>
        <x:Type TypeName="HorizontalAlignment" />
    </ObjectDataProvider.MethodParameters>
</ObjectDataProvider>

Next, I created a DataTemplate in Window.Resources so I could properly display the enumerator item names.

<DataTemplate x:Key="alignComboItem" >
    <StackPanel Orientation="Horizontal" Margin="0,1.5,0,0" >
        <TextBlock Text="{Binding Path=Value}" Margin="5,0,0,0" />
    </StackPanel>
</DataTemplate>

Finally, for the ComboBox itself I set up the appropriate bindings. Notice the binding for the ItemsSource property - it's Binding Source=, instead of the usual Binding Path=.

<ComboBox ... ItemsSource="{Binding Source={StaticResource HorzAlign}}" 

             SelectedValue="{Binding Path=Config.BannerImageAlignment}" />

Selecting Colors

I wanted to keep the form as simple as possible, so I figured I'd setup some comboboxes that allow you to select the desired colors by name instead of doing up some fancy color picker control. To put it bluntly, this was a royal pain in the anal pore, because the WPF Colors object does not lend itself to quick/easy binding to a control. In fact, supporting this idea is the sole reason for the "demo mode" code (notice that the word "demonic" starts with the word "demo"). After taking several runs at this, I settled for the following.

First, I added appropriate string properties to the WizardConfig class to hold the names of the desired color, and added SolidColorBrush properties that use thos names (only one of each is shown for the purpose of example). I also added a helper function that is only used to ease the chore of typing.

#if __DEMO__
string bannerBackgroundColorName = "LightSteelBlue";
#else
...
#endif

public SolidColorBrush BannerBackgroundBrush
{
#if __DEMO__
    get { return new SolidColorBrush(this.NameToColor(this.bannerBackgroundColorName)); }
#else
...
#endif
}

#if __DEMO__
private Color NameToColor(string name)
{
    Color color = (Color)(ColorConverter.ConvertFromString(name));
    return color;
}
#endif

Next, I created a class to represent the named WPF color, and using Reflection, initialized a collection of the colors in the window's constructor. To make it more efficient, I added a SolidColorBrush object to each WpfColor item to satisfy the requirements of the associated XAML data template.

public class WpfColor
{
    public string          Name  { get; set; }
    public SolidColorBrush Brush { get; set; }
}

public MainWindow()
{
	this.WpfColors = new List<WpfColor>();
	PropertyInfo[] properties = typeof(Colors).GetProperties(BindingFlags.Public | BindingFlags.Static);
	foreach(PropertyInfo prop in properties)
	{
		this.WpfColors.Add(new WpfColor()
        { 
            Name = prop.Name, 
            Brush = new SolidColorBrush((Color)(ColorConverter.ConvertFromString(prop.Name))) });
	}
	...
}

Next, I created a data template in the windows XAML.

<DataTemplate x:Key="colorComboItem2" >
    <StackPanel Orientation="Horizontal" Margin="0,1.5,0,0" >
        <Rectangle Width="13" Height="13" Stroke="Black" Fill="{Binding Path=Brush}" />
        <TextBlock Text="{Binding Path=Name}" Margin="5,0,0,0" />
    </StackPanel>
</DataTemplate>

Finally, I added a Combo box with appropriate bindings to the XAML.

<ComboBox ItemsSource="{Binding Path=WpfColors}" 

          ItemTemplate="{StaticResource colorComboItem2}" 

          SelectedValuePath="Name" 

          SelectedValue="{Binding Path=Config.BannerTitleColorName}" />

Wizard Form - The Wizard Pages

To create a wizard page, perform the following steps:

0) - Create a new UserControl, and name it appropriately.

1) - In the XAML, add a namespace reference to the WpfControls.Controls namespace (I used "wiz")

2) - In the XAML, change the class name from UserControl to wiz:WizardPageBase.
 

At this point, your XAML should look something like this:

<wiz:WizardPageBase x:Class="WpfWizard.WizPgIntro"

             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 

             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 

             xmlns:wiz="clr-namespace:WpfCommon.Controls;assembly=WpfCommon"

             mc:Ignorable="d" 

             d:DesignHeight="300" d:DesignWidth="600" >
    <Grid>
    </Grid>
</wiz:WizardPageBase>
3) - In the XAML.CS, add a using statement for WpfCommon.Controls.

4) - In the XAML.CS, change the inherited object type from UserControl, to WizardBasePage.

5) - In the XAML.CS, add the following constructor overload:
 
public PageObject(string pageName, string shortName, string subtitle)
       :base(pageName, shortName, subtitle)
{
    this.InitializeComponent();
}
6) - In the XAML.CS, add the override to the abstract method OnVisibleChanged. This is the method where you would initialize the page when it becomes visible.
 
protected override void OnVisibleChanged()
{
    // TO-DO: add initization code here, such as loading data, 
    //        and/or setting button state.
}

Congradulations. You've got a fully implemented wizard page (with no real functionality of course).

The Wizard Form Itself

To create a wizard form, Perform the following steps:

0) - Create a new Window, and name it appropriately.

1) - In the XAML, add a namespace reference to the WpfControls.Controls namespace (I used "wiz")

2) - In the XAML, change the class name from Window to wiz:WizardWindowBase.

3) - I like to set the backgrounds of my forms to something other than white, so while I'm in the XAML, I take this opportunity to make the desired color change (as well as other changes such as font size, form size, etc). At this point, you should have something that looks like this:
<wiz:WizardWindowBase x:Class="WpfWizard.WndWizard"

        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

        xmlns:wiz="clr-namespace:WpfCommon.Controls;assembly=WpfCommon"

        Title="Wizard Form" Height="400" Width="800" Background="#eaeaea" 

        WindowStartupLocation="CenterOwner">
    <Grid x:Name="gridMain" >
    </Grid>
</wiz:WizardWindowBase>
4) - In the XAML.CS, add a using statement for WpfCommon.Controls.

5) - In the XAML.CS, change the inherited object type from Window, to WizardWindowPage.

6) - In the XAML.CS, add the following lines to the constructor:
 
this.DataContext = this;
this.InitWizard();
7) - In the XAML.CS, add the following method. Take note of the comments at various points in the following code block, and of course, replace all examples with your own code. Remember that all of the necessary objects for a basic wizard (such as the WizConfig, SharedData, and Pages objects) are created in the base class, and are available for immediate use.
 
protected override void InitWizard(WizardConfig config=null)
{
    if (!this.isInitialized)
    {
        // TO-DO: create pages here. Example:
        WizPgIntro pg0 = new WizPgIntro("Introduction",  "page0",  string.Empty );

        // TO-DO: Add pages to the page collection (defined and instantiated 
        //        in the base WizardWindowBase class). Example: 
		this.Pages.Add(pg0);

        // Add the pages to the wizard control, and setup event hooks (see 
        // WpfCommon.Controls.WndWizardBase.cs). This method can be overridden, 
		// but MUST be called.
        this.ConfigureWizard(this.gridMain);

		// TO-DO: If you want to change the config settings from their default 
        // values, this is where you do it.
        if (this.Wizard != null)
        {
            // Examples:
            this.WizConfig.ShowResetButton      = false;
            this.WizConfig.ShowNavPanel         = true;
            this.WizConfig.ShowPage1OfN         = false;
            this.WizConfig.ContentBannerHeight  = 80;
            this.WizConfig.NavListWidth         = 250;
            this.WizConfig.ShowBannerImage      = false;
            this.WizConfig.BannerTextAlignment  = HorizontalAlignment.Left;
        }

        // You only have to call this method if you changed the config defaults.
        this.Wizard.UpdatedConfig();

        // TO-DO: Add shared data if applicable. Example:
        this.Wizard.SharedData.Add(new WizardSharedDataItem("SampleData", sample));
    }
}

The Wizard in the Sample Application

The sample application is VERY limited as far as what's going on. There are five sample wizard pages. The intro and finish pages don't do anything other than display private static data. The pages in between is where wee exercise the conditional page sequence and data shared between pages.

Sample Page 1

This page presents a CheckBox that, when checked when the Next button is clicked, causes the wizard control to display Sample Page 3 instead of Sample Page 2. If you're also displaying the navigation listbox, you will see the listbox update according to the checkbox state. This is determined by setting the NextPage property:

public override string NextPage
{
    get
    {
        return (this.cbNextPage.IsChecked == true) ? "page3" : "page2";
    }
    set
    {
        base.NextPage = "";
    }
}

Sample Page 2

This page presents the value of a SharedData item, and a checkbox that enables/disables the next button. This exercises the virtual UpdateButtons method. If the checkbox is unchecked, the Next button is disabled (but you can still select the page in the navigation list box). In this case, the UpdateButtons() interaction is triggered by a call from the Checked and Unchecked handlers.

Notice that we check the IsReady property before we handle the event. The reason is that if you set default values in the XAML, the event to fire BEFORE the page has been added to the parent wizard control, which may cause an exception to be thrown.

public override void UpdateButtons()
{
    this.ParentWiz.nextButtonPanel.IsEnabled = (this.cbEnableNext.IsChecked == true);
}

private void CheckBox_Checked(object sender, RoutedEventArgs e)
{
    if (this.IsReady)
    {
        this.UpdateButtons();
    }
}

Additionally, clicking the Next button causes a new SharedData item to be added for display in Sample Page 3. This new item is NOT added (intentionally) if you use the Navigation list box to display Sample Page 3.

public override void ButtonClick(object sender, ref RoutedEventArgs e)
{
    if (sender is Button )
    {
        Button button = sender as Button;
        switch (button.Name)
        {
            case "btnNext" : 
                this.SharedData.AddUpdateItem("SampleDataChanged", 
                                               string.Format("{0} for a different page", 
                                                             this.SampleData)); 
                break;
        }
        e.Handled = false;
    }
    else
    {
    }
}

Sample Page 3

This page presents both shared data items (if both exist), and removes that data when the user clicks the Prev button.

public string SampleData 
{ 
    get 
    { 
        return this.GetValue("SampleData"); 
    } 
}

public string SampleDataChanged 
{ 
    get 
    { 
        string data = this.GetValue("SampleDataChanged");
        return string.IsNullOrEmpty(data) ? "SharedData item not present" : data; 
    } 
}

public override void ButtonClick(object sender, ref RoutedEventArgs e)
{
    if (sender is Button)
    {
        Button button = sender as Button;
        switch (button.Name)
        {
            case "btnPrev" : 
                this.SharedData.DeleteItem("SampleDataChanged");
                break;
        }
        e.Handled = false;
    }
    else
    {
    }
}

Points of Interest, Caveats, and Emptors

Today's point of interest is Fort Stockton, TX - home of the world's largest roadrunner. Google it, and prepare to be amazed.

History

  • 14 Apr 2016 - Initial release.
     

License

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

Share

About the Author

John Simmons / outlaw programmer
Software Developer (Senior) Paddedwall Software
United States United States
I've been paid as a programmer since 1982 with experience in Pascal, and C++ (both self-taught), and began writing Windows programs in 1991 using Visual C++ and MFC. In the 2nd half of 2007, I started writing C# Windows Forms and ASP.Net applications, and have since done WPF, Silverlight, WCF, web services, and Windows services.

My weakest point is that my moments of clarity are too brief to hold a meaningful conversation that requires more than 30 seconds to complete. Thankfully, grunts of agreement are all that is required to conduct most discussions without committing to any particular belief system.

You may also be interested in...

Comments and Discussions

 
QuestionI found a small issue Pin
John Simmons / outlaw programmer9-Nov-16 4:09
memberJohn Simmons / outlaw programmer9-Nov-16 4:09 
QuestionTexas car Pin
Dennis Dykstra19-Apr-16 13:58
memberDennis Dykstra19-Apr-16 13:58 
QuestionFive stars Pin
FrodoFlee14-Apr-16 17:03
memberFrodoFlee14-Apr-16 17:03 
QuestionAnd the winner of the most gratuitous placement of a picture of a car goes to... Pin
Chris Maunder14-Apr-16 8:06
adminChris Maunder14-Apr-16 8:06 
AnswerRe: And the winner of the most gratuitous placement of a picture of a car goes to... Pin
John Simmons / outlaw programmer14-Apr-16 22:40
memberJohn Simmons / outlaw programmer14-Apr-16 22:40 
GeneralCar Pin
Warrick Procter13-Apr-16 23:11
memberWarrick Procter13-Apr-16 23:11 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Cookies | Terms of Use | Mobile
Web01-2016 | 2.8.180920.1 | Last Updated 13 Apr 2016
Article Copyright 2016 by John Simmons / outlaw programmer
Everything else Copyright © CodeProject, 1999-2018
Layout: fixed | fluid