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

80 阅读6分钟

为何模块化

模块化是一种分治思想,不仅可以分离复杂的业务逻辑,还可以进行不同任务的分工。

模块与模块之间相互独立,从而构建一种松耦合的应用程序,便于开发和维护。

开发技术

.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 属性。

代码如下:

/// <summary>
/// 视图模型基类
/// </summary>
public abstract class BaseViewModel : BindableBase
{
    private DelegateCommand _loadCommand;
    protected IEventAggregator EventAggregator { get; } 
    //事件聚合器
    protected IRegionManager RegionManager { get; } 
    // 区域管理器
    public DelegateCommand LoadCommand => _loadCommand ??= new(OnLoad); 
    //界面加载命令

    public BaseViewModel()
    {
        RegionManager = ContainerLocator.Current.Resolve<IRegionManager>();
        EventAggregator = ContainerLocator.Current.Resolve<IEventAggregator>();
    }

    /// <summary>
    /// 界面加载时,由Loaded事件触发
    /// </summary>
    protected virtual void OnLoad()
    {
    }

    /// <summary>
    /// 根据区域名称查找视图
    /// </summary>
    /// <param name="regionName">区域名称</param>
    protected TView TryFindView<TView>(string regionName) where TView : class
    {
        return RegionManager.Regions[regionName].Views
                 .Where(v => v.GetType() == typeof(TView))
                 .FirstOrDefault() as TView;
    }
}

Lapis.ModuleALapis.ModuleB: 对应前端业务模块A和B, 模块A包含 PageOne 和 PageTwo 两个视图及视图模型,模块B只含 PageThree 一个视图及视图模型。

按照Prism框架规定,视图模型最好以 视图名称 + ViewModel 来命名。

如图所示:

297716dad383af284f418b925efcb3bc_895729-20230813093245448-1084096084.png

其中,ModuleA 和 ModuleB 表示模块类,用于初始化模块和注册类型。

ModuleA 代码如下:

[Module(ModuleName = "ModuleA", OnDemand = true)]
public class ModuleA : IModule
{
    public void OnInitialized(IContainerProvider containerProvider)
    {
        var regionManager = containerProvider.Resolve<IRegionManager>();
        regionManager.RegisterViewWithRegion(ModuleARegionNames.RegionOne, typeof(PageOne)); 
        // 将页面一注册到区域一
        regionManager.RegisterViewWithRegion(ModuleARegionNames.RegionTwo, typeof(PageTwo)); 
        // 将页面二注册到区域二
    }

    public void RegisterTypes(IContainerRegistry containerRegistry)
    {
    }
}

第7和第8行代码:分别将 PageOne 和 PageTwo 注册到 RegionOne 和 RegionTwo。

为了方便,区域名称用字符串常量表示。

Lapis.Shell: 是一个启动模块,负责启动/初始化应用程序(加载模块和资源),它包含App启动类、主窗口、侧边菜单和Tab页内容视图及对应的视图模型等。

其中 PageSelectedEvent 是一个页面选中事件,用于 ViewModel 之间传递消息,起到解耦作用。

如图所示:

MainWindow 此处作为启动窗口/主窗口。

为了让 MainWindow 代码保持简洁,我们只把它当作布局页面来使用。

代码片段如下:

<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="auto" />
        <ColumnDefinition />
    </Grid.ColumnDefinitions>
    <!--  侧边菜单栏内容  -->
    <ContentControl Name="sideMenuContentControl" Width="200px" Margin="5" />
    <!--  Tab页主内容  -->
    <ContentControl Name="tabPagesContentControl" Grid.Column="1" Margin="0,5,5,5" />
</Grid>

第7和第9行代码:sideMenuContentControl 和 tabPagesContentControl 是两个内容控件,用来呈现左侧菜单和Tab页面视图。

看到这里,大家一定会问:ContentControl 是通过什么来关联视图的?

没错,就是上面提到的Region,我们可以在MainWindow.cs中进行区域设置,代码如下:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        RegionManager.SetRegionName(this.sideMenuContentControl, ShellRegionNames.SideMenuContentRegion);
        RegionManager.SetRegionName(this.tabPagesContentControl, ShellRegionNames.TabPagesContentRegion);
    }
}

然后,同样在 ShellModule 类里对 SideMenuContent 和 TabPagesContent 视图进行区域注册,这样主窗口就能显示左侧菜单和Tab页面了。

代码如下:

[Module(ModuleName = "ShellModule", OnDemand = true)]
public class ShellModule : IModule
{
    public void OnInitialized(IContainerProvider containerProvider)
    {
        var regionManager = containerProvider.Resolve<IRegionManager>();
        regionManager.RegisterViewWithRegion(ShellRegionNames.SideMenuContentRegion, typeof(SideMenuContent)); 
        // 注册侧边菜单内容视图
        regionManager.RegisterViewWithRegion(ShellRegionNames.TabPagesContentRegion, typeof(TabPagesContent)); 
        // 注册Tab页面内容视图
    }

    public void RegisterTypes(IContainerRegistry containerRegistry)
    {
    }
}

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

代码如下:

<unity:PrismApplication
    x:Class="Lapis.Shell.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:Lapis.Shell"
    xmlns:unity="http://prismlibrary.com/">
    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/SkinDefault.xaml" />
                <ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/Theme.xaml" />
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Application.Resources>
</unity:PrismApplication>
public partial class App : PrismApplication
{
    protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog)
    {
        base.ConfigureModuleCatalog(moduleCatalog);
        //
        moduleCatalog.AddModule<ShellModule>();     
        //添加宿主模块
        moduleCatalog.AddModule<ModuleA.ModuleA>();  
        //添加业务模块A
        moduleCatalog.AddModule<ModuleB.ModuleB>();  
        //添加业务模块B
    }

    protected override Window CreateShell()
    {
        return Container.Resolve<MainWindow>(); 
        //返回主窗体
    }

    protected override void RegisterTypes(IContainerRegistry containerRegistry)
    {
    }
}

接下来,要做的就是左侧菜单和Tab页面之间的交互动作。

不同于传统Winform的事件驱动机制,我们使用MVVM模式将视图和UI逻辑分离。

因此一般情况下,所有的界面逻辑都应该在 ViewModel 里完成。

SideMenuContentViewModel 通过事件聚合器发布页面选中事件,TabPagesContentViewModel 则通过订阅该事件来进行页面切换,代码如下:

/// <summary>
/// 侧边菜单内容视图模型
/// </summary>
public class SideMenuContentViewModel : BaseViewModel
{
    private DelegateCommand<string> _menuSelectedCommand;

    private List<PageInfo> _pages = new()
    {
        new PageInfo { Id = "1" ,RegionName = "RegionOne", DisplayName = "子菜单1" },
        new PageInfo { Id = "2", RegionName = "RegionTwo", DisplayName = "子菜单2" },
        new PageInfo { Id = "3", RegionName = "RegionThree", DisplayName = "子菜单3" },
    };

    public DelegateCommand<string> MenuSelectedCommand => _menuSelectedCommand ??= new DelegateCommand<string>(ExecuteMenuSelectedCommand);

    private void ExecuteMenuSelectedCommand(string id)
    {
        var info = _pages.Find(x => x.Id == id);
        if (info != null)
        {
            EventAggregator.GetEvent<PageSelectedEvent>().Publish(info);
        }
    }
}
/// <summary>
/// Tab页面内容视图模型
/// </summary>
public class TabPagesContentViewModel : BaseViewModel
{
    private TabControl _tabControl;

    protected override void OnLoad()
    {
        _tabControl = TryFindView<TabPagesContent>(ShellRegionNames.TabPagesContentRegion)?.FindName("tabControl") as TabControl;

        EventAggregator.GetEvent<PageSelectedEvent>().Subscribe(OnPageSelected);
    }

    /// <summary>
    /// 页面选中事件处理
    /// </summary>
    /// <param name="page"></param>
    private void OnPageSelected(PageInfo page)
    {
        try
        {
            var existItem = FindItem(_tabControl, page.RegionName);
            if (existItem != null)
            {
                existItem.IsSelected = true;
            }
            else
            {
                // 创建页面区域控件
                var pageContentControl = new ContentControl();
                pageContentControl.SetRegionName(page.RegionName);

                var item = new TabItem
                {
                    Name = page.RegionName,     
                    // 区域名称,如:RegionOne、RegionTwo
                    Header = page.DisplayName,  
                    // 页面名称
                    IsSelected = true,
                    Content = pageContentControl
                };

                _tabControl.Items.Add(item);
            }
        }
        catch { }
    }

    private TabItem FindItem(TabControl tc, string name)
    {
        foreach (TabItem item in tc.Items)
        {
            if (item.Name == name)
            {
                return item;
            }
        }
        return null;
    }
}

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

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

总结

本文章探讨了如何使用WPF 来开发遵循MVVM架构模式的模块化桌面应用程序。通过将用户界面逻辑与业务逻辑分离,MVVM促进了代码的可测试性和可维护性。同时,采用模块化设计可以让应用更易于扩展和管理,支持团队并行开发。

最后

如果你觉得这篇文章对你有帮助,不妨点个赞支持一下!你的支持是我继续分享知识的动力。如果有任何疑问或需要进一步的帮助,欢迎随时留言。

也可以加入微信公众号 [DotNet技术匠] 社区,与其他热爱技术的同行一起交流心得,共同成长!

优秀是一种习惯,欢迎大家留言学习!

作者:天行健君子以自强

出处:cnblogs.com/fengjq/p/17630386.html

声明:网络内容,仅供学习,尊重版权,侵权速删,歉意致谢!