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

使用 Prism 4 创建视图切换应用程序

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (57投票s)

2011年3月6日

CPOL

20分钟阅读

viewsIcon

291085

downloadIcon

15781

如何使用 WPF 和 Unity 依赖注入 (DI) 容器快速搭建一个 Prism 4 行业应用程序。

引言

就像一个老笑话的笑点:“什么,又是你?” 是的,又是 Prism 更新的时候了,这次我们更新到了 4.0 版本,Prism 的版本号与 .NET Framework 的当前版本号保持一致。好消息是,这次升级是相当值得的,它改进了导航、MVVM 指导,并且引入了一个服务定位器,允许我们使用 Unity 或 .NET 4.0 中集成的 Managed Extensibility Framework。

本文是我之前文章《Prism 2.1 for WPF 入门》的更新。Prism 4 包含相当不错的文档和一些 QuickStarts,所以我不会花费太多时间解释 Prism 的背景和理论。本文将重点介绍如何使用 WPF 和 Unity 依赖注入 (DI) 容器来搭建一个 Prism 4 行业应用程序。如果您在深入阅读本文之前需要教程,可以尝试 Prism 4 附带的动手实验

本文比我之前的 Prism 文章更进一步。之前的文章展示了一个非常基础的 UI——你实际上无法在生产环境中使用的 UI。本文演示了一个更复杂的 UI,其中包括:

  • 应用程序顶部的功能区;以及
  • 左下角的 Outlook 风格任务按钮。

你可能会问:“有什么大不了的?”毕竟,在 Shell 中添加一个功能区和几个按钮是相当容易的。如果控件是由开发人员在设计时添加的,那确实如此。然而,这种第一种方法会导致 Shell 和其模块之间紧密耦合。

考虑以下情况:如果我们想在未来的开发中向这种第一种应用程序添加模块,我们必须这样做:

  • 打开 Shell;
  • 在 Shell 中添加一个新的 TaskButton 控件;
  • 更改 Shell 中的 Ribbon
  • 重新编译 Shell;以及
  • 重新测试 Shell。

这完全违背了使用 Prism 的初衷,Prism 的设计目的是让模块尽可能松耦合。理想情况下,要稍后添加新模块,我们应该能够将其放入一个指定文件夹,Prism 将发现它并将其加载到 Shell 中,同时加载其自己的 TaskButtonRibbonTab 以及视图。

演示应用程序就是这样做的——它提供了商业级的用户交互,而无需将 Shell 与其模块耦合。这使得模块尽可能独立和隔离,以便我们可以独立开发每个模块。诀窍在于让每个模块加载自己的 TaskButton 和自己的 RibbonTab

本文是另一篇我撰写的 CodeProject 文章《Prism 4 应用程序清单》的配套文章,该文章提供了构建 Prism 4 应用程序的详细步骤清单。我使用此清单来构建演示应用程序,因此该清单除了提供设置 Prism 4 应用程序的通用指南外,还将为演示提供一个很好的演练。但是,在查看清单之前,让我们先看一下演示应用程序的总体结构。

演示应用程序的组成部分

演示应用程序的 UI 以 Outlook 2010 为模型。它使用自定义的 TaskButton 控件,该控件在我之前的文章《创建 WPF 自定义控件》中有详细介绍。该控件模仿了 Outlook 主窗口左下角的 Mail、Calendar、Contacts 和 Tasks 按钮的行为。

Outlook 风格的界面提供了极大的灵活性,并且适用于各种应用程序。它尤其适用于需要切换多个模块、一次只处理一个模块的应用程序,就像 Outlook 在邮件、日历、联系人和任务之间切换一样。大多数业务用户都熟悉 Outlook UI,并且应该会发现基于它的应用程序相对容易上手。

Shell:Shell 有四个命名区域。

这些区域在行为上与 Outlook 2010 中的对应区域相似。

  • Ribbon Region:此区域包含应用程序的功能区。功能区本身及其“主页”选项卡已硬编码到 Shell 中。
  • TaskButton Region:此区域用于切换模块。此区域中的按钮行为类似于 Outlook 的 Mail、Calendar、Contacts 和 Tasks 按钮。
  • Navigator Region:此区域用于在活动模块内的视图之间进行导航。其行为类似于 Outlook 的 Navigator Region。例如,在 Outlook 的 Mail 模块中,Navigator Region 包含各种电子邮件文件夹的文件夹列表,例如收件箱、已发送邮件和已删除邮件。为简单起见,演示应用程序加载了一个包含 TextBlock 的视图,该视图仅用于标识自身。
  • Workspace Region:此区域包含进行实际工作的视图。为简单起见,模块的 Workspace 区域视图与 Navigator 视图一样,仅标识自身。

Modules:演示应用程序有两个模块,模块 A 和模块 B。每个模块加载其 TaskButtonRibbonTab 以及用于 Navigator 和 Workspace 区域的简单视图。模块使用模块发现进行加载,这最大限度地减少了与 Shell 的耦合。如果您查看 Shell 项目的引用,您会发现它不包含对模块项目的引用。换句话说,Shell 对其托管的模块一无所知。

TaskButton 控件在应用程序启动时加载和激活,届时模块将被发现并加载到 Prism 中。每个模块的 RibbonTab 及其视图都已注册到 Unity 容器中,但直到用户导航到该模块时才加载。当一个模块的控件被加载(我称之为“激活”模块)时,另一个模块的控件将被卸载(该模块被“停用”)。所有 TaskButton 控件始终保持加载和激活状态。

Bootstrapper:拼图的第三块是 Bootstrapper,它控制应用程序初始启动时的配置过程。演示应用程序的 Bootstrapper 是标准的,应该不言自明。

DI Container:Prism 应用程序的最后一个元素是依赖注入 (DI) 容器,也简称为容器。DI 容器本质上是一个工厂,可以创建已向容器注册的任何类型的对象。如果您不熟悉容器,请花点时间了解它们的工作原理,然后再继续。如果您以前从未用过容器,您会惊叹于它们在简化复杂对象创建方面所做的贡献。

Prism 4 原生支持两个 DI 容器:Unity 2.0 和随 .NET 4 一起提供的 Managed Extensibility Framework。但是,Prism 是容器无关的——它可以支持其他容器(如 Windsor Castle),但您需要找到或编写一个适配器类,以便 Prism 可以与容器通信。

关于哪个容器最好存在很多争论——我认为它们都相当不错,选择很大程度上取决于个人喜好。我使用 Unity 已经有一段时间了,所以演示应用程序使用的是 Unity 2.0。演示应用程序应该可以不费力地适应另一个容器。

MVVM pattern:演示应用程序遵循 MVVM 模式。如果您不熟悉该模式,请参阅 Microsoft Prism 开发人员指南的第 5 章。演示应用程序使用视图优先方法来实现 MVVM。术语可能有点令人困惑,因为在这种方法中,首先创建视图,然后由视图实例化其 View Model,或者由另一个组件将 View Model 注入到视图中。无论哪种情况,视图都必须了解其 View Model,但 View Model 对使用它的视图一无所知。结果是视图对 View Model 存在依赖关系。

这就是我偏爱视图优先方法的原因:View Model 创建了一个 API,该 API 定义了 View Model 和使用它的任何视图之间的约定。只要视图符合此约定,就可以在不重新打开 View Model 的情况下更改视图。设计师(和客户)出了名地喜欢随意更改视图,因此视图非常不稳定。如果我们遵循“越不稳定的组件越依赖于越稳定的组件”的原则,那么视图应该依赖于 View Model。

换句话说,一旦 API 确定,客户和设计师就可以随意修改 UI,而不会干扰应用程序的其余部分,只要他们遵守约定。

请注意,演示应用程序并未为每个模块视图实现 View Models。Workspace 和 Navigator 视图没有任何需要 View Module 的操作,并且 Ribbon 也没有连接。因此,演示应用程序中唯一的 View Models 是模块 TaskButton 控件的一些非常简单的 View Models。

应用程序清单

如果您想要关于如何设置 Prism 应用程序的分步说明,可以查阅配套文章《Prism 4 应用程序清单》。如上所述,我使用了此清单来设置演示应用程序,因此它将为您提供有关其开发方式的良好演练。

如果您不需要演练,那么您可以继续阅读本文,我们将在此讨论演示应用程序中提出的具体问题。

TaskButtons

TaskButton 控件实际上相当简单。每个模块在启动时将其 TaskButton 加载到 Task Button Region,届时 Bootstrapper 会填充模块目录并加载应用程序的模块。所有模块的 TaskButton 控件都会立即可用,并且无论哪个模块处于活动状态,它们都将保持激活状态。

TaskButton XAML:每个模块的 TaskButton 都被定义为一个视图。TaskButton 被包装在一个 UserControl 中,这有助于在 Shell 中的按钮之间添加边距。UserControl 标记非常简单。

<UserControl x:Class="Prism4Demo.ModuleA.Views.ModuleATaskButton" 
             ... >
    <fsc:TaskButton x:Name="TaskButton"
        Command="{Binding ShowModuleAView}" 
        IsChecked="{Binding IsChecked}"
        MinWidth="150" 
        Foreground="Black" 
        Image="Images/module_a.png" 
        Text="Module A" 
        Margin="5,2,5,2" 
        Background="{Binding Path=Background, RelativeSource={RelativeSource 
                     FindAncestor, AncestorType={x:Type Window}}}" />
</UserControl>

UserControl 包含一个 TaskButton 控件,该控件是从 RadioButton 派生的自定义控件。TaskButton 绑定到几个 View Model 命令属性。

  • Command 属性管理 TaskButton 单击时触发的导航。它绑定到一个 ICommand 对象,并在本文后面详细讨论。
  • IsChecked 属性控制按钮是否被选中。

此时值得注意的是,我更倾向于使用完整的 ICommand 对象,而不是 Prism 提供的 DelegateCommand 功能。这意味着我 Prism 应用程序中的每个命令都包含在一个单独的 ICommand 类中,我将其存储在每个项目的单独的Commands文件夹中。我喜欢 ICommand 类对我的命令代码进行隔离的方式,并且我发现这种方法使我的 View Models 保持相对整洁。

Task Button View Models:您可以在演示项目两个模块的命令中看到这种方法的示例。每个 View Model 的构造函数都调用 View Model 的 Initialize() 方法。Initialize() 方法将 View Model 中的所有命令属性实例化为相应的 ICommand 对象,如下所示:

#region Command Properties

/// <summary>
/// Loads the view for Module A.
/// </summary>
public ICommand ShowModuleAView { get; set; }   

#endregion

#region Private Methods

/// <summary>
/// Initializes the view model.
/// </summary>
private void Initialize()
{
    // Initialize command properties
    this.ShowModuleAView = new ShowModuleAViewCommand(this);

    // Initialize administrative properties
    this.IsChecked = false;

    ...
}

#endregion

正如我们上面所见,使用 View Model 的 TaskButton 绑定到 ShowModuleAView 命令属性。

将 Task Buttons 注册到 Prism:现在我们转向模块初始化类。每个初始化类的 Initialize() 方法将其模块的视图注册到 Prism。Task Button 视图已注册到 Prism Region Manager,以便立即加载并保持可用,并在应用程序的整个生命周期内可用。

#region IModule Members

/// <summary>
/// Initializes the module.
/// </summary>
public void Initialize()
{
    /* We register always-available controls with the Prism Region Manager, and on-demand 
     * controls with the DI container. On-demand controls will be loaded when we invoke
     * IRegionManager.RequestNavigate() to load the controls. */

    // Register task button with Prism Region
    var regionManager = ServiceLocator.Current.GetInstance<IRegionManager>();
    regionManager.RegisterViewWithRegion("TaskButtonRegion", typeof(ModuleATaskButton));

    ...
}

#endregion

Shell 的 Task Button 区域:在 Shell 中,TaskButtonRegion 只是一个 StackPanel,带有一个 BorderControl 以提供区域顶部的水平分隔线,以及一个 ItemsControl 来容纳 TaskButton 控件。

Ribbon 控件

Ribbon 控件带来了一些有趣的问题。Ribbon 控件的快速访问工具栏以及应用程序和主页选项卡通常提供跨整个应用程序可用的功能。为避免重复,此功能应放在 Shell 中。但是,模块经常需要将其自身的功能添加到 Ribbon 中,并且为了避免紧密耦合,此功能应位于每个模块中。解决方案是让每个模块将其自己的 RibbonTab 控件添加到 Ribbon 中。

RibbonRegionAdapter:不幸的是,Ribbon 无法原生托管 Prism 区域。Prism 仅定义了几个区域控件,最值得注意的是 ContentControl(用于单个视图)和 ItemsControl(用于多个控件)。好消息是 Prism 可以扩展,通过使用区域适配器(Region Adapters)允许其他控件托管区域。RibbonRegionAdapter 为 Ribbon 执行此功能。区域适配器在 Microsoft Prism 开发人员指南 的附录 E 中有详细介绍。RibbonRegionAdapter 的代码包含在本 文章的附录 A 中。

Bootstrapper 在 ConfigureRegionAdapterMappings() 重写中注册了区域适配器。

/// <summary>
/// Configures the default region adapter mappings to use in the application, in order 
/// to adapt UI controls defined in XAML to use a region and register it automatically.
/// </summary>
/// <returns>The RegionAdapterMappings instance containing all the mappings.</returns>
protected override RegionAdapterMappings ConfigureRegionAdapterMappings()
{
    // Call base method
    var mappings = base.ConfigureRegionAdapterMappings();
    if (mappings == null) return null;

    // Add custom mappings
    mappings.RegisterMapping(typeof(Ribbon), 
        ServiceLocator.Current.GetInstance<RibbonRegionAdapter>());

    // Set return value
    return mappings;
}

一旦 Bootstrapper 完成了它的工作,Prism 就可以使用 Ribbon 作为区域。区域被声明为 Ribbon 的附加属性。

<ribbon:Ribbon x:Name="ApplicationRibbon" 
                Grid.Row="0"  
                Background="Transparent"  
                prism:RegionManager.RegionName="RibbonRegion">

设置 Ribbon:请注意,Ribbon 的应用程序菜单、快速访问工具栏和主页选项卡是在 Shell 中定义的。在生产应用程序中,Ribbon 项目将以与 TaskButton 对象相同的方式连接到 ShellWindow View Model 中的 ICommand 属性。为保持简单,演示应用程序的 Ribbon 项目没有连接到任何内容。

每个模块都定义了一个用于加载的 RibbonTab 的视图。

<ribbon:RibbonTab x:Class="Prism4Demo.ModuleA.Views.ModuleARibbonTab"
         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:ribbon="clr-namespace:Microsoft.Windows.
                       Controls.Ribbon;assembly=RibbonControlsLibrary"
         mc:Ignorable="d" 
         Header="Module A">

    <!-- See code-behind for implementation 
       of IRegionMemberLifetime interface. This interface
       causes the RibbonTab to be unloaded from the Ribbon when we switch views. -->

    <ribbon:RibbonGroup Header="Group A1">
        <ribbon:RibbonButton LargeImageSource="Images\LargeIcon.png" Label="Button A1" />
        <ribbon:RibbonButton SmallImageSource="Images\SmallIcon.png" Label="Button A2" />
        <ribbon:RibbonButton SmallImageSource="Images\SmallIcon.png" Label="Button A3" />
        <ribbon:RibbonButton SmallImageSource="Images\SmallIcon.png" Label="Button A4" />
    </ribbon:RibbonGroup>

</ribbon:RibbonTab >

请注意,我们没有将 RibbonTab 包装在 UserControl 中。View 类继承自 RibbonTab,而不是 UserControl,如下所示:

public partial class ModuleARibbonTab : RibbonTab, IRegionMemberLifetime
{
    #region Constructor

    public ModuleARibbonTab()
    {
        InitializeComponent();
    }

    #endregion

    #region IRegionMemberLifetime Members

    public bool KeepAlive
    {
        get { return false; }
    }

    #endregion
}

这种方法是强制性的。如果我们用 UserControl 包装 RibbonTab,当 Prism 加载它时,它将不会出现在 Ribbon 中。

IRegionMemberLifetime 接口:另请注意,ModuleARibbonTab 类实现了 IRegionMemberLifetime 接口。此接口由 Prism 提供,它控制用户导航离开视图时是否将视图从区域中移除。例如,RibbonRegionAdapter 的行为类似于 ItemsControl,因为它可以同时显示多个 RibbonTab 控件。如果没有 IRegionMemberLifetime 接口,当用户从模块 A 导航到模块 B 时,它将加载模块 B 的 RibbonTab,而不会卸载模块 A 的选项卡。结果是用户将看到两个模块的 RibbonTab 控件。

但我们想要的行为是,当导航到模块 B 时,模块 A 的 RibbonTab 被卸载,以便任何时候只显示活动模块的 RibbonTab。这就是 IRegionMemberLifetime 接口所执行的任务。该接口由一个名为 KeepAlive 的属性组成。如果我们将其设置为 false,则在用户导航离开实现该接口的视图时,该视图将被卸载。

结果是,当我们单击模块 A 的 Task Button 时,模块 A 的 Ribbon Tab 会出现,当我们单击模块 B 的 Task Button 时,Ribbon Tab 将被模块 B 的 Ribbon Tab 替换。

IRegionMemberLifetime 接口可以实现为 View 或 View Model。在演示应用程序中,我在 View 上实现了该接口,因为 KeepAlive 属性值是硬编码的,我们不必在代码中与其交互。如果我们确实需要代码交互(如果我们需要在运行时更改 KeepAlive 属性的值),那么该接口将在 View Model 上实现。

注册 RibbonTab 控件RibbonTab 控件的注册方式与 TaskButton 控件不同。我们不希望 RibbonTab 控件在用户导航到宿主模块之前可用,因此我们将 RibbonTab 控件注册到 Unity 容器,而不是 Region Manager。

#region IModule Members

/// <summary>
/// Initializes the module.
/// </summary>
public void Initialize()
{
    ...

    /* View objects have to be registered with Unity using the overload shown below. By
     * default, Unity resolves view objects as type System.Object, which this overload 
     * maps to the correct view type. See "Developer's Guide to Microsoft Prism" (Ver 4), 
     * p. 120. */

    // Register other view objects with DI Container (Unity)
    var container = ServiceLocator.Current.GetInstance<IUnityContainer>();
    container.RegisterType<Object, ModuleARibbonTab>("ModuleARibbonTab");

    ...
}

#endregion

请注意,如果您使用的是 Unity 2.0 容器,它有一个需要处理的怪癖。默认情况下,Unity 将所有请求解析为 System.Object 类型。要获取视图请求时的正确类型,您必须使用类型映射重载来注册类型,并将 System.Object 作为 TFrom 类型参数,将视图的实际类型作为 TTo 类型参数。

container.RegisterType<Object, ModuleARibbonTab>("ModuleARibbonTab");

否则,您的模块将加载,但最多只能显示字符串“System.Object”。

Workspace 和 Navigator 视图

如前所述,两个模块的 Workspace 和 Navigator 视图都是简单的占位符。Shell 中的 Workspace 和 Navigator 区域被声明为 ContentControl 对象,因此我们可以轻松跳过在这些视图上实现 IRegionMemberLifetime 接口——ContentControl 一次只能显示一个视图。为了清楚地表明这些视图在不活动时应被移除,我还是在 Workspace 和 Navigator 视图上实现了该接口。

导航

Prism 4 添加了一个新的导航 API,仅凭这一点就足以证明升级的价值。

  • 通过添加 RequestNavigate() 方法,导航得到了简化。
  • 现在可以在导航过程中传递参数。
  • RequestNavigate() 方法可以指定一个回调方法,在导航完成后调用。
  • Prism 现在可以通知视图用户正在导航到它或从它导航离开。
  • 现在可以更容易地重用现有视图来显示新信息。

    我将不再详细解释 Prism 4 的导航,因为 Microsoft Prism 开发人员指南 有专门的一章讨论该主题。

演示应用程序使用 Prism 4 导航来加载和卸载其模块的视图。导航代码在用户单击 TaskButton 时触发,该 TaskButton 绑定到其自身 View Model 中的一个命令属性。此 View Model 包含在与 TaskButton 相同的模块中,可以在模块的ViewModels文件夹中找到。

这是绑定 TaskButton 的 XAML:

<fsc:TaskButton Command="{Binding ShowModuleAView}"
... />

正如我们之前注意到的,Command 属性在 View Model 中初始化为一个 ICommand 类的实例,该类包含在模块的Commands文件夹中。ICommand 类在其 Execute() 方法中包含实际的导航代码:

/// <summary>
/// Executes the ShowModuleAViewCommand
/// </summary>
public void Execute(object parameter)
{
    // Initialize
    var regionManager = ServiceLocator.Current.GetInstance<IRegionManager>();

    // Show Ribbon Tab
    var moduleARibbonTab = new Uri("ModuleARibbonTab", UriKind.Relative);
    regionManager.RequestNavigate("RibbonRegion", moduleARibbonTab);

    // Show Navigator
    var moduleANavigator = new Uri("ModuleANavigator", UriKind.Relative);
    regionManager.RequestNavigate("NavigatorRegion", moduleANavigator);

    /* We invoke the NavigationCompleted() callback method in the next  
     * navigation request since it is the last request we have to make. */

    // Show Workspace
    var moduleAWorkspace = new Uri("ModuleAWorkspace", UriKind.Relative);
    regionManager.RequestNavigate("WorkspaceRegion", 
                  moduleAWorkspace, NavigationCompleted);
}

导航请求相对简单。我们只需调用 IRegionManager.RequestNavigate() 并传递区域名称和包含我们想要加载的视图名称的 URI 对象。

模块之间的通信

应用程序的 TaskButton 应该是单选的。也就是说,当一个被选中时,其他应该被取消选中。通常,这会由 TaskButton 对象自动处理(因为它继承自 RadioButton),但不幸的是,当按钮被插入到 Prism 区域时,此功能不起作用。因此,我们将不得不编写代码来实现它。

导航回调方法:请注意,在最后一个导航请求中,我们传递了一个额外的参数,NavigationCompleted。此参数是 Prism 在导航完成后应调用的回调方法的名称。演示应用程序使用此回调方法以及 Composite Presentation Event (CPE) 来实现 TaskButton 的单选行为。

Composite Presentation Events:CPE 是 Prism 模块之间松耦合通信的关键。它们使任何 Prism 组件(Shell、模块)都能与任何其他组件通信,而无需直接了解它。CPE 使用基于事件对象的发布/订阅模型,这些事件对象继承自 CompositePresentationEvent<T> 类。以下是 NavigationCompletedEvent 的声明,演示应用程序使用它来触发单选行为:

using Microsoft.Practices.Prism.Events;

namespace Prism4Demo.Common.Events
{
    /// <summary>
    /// A composite Presentation event 
    /// </summary>
    public class NavigationCompletedEvent : CompositePresentationEvent<string>
    {
    }
}

CPE 被设计为可以跨越程序集边界,而普通的 .NET 事件无法做到。Prism 的事件聚合器(Event Aggregator)提供了一个事件注册表来实现这一点。

  • 当声明 CPE 时,它会被添加到事件聚合器中。
  • 希望收到事件通知的 Prism 组件会在事件聚合器中订阅其 CPE。
  • Prism 组件通过向事件聚合器发布 CPE 来引发 CPE,事件聚合器会通知该事件的所有订阅者。

由于 CPE 在整个应用程序中使用,因此它的类位于演示应用程序的Common项目中,位于Events文件夹中。每个模块都引用此项目,但Common项目对订阅它的模块一无所知。因此,我们可以添加和删除模块而不重新打开Common项目,从而保持应用程序的松耦合。

CPE 类声明只需为 CompositePresentationEvent<T> 类分配一个类型。该类型指示事件发布时将携带的“有效负载”。在 NavigationCompletedEvent 中,类型是字符串——我们只需要传递事件发布者的名称。但在更复杂的应用程序中,我们可以传递一个自定义类型,其中包含与事件一起传递的任何数据。

实现单选行为:让我们回到当前的任务。我们需要为演示应用程序的任务按钮实现单选行为。下面是我们完成此任务的方法:

首先,我们声明 CPE,如上所示。请注意,我们不必显式注册 CPE 到事件聚合器。

接下来,希望收到事件通知的组件会在事件聚合器中订阅该事件。在演示应用程序中,需要收到通知的是与每个 Task Button 相关联的 View Model。因此,View Model 在其 Initialize() 方法中订阅 CPE:

#region Private Methods

/// <summary>
/// Initializes the view model.
/// </summary>
private void Initialize()
{
    ...

    // Subscribe to Composite Presentation Events
    var eventAggregator = ServiceLocator.Current.GetInstance<IEventAggregator>();
    var navigationCompletedEvent = eventAggregator.GetEvent<NavigationCompletedEvent>();
    navigationCompletedEvent.Subscribe(OnNavigationCompleted, ThreadOption.UIThread);
}

#endregion

正如我们上面所见,单击 Task Button 会触发与关联的 ICommand 对象的一系列导航请求。其中最后一个请求传递了回调方法的名称 NavigationCompleted()

/* We invoke the NavigationCompleted() callback 
 * method in our final  navigation request. */

// Show Workspace
var moduleAWorkspace = new Uri("ModuleAWorkspace", UriKind.Relative);
regionManager.RequestNavigate("WorkspaceRegion", 
              moduleAWorkspace, NavigationCompleted);

发布 CPE:当导航请求完成后,将调用回调方法,并通过事件聚合器发布 CPE 来引发它。

#region Private Methods

/// <summary>
/// Callback method invoked when navigation has completed.
/// </summary>
/// <param name="result">Provides information
///        about the result of the navigation.</param>
private void NavigationCompleted(NavigationResult result)
{
    // Exit if navigation was not successful
    if (result.Result != true) return;

    // Publish ViewRequestedEvent
    var eventAggregator = ServiceLocator.Current.GetInstance<IEventAggregator>();
    var navigationCompletedEvent = eventAggregator.GetEvent<NavigationCompletedEvent>();
    navigationCompletedEvent.Publish("ModuleA");
}

#endregion

CPE 事件处理程序:当发布 CPE 时,事件聚合器会通知所有订阅者——在本例中是 Task Button View Models——这些订阅者会处理该事件。这是模块 A 中的事件处理程序:

#region Event Handlers

/// <summary>
/// Sets the IsChecked state of the Task Button when navigation is completed.
/// </summary>
/// <param name="publisher">The publisher of the event.</param>
private void OnNavigationCompleted(string publisher)
{
    // Exit if this module published the event
    if (publisher == "ModuleA") return;

    // Otherwise, uncheck this button
    this.IsChecked = false;
}

事件处理程序首先检查事件是从哪个模块发布的。如果事件是从其宿主模块发布的,则处理程序不执行任何操作。它的 Task Button 被单击了,不需要任何更改。但如果事件是由另一个模块发布的,那么它的 Task Button 的 IsChecked 状态应设置为 false。事件处理程序在 View Model 中设置此属性。模块 Task Button 的 IsChecked 属性绑定到此属性,因此按钮会被取消选中。最终结果是,除了被单击的按钮之外,所有 Task Buttons 都被取消选中。

是否值得?:我们使用 CPE 通信模型来执行相对简单的任务。但该模型具有高度可伸缩性,通过开发适当的数据类作为 CPE 的有效负载,可用于执行几乎任何复杂度的任务。如果为了解决一个简单问题而采取一系列看似过于复杂的步骤,请考虑该方法所带来的好处:

  • 首先,事件可以跨越程序集边界传递。
  • 第二,最重要的是,Prism 组件可以相互通信,而对另一个组件的了解最少。在大多数情况下,项目引用可以减少到仅对 Common 项目的引用,而 Common 项目对依赖于它的模块一无所知。

密切关注依赖关系的方向将保持 Prism 应用程序所有组件的松耦合,从而允许它们独立于彼此进行开发和测试。众所周知,开发一组小型项目远比开发一个大型项目要容易。

结论

希望本文及其配套文章能帮助您构建 Prism 视图切换应用程序必需的基础结构。一如既往,我欢迎其他 CodeProject 用户进行同行评审。请通过在本文末尾的评论部分发帖,告知我您发现的任何错误或任何建议。

附录 A:RibbonRegionAdapter

演示应用程序中使用的 RibbonRegionAdapter 的代码如下所示。该类位于 Shell 项目的Utility文件夹中。

我要感谢来自威斯康星州 La Crosse 的 Scott,他在 Code Review 网站上发布了他用于 Ribbon Region Adapter 的代码。演示应用程序中的这个 RibbonRegionAdapter 是基于他的工作。

using System.Collections.Specialized;
using System.Windows;
using Microsoft.Practices.Prism.Regions;
using Microsoft.Windows.Controls.Ribbon;

namespace PrismRibbonDemo
{
    /// <summary>
    /// Enables use of a Ribbon control as a Prism region.
    /// </summary>
    /// <remarks> See Developer's Guide to Microsoft Prism (Ver. 4), p. 189.</remarks>
    public class RibbonRegionAdapter : RegionAdapterBase<Ribbon>
    {
        /// <summary>
        /// Default constructor.
        /// </summary>
        /// <param name="behaviorFactory">Allows the registration
        /// of the default set of RegionBehaviors.</param>
        public RibbonRegionAdapter(IRegionBehaviorFactory behaviorFactory)
            : base(behaviorFactory)
        {
        }

        /// <summary>
        /// Adapts a WPF control to serve as a Prism IRegion. 
        /// </summary>
        /// <param name="region">The new region being used.</param>
        /// <param name="regionTarget">The WPF control to adapt.</param>
        protected override void Adapt(IRegion region, Ribbon regionTarget)
        {
            region.Views.CollectionChanged += (sender, e) =>
            {
                switch (e.Action)
                {
                    case NotifyCollectionChangedAction.Add:
                        foreach (FrameworkElement element in e.NewItems)
                        {
                            regionTarget.Items.Add(element);
                        }
                        break;

                    case NotifyCollectionChangedAction.Remove:
                        foreach (UIElement elementLoopVariable in e.OldItems)
                        {
                            var element = elementLoopVariable;
                            if (regionTarget.Items.Contains(element))
                            {
                                regionTarget.Items.Remove(element);
                            }
                        }
                        break;
                }
            };
        }

        protected override IRegion CreateRegion()
        {
            return new SingleActiveRegion();
        }
    }
}
© . All rights reserved.