WPF 的 Prism 2.1 入门






4.93/5 (38投票s)
如何使用演示应用程序开始使用 WPF 的 Prism 2.1

引言
本文更新并替换了我早期关于 Prism 1.0 的文章(更正式的名称是复合应用程序库,或“CAL”)。本文的主要变化是使用了Prism 2.1,并更详细地解释了应用程序的设置方式。我保留了旧文章,供仍在G使用 Prism 1.0 的人参考。
什么是 Prism?
Prism 是一个用于使用 WPF 或 Silverlight 创建复合应用程序的框架。本文涵盖了 WPF 复合应用程序的创建。Silverlight 复合应用程序与其 WPF 对应应用程序具有共同的特性,但 WPF 和 Silverlight 之间的差异意味着两种应用程序的构建方式将存在显著差异。尽管如此,如果你能构建一个 WPF 复合应用程序,那么你离知道如何构建一个 Silverlight 复合应用程序就不远了。
为什么要使用复合应用程序?
复合应用程序最初是为了帮助企业客户将多个旧版应用程序集成到单个用户界面中,从而创建集成系统的外观。假设一家公司有两个旧版系统。他们可以创建一个类似于演示应用程序的复合应用程序,允许两个旧版系统显示在单个窗口中。他们基本上必须重写两个系统的用户界面以在复合应用程序主窗口中显示,但如果旧版应用程序是根据良好的关注点分离编写的,那么对旧版应用程序后端进行的任何更改都将是最小的。
不久之后,架构师和开发人员认识到复合应用程序在构建传统桌面应用程序方面提供了明显的优势。复合应用程序方法允许开发团队将应用程序分解为准独立的模块,这些模块加载到 Shell 项目中。这种有效划分应用程序的能力带来了几个关键优势
- 模块可以分配给不同的开发组,并或多或少地独立开发。
- 每个模块都可以单独测试,与它与其他模块的交互分开。
- 模块可以根据需要从应用程序中换进换出。例如,如果我们的主菜单是作为模块创建的,那么将其替换为 Ribbon 模块是相当简单的。
- 程序的维护变得更加容易,因为更改通常可以隔离到特定的模块。
这些是复合应用程序模型作为桌面应用程序(现在是 Silverlight 应用程序)的架构模式变得相当流行的主要原因。
关于本文
本文旨在帮助您开始使用 Prism 2.1 创建 WPF 应用程序。具体来说,它解释了 Prism 背后的许多基本概念,以及如何执行以下任务
- 在 WPF 应用程序中设置 Prism 2.1;
- 以最小耦合设置应用程序模块之间的通信;以及
- 按需显示工作区视图
请注意,第三点描述的是显示视图,而不是加载和卸载模块。这两个概念很容易混淆。本文描述的方法是在启动时加载所有模块(及其视图),然后按需激活(显示)和停用(隐藏)视图。
我不会描述 Prism 2.1 的所有内容;Prism 文档在这方面做得很好。相反,我将描述一条相当狭窄的 Prism 2.1 路径——一种让应用程序启动并运行的方法。一旦你理解了这条路径,你就可以很容易地开始扩展你对 Prism 其他功能的了解,以找到可能更适合你开发需求的方法。
本文假设您熟悉 WPF 开发和 Model-View-ViewModel (MVVM) 模式。Prism 文档讨论了一种不同的模式,Model-View-Presenter,这种模式似乎在 WPF 开发人员中越来越不受欢迎,取而代之的是 MVVM。我写了一篇关于 MVVM 的 CodeProject 文章,这可能会帮助您更多地了解 MVVM 模式。
请注意,本文中我指的是 Prism 2.0 文档,而不是 Prism 2.1 文档。那是因为 2.0 文档可下载为 PDF,而 Prism 2.1 中的更改相对较小。
欢迎您的评论
我写这篇文章有两个原因
- 首先,我希望它能帮助其他开发人员克服 Prism 2.1 的学习曲线。
- 其次,我非常感兴趣对我在使用 Prism 设置 WPF 应用程序的方法进行同行评审。
对于构建 Prism 应用程序的最佳方法,似乎尚未达成普遍共识。我认为我提出了一种很好的解决问题的方法,但可能还有其他更好的方法来构建 Prism 应用程序和实现按需显示框架。如果您有替代方法,或者改进我现有方法的建议,我欢迎您的评论。我将不时更新本文,以收录最佳建议,并注明每条建议的作者。
演示应用程序
与本文的早期版本一样,本文随附的演示应用程序 Prism2Demo
非常简单。它是在 Visual Studio 2008 中创建的,可以轻松转换为 Visual Studio 2010。在此版本中,应用程序实现了 Windows 资源管理器界面,主窗口左侧是导航窗格,右侧是工作区窗格。UI 显示在本文顶部的屏幕截图中。
导航器有两个按钮,每个按钮都显示来自不同工作区模块的视图。演示应用程序执行最少的工作,以展示如何设置带有按需显示的 Prism 2.0 应用程序。为了尽可能简单,演示不执行任何数据访问或任何类型的实际处理。每个按钮只是激活一个显示其所属模块名称的视图。
创建 Prism 2.1 WPF 应用程序
了解演示应用程序如何工作的最佳方法是逐步查看它是如何创建的。因此,从现在开始,我将描述我用于创建演示应用程序的过程,并解释其基本概念、采取的行动和做出的选择。
步骤 1:创建 WPF 解决方案
以正常方式在 Visual Studio 中创建 WPF 解决方案。随解决方案创建的项目将用作解决方案的 Shell 项目。
步骤 2:向解决方案项目添加引用
向 Shell 项目添加对以下 Prism 2.1 DLL 的引用
- Microsoft.Practices.Composite.dll
- Microsoft.Practices.Composite.Presentation.dll
- Microsoft.Practices.Composite.UnityExtensions.dll
- Microsoft.Practices.Unity.dll
- Microsoft.Practices.ObjectBuilder2.dll
- Microsoft.Practices.ServiceLocation.dll
这些 DLL 提供的服务在 Prism 文档的第 129 页有解释。
步骤 3:重命名主窗口
将 Shell 项目中的主窗口重命名为“Shell”。此窗口将是项目的 Shell 窗口。此窗口将包含区域,这些区域将用作应用程序提供的模块视图的占位符。
步骤 4:设置应用程序的 Bootstrapper
Prism 应用程序的启动方式与常规 WPF 应用程序不同。通常,App.xaml 标记提供一个 StartupUri
,用于指定启动时应打开哪个窗口。Prism 应用程序具有更复杂的启动过程,需要使用 Bootstrapper
类。
Prism Bootstrapper
派生自作为 Prism 一部分提供的 UnityBootstrapper
。Unity 是 Microsoft 提供的一个控制反转容器。它默认与 Prism 一起工作,但如果您喜欢不同的 IOC 容器,Prism 文档描述了如何编写一个简单的适配器,以便您可以使用您选择的容器。在本文中,我们将使用默认的 UnityBootstrapper
。如果您不熟悉控制反转或 IOC 容器,网络上有一些很好的资源。以下是我推荐的几个
演示应用程序 Bootstrapper
覆盖了 UnityBootstrapper
提供的三个方法
ConfigureContainer()
:此覆盖用于向 IOC 容器注册服务。演示应用程序使用它注册一个ModuleServices
类,演示应用程序的模块稍后将从 IOC 容器解析该类。CreateShell()
:此覆盖替换了 App.xaml 中的StartupUri
。它实例化 Shell 窗口,显示它,并将其返回给 Prism,以便 Prism 可以使用 Shell。GetModuleCatalog()
:此覆盖指定 Prism 应如何定位应用程序的模块。有几种方法,所有这些方法都在 Prism 文档中有所描述。演示应用程序使用“目录搜索”方法,这也是本文中我将描述的方法。
使用目录搜索方法的主要原因是放松 Shell 项目与模块之间的耦合。请注意,Shell 项目不包含对模块项目的引用,并且 Shell 项目的代码未引用模块中的任何内容。实际上,Shell 项目完全不知道它正在加载的模块。这意味着,我们可以在不担心破坏 Shell 的情况下开发和维护模块。这是松散耦合的一个主要好处。
这是演示应用程序的 Bootstrapper
代码
using System.Windows;
using Microsoft.Practices.Composite.Modularity;
using Microsoft.Practices.Composite.UnityExtensions;
using Prism2Demo.Common;
namespace Prism2Demo
{
public class Bootstrapper : UnityBootstrapper
{
/// <summary>
/// Registers types with the Unity container.
/// </summary>
protected override void ConfigureContainer()
{
base.ConfigureContainer();
Container.RegisterType<IModuleServices, ModuleServices>();
}
/// <summary>
/// Creates the application shell.
/// </summary>
protected override DependencyObject CreateShell()
{
var shell = new Shell();
shell.Show();
return shell;
}
/// <summary>
/// Populates the module catalog.
/// </summary>
protected override IModuleCatalog GetModuleCatalog()
{
/* Note that each module in this application has a post-build
* event that copies the module to a 'Modules' subfolder in the
* output folder. Prism will use the DirectoryModuleCatalog that
* we create here to scan that folder to populate the catalog. */
// Create a new module catalog and pass it to Prism
var catalog = new DirectoryModuleCatalog();
catalog.ModulePath = @".\Modules";
return catalog;
}
}
}
特别注意 GetModuleCatalog()
覆盖方法。我们使用 DirectoryModuleCatalog
实现 IModuleCatalog
接口,它将在指定目录中搜索模块。目录对象的 ModulePath
属性指定应搜索哪个目录,对于演示应用程序,它是 Shell 项目应用程序输出文件夹中的子文件夹。我们将很快描述如何将模块程序集获取到该文件夹。
在 Prism2Demo
中,我们使用 Shell 项目应用程序输出文件夹中的一个子文件夹,名为 Modules。如下所示,每个模块都有一个后期构建事件,将模块复制到此子文件夹,以便 Prism 可以找到它。
步骤 5:修改应用程序启动
如上所述,我们不使用 App.xaml 提供的 StartupUri
。相反,我们将使用其代码隐藏在启动时调用 Bootstrapper
。为此,请覆盖 App.xaml.cs 中的 OnStartup()
方法。该类应如下所示
using System.Windows;
namespace HelloWorld.Desktop
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
var bootstrapper = new Bootstrapper();
bootstrapper.Run();
}
}
}
然后,打开 App.xaml 并删除 StartupUri
引用,该引用被代码隐藏文件中的 OnStartup()
覆盖所取代。
步骤 6:在 Shell 中创建区域
接下来,我们需要在 Shell 窗口中定义区域。演示应用程序实现了一个 Windows 资源管理器界面,因此它有两个区域:一个 NavigatorRegion
和一个 WorkspaceRegion
。以下是 Shell.xaml 中声明这些区域的 XAML 标记
<!-- Define regions-->
<DockPanel>
<ContentControl cal:RegionManager.RegionName="NavigatorRegion"
DockPanel.Dock="Left" Width="200" />
<ContentControl cal:RegionManager.RegionName="WorkspaceRegion" />
</DockPanel>
WPF 控件充当区域的占位符。通常使用的两个是
ContentControl
:当区域将容纳单个视图时使用此控件。视图(通常是用户控件)将扩展以填充区域。ItemsControl
:当区域将容纳多个视图(例如列表)时使用此控件。视图不会垂直扩展以填充区域;相反,它们通常会堆叠。
在演示应用程序中,我们对这两个区域都使用 ContentControls
,排列在设置为 last-child-fill 的 DockPanel
中。这使得工作区区域在调整大小时扩展以填充窗口。
要向 Shell 添加区域,首先向 Shell 窗口 XAML 添加以下命名空间声明
xmlns:cal="http://www.codeplex.com/CompositeWPF"
然后,添加区域以定义 Shell 的布局,如上所示。请注意,区域可以在 Visual Studio 或 Expression Blend 中添加。布局的定义方式与传统 WPF 窗口布局的定义方式相同。唯一的区别是每个区域都需要一个区域名称,该名称使用 RegionManager.RegionName
附加属性进行分配。
步骤 7:创建模块
模块是复合应用程序的核心。一个模块可以在一个区域中有一个视图,在一个区域中有多个视图,或者在多个区域中有多个视图。那么,如何决定有多少个模块,以及多少个区域中有多少个视图呢?我建议考虑如何划分应用程序。
- 将应用程序提供的各种功能进行分类的逻辑点在哪里?
- 其用例如何组合在一起?如果用例可以分组在主要标题下,那么这些标题可能至少提供了如何划分应用程序的初步指导。
使用 MVVM 模式设计的典型模块将包含几个元素
模块有一个 Initializer
类、一个或多个视图以及一个或多个视图模型。通常每个视图有一个视图模型(如演示应用程序的 Navigator 模块),但并非总是如此。Prism 调用 Initializer
类的 Initialize()
方法来执行模块上的初始化任务,例如配置模块视图和视图模型以及将视图添加到 Shell 中的区域。初始化是在 Prism 将模块添加到模块目录时完成的,我们将在下面讨论。
演示应用程序设计上非常简单。它有一个导航器和两个工作区。应用程序带来的主要问题是导航器必须指导这些工作区的显示,同时尽可能少地了解它们。
我们已将导航器和工作区之间的耦合简化为单个字符串,该字符串指定要显示的工作区。导航器将触发一个事件,该事件将携带该字符串作为其“有效负载”。但稍后会详细介绍。首先,我们必须创建我们的模块;然后,我们将弄清楚如何在它们之间实现松散耦合的通信。
Prism 应用程序中的每个模块都是一个单独的类库项目。为应用程序将使用的每个模块创建一个项目。演示应用程序遵循此命名约定来命名模块
<SolutionName>.Modules.<ModuleName>
例如,工作区 A 模块中的项目名为“Prism2Demo.Modules.WorkspaceA
”。
每个模块应包含对以下 WPF 程序集的引用
- PresentationCore.dll
- PresentationFramework.dll
- WindowsBase.dll
此外,模块应包含对以下 Prism 程序集的引用
- Microsoft.Practices.Composite.dll
- Microsoft.Practices.Composite.Presentation.dll
- Microsoft.Practices.Unity.dll
完成此操作后,我们就可以为模块创建 Initializer
类了。
步骤 8:向每个模块添加后期构建事件
在我们深入了解模块细节之前,我们需要确保能够将每个模块的程序集传输到 Prism 可以使用目录搜索找到它们的位置。为了实现这个结果,每个模块都需要一个后期构建事件,将模块程序集从模块项目的应用程序输出文件夹复制到 Shell 项目应用程序输出文件夹中的公共 Modules 子文件夹。该事件的命令应如下所示
xcopy "$(TargetDir)<Module DLL>"
"$(SolutionDir)<Solution Name>\bin\$(ConfigurationName)\Modules\" /Y
其中
- <Module DLL> = 模块 DLL(例如,Prism2Demo.Modules.Navigator.dll)
- <Solution Name> = 解决方案名称(例如,Prism2Demo)
要创建后期构建事件,请打开模块项目的“属性”页,切换到“生成事件”选项卡,并在“后期生成事件命令行”框中添加命令。这是导航器项目的页面
步骤 9:向每个模块添加初始化器类
现在我们可以将模块程序集复制到 Prism 可以找到它的位置,我们就可以开始配置模块了。每个模块都有一个实现 IModule
接口的 Initializer
类。演示应用程序遵循此命名约定来命名 Initializer
类
<ModuleName>Module
例如,演示应用程序中 WorkspaceA 模块的 Initializer
类名为“WorkspaceAModule.cs”。创建 Initializer
类最简单的方法是重命名随模块项目创建的 Class1.cs 类,添加下面描述的类属性,并指定该类派生自 IModule
。
初始化程序类通常用于注册视图或将视图添加到区域等任务。IModule
接口指定了一个方法:Initialize()
。这是 Prism2Demo
中 Navigator 模块的 Initializer
类
using Microsoft.Practices.Composite.Modularity;
using Microsoft.Practices.Composite.Regions;
using Microsoft.Practices.Unity;
namespace Prism2Demo.Modules.Navigator
{
[Module(ModuleName = "Navigator")]
public class NavigatorModule : IModule
{
#region Fields
// Member variables
private readonly IUnityContainer m_Container;
private readonly IRegionManager m_RegionManager;
#endregion
#region Constructor
public NavigatorModule(IUnityContainer container,
IRegionManager regionManager)
{
m_Container = container;
m_RegionManager = regionManager;
}
#endregion
#region IModule Members
public void Initialize()
{
// Register view model
this.RegisterViewsAndServices();
/* Note that we instantiate the view directly, since we don't need to inject
* any dependencies. We use Unity to resolve the view model, since it has a
* dependency that needs to be injected. Then we use View Injection to add
* the view to the Navigator region. The view model was registered with the
* Unity container in the Bootstrapper. */
// Set up view and view model
var navigatorView = new MainView();
navigatorView.DataContext = m_Container.Resolve<INavigatorViewModel>();
m_RegionManager.Regions["NavigatorRegion"].Add(navigatorView);
}
#endregion
#region Private Methods
private void RegisterViewsAndServices()
{
m_Container.RegisterType<INavigatorViewModel, NavigatorViewModel>();
}
#endregion
}
}
请注意,模块的初始化程序类带有一个或多个属性,Prism 在初始化模块时会使用这些属性
Module
属性是必需的;它指定模块的名称。如果模块是要按需加载而不是在启动时加载,则Module
属性还将包含一个指定按需加载的第二个参数
[Module(ModuleName = "MyModule", OnDemand = true)]
ModuleDependency
属性(未在 Navigator Initializer
类中使用)用于指定当前模块所依赖的任何模块。Prism 将首先加载依赖模块。[ModuleDependency("SomeModule")]
另请注意,Unity 容器和 Prism 区域管理器是通过构造函数注入的,并设置为成员变量。Prism 在启动时将区域管理器实例注册到 Unity 容器中,因此区域管理器立即可用于构造函数注入。导航器模块演示中的初始化类仅将其视图注册到区域管理器。
Initialize()
方法通常执行三个任务
- 它注册我们以后可能想要解析的任何视图和服务,使用 Unity 容器。
- 它执行模块视图的任何所需创建和配置。
- 它将模块视图添加到 Shell 区域。
在导航器模块中,您可以看到我们将导航器视图模型注册到 Unity 容器。我们将使用 Unity 解析视图模型,而不是自己实例化它,以利用依赖注入。然后,我们直接实例化导航器的视图(因为我们不需要注入任何依赖项),将视图模型设置为其 DataContext
,并将视图添加到 Shell 的导航器区域。我们将在下面进一步讨论这个过程。
步骤 10:向每个模块添加视图
模块视图通常是 WPF 用户控件。视图像任何其他 WPF 用户控件一样设计,使用 Visual Studio 和 Expression Blend。模块可能包含多个视图,尽管演示应用程序中的每个模块都包含一个视图。这是 Prism2Demo
应用程序中的 Navigator 模块视图
<UserControl x:Class="Prism2Demo.Modules.Navigator.MainView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Background="Beige">
<Grid>
<StackPanel VerticalAlignment="Center">
<TextBlock Text="Navigator" Foreground="Green" HorizontalAlignment="Center"
VerticalAlignment="Center" FontFamily="Calibri"
FontSize="24" FontWeight="Bold" />
<Button Command="{Binding ShowWorkspaceA}"
Margin="5" Width="125">Show Workspace A</Button>
<Button Command="{Binding ShowWorkspaceB}"
Margin="5" Width="125">Show Workspace B</Button>
</StackPanel>
</Grid>
</UserControl>
有两种将视图添加到区域的方法
- 视图发现:我们只需向 Prism 的 Region Manager 注册视图,然后让 Prism 负责实例化和显示视图。
- 视图注入:我们编写代码来显式实例化和初始化视图,并将它们添加到区域。
演示应用程序使用视图注入,原因如下:在 Initialize()
方法中,我们使用 Unity 容器解析(实例化)Navigator 视图的视图模型。正如我上面提到的,演示应用程序在 Navigator 模块中展示了 MVVM 模式的实现。Prism 文档更倾向于 MVP 模式,但随着 MVVM 的兴起,MVP 模式似乎在 WPF 社区中失宠。
MVP 假定 Presenter 将创建 View,或者 View 将创建 Presenter。这两种方法都在两个类之间创建了显式依赖。MVVM 假定第三个类将实例化 View 和 View Model,并且该类将 View Model 设置为 View 的 DataContext
。这导致更松散的耦合,只有从 View 到 View Model 的隐式依赖。
视图发现与 MVP 模式配合得很好,Presenter 将创建视图,反之亦然。Prism 文档包含这两种方法的示例。但是,它与 MVVM 模式配合得不好,我们需要第三个类来实例化视图和视图模型并将它们连接起来。因此,演示应用程序在其 Initialize()
方法中使用了视图注入。
Navigator 的 Initialize()
方法直接实例化 Navigator 的视图,因为我们不需要向其中注入任何依赖项。然后,它使用 Unity 容器解析视图模型(该视图模型已在 Bootstrapper 中注册到 Unity),以便将容器注入视图模型。视图模型及其组件(命令和服务)稍后将使用容器进行各种操作。视图模型设置为视图的 DataContext
,并将视图添加到 Shell 的 Navigator 区域。
至此,我们有了一个基本的 Prism 应用程序,但它没有实际功能。现在,我们需要开始添加该功能。我们将从创建一个项目开始,在该项目中放置将在模块之间使用的公共元素。
步骤 11:向解决方案添加一个通用项目
一个功能性 Prism 应用程序将包含许多基类和接口。我们通过将基类和接口放置在通用项目中来集中依赖项并避免重复。模块将包含对该项目的引用,而不是对其他项目或 Shell 项目的引用。该项目在 Prism 文档中称为 Infrastructure
项目。我更喜欢称之为 Common
项目,这也是演示应用程序中项目的名称。
设计 Prism 应用程序时遇到的一个问题是,将多个模块使用的资源字典放置在哪里。通常将它们放置在 shell 应用程序级别的方法本身不起作用,因为 Prism 模块或多或少独立于 Shell。模块必须跨 shell 边界才能访问公共资源字典。可以使用“包 URL”来跨越这些边界。
包 URL 只是一个 URL,其中包含对用户机器上另一个程序集的引用,通常是同一解决方案中的另一个项目。它们看起来很奇怪,但它们能完成任务。我不会在这里花太多时间讨论它们,因为它们在 MSDN 上有详细文档。以下是一个包 URL 的示例,它引用 Prism 应用程序的 Common
项目中名为 Styles.xaml 的资源字典。该字典在解决方案中另一个项目的应用程序级别被引用
<Application.Resources>
<ResourceDictionary>
<!-- Resource Dictionaries -->
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/Common;
component/Dictionaries/Styles.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
请注意,我们在演示应用程序中不使用资源字典,因此它不包含任何此类引用。
显然,可以将解决方案范围的资源字典存储在 Shell 项目中,并使用包 URL 按照上述方式引用它们。但是,我更喜欢将模块和 Shell 之间的依赖项降至最低,因为在开发应用程序时,Shell 是更改最频繁的对象之一。因此,我更喜欢将解决方案范围的资源字典放在更改频率较低的 Common
项目中。
步骤 12:向 Shell 添加一个视图模型
如果 Shell 需要一个视图模型,就添加一个。大多数通信应该发生在模块之间,因此模块视图模型将受到大部分关注。但是,Shell 可能需要一个用于菜单栏、功能区或类似控件的视图模型。请注意,演示应用程序不使用 Shell 视图模型。
步骤 13:创建复合演示事件
使用 Prism 的全部目的是尽可能保持模块彼此独立和隔离。这个目标与模块之间需要相互通信的需求直接冲突,这往往会将模块耦合在一起。
解决冲突的一种方法是使用复合演示事件(“CPEs”,也称为“聚合事件”)。Prism 包含一个事件聚合器(“EA”),用作 CPEs 的注册表,CPEs 是从 CompositePresentationEvent
类派生的类。EA 负责实例化 CPEs 并管理事件的发布和订阅。结果是模块无需了解彼此。它们只需要了解 EA。
Prism2Demo
应用程序中的导航器模块包含两个按钮,用于激活和停用两个工作区模块的视图。导航器模块通过触发一个 CPE 来执行此任务,该 CPE 将所请求视图的模块名称作为其有效负载。如果这是一个普通的 .NET 事件,工作区模块需要了解导航器模块才能订阅其事件。但是,CPE 作为 EA 中的一个单独类存在,这意味着我们可以切断导航器模块和工作区模块之间的联系。相反,所有模块都了解 EA。
CPE 是简单的类,它们只继承自 CompositePresentationEvent
基类,并指定它们携带的有效负载类型。以下是演示应用程序使用的 ViewRequestedEvent
的类声明
using Microsoft.Practices.Composite.Presentation.Events;
namespace Prism2Demo.Common.Events
{
public class ViewRequestedEvent : CompositePresentationEvent<string>
{
}
}
在这种情况下,事件将携带一个 string
,该 string
指定 Navigator 请求了哪个视图,Navigator 发布该事件。每个工作区模块都将订阅该事件,并检查该 string
以确定是否需要显示其视图。
通常,您需要为模块之间以及模块与 Shell 之间通信的每个元素创建一个 CPE。
步骤 14:发布 CPE
导航器在绑定到其视图中按钮的 ICommand
中发布 ViewRequestedEvent
。ICommand
通常用作 MVVM 模式的一部分。它们之所以受欢迎,是因为它们通过 Command
属性提供了与 WPF 控件(例如按钮和菜单项)的轻松绑定。
ICommand
还通过 ICommand
接口提供的 CanExecute()
方法提供了一种启用或禁用控件的简便方法。一旦 WPF 控件绑定到 ICommand
,它将根据开发人员在 CanExecute()
方法中提供的逻辑自动启用和禁用。
导航器的视图模型类包含一个 ActiveWorkspace
属性,视图模型的 ICommand
使用该属性来确定导航器上的每个按钮是否应该启用。ICommand
将发布 ViewRequestedEvent
的任务委托给服务类 CommandServices
,这种方法消除了 ICommand
类之间的重复。以下是实际发布事件的代码
// Publish ViewRequestedEvent
var eventAggregator = viewModel.Container.Resolve<IEventAggregator>();
var viewRequestedEvent = eventAggregator.GetEvent<ViewRequestedEvent>();
viewRequestedEvent.Publish(workspaceName);
导航器只是从事件聚合器获取事件,然后发布该事件,并带有一个指定已请求哪个视图的 string
。对 GetEvent<T>()
的调用将导致事件对象实例化(如果它尚不存在),因此发布者或订阅者都无需检查事件是否存在。在此示例中,工作区名称是事件对象的有效负载,因此我们只需发布事件并传递它要传递的有效负载。EA 会处理其余部分。
步骤 15:订阅 CPE
订阅事件的过程非常相似。我们调用 EA 来获取事件,然后订阅它。以下是导航器模块(在其 Initialize()
方法中)用于订阅 ViewRequestedEvent
的代码
// Subscribe to ViewRequestedEvent
var eventAggregator = m_Container.Resolve<IEventAggregator>();
var viewRequestedEvent = eventAggregator.GetEvent<ViewRequestedEvent>();
viewRequestedEvent.Subscribe(this.ViewRequestedEventHandler,
ThreadOption.PublisherThread, true);
我们使用的 Subscribe()
重载创建了对我们用作事件处理程序的委托的强引用——重载的第三个参数指定了一个布尔值表示强引用。我尝试让 CPE 订阅与弱引用一起工作,但效果不佳,我也不确定原因。似乎委托被过早地垃圾回收了,但这并不确定。如果有人能对此问题提供更多见解,请发表评论。
无论如何,使用强委托引用意味着每个模块在关闭之前必须取消订阅其 CPE。因此,每个具有 CPE 订阅的模块都应该实现一个析构函数,该析构函数将取消订阅所有已订阅的 CPE。以下是工作区模块中使用的代码
#region Destructor
/// <summary>
/// Releases resources and references before this object is destroyed.
/// </summary>
~WorkspaceAModule()
{
// Initialize
var eventAggregator = m_Container.Resolve<IEventAggregator>();
var viewRequestedEvent = eventAggregator.GetEvent<ViewRequestedEvent>();
// Unsubscribe from ViewRequestedEvent
viewRequestedEvent.Unsubscribe(ViewRequestedEventHandler);
}
#endregion
至此,Prism 2.0 应用程序的所有基本功能(如演示应用程序所示)都已实现。当然,在实际应用程序中,您将拥有更多的模块,并且每个模块中都有更多的功能。
最明显的问题是如何将数据访问层集成到复合应用程序中。通常,我为每个模块提供自己的数据访问层。这种方法保留了每个模块的独立性和隔离性,但可能导致大量代码重复。我通过让模块调用 Common 项目或单独的 DataAccess
项目中的数据访问服务来避免这个问题。这样,数据访问可以轻松地进行模拟,并且可以通过将单个模块仅连接到 DataAccess
项目来进行数据测试。
创建安装项目
演示应用程序包含一个安装项目,展示了为 Prism 2.1 应用程序设置安装程序的基本知识。必须处理的主要问题是将模块程序集放置在安装项目中的正确位置。请记住,Prism 开发的目标是最大限度地减少耦合。因此,演示项目的 Shell 应用程序完全不知道它所承载的模块。这意味着,在构建安装项目时,.NET 编译器不会将模块视为 Shell 项目的依赖项。
解决该问题的方法是手动将模块从 Shell 项目输出文件夹中的 Modules 文件夹复制到安装项目中的 Application 文件夹。我们假设您已经在 Visual Studio 中为您的 Prism 应用程序创建了一个安装项目,并且已经按照正常步骤用您的项目输出填充了该项目。完成此操作后,打开安装项目的文件系统编辑器,并在 Application 文件夹中添加一个 Modules 子文件夹。然后,使用上下文菜单上的“添加文件”选项,选择 Shell 项目输出文件夹中 Modules 子文件夹中的所有文件,并将它们添加到您在安装项目中创建的 Modules 子文件夹中。
请注意,无论何时从应用程序中添加或删除模块,您都需要通过再次执行这些步骤来更新安装项目。由于 Shell 项目和模块之间没有依赖关系,因此 Visual Studio 在构建安装项目时将对模块一无所知,这意味着它将遗漏任何添加或删除的模块。
接下来做什么?
至此,您应该拥有一个 Prism 2.1 应用程序的基本框架,它可以按需显示模块视图。本文应为您提供足够的背景知识,以便深入研究 Prism 文档并了解该库的详细信息。WPF 和 Silverlight 的复合应用程序指南(2.1 版,2009 年 10 月)包含大量动手实验和快速入门,以及详细的参考应用程序。