为何模块化

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

开发技术

.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,分别定义了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; } // 区域管理器
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 /// 界面加载时,由Loaded事件触发
19 /// </summary>
20 protected virtual void OnLoad()
21 {
22 }
23
24 /// <summary>
25 /// 根据区域名称查找视图
26 /// </summary>
27 /// <param name="regionName">区域名称</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 }

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<IRegionManager>();
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 <!-- Tab页主内容 -->
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<IRegionManager>();
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<ShellModule>(); //添加宿主模块
8 moduleCatalog.AddModule<ModuleA.ModuleA>(); //添加业务模块A
9 moduleCatalog.AddModule<ModuleB.ModuleB>(); //添加业务模块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 }

App.xaml.cs

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

 1     /// <summary>
2 /// 侧边菜单内容视图模型
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 }

SideMenuContentViewModel.cs

 1     /// <summary>
2 /// Tab页面内容视图模型
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 // 创建页面区域控件
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)

WPF如何构建MVVM+模块化的桌面应用的更多相关文章

  1. maven构建的模块化的JavaWeb工程

    最近对maven构建的模块化的JavaWeb工程,比较感兴趣,所以自己就想从头弄一个出来,在此做一个记录,供以后学习. 前置条件:电脑上有eclipse(或者myeclipse,记事本也可以,那样就得 ...

  2. WPF ContextMenu 在MVVM模式中绑定 Command及使用CommandParameter传参

    原文:WPF ContextMenu 在MVVM模式中绑定 Command及使用CommandParameter传参 ContextMenu无论定义在.cs或.xaml文件中,都不继承父级的DataC ...

  3. WPF 高级篇 MVVM (MVVMlight) 依赖注入使用Messagebox

    原文:WPF 高级篇 MVVM (MVVMlight) 依赖注入使用Messagebox MVVMlight 实现依赖注入 把弹框功能 和接口功能注入到各个插件中 使用依赖注入 先把所有的ViewMo ...

  4. WPF 高级篇 MVVM 附加属性

    原文:WPF 高级篇 MVVM 附加属性 WPF 特性之一 附加属性 在本文里实现文本框内容的验证 public class TextBoxHelper:DependencyObject { publ ...

  5. .NET CORE(C#) WPF简单菜单MVVM绑定

    微信公众号:Dotnet9,网站:Dotnet9,问题或建议:请网站留言, 如果对您有所帮助:欢迎赞赏. .NET CORE(C#) WPF简单菜单MVVM绑定 阅读导航 本文背景 代码实现 本文参考 ...

  6. [WPF] 使用 MVVM Toolkit 构建 MVVM 程序

    1. 什么是 MVVM Toolkit 模型-视图-视图模型 (MVVM) 是用于解耦 UI 代码和非 UI 代码的 UI 体系结构设计模式. 借助 MVVM,可以在 XAML 中以声明方式定义 UI ...

  7. 【2016-10-24】【坚持学习】【Day11】【WPF】【MVVM】

    今天学习wpf的mvvm 人家说,APS.NET ===>MVC WPF===>MVVM 用WPF不用mvvm的话,不如用winform... 哈哈,题外话. 定义: MVVM: WPF的 ...

  8. C#人爱学不学9[C#5.0异步实例+WPF自己的MVVM Async应用 1/12]

    文章摘要: 1. 通过简单DEMO.让读者理解Task和Task<T>    学习过程中,掌握async和await 2. 理解同步和异步的执行 3. Task.Factory.Start ...

  9. WPF中使用MVVM模式进行简单的数据绑定

    计划慢慢整理自己在WPF学习和工作应用中的一些心得和想法,先从一个简单的用法说起 在WPF中,XAML标记语言中绑定数据,而数据源就是指定为ViewModel类,而非界面本身的逻辑代码类 这样一定程度 ...

  10. 简单的介绍下WPF中的MVVM框架

    最近在研究学习Swift,苹果希望它迅速取代复杂的Objective-C开发,引发了一大堆热潮去学它,放眼望去各个培训机构都已打着Swift开发0基础快速上手的招牌了.不过我觉得,等同于无C++基础上 ...

随机推荐

  1. 2023-01-10:智能机器人要坐专用电梯把货物送到指定地点, 整栋楼只有一部电梯,并且由于容量限制智能机器人只能放下一件货物, 给定K个货物,每个货物都有所在楼层(from)和目的楼层(to),

    2023-01-10:智能机器人要坐专用电梯把货物送到指定地点, 整栋楼只有一部电梯,并且由于容量限制智能机器人只能放下一件货物, 给定K个货物,每个货物都有所在楼层(from)和目的楼层(to), ...

  2. 2021-05-27:定义何为step sum?比如680,680+68+6=754,680的step sum叫754。

    2021-05-27:定义何为step sum?比如680,680+68+6=754,680的step sum叫754.给定一个整数num,判断它是不是某个数的step sum? 福大大 答案2021 ...

  3. 2021-07-07:股票问题4。给定一个整数数组 prices ,它的第 i 个元素 prices[i] 是一支给定的股票在第 i 天的价格。设计一个算法来计算你所能获取的最大利润。你最多可以完成

    2021-07-07:股票问题4.给定一个整数数组 prices ,它的第 i 个元素 prices[i] 是一支给定的股票在第 i 天的价格.设计一个算法来计算你所能获取的最大利润.你最多可以完成 ...

  4. F对象和Q对象

    F对象 批量计算 Q对象,与或非

  5. django之drf(部分讲解)

    序列化类常用字段和字段参数 drf在Django字段类型的基础上派生了自己的字段类型以及字段参数 序列化器的字段类型用于处理原始值和内部数据类型直接的转换 还可以用于验证输入.以及父对象检索和设置值 ...

  6. Python 安装教程,新手入门(超详细)含Pycharm开发环境安装教程

    目录 一.Python介绍 二.Python安装教程 (一)Python的下载 (二)Python的安装 三.Pycharm开发工具的安装 (一)Pycharm介绍 (二)Pycharm的下载 (三) ...

  7. SQL Server 2008/2012 完整数据库备份+差异备份+事务日志备份 数据库完整还原(一)

    还原方案 数据库级(数据库完整还原) 还原和恢复整个数据库.数据库在还原和恢复操作期间会处于离线状态.SQL SERVER不允许用户备份或还原单个表.还原方案是指从一个或多个备份中还原数据.继而恢复数 ...

  8. 第四章 IDEA的安装与使用

    网上一大推的教程 ‍

  9. pta第三阶段题目集

    (1)前言 pta第三阶段作业中,主要包含了如下的主要内容: 1.全程贯穿了课程设计的程序,每一次都是上一次的迭代和修改,难度较大,中间涉及到先是类与类之间的多态和继承关系,后面的修改中,转变为了组合 ...

  10. MAC地址、IP地址与子网———计算机网络

    计算机具有强大的功能.除了体现与计算机本身具有的计算能力外,其他的功能大多是基于与其他计算机联网提供的. 然而,计算机之间的联网不是一根网线就能解决嘛? 答案当然是否定的.实际上计算机间的交流过程十分 ...