為何模塊化
模塊化是一種分治思想,不僅可以分離復(fù)雜的業(yè)務(wù)邏輯,還可以進(jìn)行不同任務(wù)的分工。模塊與模塊之間相互獨(dú)立,從而構(gòu)建一種松耦合的應(yīng)用程序,便于開發(fā)和維護(hù)。
開發(fā)技術(shù)
.NET 6 + WPF + Prism (v8.0.0.1909) + HandyControl (v3.4.0)
知識準(zhǔn)備
什么是MVVM
Model-View-ViewModel?是一種軟件架構(gòu)設(shè)計(jì),它是一種簡化用戶界面的事件驅(qū)動(dòng)編程方式。Model:數(shù)據(jù)模型,用來存儲(chǔ)數(shù)據(jù)。 View:視圖界面,用來展示UI界面和響應(yīng)用戶交互。ViewModel:連接View和Model的中間件,起到了橋梁的作用。
什么是Prism
Prism?是一套桌面開發(fā)框架,用于在WPF和Xamarin Forms中構(gòu)建松耦合、可維護(hù)、可以測試的XAML應(yīng)用程序。Prism提供了一組設(shè)計(jì)模式的實(shí)現(xiàn),這些模式有助于編寫結(jié)構(gòu)良好且可維護(hù)的XAML應(yīng)用程序,包括MVVM、依賴注入、命令、事件聚合器等。
什么是HandyControl
HandyControl?是一套WPF控件庫,它幾乎重寫了所有原生樣式,同時(shí)包含80余款自定義控件。
搭建項(xiàng)目
假設(shè)現(xiàn)在有一套叫Lapis的業(yè)務(wù)系統(tǒng),包含A和B兩塊業(yè)務(wù)。業(yè)務(wù)A含有<頁面1>和<頁面2>,業(yè)務(wù)B含有<頁面3>。界面設(shè)計(jì)如下:
下面我們就按照上述要求,來搭建一套MVVM + 模塊化的桌面應(yīng)用程序。
首先,新建一個(gè)名為Lapis.WpfDemo的解決方案,分別創(chuàng)建以下四個(gè)不同項(xiàng)目:其中Lapis.Shell是WPF應(yīng)用程序,其余是WPF類庫。如圖所示:
Lapis.Share:?是一個(gè)共享庫,用來定義抽象基類和一些公共方法,供上層調(diào)用。它引用了Prism.Wpf、Prism.Core和HandyControl第三方Nuget包。BaseViewModel?是一個(gè)視圖模型基類,繼承自?BindableBase,分別定義了EventAggregator、RegionManager、LoadCommand?屬性。代碼如下:


1 /// <summary> 2 /// 視圖模型基類 3 /// </summary> 4 public abstract class BaseViewModel : BindableBase 5 { 6 private DelegateCommand _loadCommand; 7 protected IEventAggregator EventAggregator { get; } //事件聚合器 8 protected IRegionManager RegionManager { get; } // 區(qū)域管理器 9 public DelegateCommand LoadCommand => _loadCommand ??= new(OnLoad); //界面加載命令 10 11 public BaseViewModel() 12 { 13 RegionManager = ContainerLocator.Current.Resolve<IRegionManager>(); 14 EventAggregator = ContainerLocator.Current.Resolve<IEventAggregator>(); 15 } 16 17 /// <summary> 18 /// 界面加載時(shí),由Loaded事件觸發(fā) 19 /// </summary> 20 protected virtual void OnLoad() 21 { 22 } 23 24 /// <summary> 25 /// 根據(jù)區(qū)域名稱查找視圖 26 /// </summary> 27 /// <param name="regionName">區(qū)域名稱</param> 28 protected TView TryFindView<TView>(string regionName) where TView : class 29 { 30 return RegionManager.Regions[regionName].Views 31 .Where(v => v.GetType() == typeof(TView)) 32 .FirstOrDefault() as TView; 33 } 34 }
Lapis.ModuleA?和?Lapis.ModuleB:?對應(yīng)前端業(yè)務(wù)模塊A和B,? 模塊A包含?PageOne?和?PageTwo?兩個(gè)視圖及視圖模型,模塊B只含?PageThree?一個(gè)視圖及視圖模型。按照Prism框架規(guī)定,視圖模型最好以?視圖名稱 + ViewModel?來命名。如圖所示:
?其中,ModuleA?和?ModuleB?表示模塊類,用于初始化模塊和注冊類型。ModuleA?代碼如下:
1 [Module(ModuleName = "ModuleA", OnDemand = true)] 2 public class ModuleA : IModule 3 { 4 public void OnInitialized(IContainerProvider containerProvider) 5 { 6 var regionManager = containerProvider.Resolve<IRegionManager>(); 7 regionManager.RegisterViewWithRegion(ModuleARegionNames.RegionOne, typeof(PageOne)); // 將頁面一注冊到區(qū)域一 8 regionManager.RegisterViewWithRegion(ModuleARegionNames.RegionTwo, typeof(PageTwo)); // 將頁面二注冊到區(qū)域二 9 } 10 11 public void RegisterTypes(IContainerRegistry containerRegistry) 12 { 13 } 14 }
第7和第8行代碼:分別將?PageOne?和?PageTwo?注冊到?RegionOne?和?RegionTwo。為了方便,區(qū)域名稱用字符串常量表示。
Lapis.Shell:?是一個(gè)啟動(dòng)模塊,負(fù)責(zé)啟動(dòng)/初始化應(yīng)用程序(加載模塊和資源),它包含App啟動(dòng)類、主窗口、側(cè)邊菜單和Tab頁內(nèi)容視圖及對應(yīng)的視圖模型等。其中?PageSelectedEvent?是一個(gè)頁面選中事件,用于 ViewModel 之間傳遞消息,起到解耦作用。如圖所示:
MainWindow?此處作為啟動(dòng)窗口/主窗口。為了讓?MainWindow?代碼保持簡潔,我們只把它當(dāng)作布局頁面來使用。代碼片段如下:
1 <Grid> 2 <Grid.ColumnDefinitions> 3 <ColumnDefinition Width="auto" /> 4 <ColumnDefinition /> 5 </Grid.ColumnDefinitions> 6 <!-- 側(cè)邊菜單欄內(nèi)容 --> 7 <ContentControl Name="sideMenuContentControl" Width="200px" Margin="5" /> 8 <!-- Tab頁主內(nèi)容 --> 9 <ContentControl Name="tabPagesContentControl" Grid.Column="1" Margin="0,5,5,5" /> 10 </Grid>
第7和第9行代碼:sideMenuContentControl?和?tabPagesContentControl?是兩個(gè)內(nèi)容控件,用來呈現(xiàn)左側(cè)菜單和Tab頁面視圖??吹竭@里,大家一定會(huì)問:ContentControl?是通過什么來關(guān)聯(lián)視圖的?沒錯(cuò),就是上面提到的Region,我們可以在MainWindow.cs中進(jìn)行區(qū)域設(shè)置,代碼如下:
1 public partial class MainWindow : Window 2 { 3 public MainWindow() 4 { 5 InitializeComponent(); 6 RegionManager.SetRegionName(this.sideMenuContentControl, ShellRegionNames.SideMenuContentRegion); 7 RegionManager.SetRegionName(this.tabPagesContentControl, ShellRegionNames.TabPagesContentRegion); 8 } 9 }
然后,同樣在?ShellModule?類里對?SideMenuContent?和?TabPagesContent?視圖進(jìn)行區(qū)域注冊,這樣主窗口就能顯示左側(cè)菜單和Tab頁面了。代碼如下:


1 [Module(ModuleName = "ShellModule", OnDemand = true)] 2 public class ShellModule : IModule 3 { 4 public void OnInitialized(IContainerProvider containerProvider) 5 { 6 var regionManager = containerProvider.Resolve<IRegionManager>(); 7 regionManager.RegisterViewWithRegion(ShellRegionNames.SideMenuContentRegion, typeof(SideMenuContent)); // 注冊側(cè)邊菜單內(nèi)容視圖 8 regionManager.RegisterViewWithRegion(ShellRegionNames.TabPagesContentRegion, typeof(TabPagesContent)); // 注冊Tab頁面內(nèi)容視圖 9 } 10 11 public void RegisterTypes(IContainerRegistry containerRegistry) 12 { 13 } 14 }
App?是WPF應(yīng)用啟動(dòng)入口,由于使用了第三方Prism框架和HandyControl控件庫,我們需要對?App.xaml?和?App.xaml.cs?兩個(gè)文件做一些修改。代碼如下:


1 <unity:PrismApplication 2 x:Class="Lapis.Shell.App" 3 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 4 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 5 xmlns:local="clr-namespace:Lapis.Shell" 6 xmlns:unity="http://prismlibrary.com/"> 7 <Application.Resources> 8 <ResourceDictionary> 9 <ResourceDictionary.MergedDictionaries> 10 <ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/SkinDefault.xaml" /> 11 <ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/Theme.xaml" /> 12 </ResourceDictionary.MergedDictionaries> 13 </ResourceDictionary> 14 </Application.Resources> 15 </unity:PrismApplication>


1 public partial class App : PrismApplication 2 { 3 protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog) 4 { 5 base.ConfigureModuleCatalog(moduleCatalog); 6 // 7 moduleCatalog.AddModule<ShellModule>(); //添加宿主模塊 8 moduleCatalog.AddModule<ModuleA.ModuleA>(); //添加業(yè)務(wù)模塊A 9 moduleCatalog.AddModule<ModuleB.ModuleB>(); //添加業(yè)務(wù)模塊B 10 } 11 12 protected override Window CreateShell() 13 { 14 return Container.Resolve<MainWindow>(); //返回主窗體 15 } 16 17 protected override void RegisterTypes(IContainerRegistry containerRegistry) 18 { 19 } 20 }
接下來,要做的就是左側(cè)菜單和Tab頁面之間的交互動(dòng)作。不同于傳統(tǒng)Winform的事件驅(qū)動(dòng)機(jī)制,我們使用MVVM模式將視圖和UI邏輯分離。因此一般情況下,所有的界面邏輯都應(yīng)該在?ViewModel?里完成。SideMenuContentViewModel?通過事件聚合器發(fā)布頁面選中事件,TabPagesContentViewModel?則通過訂閱該事件來進(jìn)行頁面切換,代碼如下:


1 /// <summary> 2 /// 側(cè)邊菜單內(nèi)容視圖模型 3 /// </summary> 4 public class SideMenuContentViewModel : BaseViewModel 5 { 6 private DelegateCommand<string> _menuSelectedCommand; 7 8 private List<PageInfo> _pages = new() 9 { 10 new PageInfo { Id = "1" ,RegionName = "RegionOne", DisplayName = "子菜單1" }, 11 new PageInfo { Id = "2", RegionName = "RegionTwo", DisplayName = "子菜單2" }, 12 new PageInfo { Id = "3", RegionName = "RegionThree", DisplayName = "子菜單3" }, 13 }; 14 15 public DelegateCommand<string> MenuSelectedCommand => _menuSelectedCommand ??= new DelegateCommand<string>(ExecuteMenuSelectedCommand); 16 17 private void ExecuteMenuSelectedCommand(string id) 18 { 19 var info = _pages.Find(x => x.Id == id); 20 if (info != null) 21 { 22 EventAggregator.GetEvent<PageSelectedEvent>().Publish(info); 23 } 24 } 25 }


1 /// <summary> 2 /// Tab頁面內(nèi)容視圖模型 3 /// </summary> 4 public class TabPagesContentViewModel : BaseViewModel 5 { 6 private TabControl _tabControl; 7 8 protected override void OnLoad() 9 { 10 _tabControl = TryFindView<TabPagesContent>(ShellRegionNames.TabPagesContentRegion)?.FindName("tabControl") as TabControl; 11 12 EventAggregator.GetEvent<PageSelectedEvent>().Subscribe(OnPageSelected); 13 } 14 15 /// <summary> 16 /// 頁面選中事件處理 17 /// </summary> 18 /// <param name="page"></param> 19 private void OnPageSelected(PageInfo page) 20 { 21 try 22 { 23 var existItem = FindItem(_tabControl, page.RegionName); 24 if (existItem != null) 25 { 26 existItem.IsSelected = true; 27 } 28 else 29 { 30 // 創(chuàng)建頁面區(qū)域控件 31 var pageContentControl = new ContentControl(); 32 pageContentControl.SetRegionName(page.RegionName); 33 34 var item = new TabItem 35 { 36 Name = page.RegionName, // 區(qū)域名稱,如:RegionOne、RegionTwo 37 Header = page.DisplayName, // 頁面名稱 38 IsSelected = true, 39 Content = pageContentControl 40 }; 41 42 _tabControl.Items.Add(item); 43 } 44 } 45 catch { } 46 } 47 48 private TabItem FindItem(TabControl tc, string name) 49 { 50 foreach (TabItem item in tc.Items) 51 { 52 if (item.Name == name) 53 { 54 return item; 55 } 56 } 57 return null; 58 } 59 }
整個(gè)UI交互過程,如圖所示:
?
?
至此,整個(gè)桌面前端應(yīng)用就基本完成了。界面如圖所示:
參考資料
歡迎使用HandyControl | HandyOrg
Introduction to Prism | Prism (prismlibrary.com)文章來源:http://www.zghlxwxcb.cn/news/detail-648471.html
.NET Core 3 WPF MVVM框架 Prism系列文章索引 - RyzenAdorer - 博客園 (cnblogs.com)文章來源地址http://www.zghlxwxcb.cn/news/detail-648471.html
到了這里,關(guān)于WPF如何構(gòu)建MVVM+模塊化的桌面應(yīng)用的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!