Click here to Skip to main content
15,886,840 members
Articles / Desktop Programming / WPF

A (Mostly) Declarative Framework for Building Simple WPF-based Wizards

Rate me:
Please Sign up or sign in to vote.
5.00/5 (2 votes)
7 Mar 2011LGPL322 min read 19.3K   229   15   1
A declarative framework for building WPF wizards.

Introduction

Wizards have to be one of the more popular approaches to walking a user through a process. I know I appreciate them. But writing them has always struck me as involving a lot of boiler-plate code. And trying to structure things to make the individual pages reusable gets pretty complicated pretty quickly.

The wizard framework is part of a growing collection of utilities that I've made available in a series of assemblies. Here's how you can download the latest version of the software:

Installerhttp://www.jumpforjoysoftware.com/system/files/Olbert.Utilities-Setup-0.5.1.exe
Help (CHM) filehttp://www.jumpforjoysoftware.com/system/files/OlbertUtilitiesHelp.chm
Source code: Trunkhttps://svn.olbert.com/Olbert.Utilities/trunk/
Source code: v0.5.1https://svn.olbert.com/Olbert.Utilities/tag/0.5.1/

The product page for the library is located at http://www.jumpforjoysoftware.com/product/5. You might be interested in looking at what else I've made available on the site, at http://www.jumpforjoysoftware.com/.

There are a couple of wizards built with the framework contained in the library collection. One is Olbert.Utilities.WPF.DatabaseSelector, which as you might imagine, is a wizard for selecting a SQL Server database. Another is Olbert.Utilities.nHydrate.Installer.nHydrateDatabaseSelector, which extends DatabaseSelector for use in selecting SQL Server databases configured to work with the nHydrate data modeling environment.

If you haven't checked out nHydrate, you should (http://nhydrate.codeplex.com/; there are also a number of articles about nHydrate available on CodeProject). It's a cool tool!

Background

The concept of a wizard sounds like a workflow to me, so I spent quite some time reading up on Windows Workflow, figuring there must be some nifty support tools for crafting wizards buried in it. There probably are, but I wasn't able to find the kind of simple framework which handled the plumbing of what I wanted to do in a wizard while I focused on defining the "what". So in the end, I decided to roll my own. But fair warning: the framework I'm describing here could be totally obsolete even before it's published.

As I was developing the wizard framework, I ran into some interesting limitations in the current version of XAML. Because the kind of wizards I usually write work by modifying a state variable (i.e., as the wizard progresses, the state of the wizard gets more and more defined) and state variables are very solution-dependent, it would be convenient to create a Generics-based wizard-hosting window. That's not supported all too well in WPF 4, although apparently it will be supported in the future.

But for now, that means my design had to give up strong typing of the wizard's state. For similar reasons, I also had to give up strong typing of the wizard hosting window. As a result, the plumbing routines that handle state management behind the scenes do a lot of Type checking, because the code can't be sure that you haven't passed it a SqlServerConfigurationState object when it was expecting a MyFavoriteVideoGameState object. Among other things, this caused me to gain a new appreciation for the value of strong typing :).

Another facet of WPF programming which I was aware of, but had to really come to terms with in building and using the wizard framework, involves the distinction between functional inheritance and visual inheritance. It would be really convenient to derive a window class from another window class and have all of the UI elements in the base class be carried over into the new, derived class. Unfortunately, that's not possible in WPF today, and probably not in the future, either, I suspect. If you try to do that today, you'll get a nice error message from the compiler reminding you that you can't derive the UI portion of one window from another window class which itself has a UI defined for it.

But you can work around the limitation pretty easily, at the expense of some redundancy in your coding. You just need to remember that while the XAML file for a window and the code-behind file for a window are intimately related, they are not inextricably linked. They each define a different aspect of what it means to be that particular kind of window. The XAML file describes what the window looks like, while the code-behind describes how the window functions. Granted, implementing that functionality often requires referencing components in the UI -- think about getting the text contained in a TextBox -- but you don't have to do that by directly referencing the control in question. You can do it indirectly, by defining virtual methods or properties. So instead of writing something like this:

C#
// code fragment from some method
string myTextBoxValue = tbxMyTextBox.Text;

you can write it like this:

C#
protected virtual string MyTextBoxValue
{
    get { return null; }
    // implement in the derived class
}

// code fragment from some method
string myTextBoxValue = MyTextBoxValue;

Granted, you now have to write an override to "link" the MyTextBoxValue to a particular control in the UI of the derived class. But you've gained the ability to reuse the functionality of your base window class "inside" multiple UIs.

In practice, this approach would get awfully tedious and annoying if there were a lot of links between your window's functionality and its UI. In that case, I bet it would be easier to think about writing a code-generation template. But if there aren't too many links between the UI and the functionality, it's a practical approach.

Using the code

In the case of the wizard framework, there's only one link:

C#
/// Gets the Frame object which will contain the various WizardPage objects. This base
/// implementation always returns null, and must be overridden in your derived class.
/// </summary>
public virtual Frame PageFrame 
{
    get { return null; } 
}

/// <summary>
/// Gets the object used to hold the wizard's state. This base
/// implementation always returns null, and must be overridden in your derived class.
/// </summary>
public virtual object WizardState 
{
      get { return null; }
     protected set { }
} ]]>

Your derived UI must contain a Frame element for hosting the individual wizard pages, because each page must derive from the custom WizardPage class defined in the library. The PageFrame property makes that element accessible to the code in the WizardWindow class. The WizardState property, while not a link to a UI element, must also be defined in your derived wizard-hosting window class. It gives the plumbing code access to whatever object you're using to hold the wizard's state information.

How it works: Configuring the wizard hosting window

You build a wizard using the framework by making a bunch of declarations about each page of the wizard in a window you derive from a special Window class, WizardWindow. WizardWindow provides the plumbing for the wizard, as well as gives you opportunities to affect the page flow (e.g., by letting you cancel the user's transition to the next page of the wizard).

Defining the XAML for your wizard-hosting window is done by simply adding a new Window class to your project and then modifying it to derive from WizardWindow. This involves adding some stuff to the window's XAML file and editing the code-behind. Here's what a modified XAML file looks like:

XML
<![CDATA[<maowiz:WizardWindow x:Class="MyWizardApp.SimpleWizard"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:maowiz="clr-namespace:Olbert.Utilities.WPF;
                  assembly=Olbert.Utilities.WPF.Wizard"
    x:Name="TheWindow"
    RegularPageNextText="Next"
    LastPageNextText="Finish">
<!-- body omitted for clarity -->
</maowiz:WizardWindow>]]>

The important things to recognize here are that you have to define a namespace that refers to the assembly where WizardWindow is defined, which you use as the opening declaration of your window's UI definition. In the example, I've given that required namespace the prefix "maowiz".

You might be confused by the opening declaration referring to WizardWindow rather than your custom window's class. In reality, the declaration refers to both. You can read that first line as "create a window UI whose functionality derives from the WizardWindow class and link it to the functionality defined in the SimpleWizard class defined in the current assembly".

Naturally, the local class you're linking your UI to must be derived from the same class that declares the "starting point" for the XAML definition. If you don't, you'll get a compiler error message reminding you that the UI's base class must be the same as the code-behind's base class. The required modification to the code-behind to achieve this is very simple. You just replace the base class name in the class declaration line:

C#
<![CDATA[namespace MyWizardApp
{
    // this used to derive from plain old Window
    public partial class SimpleWizard : WizardWindow
    {]]>

By the way, when making these changes, it's not uncommon to have to compile the project twice to get everything "fixed up" correctly. On occasions, I've even had to do a Rebuild All. This isn't surprising given all the housekeeping Visual Studio has to do behind the scenes, but it can be a bit disconcerting the first time you get some wonderfully unhelpful error message like "Unknown build error, required file could not be found".

You'll also need to override WizardWindow's PageFrame and WizardState properties to point to the objects you're using in your window's UI to hold the wizard pages (which must be a Frame element) and your wizard's state.

C#
<![CDATA[
/// <summary>
/// Overrides the base implementation to get the Frame object within the window which 
/// hosts the individual wizard pages
/// </summary>
public override Frame PageFrame
{
    get { return frmWizard; }
}

/// <summary>
/// Overrides the base implementation to get the object that holds the wizard's
/// state
/// </summary>
public override object WizardState { get; protected set; }]]>

Since you'll want the wizard to be able to switch pages, you need to hook up WizardWindow's navigation methods to your UI's navigation elements:

C#
<![CDATA[
private void btnCancel_Click( object sender, RoutedEventArgs e )
{
    OnWizardCanceled();
}

private void btnPrevious_Click( object sender, RoutedEventArgs e )
{
    MoveToPreviousPage();
}

private void btnNext_Click( object sender, RoutedEventArgs e )
{
    MoveToNextPage();
}]]>

Finally, you need to overload WizardWindow's OnWizardLoaded() method to start up the wizard:

C#
<![CDATA[
/// <summary>
/// Overrides the base implementation to start the wizard on the introductory page
/// </summary>
protected override void OnWizardLoaded()
{
    base.OnWizardLoaded();

    // start it up!
    PageNumber = 0;
}]]>

Because all page shifts are initiated by changing WizardWindow's PageNumber property, you start the wizard by setting that property to the page number of whatever page you want to show first. This isn't done for you in the plumbing code because sometimes you don't want to start a wizard on the first page (e.g., it might be an introductory page that you want to give experienced users a chance to skip).

There are a host of other methods you can override and events you can respond to defined in the WizardWindow class. One of those, OnWizardCompleted(), may be of particular interest, since it's called when the wizard is successfully completed, giving you an opportunity to do something with whatever the user just defined. You don't always need to override it. In fact, if all your wizard is doing is defining a particular state variable, you could display it modally and then extract the final state information after ShowDialog() returns successfully.

One final point about declaring the wizard hosting window: RegularPageNextText and LastPageNextText are dependency properties of WizardWindow which let you define the text displayed by your wizard in the Next button. Setting them in your XAML window declaration isn't enough, by itself, for the values you specify to show up in the wizard when it runs. You have to also bind WizardWindow's CurrentNextText property to the content of the "Next" button on your window's UI:

C#
<![CDATA[
    <Button Name="btnNext"
            HorizontalAlignment="Right"
            VerticalAlignment="Bottom"
            Margin="3" 
            IsEnabled="{Binding ElementName=TheWindow, Path=CanMoveNext}"
            Content="{Binding ElementName=TheWindow, 
                      Path=CurrentNextText, NotifyOnSourceUpdated=True}"
            Click="btnNext_Click" />]]>

This can't be done in code because WizardWindow, not being tied to any particular UI, doesn't "know" which UI element corresponds to the "Next" button (actually, as I write this, it occurs to me this could be done by WizardWindow if it defined a virtual property that you'd link to a particular UI element by overriding the property in your derived class; one more for the to-do list...).

The above code fragment also shows another optional but strongly advisable to use feature of WizardWindow: controlling the ability of the user to move by enabling or disabling the wizard navigation buttons. In the example, this is being done by binding the "Next" button's IsEnabled property to WizardWindow's CanMoveNext property. CanMoveNext and its counterparts CanMovePrevious and CanCancel can be set by either your derived wizard-hosting window or individual wizard pages. But that will only work if you wire up the functionality.

By the way, the binding expressions in the example use the name of the wizard-hosting window (not being particularly clever, I called it "TheWindow"). This is a critical optional property of a window (or control, for that matter) declaration. There are other ways of giving the .NET Framework the information it needs to "find" the window at runtime, but the easiest is simply to name the window and use that.

You may wonder why you have to identify a window to be able to access its properties when the property access is taking place inside the definition of the window itself. I admit I find that pretty confusing myself. I think the reason has something to do with the fact that the XAML file "puts a face on" the code-behind file, but isn't itself part of the entity defined by the code-behind. In any event, it's easy enough to do once you get the hang of it.

How it works: Data flow

Before getting into the details of adding WizardPages to the wizard, it's worth talking about how data moves through the wizard as it runs. For each page displayed, the following sequence of activities occurs:

  • If we're already displaying a wizard page and we're moving forward to a later page:
    • retrieve the values of the bound properties on the current WizardPage
    • give the wizard codebase a chance to modify the bound properties
    • give the wizard codebase a chance to validate the modified property collection; if validation fails, cancel the page transition
    • if validation succeeds, update the state object to reflect the modified page property values
    • give the application codebase one last chance to cancel the transition
  • If we're already displaying a wizard page but we're moving backwards to an earlier page, don't bother processing the current page's values; just proceed with the transition
  • If we're displaying the first wizard page, just proceed with the transition
  • If we're moving off of a wizard page, give the "expiring" page a chance to do any necessary cleanup (e.g., of resources it may have consumed)
  • Create an instance of the new, soon-to-be-displayed WizardPage-derived class
  • Retrieve the values of all properties in the state object bound to a property on the new WizardPage from the state object
  • Give the wizard codebase a chance to modify the retrieved bound values
  • Assign the modified state values to the WizardPage properties to which they are bound
  • Display the new wizard page and give its codebase a chance to do any final initialization
  • Allow user interaction with the WizardPage
  • A page transition triggered by the user occurs
  • And repeat until there are no more wizard pages to display

These steps are handled by various protected virtual methods in the WizardWindow class. Many of those methods also raise corresponding events which you can respond to from other code if your "wizard" is actually running within another application.

How it works: Custom wizard page resources

At this point, your wizard won't do much of anything because you haven't defined any wizard pages for it to process. This is done declaratively in your wizard hosting window's XAML file.

XML
<![CDATA[
<maowiz:WizardWindow x:Class="MyWizardApp.SimpleWizard"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:maowiz="clr-namespace:Olbert.Utilities.WPF;
                      assembly=Olbert.Utilities.WPF.Wizard">
        xmlns:local="clr-namespace:Olbert.Utilities.WPF"
        x:Name="TheWindow"
        RegularPageNextText="Next"
        LastPageNextText="Finish"
        Height="300" Width="300">
    <Window.Resources>
        <local:DatabaseStateProcessor x:Key="DatabasePreProcessor" />
        <local:SummaryStateProcessor x:Key="SummaryPreProcessor" />
        <local:CredentialsValidator x:Key="CredentialsValidator" />
        <local:DatabaseValidator x:Key="DatabaseValidator" />
        <local:NullableBooleanToBooleanConverter 
                   x:Key="NullableBooleanToBooleanConverter" />
    </Window.Resources>
<!-- body omitted for clarity -->
</maowiz:WizardWindow>]]>

The first step in declaring the pages to be used in the wizard involves declaring resources that some of the page declarations will use. If you don't need those elements of a page declaration, you won't need to define anything in the resources section. But you probably will, as they give you the ability to modify data as it flows between your state object and individual pages, and to do validation of user input after a page is completed but before the values are posted back to the state object.

There are three "access points" in the data flow, each of which you tap into by deriving a class from a base class and then instantiating that derived class as a resource:

Modifying state data before binding it to a WizardPage: StateValuesProcessor

A key concept in the wizard framework is that of binding some resource or property to a property exposed by a WizardPage. Binding takes place twice, once when a WizardPage is initialized for display by the wizard-hosting window, and once when the WizardPage is "completed", signified by the user initiating navigation to the next page. The wizard framework gives you an opportunity to modify what will get bound to the WizardPage in that initial binding by defining and referencing a class derived from StateValuesProcessor.

StateValuesProcessor contains just one method, which you override in a derived class to define the functionality you want:

C#
<![CDATA[
/// <summary>
/// Provides an access point for modifying the values
/// retrieved from state object(s) before they are
/// used to set the values of WizardPage properties.
/// <para>State information can be stored in three
/// places: a specific state object, the WizardWindow instance
/// hosting the wizard, and the application resource
/// dictionary. Values retrieved from all three areas
/// are sent to this method via the values argument,
/// where they can be modified, added to, or deleted.</para>
/// </summary>
/// <param name="values">the collection of retrieved state property values</param>
/// <param name="state">the state object</param>
/// <param name="wizWindow">the WizardWindow object</param>
/// <returns>the collection of state property values
/// which will be used to set the values of
/// WizardPage properties</returns>
public virtual PropertyBindingValues ModifyStateValues( PropertyBindingValues values, 
               object state, WizardWindow wizWindow )
{
    return values;
}]]>

By default, ModifyStateValues simply returns the PropertyBindingValues collection it was given. But you can modify that collection before returning it in your derived class. You might want to do this, for example, in order to define a "virtual property" that doesn't exist in your state object. Consider the following example:

C#
<![CDATA[
/// <summary>
/// A class, derived from StateValuesProcessor,
/// for creating the property values bound to the database
/// selection page of the DatabaseSelector wizard
/// </summary>
public class DatabaseStateProcessor : StateValuesProcessor
{
    private static HashSet<SqlServerPermissions> reqdPermissions = 
      new HashSet<SqlServerPermissions>(
            new SqlServerPermissions[] {
                SqlServerPermissions.CreateTable, 
                SqlServerPermissions.CreateView, 
                SqlServerPermissions.CreateFunction, 
                SqlServerPermissions.CreateProcedure });

    /// <summary>
    /// Overrides the base implementation to add the property
    /// bindings required for the database selection
    /// page of the DatabaseSelector wizard
    /// </summary>
    /// <param name="values">the initial bound property values</param>
    /// <param name="state">the wizard's primary state object</param>
    /// <param name="wizWindow">the wizard's host window</param>
    /// <returns>a collection of bound property values suitable
    ///         for initializing the database selection page
    /// of the DatabaseSelector wizard</returns>
    public override PropertyBindingValues ModifyStateValues( 
           PropertyBindingValues values, object state, WizardWindow wizWindow )
    {
        PropertyBindingValues retVal = base.ModifyStateValues(values, state, wizWindow);

        PropertyBinder newBinding = new PropertyBinder()
        {
            PageProperty = "RequiredPermissions",
            PageType = typeof(DatabasePage),
            StateType = typeof(SqlConfigInfo)
        };

        values.Add(new PropertyBindingValue(newBinding, reqdPermissions, true));

        return retVal;
    }
}]]>

Here we are adding a "virtual property" which will be bound to a property called RequiredPermissions on the wizard page to which this derived class relates. RequiredPermissions could've been made a property of the state object used by the wizard. But I chose not to because I wanted to keep the state object "focused" on defining the elements of a SQL connection string.

What this also demonstrates is that a wizard's entire state is defined by more than just its state object. The entire state also encompasses the wizard-hosting window itself (which is why that window is passed to ModifyStateValues as an argument) as well as any "externals" that you want to include, like the static field reqdPermissions.

You may wonder how the derived class DatabaseStateProcessor gets associated with a particular wizard page and wizard window. The answer is simple: you declare the association when you declare a wizard page in the hosting window's XAML file. You'll see how that's done a little later.

Validating page data before binding it to the wizard's state: WizardPageValidator

A key concept in the wizard framework is that of binding some resource or property to a property exposed by a WizardPage. Binding takes place twice, once when a WizardPage is initialized for display by the wizard-hosting window, and once when the WizardPage is "completed", signified by the user initiating navigation to the next page. The wizard framework gives you an opportunity to validate the page data before binding it back to the state object, which lets you cancel the update (and the transition to the next page). You do this by defining and referencing a class derived from WizardPageValidator.

WizardPageValidator contains just one method, which you override in a derived class to define the functionality you want:

C#
<![CDATA[
/// <summary>
/// Performs custom validation of WizardPage property
/// values after they are retrieved from the page
/// and before they are used to set values on
/// the primary state object.
/// </summary>
/// <param name="propValues">the WizardPage property values to be validated</param>
/// <param name="state">the primary wizard state object</param>
/// <param name="wizWindow">the WizardWindow hosting the wizard</param>
/// <returns>a ValidationResult describing the results of the custom validation</returns>
public abstract PageBindingResult Validate( PropertyBindingValues propValues, 
                object state, WizardWindow wizWindow );]]>

You implement your validation logic by defining a Validate method in your derived class. Consider the following example:

C#
<![CDATA[
/// <summary>
/// A class, derived from WizardPageValidator,
/// which checks to ensure the selected database is accessible
/// </summary>
public class DatabaseValidator : WizardPageValidator
{
    /// <summary>
    /// Checks to see if the selected database is accessible
    /// </summary>
    /// <param name="propValues">the property binding values
    /// returned from the database selection page</param>
    /// <param name="state">the primary state object</param>
    /// <param name="wizWindow">the WizardWindow hosting the wizard (ignored)</param>
    /// <returns></returns>
    public override PageBindingResult Validate( 
           PropertyBindingValues propValues, object state, WizardWindow wizWindow )
    {
        SqlConfigInfo config = ( (SqlConfigInfo) state ).Copy();

        PageBindingResult result = propValues.GetValueForPage("Database");
        if( !result.IsValid ) return result;

        config.Database = (string) ( (PageBindingValue) result ).Value;

        switch( DatabaseImage.IsAccessible(config.GetConnectionString(), config.Database) )
        {
            case DatabaseImage.DatabaseAccessibility.Accessible:
                return new PageBindingValue();

            case DatabaseImage.DatabaseAccessibility.DoesNotExist:
                SqlServerImage server = new SqlServerImage(config.GetConnectionString());

                if( server.CanCreate ) return new PageBindingValue();
                else return PageBindingOutcome.InvalidValue.ToError(
                  "database doesn't exist and credentials don't support database creation");

            default:
                return PageBindingOutcome.InvalidValue.ToError(
                  "the database cannot be accessed with the specified user credentials");
        }
    }
}]]>

Here we are checking the database name returned by a particular wizard page to see if, indeed, it references a database we can access using server and credential information the wizard gathered in an earlier step. That earlier information is contained in the state object passed to the Validate() method.

You may wonder how the derived class DatabaseValidator gets associated with a particular wizard page and wizard window. The answer is simple: you declare the association when you declare a wizard page in the hosting window's XAML file. You'll see how that's done a little later.

Modifying page data before binding it to the wizard's state: PageValuesProcessor

A key concept in the wizard framework is that of binding some resource or property to a property exposed by a WizardPage. Binding takes place twice, once when a WizardPage is initialized for display by the wizard-hosting window, and once when the WizardPage is "completed", signified by the user initiating navigation to the next page. The wizard framework gives you an opportunity to modify the page data after it's validated but before binding it back to the state object. You do this by defining and referencing a class derived from PageValuesProcessor.

PageValuesProcessor contains just one method, which you override in a derived class to define the functionality you want:

C#
<![CDATA[
/// <summary>
/// Provides an access point for modifying the values
/// retrieved from a WizardPage before they are
/// used to set the values of the primary state object.
/// <para>State information can be stored in three
/// places: a specific state object, the WizardWindow instance
/// hosting the wizard, and the application resource
/// dictionary. Values retrieved from the just-completed
/// WizardPage are sent to this method via the values argument,
/// where they can be modified, added to, 
/// or deleted.</para>
/// </summary>
/// <param name="values">the collection of retrieved WizardPage property values</param>
/// <param name="state">the state object</param>
/// <param name="wizWindow">the WizardWindow object</param>
/// <returns>the collection of WizardPage property
/// values which will be used to set the values of
/// the state object</returns>
public virtual PropertyBindingValues ModifyPageValues( 
       PropertyBindingValues values, object state, WizardWindow wizWindow )
{
    return values;
}]]>

You implement your modification logic by making changes to the PropertyBindingValues collection passed to the method. By default, the base implementation simply returns that collection, unchanged. I don't have an example of using this functionality handy, but it's pretty similar to what was described for the StateValuesProcessor example, except that you have to specify the StateProperty name rather than the PageProperty name when you add a "virtual property" to the collection. That is because you are changing a property on the state object, not on a WizardPage object.

How it works: Wizard page declarations

Once you've declared whatever custom resources your wizard pages will need, you need to declare the wizard pages that make up the wizard. You do this by adding entries to the PageBindings collection exposed by the WizardWindow class:

C#
<![CDATA[
<maowiz:WizardWindow x:Class="MyWizardApp.SimpleWizard"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:maowiz="clr-namespace:Olbert.Utilities.WPF;
                      assembly=Olbert.Utilities.WPF.Wizard">
        xmlns:local="clr-namespace:Olbert.Utilities.WPF"
        x:Name="TheWindow"
        RegularPageNextText="Next"
        LastPageNextText="Finish"
        Height="300" Width="300">
    <Window.Resources>
        <local:DatabaseStateProcessor x:Key="DatabasePreProcessor" />
        <local:SummaryStateProcessor x:Key="SummaryPreProcessor" />
        <local:CredentialsValidator x:Key="CredentialsValidator" />
        <local:DatabaseValidator x:Key="DatabaseValidator" />
        <local:NullableBooleanToBooleanConverter x:Key="NullableBooleanToBooleanConverter" />
    </Window.Resources>
    <maowiz:WizardWindow.PageBinders>
        <maowiz:PageBinder Title="Introduction" 
               PageType="local:IntroPage" SourcePath="db_selector/pages/IntroPage.xaml">
            <maowiz:ResourceBinder PageProperty="Document" StateProperty="IntroDoc" />
        </maowiz:PageBinder>
        <maowiz:PageBinder Title="Choose Server" 
              PageType="local:ServerPage" SourcePath="db_selector/pages/ServerPage.xaml">
            <maowiz:PropertyBinder PageProperty="Server" />
            <maowiz:ResourceBinder PageProperty="MoreInfo" StateProperty="MoreServerInfo" />
        </maowiz:PageBinder>
        <maowiz:PageBinder Title="Credentials" 
               PageType="local:CredentialsPage" 
               SourcePath="db_selector/pages/CredentialsPage.xaml"
               Validator="{StaticResource CredentialsValidator}">
            <maowiz:PropertyBinder PageProperty="UserID" />
            <maowiz:PropertyBinder PageProperty="Password" />
            <maowiz:PropertyBinder PageProperty="IntegratedSecurity" 
                 Converter="{StaticResource NullableBooleanToBooleanConverter}" />
        </maowiz:PageBinder>
        <maowiz:PageBinder Title="Choose Database" 
             PageType="local:DatabasePage" SourcePath="db_selector/pages/DatabasePage.xaml"
             StateValuesProcessor="{StaticResource DatabasePreProcessor}"
             Validator="{StaticResource DatabaseValidator}">
          <maowiz:PropertyBinder PageProperty="Database" />
          <maowiz:ResourceBinder PageProperty="MoreInfo" StateProperty="MoreDatabaseInfo" />
        </maowiz:PageBinder>
        <maowiz:PageBinder Title="Action Summary" PageType="local:SummaryPage" 
               SourcePath="db_selector/pages/SummaryPage.xaml"
               StateValuesProcessor="{StaticResource SummaryPreProcessor}">
            <maowiz:PropertyBinder PageProperty="Server" />
            <maowiz:PropertyBinder PageProperty="Database" />
        </maowiz:PageBinder>
    </maowiz:WizardWindow.PageBinders>
</maowiz:WizardWindow>]]>

The syntax may look a little odd, but it's straightforward once you remember that the first declaration defines the specific property (i.e., the PageBinders collection) of the WizardWindow object to which you are adding entries. Each of those entries, in turn, describes an instance of PageBinder that you want the XAML processor to create for you and add to the collection when it creates the window. Because both the PageBinders collection and the PageBinder class are part of the framework, you need to prefix references to them with the namespace prefix which will let the XAML processor figure out the specific class to which your directives are referring.

Each PageBinder directive can have various properties assigned to it, and can also have individual ResourceBinder or PropertyBinder directives added "within" it. That's because the PageBinder class is itself a collection of ResourceBinder objects (PropertyBinders derive from ResourceBinders and so can be added to a collection of ResourceBinder objects).

While the help file contains detailed information about each of the PageBinder properties, here's a summary table describing what they are and what they do:

PropertyWhat it doesComments
PageTypeSets the Type of WizardPage you're adding to the wizard.Required.
TitleSets a title for the WizardPage which can be accessed and displayed in a navigation control.Optional, but desirable.
SourcePathThe path to the XAML file for the page in the Visual Studio project that defines it.Required... and you must get it right. The syntax looks like a file path declaration which starts at the "root" of the Visual Studio project, but does not contain a leading '/'. The wizard framework requires that the page's XAML interface file be defined in the same assembly which defines the page's class. That's equivalent to saying the code-behind and XAML files for a WizardPage must be in the same assembly, and "linked" to each other the way Visual Studio links them when it creates a new Page object. Frankly, this may be a requirement imposed by Visual Studio itself, although I don't know that for sure. What I do know is that I've never put the code-behind file for a XAML file any place else :).
StateValuesProcessorSets the object you're using to modify the values retrieved from the state object before binding them to the WizardPage. The value will be a StaticResource reference to a previously-defined instance in the wizard's "resource environment". Typically that will be a resource entry in the window's resource declaration section. Optional. If you don't define one, the values will be retrieved and bound to the page "as is". Note that the value you assign is actually a reference to a static resource you previously defined in the window's resource section (or in the application resource file, if you're using one).
PageValuesProcessorSets the object you're using to modify the values retrieved from the page before binding them to the state object. The value will be a StaticResource reference to a previously-defined instance in the wizard's "resource environment". Typically, that will be a resource entry in the window's resource declaration section. Optional. If you don't define one, the values will be retrieved and bound to the state "as is". Note that the value you assign is actually a reference to a static resource you previously defined in the window's resource section (or in the application resource file, if you're using one).
ValidatorSets the object you're using to validate the (possibly modified) values retrieved from the page before binding them to the state object. The value will be a StaticResource reference to a previously-defined instance in the wizard's "resource environment". Typically that will be a resource entry in the window's resource declaration section. Optional. If you don't define one, the values will be assumed valid. Note that the value you assign is actually a reference to a static resource you previously defined in the window's resource section (or in the application resource file, if you're using one).

Of course, a PageBinder that doesn't contain any ResourceBinder or PropertyBinder declarations would make for a pretty limited wizard, one where no page could display or manipulate any part of the wizard's state. Resource/PropertyBinder declarations define which properties of a WizardPage are "tied" to which properties of the state object, or which object in the wizard's "resource environment".

ResourceBinders are currently "read only" in that they can be bound to wizard page properties, but changing the values in a wizard page will not result in the value of the resource object being changed. That's just a design limitation at this point, and could change in the future.

Again, while there is more complete documentation on each of the properties of a ResourceBinder or PropertyBinder declaration, here's a summary table describing what they are and what they do (all of these properties exist in both the ResourceBinder and PropertyBinder classes):

PropertyWhat it doesComments
PagePropertySets the name of the property on the wizard page that's involved in the binding.Required, sort of; you have to define at least one of either PageProperty or StateProperty. You can define both. This flexibility is intended to reduce typing when PageProperty and StateProperty have the same value, which is often. Defining either one and not the other will cause both of them to have the same value. Cannot be set to blank or empty.
StatePropertySets the name of the property on the state object that's involved in the binding.Required, sort of; you have to define at least one of either PageProperty or StateProperty. You can define both. This flexibility is intended to reduce typing when PageProperty and StateProperty have the same value, which is often. Defining either one and not the other will cause both of them to have the same value. Cannot be set to blank or empty.
ConverterSets the converter class to be used to convert the value between different Types when it is being assigned to the page or the state object. The value must be a previously-defined StaticResource, and the underlying class must implement IValueConverter. The IValueConverter.Convert() method is called when the value is being assigned to the wizard page. The IValueConverter.ConvertBack() method is called when the value is being assigned to the state object. Optional. If nothing is assigned, the value being passed back and forth is assumed to be compatible with both the page's property and the state's property.
ConverterParameterDefines a string parameter passed to the IValueConverter.Convert() and IValueConverter.ConvertBack() methods if and when they are called. Optional.

History

  • February 28, 2011: 0.5.1 (Initial release).

License

This article, along with any associated source code and files, is licensed under The GNU Lesser General Public License (LGPLv3)


Written By
Jump for Joy Software
United States United States
Some people like to do crossword puzzles to hone their problem-solving skills. Me, I like to write software for the same reason.

A few years back I passed my 50th anniversary of programming. I believe that means it's officially more than a hobby or pastime. In fact, it may qualify as an addiction Smile | :) .

I mostly work in C# and Windows. But I also play around with Linux (mostly Debian on Raspberry Pis) and Python.

Comments and Discussions

 
Questionwhere is the screenshot? Pin
Southmountain2-Dec-13 11:12
Southmountain2-Dec-13 11:12 

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.