65.9K
CodeProject 正在变化。 阅读更多。
Home

构建简单 WPF 向导的(主要)声明性框架

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2011年3月7日

LGPL3

22分钟阅读

viewsIcon

20047

downloadIcon

252

一个用于构建 WPF 向导的声明式框架。

引言

向导是引导用户完成某个过程的最流行的方法之一。我知道我很喜欢它们。但是编写它们总是让我觉得涉及到大量的样板代码。而且试图组织结构以使单个页面可重用变得非常快地复杂起来。

向导框架是我在系列程序集中提供的日益增长的实用工具集合的一部分。以下是您如何下载最新版本的软件

安装程序 http://www.jumpforjoysoftware.com/system/files/Olbert.Utilities-Setup-0.5.1.exe
帮助(CHM)文件 http://www.jumpforjoysoftware.com/system/files/OlbertUtilitiesHelp.chm
源代码:主干 https://svn.olbert.com/Olbert.Utilities/trunk/
源代码:v0.5.1 https://svn.olbert.com/Olbert.Utilities/tag/0.5.1/

该库的产品页面位于 http://www.jumpforjoysoftware.com/product/5。您可能对我在网站上提供的其他内容感兴趣,网址是 http://www.jumpforjoysoftware.com/

库集合中包含的框架构建了几个向导。一个是 Olbert.Utilities.WPF.DatabaseSelector,顾名思义,它是一个用于选择 SQL Server 数据库的向导。另一个是 Olbert.Utilities.nHydrate.Installer.nHydrateDatabaseSelector,它扩展了 DatabaseSelector,用于选择配置为与 nHydrate 数据建模环境一起使用的 SQL Server 数据库。

如果您还没有尝试过 nHydrate,您应该尝试一下 (http://nhydrate.codeplex.com/;CodeProject 上也有许多关于 nHydrate 的文章)。这是一个很酷的工具!

背景

向导的概念对我来说听起来像一个工作流,所以我花了很多时间阅读 Windows Workflow,认为它里面一定埋藏着一些用于制作向导的巧妙支持工具。可能确实有,但我无法找到那种简单的框架,能够处理我在向导中想要做的事情的底层机制,而我只需专注于定义“什么”。所以最后,我决定自己动手。但请注意:我在这里描述的框架甚至在发布之前就可能完全过时。

在开发向导框架时,我遇到了当前版本 XAML 的一些有趣的限制。因为我通常编写的向导通过修改状态变量(即,随着向导的进展,向导的状态变得越来越明确)来工作,而状态变量与解决方案高度相关,所以创建基于泛型的向导托管窗口将非常方便。这在 WPF 4 中支持得不太好,尽管显然未来会支持。

但目前,这意味着我的设计不得不放弃向导状态的强类型。出于类似的原因,我也不得不放弃向导宿主窗口的强类型。结果,幕后处理状态管理的底层例程做了大量的类型检查,因为代码无法确定您是否传递了一个 `SqlServerConfigurationState` 对象,而它期望的是 `MyFavoriteVideoGameState` 对象。除此之外,这让我对强类型的重要性有了新的认识 :)

WPF 编程的另一个方面,我虽然有所了解,但在构建和使用向导框架时必须真正面对,那就是功能继承和视觉继承之间的区别。如果能从一个窗口类派生出另一个窗口类,并让基类中的所有 UI 元素都继承到新的派生类中,那将非常方便。不幸的是,这在今天的 WPF 中是不可能的,我怀疑未来也可能不会。如果你今天尝试这样做,编译器会给你一个友好的错误消息,提醒你不能从另一个自身定义了 UI 的窗口类派生出某个窗口的 UI 部分。

但是你可以很容易地绕过这个限制,尽管这会增加一些代码冗余。你只需要记住,虽然窗口的 XAML 文件和窗口的代码后台文件密切相关,但它们并非密不可分。它们各自定义了成为那种特定类型窗口的不同方面。XAML 文件描述了窗口的外观,而代码后台描述了窗口的功能。当然,实现这些功能通常需要引用 UI 中的组件——想想获取 `TextBox` 中包含的文本——但你不必通过直接引用相关控件来做到这一点。你可以通过定义虚方法或属性来间接实现。所以,与其写这样的代码

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

你可以这样写

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

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

当然,你现在必须编写一个重载来将 `MyTextBoxValue` "链接"到派生类 UI 中的特定控件。但你获得了在多个 UI "内部"重用基窗口类功能的能力。

在实践中,如果您的窗口功能和其UI之间存在许多链接,这种方法会变得非常繁琐和恼人。在这种情况下,我敢打赌编写代码生成模板会更容易。但是,如果UI和功能之间的链接不多,这是一种实用方法。

使用代码

在向导框架的情况下,只有一个链接

/// 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 { }
} ]]>

您的派生 UI 必须包含一个 `Frame` 元素来承载各个向导页面,因为每个页面都必须派生自库中定义的自定义 `WizardPage` 类。`PageFrame` 属性使该元素可供 `WizardWindow` 类中的代码访问。`WizardState` 属性虽然不是 UI 元素的链接,但也必须在您的派生向导托管窗口类中定义。它使底层代码能够访问您用于保存向导状态信息的任何对象。

工作原理:配置向导宿主窗口

通过从一个特殊的 `Window` 类 `WizardWindow` 派生一个窗口,并在其中声明向导的每个页面,您就可以使用该框架构建一个向导。`WizardWindow` 提供了向导的底层机制,并让您有机会影响页面流程(例如,通过让您取消用户到向导下一页的过渡)。

要定义向导宿主窗口的 XAML,只需向项目添加一个新的 `Window` 类,然后将其修改为从 `WizardWindow` 派生。这涉及到在窗口的 XAML 文件中添加一些内容并编辑代码后台。下面是一个修改后的 XAML 文件示例

<![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>]]>

这里需要注意的重要事项是,您必须定义一个引用定义 `WizardWindow` 的程序集的命名空间,并将其用作窗口 UI 定义的起始声明。在示例中,我为这个必需的命名空间指定了前缀“maowiz”。

您可能会对引用 `WizardWindow` 而不是自定义窗口类的起始声明感到困惑。实际上,该声明同时引用两者。您可以将第一行理解为“创建一个窗口 UI,其功能源自 `WizardWindow` 类,并将其链接到当前程序集中定义的 `SimpleWizard` 类中定义的功能”。

当然,您将 UI 链接到的本地类必须派生自与 XAML 定义的“起始点”声明相同的类。如果不是这样,您将收到一个编译器错误消息,提醒您 UI 的基类必须与代码后台的基类相同。为此,对代码后台的必要修改非常简单。您只需替换类声明行中的基类名称即可

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

顺便说一句,在进行这些更改时,为了正确“修复”所有内容,通常需要编译项目两次。有时,我甚至不得不进行“全部重新生成”。考虑到 Visual Studio 在幕后需要完成的所有内务管理工作,这并不奇怪,但第一次遇到诸如“未知生成错误,找不到所需文件”之类的完全无用的错误消息时,可能会有点令人不安。

您还需要覆盖 `WizardWindow` 的 `PageFrame` 和 `WizardState` 属性,以指向您在窗口 UI 中用于保存向导页面(必须是 `Frame` 元素)和向导状态的对象。

<![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; }]]>

由于您希望向导能够切换页面,因此您需要将 `WizardWindow` 的导航方法与您 UI 的导航元素连接起来

<![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();
}]]>

最后,您需要重载 `WizardWindow` 的 `OnWizardLoaded()` 方法以启动向导

<![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;
}]]>

因为所有页面切换都是通过更改 `WizardWindow` 的 `PageNumber` 属性启动的,所以您可以通过将该属性设置为您要首先显示的页面的页码来启动向导。这在底层代码中没有为您完成,因为有时您不想在第一页启动向导(例如,它可能是一个介绍性页面,您希望给有经验的用户一个跳过的机会)。

`WizardWindow` 类中定义了许多其他可以重写的方法和可以响应的事件。其中一个,`OnWizardCompleted()`,可能特别有趣,因为它在向导成功完成时被调用,让您有机会处理用户刚刚定义的任何内容。您不总是需要重写它。事实上,如果您的向导所做的只是定义一个特定的状态变量,您可以以模态方式显示它,然后在 `ShowDialog()` 成功返回后提取最终状态信息。

关于声明向导宿主窗口的最后一点:`RegularPageNextText` 和 `LastPageNextText` 是 `WizardWindow` 的依赖属性,它们允许您定义向导在“下一步”按钮中显示的文本。仅仅在 XAML 窗口声明中设置它们不足以让您指定的值在向导运行时显示出来。您还必须将 `WizardWindow` 的 `CurrentNextText` 属性绑定到窗口 UI 上“下一步”按钮的内容

<![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" />]]>

这不能在代码中完成,因为 `WizardWindow` 未绑定到任何特定的 UI,它不“知道”哪个 UI 元素对应于“下一步”按钮(实际上,在我写这篇文章时,我想到如果 `WizardWindow` 定义了一个虚属性,您可以通过在派生类中重写该属性来将其链接到特定的 UI 元素,那么这就可以实现;待办事项清单上又多了一项...)。

上面的代码片段还展示了 `WizardWindow` 的另一个可选但强烈建议使用的功能:通过启用或禁用向导导航按钮来控制用户移动的能力。在示例中,这是通过将“下一步”按钮的 `IsEnabled` 属性绑定到 `WizardWindow` 的 `CanMoveNext` 属性来实现的。`CanMoveNext` 及其对应项 `CanMovePrevious` 和 `CanCancel` 可以由您的派生向导宿主窗口或单个向导页面设置。但这只有在您连接了功能后才能工作。

顺便说一句,示例中的绑定表达式使用了向导宿主窗口的名称(我不怎么聪明,我称之为“`TheWindow`”)。这是窗口(或控件)声明的一个关键的可选属性。还有其他方法可以向 .NET Framework 提供在运行时“查找”窗口所需的信息,但最简单的方法就是简单地命名窗口并使用它。

您可能会想,当属性访问发生在窗口本身的定义内部时,为什么您必须识别一个窗口才能访问其属性。我承认我自己也觉得这很令人困惑。我认为原因与 XAML 文件“给”代码后台文件“一个面孔”,但它本身不是由代码后台定义的实体这一事实有关。无论如何,一旦您掌握了窍门,它就足够容易做到了。

工作原理:数据流

在深入了解如何向向导添加 `WizardPage` 之前,有必要讨论一下向导运行时数据如何在向导中流动。对于每个显示的页面,会发生以下活动序列

  • 如果我们已经显示了一个向导页面,并且我们正在前进到后续页面
    • 检索当前 `WizardPage` 上绑定属性的值
    • 给予向导代码库修改绑定属性的机会
    • 给予向导代码库验证修改后的属性集合的机会;如果验证失败,则取消页面转换
    • 如果验证成功,更新状态对象以反映修改后的页面属性值
    • 给予应用程序代码库最后一次取消转换的机会
  • 如果我们已经显示了一个向导页面但正在后退到之前的页面,请不要费心处理当前页面的值;只需继续转换
  • 如果我们正在显示第一个向导页面,只需继续转换
  • 如果我们要离开一个向导页面,请给“即将过期”的页面一个机会进行任何必要的清理(例如,它可能已消耗的资源)
  • 创建新的、即将显示的 `WizardPage` 派生类的实例
  • 从状态对象中检索状态对象中绑定到新 `WizardPage` 属性的所有属性的值
  • 给予向导代码库修改检索到的绑定值的机会
  • 将修改后的状态值分配给它们绑定的 `WizardPage` 属性
  • 显示新的向导页面并给予其代码库进行任何最终初始化的机会
  • 允许用户与 `WizardPage` 交互
  • 用户触发页面转换
  • 并重复,直到没有更多向导页面可显示

这些步骤由 `WizardWindow` 类中的各种受保护虚方法处理。其中许多方法还会引发相应的事件,如果您的“向导”实际在另一个应用程序中运行,您可以从其他代码响应这些事件。

工作原理:自定义向导页面资源

此时,您的向导不会做太多事情,因为您还没有为其定义任何要处理的向导页面。这是在向导宿主窗口的 XAML 文件中以声明方式完成的。

<![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>]]>

声明向导中要使用的页面的第一步是声明某些页面声明将使用的资源。如果您不需要页面声明的这些元素,则无需在资源部分定义任何内容。但您可能需要,因为它们使您能够修改数据在状态对象和各个页面之间流动时的数据,并在页面完成后但在值发布回状态对象之前对用户输入进行验证。

数据流中有三个“访问点”,您通过从基类派生一个类,然后将该派生类实例化为资源来利用它们

在将状态数据绑定到 `WizardPage` 之前修改它:StateValuesProcessor

向导框架中的一个关键概念是将某些资源或属性绑定到 `WizardPage` 公开的属性。绑定发生两次,一次是在向导宿主窗口初始化 `WizardPage` 以供显示时,另一次是在 `WizardPage` “完成”时,通过用户启动到下一页的导航来表示。向导框架通过定义和引用派生自 `StateValuesProcessor` 的类,为您提供了在初始绑定中修改将绑定到 `WizardPage` 的内容的机会。

`StateValuesProcessor` 只包含一个方法,您可以在派生类中重写它以定义所需的功能

<![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;
}]]>

默认情况下,`ModifyStateValues` 仅返回给定它的 `PropertyBindingValues` 集合。但是您可以在派生类中返回它之前修改该集合。例如,您可能希望这样做,以便定义一个在状态对象中不存在的“虚拟属性”。考虑以下示例

<![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;
    }
}]]>

这里我们正在添加一个“虚拟属性”,它将被绑定到向导页面上一个名为 `RequiredPermissions` 的属性,这个派生类与该向导页面相关。`RequiredPermissions` 可以作为向导使用的状态对象的一个属性。但我选择不这样做,因为我想让状态对象“专注于”定义 SQL 连接字符串的元素。

这也表明向导的整个状态不仅仅由其状态对象定义。整个状态还包括向导宿主窗口本身(这就是为什么该窗口作为参数传递给 `ModifyStateValues`)以及您想要包含的任何“外部”内容,例如静态字段 `reqdPermissions`。

您可能想知道派生类 `DatabaseStateProcessor` 如何与特定的向导页面和向导窗口关联。答案很简单:您在宿主窗口的 XAML 文件中声明向导页面时声明该关联。您稍后将看到如何完成。

在将页面数据绑定到向导状态之前验证它:WizardPageValidator

向导框架中的一个关键概念是将某些资源或属性绑定到 `WizardPage` 公开的属性。绑定发生两次,一次是在向导宿主窗口初始化 `WizardPage` 以供显示时,另一次是在 `WizardPage` “完成”时,通过用户启动到下一页的导航来表示。向导框架为您提供在将页面数据绑定回状态对象之前验证它的机会,这使您可以取消更新(以及到下一页的转换)。您可以通过定义和引用派生自 `WizardPageValidator` 的类来完成此操作。

`WizardPageValidator` 只包含一个方法,您可以在派生类中重写它以定义所需的功能

<![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 );]]>

您通过在派生类中定义 `Validate` 方法来实现验证逻辑。请考虑以下示例

<![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");
        }
    }
}]]>

在这里,我们正在检查特定向导页面返回的数据库名称,以查看它是否确实引用了我们可以使用向导在早期步骤中收集的服务器和凭据信息访问的数据库。早期信息包含在传递给 `Validate()` 方法的状态对象中。

您可能想知道派生类 `DatabaseValidator` 如何与特定的向导页面和向导窗口关联。答案很简单:您在宿主窗口的 XAML 文件中声明向导页面时声明该关联。您稍后将看到如何完成。

在将页面数据绑定到向导状态之前修改它:PageValuesProcessor

向导框架中的一个关键概念是将某些资源或属性绑定到 `WizardPage` 公开的属性。绑定发生两次,一次是在向导宿主窗口初始化 `WizardPage` 以供显示时,另一次是在 `WizardPage` “完成”时,通过用户启动到下一页的导航来表示。向导框架为您提供在验证页面数据之后但在将其绑定回状态对象之前修改它的机会。您可以通过定义和引用派生自 `PageValuesProcessor` 的类来完成此操作。

`PageValuesProcessor` 只包含一个方法,您可以在派生类中重写它以定义所需的功能

<![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;
}]]>

您通过修改传递给方法的 `PropertyBindingValues` 集合来实现修改逻辑。默认情况下,基本实现只是返回该集合,不做任何更改。我手头没有使用此功能的示例,但这与 `StateValuesProcessor` 示例中描述的非常相似,只是在向集合添加“虚拟属性”时,您必须指定 `StateProperty` 名称而不是 `PageProperty` 名称。这是因为您正在更改状态对象上的属性,而不是 `WizardPage` 对象上的属性。

工作原理:向导页面声明

一旦您声明了向导页面将需要的任何自定义资源,您就需要声明构成向导的向导页面。您可以通过向 `WizardWindow` 类公开的 `PageBindings` 集合添加条目来完成此操作

<![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>]]>

语法可能看起来有点奇怪,但一旦你记住第一个声明定义了 `WizardWindow` 对象的特定属性(即 `PageBinders` 集合),你就可以轻松理解它,你正在向其中添加条目。反过来,这些条目中的每一个都描述了一个 `PageBinder` 实例,你希望 XAML 处理器在你创建窗口时为你创建并添加到集合中。因为 `PageBinders` 集合和 `PageBinder` 类都是框架的一部分,所以你需要用命名空间前缀来引用它们,这将让 XAML 处理器弄清楚你的指令所引用的具体类。

每个 `PageBinder` 指令都可以分配各种属性,并且还可以在其“内部”添加单个 `ResourceBinder` 或 `PropertyBinder` 指令。这是因为 `PageBinder` 类本身就是 `ResourceBinder` 对象的集合(`PropertyBinder`s 派生自 `ResourceBinder`s,因此可以添加到 `ResourceBinder` 对象的集合中)。

虽然帮助文件包含有关每个 `PageBinder` 属性的详细信息,但这里有一个摘要表,描述了它们是什么以及它们的作用

属性 它的作用 注释
PageType 设置您添加到向导中的 `WizardPage` 类型。 必填项。
标题 设置 `WizardPage` 的标题,该标题可以在导航控件中访问和显示。 可选,但最好有。
SourcePath 在定义该页面的 Visual Studio 项目中,页面 XAML 文件的路径。 必需...而且你必须写对。语法看起来像一个文件路径声明,从 Visual Studio 项目的“根”开始,但不包含开头的“/”。向导框架要求页面的 XAML 接口文件必须在定义页面类的同一个程序集中定义。这相当于说 `WizardPage` 的代码后台文件和 XAML 文件必须在同一个程序集中,并且以 Visual Studio 创建新 `Page` 对象时链接它们的方式“链接”在一起。坦率地说,这可能是 Visual Studio 本身强加的要求,尽管我不确定。我所知道的是,我从来没有将 XAML 文件的代码后台文件放在其他任何地方 :)。
StateValuesProcessor 设置您用于修改从状态对象检索到的值,然后将其绑定到 `WizardPage` 的对象。该值将是向导“资源环境”中先前定义的实例的 `StaticResource` 引用。通常,这将是窗口资源声明部分中的一个资源条目。 可选。如果您不定义一个,值将“原样”检索并绑定到页面。请注意,您分配的值实际上是您之前在窗口的资源部分(或者,如果您正在使用应用程序资源文件,则是在应用程序资源文件)中定义的静态资源的引用。
PageValuesProcessor 设置用于修改从页面检索到的值,然后将其绑定到状态对象。该值将是向导“资源环境”中先前定义的实例的 `StaticResource` 引用。通常,这将是窗口资源声明部分中的一个资源条目。 可选。如果不定义,值将“原样”检索并绑定到状态。请注意,您分配的值实际上是您之前在窗口的资源部分(或者,如果您正在使用应用程序资源文件,则是在应用程序资源文件)中定义的静态资源的引用。
验证器 设置您用于验证从页面检索到的(可能已修改的)值,然后将其绑定到状态对象。该值将是向导“资源环境”中先前定义的实例的 `StaticResource` 引用。通常,这将是窗口资源声明部分中的一个资源条目。 可选。如果不定义,则假定这些值有效。请注意,您分配的值实际上是您之前在窗口资源部分(或者,如果您正在使用应用程序资源文件,则在应用程序资源文件)中定义的静态资源的引用。

当然,一个不包含任何 `ResourceBinder` 或 `PropertyBinder` 声明的 `PageBinder` 将会是一个非常受限制的向导,一个页面无法显示或操作向导状态的任何部分。`Resource/PropertyBinder` 声明定义了 `WizardPage` 的哪些属性“绑定”到状态对象的哪些属性,或者向导“资源环境”中的哪个对象。

`ResourceBinder` 目前是“只读”的,因为它们可以绑定到向导页面属性,但更改向导页面中的值不会导致资源对象的值被更改。这只是目前的一个设计限制,将来可能会改变。

同样,虽然有更完整的文档说明 `ResourceBinder` 或 `PropertyBinder` 声明的每个属性,但这里有一个摘要表,描述了它们是什么以及它们的作用(所有这些属性都存在于 `ResourceBinder` 和 `PropertyBinder` 类中)

属性 它的作用 注释
PageProperty 设置向导页面上参与绑定的属性名称。 必需,某种程度上;您必须至少定义 `PageProperty` 或 `StateProperty` 中的一个。您可以同时定义两者。这种灵活性旨在减少当 `PageProperty` 和 `StateProperty` 具有相同值时(这种情况经常发生)的输入。定义其中一个而不是另一个将导致两者具有相同的值。不能设置为空白或空字符串。
StateProperty 设置状态对象上参与绑定的属性名称。 必需,某种程度上;您必须至少定义 `PageProperty` 或 `StateProperty` 中的一个。您可以同时定义两者。这种灵活性旨在减少当 `PageProperty` 和 `StateProperty` 具有相同值时(这种情况经常发生)的输入。定义其中一个而不是另一个将导致两者具有相同的值。不能设置为空白或空字符串。
转换器 设置用于在值分配给页面或状态对象时,在不同类型之间转换值的转换器类。该值必须是先前定义的 `StaticResource`,并且底层类必须实现 `IValueConverter`。当值分配给向导页面时,会调用 `IValueConverter.Convert()` 方法。当值分配给状态对象时,会调用 `IValueConverter.ConvertBack()` 方法。 可选。如果没有赋值,则假定传入和传出的值与页面的属性和状态的属性都兼容。
ConverterParameter 定义一个字符串参数,该参数在调用 `IValueConverter.Convert()` 和 `IValueConverter.ConvertBack()` 方法时传递给它们。 可选。

历史

  • 2011年2月28日:0.5.1(首次发布)。
© . All rights reserved.