最近通过WPF开发项目,为了对WPF知识点进行总结,所以利用业余时间,开发一个学生信息管理系统【Student Information Management System】。本文主要简述如何通过WPF+Prism+MAH+WebApi进行开发基于三层架构的桌面版应用程序,仅供学习分享使用,如有不足之处,还请指正。
涉及知识点
- WPF:WPF(Windows Presentation Foundation)是(微软推出的)基于Windows的用户界面框架,提供了统一的编程模型,语言和框架,做到了分离界面设计人员与开发人员的工作;WPF提供了全新的多媒体交互用户图形界面。相比于WinForm传统开发,在WPF中,通过核心的MVVM设计思想,实现前后端的分离。
- Prism:Prism是一个用于在 WPF、Xamarin Form、Uno 平台和 WinUI 中构建松散耦合、可维护和可测试的 XAML 应用程序框架。通过Prism,可以简化原生MVVM实现方式,并引入分模块设计思想。在Prism中,每一个功能,都可以设计成一个独立的模块,各个模块之间松耦合,可维护,可测试。框架中包括 MVVM、依赖注入、Command、Message Event、导航、弹窗等功能。在后续程序功能设计中,都会用到。
- MAH:MahApps是一套基于WPF的界面组件,通过该组件,可以使用较小的开发成本实现一个相对很好的界面效果。作为后端开发,最头疼的就是如何设计美化页面,MAH可以让开发人员用最少的时间来开发Metro风格的页面。
- WebApi:一般是指ASP.NET WebApi 用于快速开发基于REST风格的数据接口的框架。
Prism的模块化思想
在应用程序开发中,如果不采用模块化思想,那么各个页面混合在一起,看起杂乱无章,具体如下所示:
当我们引入模块化思想,那么各个模块的界限将变得清晰,如下所示:
在本文示例的学生信息管理系统中,就是采用模块思想,使项目的各个模块即相对完整,又相互独立。如下所示:
在开发中,引入模块化思想,通过Prism进行代码布局,如下所示:
MVVM思想
MVVM是Model-View-ViewModel(模型-视图-视图模型)的缩写形式,它通常被用于WPF或Silverlight开发。MVVM的根本思想就是界面和业务功能进行分离,View的职责就是负责如何显示数据及发送命令,ViewModel的功能就是如何提供数据和执行命令。各司其职,互不影响。我们可以通过下图来直观的理解MVVM模式:
在本示例中,所有开发都将遵循MVVM思想的设计模式进行开发,如下所示:
页面布局
在学生信息管理系统主界面,根据传统的布局方式,主要分为上(Header),中【左(Navigation),右(Main Content)】,下(Footer)四个部分,如下所示:
创建一个模块
一个模块是一个独立的WPF类库,在项目中,一个普通的类实现了IModule接口,就表示一个模块,以学生模块为例,如下所示:
1. using Prism.Ioc; 2. using Prism.Modularity; 3. using SIMS.StudentModule.ViewModels; 4. using SIMS.StudentModule.Views; 5. using System; 6. 7. namespace SIMS.StudentModule 8. { 9. public class StudentModule : IModule 10. { 11. public void OnInitialized(IContainerProvider containerProvider) 12. { 13. 14. } 15. 16. public void RegisterTypes(IContainerRegistry containerRegistry) 17. { 18. containerRegistry.RegisterForNavigation<Student, StudentViewModel>(nameof(Student)); 19. } 20. } 21. }
注意:在模块中,需要实现两个接口方法。在此模块中的RegisterTypes方法中,可以注册导航,窗口等以及初始化工作。
如果不注册为导航,而是需要注册到某一个Region中,则可以在OnInitialized方法中进行,以导航模块为例,如下所示:
1. using Prism.Ioc; 2. using Prism.Modularity; 3. using Prism.Regions; 4. using SIMS.NavigationModule.Views; 5. using System; 6. 7. namespace SIMS.NavigationModule 8. { 9. public class NavigationModule : IModule 10. { 11. public void OnInitialized(IContainerProvider containerProvider) 12. { 13. var regionManager = containerProvider.Resolve<IRegionManager>(); 14. regionManager.RegisterViewWithRegion("NavRegion",typeof(Navigation)); 15. } 16. 17. public void RegisterTypes(IContainerRegistry containerRegistry) 18. { 19. } 20. } 21. }
View和ViewModel自动适配
View和ViewMode在注册导航时,可以手动匹配,也可以自动匹配【需要以固定的方式命名才可以自动适配】。自动适配,需要是在UserControl中,增加一句prism:ViewModelLocator.AutoWireViewModel="True"即可,以标题头为例,如下所示:
1. <UserControl x:Class="SIMS.Views.Header" 2. xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 3. xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 4. xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 5. xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 6. xmlns:local="clr-namespace:SIMS.Views" 7. mc:Ignorable="d" 8. xmlns:prism="http://prismlibrary.com/" 9. prism:ViewModelLocator.AutoWireViewModel="True" 10. xmlns:mahApps="http://metro.mahapps.com/winfx/xaml/controls" 11. d:DesignHeight="100" d:DesignWidth="800"> 12. <UserControl.Resources> 13. <ResourceDictionary> 14. <ResourceDictionary.MergedDictionaries> 15. <ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Themes/Light.Blue.xaml" /> 16. </ResourceDictionary.MergedDictionaries> 17. </ResourceDictionary> 18. </UserControl.Resources> 19. 20. <Grid Background="{DynamicResource MahApps.Brushes.Accent}"> 21. <Grid.RowDefinitions> 22. <RowDefinition Height="*"></RowDefinition> 23. <RowDefinition Height="Auto"></RowDefinition> 24. </Grid.RowDefinitions> 25. <TextBlock Grid.Row="0" Text="学生信息管理系统" Foreground="White" FontSize="32" FontWeight="Bold" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="20,5"></TextBlock> 26. <StackPanel Grid.Row="1" HorizontalAlignment="Right" Orientation="Horizontal"> 27. <TextBlock Text="Hello" Foreground="White" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="3"></TextBlock> 28. <TextBlock Text="Admin" Foreground="White" Margin="3" FontWeight="Bold"></TextBlock> 29. <TextBlock Text="|" Foreground="White" Margin="3"></TextBlock> 30. <TextBlock Text="Logout" Foreground="White" Margin="3" FontWeight="Bold"></TextBlock> 31. </StackPanel> 32. </Grid> 33. 34. </UserControl>
弹出模态窗口
在Prism中,模块中的视图都是以UserControl的形式存在,那么如果需要弹出窗体页面,就需要在ViewModel中,实现IDialogAware接口,以Login登录窗口为例,如下所示:
1. using Prism.Regions; 2. using Prism.Services.Dialogs; 3. using SIMS.Views; 4. using System; 5. using System.Collections.Generic; 6. using System.Linq; 7. using System.Text; 8. using System.Threading.Tasks; 9. using System.Windows; 10. 11. namespace SIMS.ViewModels 12. { 13. public class LoginViewModel : BindableBase, IDialogAware 14. { 15. private IRegionManager _regionManager; 16. private IContainerExtension _container; 17. 18. private string userName; 19. 20. public string UserName 21. { 22. get { return userName; } 23. set {SetProperty(ref userName , value); } 24. } 25. 26. private string password; 27. 28. public string Password 29. { 30. get { return password; } 31. set { SetProperty(ref password , value); } 32. } 33. 34. 35. public LoginViewModel(IContainerExtension container,IRegionManager regionManager) 36. { 37. this._container = container; 38. this._regionManager = regionManager; 39. } 40. 41. private void InitInfo() { 42. var footer = _container.Resolve<Footer>(); 43. IRegion footerRegion = _regionManager.Regions["LoginFooterRegion"]; 44. footerRegion.Add(footer); 45. } 46. 47. #region 命令 48. 49. private DelegateCommand loadedCommand; 50. 51. public DelegateCommand LoadedCommand 52. { 53. get 54. { 55. if (loadedCommand == null) 56. { 57. loadedCommand = new DelegateCommand(Loaded); 58. } 59. return loadedCommand; 60. } 61. } 62. 63. private void Loaded() 64. { 65. //InitInfo(); 66. } 67. 68. private DelegateCommand loginCommand; 69. 70. public DelegateCommand LoginCommand 71. { 72. get 73. { 74. if (loginCommand == null) 75. { 76. loginCommand = new DelegateCommand(Login); 77. } 78. return loginCommand; 79. } 80. } 81. 82. private void Login() { 83. if (string.IsNullOrEmpty(UserName) || string.IsNullOrEmpty(Password)) { 84. MessageBox.Show("用户名或密码为空,请确认"); 85. return; 86. } 87. if (UserName == "admin" && Password == "abc123") 88. { 89. CloseWindow(); 90. } 91. else { 92. MessageBox.Show("用户名密码不正确,请确认"); 93. return; 94. } 95. } 96. 97. private DelegateCommand cancelCommand; 98. 99. public DelegateCommand CancelCommand 100. { 101. get 102. { 103. if (cancelCommand == null) 104. { 105. cancelCommand = new DelegateCommand(Cancel); 106. } 107. return cancelCommand; 108. } 109. } 110. 111. private void Cancel() { 112. RequestClose?.Invoke(new DialogResult(ButtonResult.Cancel)); 113. } 114. 115. #endregion 116. 117. #region DialogAware接口 118. 119. public string Title => "SIMS-Login"; 120. 121. public event Action<IDialogResult> RequestClose; 122. 123. /// <summary> 124. /// 成功时关闭窗口 125. /// </summary> 126. public void CloseWindow() { 127. RequestClose?.Invoke(new DialogResult(ButtonResult.OK)); 128. } 129. 130. public bool CanCloseDialog() 131. { 132. return true; 133. } 134. 135. public void OnDialogClosed() 136. { 137. //当关闭时 138. RequestClose?.Invoke(new DialogResult(ButtonResult.Cancel)); 139. } 140. 141. public void OnDialogOpened(IDialogParameters parameters) 142. { 143. //传递解析参数 144. } 145. 146. #endregion 147. } 148. }
实现了IDialogAware接口,表示以窗口的形态出现,在需要弹出窗口的地方进行调用即可。如下所示:
1. public MainWindowViewModel(IContainerExtension container, IRegionManager regionManager, IEventAggregator eventAggregator,IDialogService dialogService) { 2. this._container = container; 3. this._regionManager = regionManager; 4. this.eventAggregator = eventAggregator; 5. this._dialogService = dialogService; 6. //弹出登录窗口 7. this._dialogService.ShowDialog("Login", null, LoginCallback, "MetroDialogWindow"); 8. this.eventAggregator.GetEvent<NavEvent>().Subscribe(Navigation); 9. }
注意:MetroDialogWindow是自定义个一个Metro风格的窗口,如果为空,则采用默认窗口风格。
模块间交互
按照模块化设计思想,虽然各个模块之间相互独立,但是难免为遇到模块之间进行交互的情况,所以Prism提供了事件聚合器,通过命令的发布和订阅来实现模块间的数据交互。以导航模块为例,当点击某一个导航时,发布一个命令,在主窗口订阅此事件,当收到事件时,将此导航对应的页面渲染到主页面区域中。步骤如下: