news 2026/11/3 2:32:17

WPF+Prism 模块化编程实战:从零构建可扩展应用架构

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
WPF+Prism 模块化编程实战:从零构建可扩展应用架构

1. 为什么我们需要模块化?从“一锅炖”到“乐高积木”

大家好,我是老张,一个在WPF和智能硬件领域摸爬滚打了十多年的老码农。今天想和大家聊聊一个让很多WPF开发者又爱又恨的话题:如何构建一个真正“能长大”的应用程序。

回想我早期做项目,一个典型的WPF应用是什么样子?一个庞大的解决方案,里面塞满了各种窗口、用户控件、业务逻辑类,所有的代码都挤在一起。主窗口的代码文件动辄几千行,想改个按钮功能,得在一堆事件处理程序里翻半天。更头疼的是,当业务需求变化,想加个新功能,或者把某个功能独立出来给另一个项目用,那简直是牵一发而动全身,改得心惊胆战。这种“一锅炖”式的架构,我们称之为“单体应用”。它在项目初期确实简单直接,但随着功能迭代,维护成本会指数级上升,最终变成一个没人敢动的“屎山”。

那么,理想的架构应该是什么样的呢?我觉得可以把它想象成玩乐高积木。每个独立的功能模块,就像一块块标准接口的乐高积木。主程序只是一个“底板”和“说明书”,负责把这些积木拼装起来。今天你想拼个汽车,就把轮子、车身的积木装上;明天想改成飞船,就把轮子模块卸了,装上推进器和机翼模块。整个过程清晰、灵活,而且不会因为改动一个部件而把整个模型搞垮。

WPF+Prism这套组合拳,就是帮我们实现“乐高式”开发的利器。Prism是微软官方推出的一个用于构建松耦合、可维护、可测试的XAML应用程序的框架。它不是一个全新的UI框架,而是建立在WPF(或WinUI、Xamarin.Forms等)之上的“架构指南”和“工具箱”。它的核心思想就是模块化和依赖注入。通过Prism,我们可以把应用拆分成多个独立的、可以动态加载的模块,每个模块只关心自己的那部分功能,模块之间通过定义好的接口进行通信,而不是直接互相引用。这样一来,应用的扩展性、可维护性和团队协作效率都会得到质的提升。

2. 迈出第一步:创建你的第一个Prism项目

光说不练假把式,咱们直接上手。我会带你从零开始,一步步搭建一个具备模块化能力的Prism项目骨架。这个过程就像盖房子先打地基,地基打好了,后面往上垒砖(加模块)就轻松了。

2.1 项目初始化与结构规划

首先,打开Visual Studio(我用的2022,其他版本也大同小异),新建一个WPF应用项目。这里有个小选择:用传统的.NET Framework还是新的.NET 6/7/8?如果你的项目有遗留依赖或者特定要求,用.NET Framework没问题。但我更推荐使用**.NET 6或更高版本**的WPF应用模板,它能享受到跨平台潜力、更好的性能和更现代的依赖注入支持。我们给项目起个名,比如PrismModularAppDemo。

项目创建好后,你会看到一个经典的MainWindow.xaml和App.xaml。先别急着写代码,我们要对项目结构做一个重要的规划。在解决方案资源管理器里,右键点击项目,新建几个文件夹:

  • Views:存放所有的视图(View),也就是XAML文件,比如窗口、用户控件。
  • ViewModels:存放所有的视图模型(ViewModel),这是Prism MVVM模式的核心,负责视图的展示逻辑。
  • Models:存放业务模型和数据实体类。
  • Services:存放各种服务类,比如数据访问、网络通信、日志等。
  • Modules:这个文件夹先建好,后面我们会把各个功能模块放在这里,或者作为模块项目的引用目录。

现在,把自动生成的MainWindow.xaml和MainWindow.xaml.cs一起,拖到Views文件夹里。移动后,VS会提示你是否要更新所有引用,点“是”。但为了保险起见,我们手动检查两个地方:

  1. 打开App.xaml文件,找到StartupUri属性,把它从StartupUri="MainWindow.xaml"修改为StartupUri="Views/MainWindow.xaml"。
  2. 打开Views/MainWindow.xaml文件,找到根标签里的x:Class属性,把它从x:Class="PrismModularAppDemo.MainWindow"修改为x:Class="PrismModularAppDemo.Views.MainWindow"。

这一步操作看似简单,却是关注点分离的良好开端。视图和视图模型分开放,结构一目了然。

2.2 引入Prism框架:安装与基础配置

接下来,就是请出我们的主角——Prism框架。我们通过NuGet包管理器来安装。右键点击项目,选择“管理NuGet程序包”。在浏览标签页中,搜索“Prism.Unity”。

注意:Prism支持多种依赖注入容器,比如Unity、DryIoc、Autofac等。Prism.Unity包就包含了Prism核心库和Unity容器的集成。对于新手,我推荐从Unity开始,它的概念相对直观。如果你熟悉其他容器,也可以选择对应的包,如Prism.DryIoc。

找到Prism.Unity包(作者是Brian Lagunas),点击安装。这会自动安装一系列依赖包,包括Prism的核心库Prism.Core和Unity容器。

安装完成后,我们首先来体验一下Prism带来的第一个便利:自动的ViewModel绑定。传统WPF MVVM中,我们需要在View的后台代码或XAML里手动设置DataContext。Prism简化了这个过程。

在ViewModels文件夹中,新建一个类,命名为MainWindowViewModel。让它继承Prism.Mvvm.BindableBase这个类,它提供了属性变更通知(INotifyPropertyChanged)的快捷实现。

using Prism.Mvvm; namespace PrismModularAppDemo.ViewModels { public class MainWindowViewModel : BindableBase { private string _title = "我的第一个Prism模块化应用"; public string Title { get { return _title; } set { SetProperty(ref _title, value); } } public MainWindowViewModel() { // 这里可以初始化一些数据 } } }

然后,打开Views/MainWindow.xaml文件。在Window标签里,添加Prism的命名空间引用,并设置一个特殊的附加属性来实现自动绑定。

<Window x:Class="PrismModularAppDemo.Views.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:prism="http://prismlibrary.com/" prism:ViewModelLocator.AutoWireViewModel="True" Title="{Binding Title}" Height="450" Width="800"> <Grid> <TextBlock Text="{Binding Title}" HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="24"/> </Grid> </Window>

关键就在prism:ViewModelLocator.AutoWireViewModel="True"这一行。Prism的视图模型定位器(ViewModelLocator)会遵循一个命名约定:自动去寻找与当前View(MainWindow)对应的ViewModel(MainWindowViewModel),并且把它设置为View的DataContext。这个约定就是为什么我们之前要把View和ViewModel分开放到对应文件夹的原因。

现在直接运行程序,你会看到窗口标题和中间的文本都显示为“我的第一个Prism模块化应用”。这说明ViewModel已经成功绑定并工作了!我们并没有在任何地方写this.DataContext = new MainWindowViewModel();,这就是约定优于配置的魅力,也是依赖注入开始起作用的地方。

3. 核心改造:让应用“模块化”起来

前面我们只是用上了Prism的MVVM绑定,这已经很方便了,但还没触及模块化的核心。接下来,我们要对启动流程进行“大手术”,把普通的WPF应用彻底改造成一个支持动态加载模块的Prism应用。

3.1 重构应用启动类:继承PrismApplication

这是最关键的一步。默认的WPF应用入口是App.xaml和它的后台代码App.xaml.cs,它继承自Application类。Prism要求我们使用它提供的PrismApplication类作为启动基类。

首先,打开App.xaml,把Application改成prism:PrismApplication,并引入Prism命名空间。

<prism:PrismApplication x:Class="PrismModularAppDemo.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:prism="http://prismlibrary.com/"> <Application.Resources> </Application.Resources> </prism:PrismApplication>

然后,打开App.xaml.cs文件。把类的继承从Application改为PrismApplication。

using Prism.Unity; using System.Windows; namespace PrismModularAppDemo { public partial class App : PrismApplication // 继承PrismApplication { // 原有的StartupUri配置已经不需要了,可以删除App.xaml中的StartupUri } }

一保存,你会发现代码报错了。因为PrismApplication是一个抽象类,它要求我们实现几个核心的方法。点击类名App下的红色波浪线,按Alt+Enter(或点击小灯泡),选择“实现抽象类”。VS会自动生成三个方法:CreateShell()、RegisterTypes()和ConfigureModuleCatalog()。

3.2 实现抽象方法:搭建模块化骨架

我们来逐一填充这些方法。

1. CreateShell() - 创建应用的主壳(Shell)

Shell可以理解为应用程序的主容器窗口,它定义了应用的总体布局结构,比如顶部菜单、底部状态栏、左侧导航栏和中间的内容区域。在这里,我们直接使用之前创建的MainWindow作为Shell。

protected override Window CreateShell() { // 使用容器来解析MainWindow,而不是直接new。 // 这样MainWindow及其依赖(如它的ViewModel)都会被依赖注入容器管理。 return Container.Resolve<Views.MainWindow>(); }

注意,这里用的是Container.Resolve<Views.MainWindow>(),而不是new Views.MainWindow()。这是依赖注入的核心:对象不由我们直接创建,而是向“容器”索取。容器负责创建对象,并自动注入它所依赖的其他对象(比如MainWindowViewModel)。

2. RegisterTypes() - 注册类型到依赖注入容器

这个方法是我们向IoC(控制反转)容器注册各种类型的地方。哪些类型需要注册呢?主要是那些接口和它们的实现类,以及一些需要容器管理的单例或共享实例。

在我们这个初始阶段,可能还没有很多服务,但我们可以先把视图模型注册一下,虽然Prism的AutoWireViewModel通常能搞定View和ViewModel的绑定,但显式注册也是一个好习惯,特别是当ViewModel有构造函数依赖时。

protected override void RegisterTypes(IContainerRegistry containerRegistry) { // 注册MainWindowViewModel,当需要MainWindowViewModel实例时,容器会提供它。 // 使用PerRequest或Transient取决于你的需求,对于View的ViewModel,通常Transient即可。 containerRegistry.Register<ViewModels.MainWindowViewModel>(); // 未来我们会在这里注册更多服务,例如: // containerRegistry.Register<IDataService, DataService>(); // containerRegistry.RegisterSingleton<ILogger, FileLogger>(); }

3. ConfigureModuleCatalog() - 配置模块目录

这是模块化的心脏。模块目录(IModuleCatalog)管理着应用中所有模块的信息:模块有哪些、它们在哪里、应该按什么顺序初始化。在这个方法里,我们告诉Prism去哪里发现和加载我们的模块。

初期,我们可以先添加一个内联的、直接引用程序集的模块。后面我们会升级为从目录动态加载。

protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog) { base.ConfigureModuleCatalog(moduleCatalog); // 方式一:直接添加一个模块类型(要求该模块所在程序集已被主程序引用) // moduleCatalog.AddModule<ModuleA.ModuleAModule>(); // 方式二:通过类型名和所在程序集名称添加(更灵活) // moduleCatalog.AddModule(typeof(ModuleA.ModuleAModule).Name, typeof(ModuleA.ModuleAModule).Assembly.FullName); // 我们现在还没有创建模块,所以先注释掉。下一章我们会创建第一个模块并在这里添加。 }

完成以上步骤后,记得删除App.xaml中之前设置的StartupUri="Views/MainWindow.xaml"属性,因为现在主窗口是由CreateShell()方法创建并显示的。

现在再次运行程序,应用应该能正常启动,并显示MainWindow。看起来和之前没什么不同,但内在的引擎已经彻底更换了。我们的应用已经从一辆“焊接死的汽车”,变成了一个拥有标准接口的“底盘”,随时可以按上各种功能“模块”。

4. 创建你的第一个功能模块

地基和骨架都有了,现在我们来造第一块“乐高积木”——一个独立的功能模块。假设我们要为一个客户管理系统添加一个“客户管理”模块。

4.1 模块项目的创建与结构

在解决方案中,我们不再把所有代码都塞进启动项目。而是为“客户管理”功能单独创建一个项目。

  1. 在解决方案上右键 -> 添加 -> 新建项目。
  2. 选择“类库(.NET Framework)”或“类库(.NET Standard/.NET Core)”。我强烈建议选择.NET Standard 2.0类库。.NET Standard是一个API规范,能被.NET Framework、.NET Core/.NET 5+等多种.NET实现引用,兼容性最好,是模块化开发的理想选择。给项目起名为CustomerModule。
  3. 在这个新类库项目中,同样引用Prism.Unity的NuGet包。因为模块也需要使用Prism的模块化接口和依赖注入。
  4. 在CustomerModule项目中,创建类似的文件夹结构:Views,ViewModels,Services等。

一个Prism模块的核心,是一个实现了IModule接口的类。这个类通常以Module结尾,例如CustomerModule。

4.2 实现IModule接口:模块的入口点

在CustomerModule项目的根目录(或者单独建一个Infrastructure文件夹),创建一个类CustomerModule.cs。

using Prism.Ioc; using Prism.Modularity; using Prism.Regions; using CustomerModule.Views; using System.Windows.Controls; namespace CustomerModule { public class CustomerModule : IModule { private readonly IRegionManager _regionManager; // 通过构造函数注入IRegionManager,用于向Shell中的区域注册视图 public CustomerModule(IRegionManager regionManager) { _regionManager = regionManager; } public void OnInitialized(IContainerProvider containerProvider) { // 模块初始化时,将客户列表视图注册到Shell中名为“MainContentRegion”的区域。 // 假设你的Shell(MainWindow)里有一个ContentControl,其RegionName="MainContentRegion" _regionManager.RegisterViewWithRegion("MainContentRegion", typeof(CustomerListView)); } public void RegisterTypes(IContainerRegistry containerRegistry) { // 在这里注册本模块特有的类型到容器中 // 例如,注册本模块的视图、服务等 containerRegistry.RegisterForNavigation<CustomerListView>(); // 注册视图用于导航 containerRegistry.Register<ICustomerService, CustomerService>(); // 注册服务 } } }

这个CustomerModule类就是模块的“大脑”。OnInitialized方法在模块被加载后调用,这里我们通常做视图注册到区域(Region)的操作。RegisterTypes方法和主程序中的类似,用于向容器注册本模块提供的服务。

4.3 定义模块视图与区域(Region)

区域(Region)是Prism中一个极其强大的概念。你可以把它理解为Shell(主窗口)中预留的、可以被动态替换内容的“占位符”。比如,主窗口左边是导航栏区域(NavigationRegion),中间是主内容区域(MainContentRegion)。

首先,我们需要修改主程序Shell(MainWindow.xaml),定义区域。我们使用Prism的附加属性来标记一个控件为区域。

<Window ...> <Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <!-- 顶部菜单栏 --> <Menu Grid.Row="0"> <MenuItem Header="模块"> <MenuItem Header="加载客户模块" Command="{Binding LoadCustomerModuleCommand}"/> </MenuItem> </Menu> <!-- 主内容区域,这里被定义为一个Region --> <ContentControl Grid.Row="1" prism:RegionManager.RegionName="MainContentRegion"/> </Grid> </Window>

在CustomerModule项目中,我们创建一个简单的视图CustomerListView.xaml(放在Views文件夹)和对应的CustomerListViewModel。

<!-- CustomerListView.xaml --> <UserControl x:Class="CustomerModule.Views.CustomerListView" ...> <StackPanel> <TextBlock Text="客户列表" FontSize="18" Margin="10"/> <ListBox ItemsSource="{Binding Customers}" DisplayMemberPath="Name" Height="200"/> <Button Content="添加测试客户" Command="{Binding AddTestCustomerCommand}" Margin="10"/> </StackPanel> </UserControl>
// CustomerListViewModel.cs using Prism.Commands; using Prism.Mvvm; using System.Collections.ObjectModel; namespace CustomerModule.ViewModels { public class CustomerListViewModel : BindableBase { public ObservableCollection<Customer> Customers { get; set; } = new ObservableCollection<Customer>(); public DelegateCommand AddTestCustomerCommand { get; private set; } public CustomerListViewModel() { AddTestCustomerCommand = new DelegateCommand(AddTestCustomer); // 初始化一些测试数据 Customers.Add(new Customer { Id = 1, Name = "张三" }); Customers.Add(new Customer { Id = 2, Name = "李四" }); } private void AddTestCustomer() { Customers.Add(new Customer { Id = Customers.Count + 1, Name = $"测试客户{Customers.Count + 1}" }); } } public class Customer { public int Id { get; set; } public string Name { get; set; } } }

4.4 在主程序中加载模块

最后一步,回到主程序(PrismModularAppDemo),我们需要告诉它去哪里找这个CustomerModule。

首先,在主项目中添加对CustomerModule类库项目的项目引用。这是最简单直接的模块加载方式(模块与主程序一起编译)。

然后,修改主程序App.xaml.cs中的ConfigureModuleCatalog方法:

protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog) { base.ConfigureModuleCatalog(moduleCatalog); // 添加CustomerModule,并指定其初始化模式为WhenAvailable(默认,主程序启动时加载) // 也可以设置为OnDemand,在需要时手动加载 moduleCatalog.AddModule<CustomerModule.CustomerModule>(); }

现在,运行主程序。由于我们在模块的OnInitialized中,将CustomerListView注册到了MainContentRegion,所以程序一启动,主内容区域就会自动显示客户列表视图。点击“添加测试客户”按钮,列表会增加新项目。

至此,你已经成功创建并加载了第一个Prism模块!这个模块完全独立于主程序,它有自己的视图、视图模型、模型甚至服务。主程序除了通过项目引用知道它的存在,在代码层面几乎没有耦合。你可以独立地开发、测试、甚至替换这个模块,只要它遵守约定的接口(比如注册到哪个区域)。

5. 进阶实战:动态加载与模块通信

直接引用项目的方式适合紧密集成的模块。但更酷的模式是动态加载:主程序在运行时,从一个指定的目录(如Modules)去发现和加载模块的DLL文件。这样,你甚至可以在不重新发布主程序的情况下,通过增删DLL文件来扩展或移除功能。

5.1 配置目录发现模块

修改主程序的ConfigureModuleCatalog方法,改用目录扫描方式:

protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog) { base.ConfigureModuleCatalog(moduleCatalog); // 获取模块目录的路径(假设在应用程序根目录下的Modules文件夹) string modulePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Modules"); // 使用DirectoryModuleCatalog来扫描指定目录下的所有模块 if (Directory.Exists(modulePath)) { var directoryCatalog = new DirectoryModuleCatalog { ModulePath = modulePath }; // 这里通常需要将directoryCatalog中的模块合并到主catalog中, // 但更常见的做法是直接使用DirectoryModuleCatalog作为主catalog。 // 我们可以重写CreateModuleCatalog方法来返回一个DirectoryModuleCatalog。 } }

更常见的做法是重写CreateModuleCatalog()方法,直接返回一个配置好的目录模块目录。

protected override IModuleCatalog CreateModuleCatalog() { // 返回一个配置了模块路径的DirectoryModuleCatalog string modulePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Modules"); return new DirectoryModuleCatalog() { ModulePath = modulePath }; }

使用这种方式,你需要将编译好的CustomerModule.dll及其依赖(如Prism相关DLL)复制到主程序运行目录下的Modules文件夹中。主程序启动时会自动扫描并加载该目录下的所有有效模块。

5.2 模块间通信:解耦的艺术

模块之间不应该直接互相引用,否则就失去了模块化的意义。Prism提供了几种优雅的通信机制:

  1. 事件聚合器(EventAggregator):这是最常用、最解耦的方式。一个模块发布(Publish)一个事件,其他模块订阅(Subscribe)这个事件。发布者和订阅者彼此不知道对方的存在。

    // 在模块A中定义事件 public class CustomerAddedEvent : PubSubEvent<Customer> { } // 模块A发布事件 _eventAggregator.GetEvent<CustomerAddedEvent>().Publish(newCustomer); // 模块B订阅事件 _eventAggregator.GetEvent<CustomerAddedEvent>().Subscribe(c => { // 处理新客户添加的逻辑,比如刷新自己的列表 RefreshCustomerList(); }, ThreadOption.UIThread); // 指定在UI线程执行
  2. 共享服务(Shared Services):将公共功能抽象成接口,放在一个公共的类库(如Infrastructure)中。所有模块都引用这个公共库,并通过依赖注入获取服务的实现。主程序负责注册这个服务的具体实现。

  3. 区域上下文(Region Context):当多个视图被注入到同一个区域时,可以通过区域上下文来传递数据。

在实际项目中,我强烈推荐使用事件聚合器来处理跨模块的业务通知。它最大限度地降低了模块间的耦合度。

5.3 导航与视图切换

Prism提供了强大的导航服务,可以让你在不同的视图之间切换,并传递参数。这比直接操作Region的Views集合更高级、更规范。

在ViewModel中,你可以注入IRegionManager或INavigationService来进行导航。

// 使用IRegionManager进行区域内的视图导航(更底层) _regionManager.RequestNavigate("MainContentRegion", "CustomerDetailView", parameters); // 使用INavigationService进行导航(更高级,与平台无关性更好) _navigationService.NavigateAsync("CustomerDetailView", parameters);

导航的目标“CustomerDetailView”是一个字符串标识,它需要在模块的RegisterTypes中通过RegisterForNavigation注册。

containerRegistry.RegisterForNavigation<CustomerDetailView, CustomerDetailViewModel>("CustomerDetailView");

踩过的坑:导航时一定要确保目标视图和视图模型已经正确注册到容器中,并且导航参数的类型是简单的、可序列化的类型,复杂对象最好传递一个ID,然后在目标视图的初始化过程中再去查询完整数据。

6. 项目组织与最佳实践建议

经过前面的实战,一个模块化应用的雏形已经出来了。但要把它用到真正的企业级项目里,还需要一些工程化的考量。

6.1 解决方案与项目结构

一个中等规模的Prism模块化应用,解决方案结构可能如下:

MyEnterpriseApp.sln ├── MyEnterpriseApp.Shell (WPF应用,启动项目) │ ├── Views (Shell的视图,如MainWindow) │ ├── ViewModels │ ├── App.xaml/.cs (PrismApplication入口) │ └── Modules 文件夹 (存放动态加载的模块DLL) ├── MyEnterpriseApp.Infrastructure (.NET Standard 类库) │ ├── Interfaces (公共接口,如 ILogger, IDataService) │ ├── Models (公共数据模型) │ ├── Events (公共事件,使用EventAggregator) │ └── Constants (公共常量,如RegionNames) ├── ModuleA (.NET Standard 类库) │ ├── ModuleAModule.cs (实现IModule) │ ├── Views │ ├── ViewModels │ └── Services ├── ModuleB (.NET Standard 类库) └── (其他模块...)

关键点:

  • Shell项目尽可能“薄”:它只负责启动、加载模块、提供主界面框架。具体的业务功能全部放在模块里。
  • 基础设施(Infrastructure)项目:这是所有模块和Shell的“公约数”。里面放大家都要用的接口、事件、模型、工具类。任何模块都不应该直接引用另一个模块,但都可以引用Infrastructure。
  • 模块项目独立:每个模块都是一个完整的、可以独立编译和测试的功能单元。

6.2 依赖注入与生命周期管理

Prism的依赖注入容器是你应用的核心“管家”。要善用它:

  • 接口与实现分离:始终针对接口进行注册和注入。containerRegistry.Register<IMyService, MyService>()。
  • 生命周期:
    • Transient:每次请求都创建一个新实例。这是默认的,适用于无状态的轻量级服务。
    • Singleton:整个应用生命周期内只有一个实例。适用于配置服务、缓存服务等。
    • Scoped:在某些容器中支持(如DryIoc),可以创建某个范围内的单例,在WPF中通常用RegisterScoped或特定区域内的单例。
  • 构造函数注入:这是首选的方式。确保你的类(尤其是ViewModel)的依赖都通过构造函数传入,容器会自动解析。

6.3 调试与部署技巧

  • 模块加载失败:最常见的错误是模块DLL或其依赖项(如特定版本的Prism)没有放在正确的位置。使用DirectoryModuleCatalog时,打开Prism的日志输出(Prism.Logging)可以帮助诊断加载问题。
  • 设计时支持:在XAML设计器中,如果ViewModel是通过容器解析的,设计时可能看不到数据。可以在ViewModel的构造函数中,通过IsInDesignMode属性来提供设计时数据。
  • 部署:对于动态加载,你需要一个清晰的部署脚本,将主程序(Shell)、基础设施库、各个模块的DLL以及它们各自的依赖,正确地复制到输出目录的相应位置(如主目录和Modules子目录)。可以使用构建后事件或专门的打包工具(如Inno Setup, WiX)来处理。

从我个人的经验来看,第一次搭建Prism模块化架构会花一些时间,可能会觉得比直接写“一锅炖”的代码更繁琐。但一旦项目规模超过3个主要功能点,或者有超过2个开发者参与,模块化带来的优势就会迅速体现出来:并行开发互不干扰、功能复用简单、定位bug范围小、测试可以针对模块进行。它迫使你思考代码的边界和职责,这本身就是对代码质量的一次巨大提升。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/3 2:32:08

SenseVoice-Small语音识别模型Java八股文精讲:高并发下的服务调用设计

SenseVoice-Small语音识别模型Java八股文精讲&#xff1a;高并发下的服务调用设计 最近在准备Java后端面试的朋友&#xff0c;应该对“高并发”这三个字不陌生。面试官总爱问&#xff0c;如果让你设计一个面对海量请求的服务调用层&#xff0c;你会怎么考虑&#xff1f;今天&a…

作者头像 李华
网站建设 2026/11/3 2:32:07

SpringAI智能客服实战:从零构建高可用AI对话引擎

最近在做一个智能客服项目&#xff0c;客户对响应速度和意图理解的准确性要求很高。传统的基于关键词和规则树的客服系统&#xff0c;在面对复杂、口语化的用户提问时&#xff0c;经常“答非所问”&#xff0c;维护那堆规则也让人头疼。正好SpringAI这个新框架出现了&#xff0…

作者头像 李华
网站建设 2026/10/27 18:13:56

构建伏羲模型Web管理后台:前端与后端技术栈选型指南

构建伏羲模型Web管理后台&#xff1a;前端与后端技术栈选型指南 最近和几个团队聊&#xff0c;发现大家把伏羲模型跑起来之后&#xff0c;都卡在了同一个地方&#xff1a;怎么给它做个好用的管理后台。模型本身能力很强&#xff0c;但总不能每次都靠命令行去启动任务、看日志、…

作者头像 李华
网站建设 2026/10/27 18:34:31

从洗衣机到无人机:Clarke-Park变换在FOC控制中的实战应用差异

从洗衣机到无人机&#xff1a;Clarke-Park变换在FOC控制中的实战应用差异 如果你已经掌握了FOC&#xff08;磁场定向控制&#xff09;的基本原理&#xff0c;甚至可能在某个开发板上成功让电机转了起来&#xff0c;那么恭喜你&#xff0c;你已经迈入了现代电机控制的大门。但真…

作者头像 李华
网站建设 2026/10/27 17:39:29

PP-DocLayoutV3企业级应用:基于Java的批量文档处理服务构建

PP-DocLayoutV3企业级应用&#xff1a;基于Java的批量文档处理服务构建 如果你在企业里负责过文档数字化项目&#xff0c;肯定遇到过这样的头疼事&#xff1a;每天有成百上千份合同、报告、票据的扫描件涌进来&#xff0c;需要从中提取关键信息。人工处理&#xff1f;效率低、…

作者头像 李华