为何模块化

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

开发技术

.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. 2022-05-16:A -> B,表示A认为B是红人, A -> B -> C,表示A认为B是红人,B认为C是红人,规定“认为”关系有传递性,所以A也认为C是红人, 给定一张有向图,方式是给定M个有

    2022-05-16:A -> B,表示A认为B是红人, A -> B -> C,表示A认为B是红人,B认为C是红人,规定"认为"关系有传递性,所以A也认为C是红 ...

  2. 2021-03-12:go中,如何确定有没有内存泄露,系统里怎么去监控整体的运行情况,日志是怎么处理的?

    2021-03-12:go中,如何确定有没有内存泄露,系统里怎么去监控整体的运行情况,日志是怎么处理的? 福哥答案2021-03-12: runtime/pprof:采集程序(非 Server)的运行 ...

  3. 2021-10-07:将有序数组转换为二叉搜索树。给你一个整数数组 nums ,其中元素已经按 升序 排列,请你将其转换为一棵 高度平衡 二叉搜索树。高度平衡 二叉树是一棵满足「每个节点的左右两个子树

    2021-10-07:将有序数组转换为二叉搜索树.给你一个整数数组 nums ,其中元素已经按 升序 排列,请你将其转换为一棵 高度平衡 二叉搜索树.高度平衡 二叉树是一棵满足「每个节点的左右两个子树 ...

  4. APP调用第三方(微信)登录(最详细的实现流程)

    最近使用weexplus做了个app 用户需要的是可以使用第三方微信实现登录(虽然网上有很多相关的什么申请开发者账户.appid.openid等资料:但是都是讲的中间的那一部分请原谅我是个菜鸟,脑补开 ...

  5. OCR -- 文本检测 - 训练DB文字检测模型

    百度飞桨(PaddlePaddle) - PP-OCRv3 文字检测识别系统 预测部署简介与总览 百度飞桨(PaddlePaddle) - PP-OCRv3 文字检测识别系统 Paddle Infer ...

  6. wait,notify,notifyAll,sleep,join等线程方法的全方位演练

    一.概念解释 1. 进入阻塞: 有时我们想让一个线程或多个线程暂时去休息一下,可以使用 wait(),使线程进入到阻塞状态,等到后面用到它时,再使用notify().notifyAll() 唤醒它,线 ...

  7. 10.1. Java性能调优

    Java性能调优是一个复杂且重要的主题,它涉及到了JVM.垃圾收集器.内存管理.多线程.代码优化等多个方面.在本节中,我们将对Java性能调优的基本概念和方法进行简要介绍. 10.1.1. 理解性能指 ...

  8. 国标GB28181协议客户端开发(一)整体流程和技术选型

    国标GB28181协议客户端开发(一)整体流程和技术选型 本系列文章将介绍国标GB28181协议设备端的开发过程.本文旨在探讨整体设计和技术选型方面的考虑,为开发人员提供指导和参考.文章将从设备端开发 ...

  9. CMU15445 (Fall 2020) 数据库系统 Project#3 - Query Execution 详解

    前言 经过前两个实验的铺垫,终于到了执行 SQL 语句的时候了.这篇博客将会介绍 SQL 执行计划实验的实现过程,下面进入正题. 总体架构 一条 SQL 语句的处理流程可以归纳为: SQL 被 Par ...

  10. JPA在事务结束时自动更新查询数据

    目录 现象 产生的原因 解决方法 现象 最近解决了一个困惑几天的bug,数据库里的某一些记录莫名其妙的被刷新了,排查过代码跟应用日志,可以确定不是代码执行的更新.直到今天看到了一条日志,在事务提交时报 ...