• WPF如何构建MVVM+模块化的桌面应用


    为何模块化

    模块化是一种分治思想,不仅可以分离复杂的业务逻辑,还可以进行不同任务的分工。模块与模块之间相互独立,从而构建一种松耦合的应用程序,便于开发和维护。

    开发技术

    .NET 6 + WPF + Prism (v8.0.0.1909) + HandyControl (v3.4.0)

    知识准备

    什么是MVVM

    Model-View-ViewModel 是一种软件架构设计,它是一种简化用户界面的事件驱动编程方式。Model:数据模型,用来存储数据。 View:视图界面,用来展示UI界面和响应用户交互。ViewModel:连接View和Model的中间件,起到了桥梁的作用。

    什么是Prism

    Prism 是一套桌面开发框架,用于在WPF和Xamarin Forms中构建松耦合、可维护、可以测试的XAML应用程序。Prism提供了一组设计模式的实现,这些模式有助于编写结构良好且可维护的XAML应用程序,包括MVVM、依赖注入、命令、事件聚合器等。

    什么是HandyControl

    HandyControl 是一套WPF控件库,它几乎重写了所有原生样式,同时包含80余款自定义控件。

    搭建项目

    假设现在有一套叫Lapis的业务系统,包含A和B两块业务。业务A含有<页面1>和<页面2>,业务B含有<页面3>。界面设计如下:

    下面我们就按照上述要求,来搭建一套MVVM + 模块化的桌面应用程序。

    首先,新建一个名为Lapis.WpfDemo的解决方案,分别创建以下四个不同项目:其中Lapis.Shell是WPF应用程序,其余是WPF类库。如图所示:

    Lapis.Share: 是一个共享库,用来定义抽象基类和一些公共方法,供上层调用。它引用了Prism.Wpf、Prism.Core和HandyControl第三方Nuget包。BaseViewModel 是一个视图模型基类,继承自 BindableBase,分别定义了EventAggregatorRegionManagerLoadCommand 属性。代码如下:

     1     /// 
     2     /// 视图模型基类
     3     /// 
     4     public abstract class BaseViewModel : BindableBase
     5     {
     6         private DelegateCommand _loadCommand;
     7         protected IEventAggregator EventAggregator { get; } //事件聚合器
     8         protected IRegionManager RegionManager { get; } // 区域管理器
     9         public DelegateCommand LoadCommand => _loadCommand ??= new(OnLoad); //界面加载命令
    10 
    11         public BaseViewModel()
    12         {
    13             RegionManager = ContainerLocator.Current.Resolve();
    14             EventAggregator = ContainerLocator.Current.Resolve();
    15         }
    16 
    17         /// 
    18         /// 界面加载时,由Loaded事件触发
    19         /// 
    20         protected virtual void OnLoad()
    21         {
    22         }
    23 
    24         /// 
    25         /// 根据区域名称查找视图
    26         /// 
    27         /// 区域名称
    28         protected TView TryFindView(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     }
    BaseViewModel.cs

    Lapis.ModuleA 和 Lapis.ModuleB: 对应前端业务模块A和B,  模块A包含 PageOne 和 PageTwo 两个视图及视图模型,模块B只含 PageThree 一个视图及视图模型。按照Prism框架规定,视图模型最好以 视图名称 + 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();
     7             regionManager.RegisterViewWithRegion(ModuleARegionNames.RegionOne, typeof(PageOne)); // 将页面一注册到区域一
     8             regionManager.RegisterViewWithRegion(ModuleARegionNames.RegionTwo, typeof(PageTwo)); // 将页面二注册到区域二
     9         }
    10 
    11         public void RegisterTypes(IContainerRegistry containerRegistry)
    12         {
    13         }
    14     }
    复制代码

    第7和第8行代码:分别将 PageOne 和 PageTwo 注册到 RegionOne 和 RegionTwo。为了方便,区域名称用字符串常量表示。

    Lapis.Shell: 是一个启动模块,负责启动/初始化应用程序(加载模块和资源),它包含App启动类、主窗口、侧边菜单和Tab页内容视图及对应的视图模型等。其中 PageSelectedEvent 是一个页面选中事件,用于 ViewModel 之间传递消息,起到解耦作用。如图所示:

    MainWindow 此处作为启动窗口/主窗口。为了让 MainWindow 代码保持简洁,我们只把它当作布局页面来使用。代码片段如下:

    复制代码
     1     <Grid>
     2         <Grid.ColumnDefinitions>
     3             <ColumnDefinition Width="auto" />
     4             <ColumnDefinition />
     5         Grid.ColumnDefinitions>
     6         
     7         <ContentControl Name="sideMenuContentControl" Width="200px" Margin="5" />
     8         
     9         <ContentControl Name="tabPagesContentControl" Grid.Column="1" Margin="0,5,5,5" />
    10     Grid>
    复制代码

    第7和第9行代码:sideMenuContentControl 和 tabPagesContentControl 是两个内容控件,用来呈现左侧菜单和Tab页面视图。看到这里,大家一定会问:ContentControl 是通过什么来关联视图的?没错,就是上面提到的Region,我们可以在MainWindow.cs中进行区域设置,代码如下:

    复制代码
    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 视图进行区域注册,这样主窗口就能显示左侧菜单和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();
     7             regionManager.RegisterViewWithRegion(ShellRegionNames.SideMenuContentRegion, typeof(SideMenuContent)); // 注册侧边菜单内容视图
     8             regionManager.RegisterViewWithRegion(ShellRegionNames.TabPagesContentRegion, typeof(TabPagesContent)); // 注册Tab页面内容视图
     9         }
    10 
    11         public void RegisterTypes(IContainerRegistry containerRegistry)
    12         {
    13         }
    14     }
    ShellModule.cs

    App 是WPF应用启动入口,由于使用了第三方Prism框架和HandyControl控件库,我们需要对 App.xaml 和 App.xaml.cs 两个文件做一些修改。代码如下:

     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>
    App.xaml
     1     public partial class App : PrismApplication
     2     {
     3         protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog)
     4         {
     5             base.ConfigureModuleCatalog(moduleCatalog);
     6             //
     7             moduleCatalog.AddModule();      //添加宿主模块
     8             moduleCatalog.AddModule();  //添加业务模块A
     9             moduleCatalog.AddModule();  //添加业务模块B
    10         }
    11 
    12         protected override Window CreateShell()
    13         {
    14             return Container.Resolve(); //返回主窗体
    15         }
    16 
    17         protected override void RegisterTypes(IContainerRegistry containerRegistry)
    18         {
    19         }
    20     }
    App.xaml.cs

    接下来,要做的就是左侧菜单和Tab页面之间的交互动作。不同于传统Winform的事件驱动机制,我们使用MVVM模式将视图和UI逻辑分离。因此一般情况下,所有的界面逻辑都应该在 ViewModel 里完成。SideMenuContentViewModel 通过事件聚合器发布页面选中事件,TabPagesContentViewModel 则通过订阅该事件来进行页面切换,代码如下:

     1     /// 
     2     /// 侧边菜单内容视图模型
     3     /// 
     4     public class SideMenuContentViewModel : BaseViewModel
     5     {
     6         private DelegateCommand<string> _menuSelectedCommand;
     7 
     8         private List _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().Publish(info);
    23             }
    24         }
    25     }
    SideMenuContentViewModel.cs
     1     /// 
     2     /// Tab页面内容视图模型
     3     /// 
     4     public class TabPagesContentViewModel : BaseViewModel
     5     {
     6         private TabControl _tabControl;
     7 
     8         protected override void OnLoad()
     9         {
    10             _tabControl = TryFindView(ShellRegionNames.TabPagesContentRegion)?.FindName("tabControl") as TabControl;
    11 
    12             EventAggregator.GetEvent().Subscribe(OnPageSelected);
    13         }
    14 
    15         /// 
    16         /// 页面选中事件处理
    17         /// 
    18         /// 
    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                     // 创建页面区域控件
    31                     var pageContentControl = new ContentControl();
    32                     pageContentControl.SetRegionName(page.RegionName);
    33 
    34                     var item = new TabItem
    35                     {
    36                         Name = page.RegionName,     // 区域名称,如: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     }
    TabPagesContentViewModel.cs

    整个UI交互过程,如图所示:

     

     

    至此,整个桌面前端应用就基本完成了。界面如图所示:

    参考资料

    欢迎使用HandyControl | HandyOrg

    Introduction to Prism | Prism (prismlibrary.com)

    .NET Core 3 WPF MVVM框架 Prism系列文章索引 - RyzenAdorer - 博客园 (cnblogs.com)

  • 相关阅读:
    主数据操作文档
    实验6 用例图
    磁盘调度算法
    MySQL系列:索引失效场景总结
    【送书活动】全网超50万粉丝的Linux大咖良许,出书了!
    R语言caTools包进行数据划分、scale函数进行数据缩放、class包的knn函数构建K近邻分类器、table函数计算混淆矩阵
    【电子通识】PE和SQE是什么职位
    适合linux的软件
    P02014186陈镐镐
    centos 使用docker安装mysql
  • 原文地址:https://www.cnblogs.com/fengjq/p/17630386.html