使用 Prism 的复合应用程序(WPF)- 第 2 部分






4.88/5 (13投票s)
这是关于使用 Prism 和 MVVM 开发 WPF 演示应用程序的系列文章的第二部分。在这一部分中,我们将通过添加通用基础设施和五个模块来增强这个演示项目。
引言
这是共有三个部分的系列文章的第二部分。在第一部分中,我们创建了 Shell 并定义了用于在 Shell 区域内加载模块的区域。在这一部分中,我们将通过添加通用基础设施和五个模块来增强在第一部分中创建的演示项目,正如第一部分的架构部分所述。我们还将看到如何在应用程序启动时或稍后按需加载模块。现在,让我们从第一部分下载代码,打开 ITHelpDeskDemoApp 解决方案,并在本文的后续章节中继续进行开发。
注意:如果您还没有阅读第一部分,我建议您先去阅读,因为它将为您提供更好的背景知识,并帮助您更好地理解本文。
目录
第一部分- 最终演示概述
- 最终演示的架构
- 什么是Prism
- 什么是Shell
- 创建Shell和定义区域
- 创建通用基础设施
- 创建模块
- 导航模块
- 员工模块
- 软件模块
- 硬件模块
- 请求模块
- 加载模块
- 演示应用程序中的问题
- 什么是事件聚合器
- 事件聚合器的使用
- 什么是IActiveAware接口
- IActiveAware的使用
创建通用基础设施
首先,在解决方案 (ITHelpDeskDemoApp) 中添加一个名为“ITHelpDesk.Common”的文件夹。然后,向此文件夹中添加一个名为 ITHelpDesk.Common.Infratructure 的类库项目。该项目的目的是存放将在应用程序所有其他项目中使用的通用代码。该项目将提供应用程序工作所需的服务、事件、模型等。在这个项目中,我们使用 xml 文件作为数据库。通过添加文件夹和文件,使 ITHelpDesk.Common.Infratucture 项目的结构与下图所示相同。
在上面“ITHelpDesk.Common.Infrastructure”项目的截图中,我们添加了 App_Data、Events、Models、Repository 和 Services 文件夹,并向其中添加了文件。以下是每个文件和文件夹的说明。
App_Data:当我们开发任何应用程序时,它可能需要与数据库交互以执行 CRUD 操作。在这个演示应用程序中,ITHelpDeskDB.xml 文件被用作数据库。我们可以用任何数据库来代替 ITHelpDeskDB.xml。这里我们使用 XML 文件,因为它对读者来说更容易,并且下载的代码无需进行任何数据库设置即可执行。
Event:该文件夹包含两个类文件:EmployeeUpdatedEvent 和 RefereshModuleEvent,我们将在本文的下一部分讨论它们。
Models:该文件夹包含应用程序所需的所有实体类。
Repository:这里我们使用仓储模式来解耦持久层和应用程序的其余部分。它包含一个名为 IHelpDeskRepository 的接口和一个名为 HelpDeskXMLRepository 的类。代码大纲如下所示。
Services:这个文件夹里只有两个文件,一个是名为 IEmployeeService 的接口,另一个是名为 EmployeeService 的类。EmployeeService 实现了 IEmployeeService 接口,如下所示。它将为应用程序提供与员工相关的服务。代码大纲如下所示。
在 EmployeeService 的构造函数中,我们传递/注入了 EmployeeService 类的依赖项。EmploeeService 类依赖于 IHelpDeskRepository。对于 IHelpDeskRepository,我们注入了 HelpDeskXMLRepository 的实例。我们将在启动项目 ITHelpDeskDemoApp 的 ITHelpDeskBootstrapper 类中配置这种关联。
创建模块
所有模块都需要放在解决方案级别的名为 ITHelpDesk.Modules 的文件夹中,以便所有模块项目都在一起。所以,让我们添加一个同名文件夹。
导航模块
在 ITHelpDesk.Modules 文件夹中添加一个名为 ITHelpDesk.Module.Navigation 的项目,并通过添加对 Prism 4 的引用以及所需的文件夹和文件,使项目结构与下图相同。
在 Views 文件夹中,添加一个名为 NavigationBar.xaml 的 UserControl。该文件包含创建带有三个按钮控件的导航栏的代码。这些按钮与 NavigationViewModel 中的三个不同的 DelegateCommand 绑定。每次点击按钮时,相关的 DelegateCommand 将被触发以调用相应的方法。该方法将把相应的模块加载到 Shell 内指定的区域中。
NavigationViewModel:NavigationViewModel 类文件中的大部分代码都简单明了,因此我们只讨论负责激活和停用模块的代码。
以下代码在点击导航栏中的“Software”按钮时执行。我们已将 loadSoftwareModule 方法作为委托绑定到该 DelegateCommand,该方法将被执行。
private void loadSoftwareModule() { // LoadModule method is responsible to load and initialize the module // It loads only if module is not initialize already. ModuleManager.LoadModule("SoftwareModule"); var requestInfoRegion = RegionManager.Regions["RequestInfoRegion"]; var newView = requestInfoRegion.GetView("SoftwareDetail"); // As RequestInfoRegion uses ContentControlRegionAdapter so at a time only one view will be activated. requestInfoRegion.Activate(newView); }
要使一个类表现为模块,该类必须实现 IModule 接口。这就是为什么在该项目的根级别,我们创建了 NavigationModule 文件,该文件实现了 IModule 接口。IModule 接口只有一个名为 Initialize 的方法。我们需要在此方法中编写模块初始化时要执行的逻辑。
在 NavigationModule 模块的构造函数中,我们注入了两个依赖项,分别名为 IUnityContainer 和 IRegionManager。在这里,IUnityContainer 负责解析依赖项,而 IRegionManager 则通过使用 RegionManagerExtensions 类的扩展方法 RegisterViewWithRegion 将视图注册到区域(NavigationBar 到 NavigationRegion)。
using Microsoft.Practices.Prism.Modularity; using Microsoft.Practices.Prism.Regions; using Microsoft.Practices.Unity; namespace ITHelpDesk.Module.Navigation { public class NavigationModule : IModule { private readonly IRegionManager regionManager; private readonly IUnityContainer container; public NavigationModule(IUnityContainer container, IRegionManager regionManager) { this.container = container; this.regionManager = regionManager; } public void Initialize() { this.regionManager.RegisterViewWithRegion("NavigationRegion", () => this.container.Resolve()); } } }
在 Initialize 方法中,我们借助 regionManager 将 NavigationBar 视图注册到 Shell 的 NavigationRegion 区域。
员工模块
该模块的项目结构与导航模块相同。它包含用于创建员工模块布局的代码,并在用户输入员工 ID 时显示员工信息。该模块加载在 Shell 的“EmployeeInfoRegion”区域内。员工模块在应用程序加载时加载。
软件模块
该模块加载在 Shell 的“RequestInfoRegion”区域内。当用户点击“Software”按钮时,软件模块按需加载。在该模块的视图页面(SoftwareDetail.xaml)中,我们编写了创建布局的代码。SoftwareDetail 视图中使用了两个组合框、一个文本框和一个按钮。第一个组合框的 ItemSource 属性绑定到 SoftwareViewModel 中的“SoftwareCategories”属性。“SoftwareCategories”属性返回一个 SoftwareCategory 的 ObservableCollection。
SoftwareViewModel ViewModel 通过在其构造函数中调用 fetchSoftwareCategory 方法来获取数据。fetchSoftwareCategory 方法使用 EmployeeService 从 ITHelpDeskDB.xml 中获取软件列表。EmployeeService 调用 Repository 中相应的方法,该方法从 ITHelpDeskDB.xml 文件中提取数据。fetchSoftwareCategory 方法的代码如下所示。
private void fetchSoftwareCategory() { List<SoftwareCategory> allsoftwareCategories = employeeService.GetSoftwareCategories(); SoftwareCategories = new ObservableCollection<SoftwareCategory>(allsoftwareCategories); }
第二个组合框的 ItemSource 属性绑定到 SoftwareViewModel 中的“SoftwareList”属性。“SoftwareList”属性通过从 SoftwareViewModel 的“SelectedSoftwareCategory”属性的 set 方法中调用 fetchSoftwareList 方法,返回一个 SoftwareList 的 ObservableCollection。fetchSoftwareList 方法的代码如下所示。
private void fetchSoftwareList(string selectedCategory) { List<SoftwareItems> allSoftwareList = employeeService.GetSoftwareItemsByCategory(selectedCategory); SoftwareList = new ObservableCollection<SoftwareItems>(allSoftwareList); }
文本框用于填写备注。“Submit Request”按钮的 Command 属性绑定到 SoftwareViewModel 中的“SubmitRequestCommand”。SubmitRequestCommand 是一个 DelegateCommand,它调用 saveRequest 方法来保存请求。以下方法负责将请求保存到 ITHelpDeskDB.xml。
private void saveRequest() { string selectedCategory = SelectedSoftwareCategory; string selectedSoftware = SelectedSoftwareName; string comment = Comment; bool result = employeeService.SaveSoftwareRequest(selectedCategory, selectedSoftware, comment); if (result == true) { Message = "Successful: Your request is accepted."; IsPopupOpen = true; } else { Message = "Unsuccessful: Please try again."; IsPopupOpen = true; } }
硬件模块
硬件模块具有与我们在 SoftwareModule 中实现的功能和代码类似。因此,请按照类似的步骤创建此模块。
所有请求模块
项目结构与我们已实现的上述模块相同。用户需要在“Enter Employee Id”字段中输入员工 ID。与当前员工 ID 关联的所有请求 ID 将填充到“Select Request Id”组合框中。当用户从组合框中选择一个请求 ID 时,将显示与所选请求 ID 相关的所有信息。
根据我们的要求,我们需要在任何时候显示与特定员工 ID 关联的所有请求 ID。当用户在 EmployeeModule 的文本框中输入员工 ID 时,会调用 EmployeeService 来获取员工信息。此时,我们将最近输入的员工 ID 存储在 EmployeeService 的一个名为 CurrentEmployeeId 的公共属性中。然后,在 RequestViewModel 的构造函数中,我们调用 fetchAllRequest 方法,该方法会调用 EmployeeService 并根据 CurrentEmployeeId 获取过滤后的数据。
到目前为止,我们已经实现了所有模块,现在我们需要编写代码以在应用程序启动时和按需加载模块。让我们在下一节中实现这个功能。
加载模块
加载模块有两种方法。在这个演示应用程序中,我们使用了如下所述的两种方法。
应用程序启动时加载:导航模块和员工模块在应用程序启动时加载。
按需加载:软件、硬件和所有请求模块按需加载。
为了实现这一点,在 ITHelpDeskBootstrapper 类中,我们重写了 ConfigureModuleCatalog 方法。ConfigureModuleCatalog 是 Bootstrapper 类的虚方法。在这个方法中,我们定义了哪些模块需要在应用程序启动时加载,哪些需要按需加载。
protected override void ConfigureModuleCatalog() { ModuleCatalog moduleCatalog = (ModuleCatalog)this.ModuleCatalog; moduleCatalog.AddModule(typeof(EmployeeModule)); moduleCatalog.AddModule(typeof(NavigationModule)); moduleCatalog.AddModule(typeof(SoftwareModule), InitializationMode.OnDemand); moduleCatalog.AddModule(typeof(HardwareModule), InitializationMode.OnDemand); moduleCatalog.AddModule(typeof(RequestModule), InitializationMode.OnDemand); }
在 ConfigureContainer 方法中,我们注册了类型(HelpDeskXMLRepository 和 EmployeeService)。这样 Unity 就会知道如何解析相应的接口。
protected override void ConfigureContainer() { Container.RegisterType<IHelpDeskRepository, HelpDeskXMLRepository>(); Container.RegisterType<IEmployeeService, EmployeeService>(new ContainerControlledLifetimeManager()); base.ConfigureContainer(); }
演示应用程序中的问题
目前,应用程序在一般流程下运行良好,但在某些情况下会显示错误的数据。下面描述了其中一种情况。
如果仔细观察,当用户在员工模块中输入员工 ID(比如说 2),然后导航到所有请求模块时,组合框中会显示与当前员工 ID 2 关联的所有请求 ID。现在,当用户将员工 ID 更改为 3 时,组合框中的请求 ID 并不会改变。组合框仍然显示旧数据。
原因和解决方案(模块间通信)将在本系列的下一篇文章中讨论。届时,我们还将研究更多在特定情况下产生错误数据的类似错误,并加以解决。
结论
在本系列文章的这一部分中,我们学习了如何向应用程序添加模块和基础设施。在本系列文章的下一部分中,我们将继续使用同一个演示项目并进一步增强它。我们将重点关注使其在当前演示应用程序无法按预期工作的特定用例下更加健壮。非常欢迎您的评论/建议和疑问。谢谢。