最近使用WPF開發項目,為了對WPF知識點進行總結,所以利用業餘時間,開發一個學生信息管理系統【Student Information Management System】。本文主要簡述如何通過WPF+Prism+MAH+WebApi進行開發基於三層架構的桌面版應用程式,僅供學習分享使用,如有不足之... ...
最近通過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提供了事件聚合器,通過命令的發佈和訂閱來實現模塊間的數據交互。以導航模塊為例,當點擊某一個導航時,發佈一個命令,在主視窗訂閱此事件,當收到事件時,將此導航對應的頁面渲染到主頁面區域中。步驟如下:
1. 定義一個事件
1 using Prism.Events; 2 using System; 3 using System.Collections.Generic; 4 using System.Linq; 5 using System.Text; 6 using System.Threading.Tasks; 7 8 namespace SIMS.Utils.Events 9 { 10 /// <summary> 11 /// 導航事件 12 /// </summary> 13 public class NavEvent : PubSubEvent<string> 14 { 15 } 16 }
2. 發佈事件
用戶點擊導航菜單時,觸發NavCommand,然後發佈命令。
1 private DelegateCommand<object> navCommand; 2 3 public DelegateCommand<object> NavCommand 4 { 5 get 6 { 7 if (navCommand == null) 8 { 9 10 navCommand = new DelegateCommand<object>(Navigation); 11 } 12 return navCommand; 13 } 14 } 15 16 private void Navigation(object obj) { 17 var menuItem = (HamburgerMenuItem)obj; 18 if (menuItem != null) { 19 var tag = menuItem.Tag; 20 if (tag!=null) { 21 this.eventAggregator.GetEvent<NavEvent>().Publish(tag.ToString()); 22 } 23 } 24 }
3. 訂閱命令
在主視窗,訂閱命令,當收到命令時,再初始化模塊信息,如下所示:
1 namespace SIMS.ViewModels 2 { 3 public class MainWindowViewModel:BindableBase 4 { 5 6 private IEventAggregator eventAggregator; 7 private IContainerExtension _container; 8 private IRegionManager _regionManager; 9 private IDialogService _dialogService; 10 public MainWindowViewModel(IContainerExtension container, IRegionManager regionManager, IEventAggregator eventAggregator,IDialogService dialogService) { 11 this._container = container; 12 this._regionManager = regionManager; 13 this.eventAggregator = eventAggregator; 14 this._dialogService = dialogService; 15 //彈出登錄視窗 16 this._dialogService.ShowDialog("Login", null, LoginCallback, "MetroDialogWindow"); 17 this.eventAggregator.GetEvent<NavEvent>().Subscribe(Navigation); 18 } 19 20 private void LoginCallback(IDialogResult dialogResult) { 21 if (dialogResult.Result != ButtonResult.OK) { 22 Application.Current.Shutdown(); 23 } 24 } 25 26 #region 事件和命令 27 28 private DelegateCommand loadedCommand; 29 30 public DelegateCommand LoadedCommand 31 { 32 get { 33 if (loadedCommand == null) { 34 loadedCommand = new DelegateCommand(Loaded); 35 } 36 return loadedCommand; } 37 } 38 39 private void Loaded() { 40 InitInfo(); 41 } 42 43 44 45 46 private void InitInfo() { 47 var header = _container.Resolve<Header>(); 48 IRegion headerRegion = _regionManager.Regions["HeaderRegion"]; 49 headerRegion.Add(header); 50 // 51 var footer = _container.Resolve<Footer>(); 52 IRegion footerRegion = _regionManager.Regions["FooterRegion"]; 53 footerRegion.Add(footer); 54 55 var welcome = _container.Resolve<Welcome>(); 56 IRegion welcomeRegion = _regionManager.Regions["ContentRegion"]; 57 welcomeRegion.Add(welcome); 58 } 59 60 private void Navigation(string source) { 61 _regionManager.RequestNavigate("ContentRegion", source); 62 //MessageBox.Show(source); 63 } 64 65