Prism WPF按需模块加载:如何优雅实现默认视图显示?
优化Prism WPF按需模块加载与视图导航的方案
我来给你几个实用的优化思路,既能解决你当前依赖约定命名(模块名+View)的不严谨问题,又能优雅地获取模块的额外信息,还完全贴合Prism的设计原则:
方案一:自定义模块特性 + 反射读取(无需重写DirectoryModuleCatalog)
这个方案最直接,不需要改动Prism的模块发现逻辑,只是利用它已经获取到的模块类型信息,通过反射读取自定义特性里的元数据。
步骤1:定义自定义元数据特性
先创建一个特性类,用来存储你需要的模块信息:
[AttributeUsage(AttributeTargets.Class, Inherited = false)] public class ModuleMetadataAttribute : Attribute { // 默认视图名称 public string DefaultViewName { get; set; } // 按钮显示的友好名称 public string DisplayName { get; set; } // 还可以扩展其他字段,比如图标路径、模块描述等 }
步骤2:在模块类上标记特性
每个按需加载的模块,同时保留原有的[Module]特性,加上自定义的元数据特性:
[Module(OnDemand = true)] [ModuleMetadata(DefaultViewName = "CustomerListView", DisplayName = "客户管理")] public class CustomerModule : IModule { private readonly IRegionManager _regionManager; public CustomerModule(IRegionManager regionManager) { _regionManager = regionManager; } public void Initialize() { // 注册视图到目标区域(这一步不能少) _regionManager.RegisterViewWithRegion(RegionNames.ContentRegion, typeof(CustomerListView)); } }
步骤3:模块发现时缓存元数据
在初始化DirectoryModuleCatalog后,遍历模块信息,通过反射读取特性并缓存:
// 先定义一个类存储带元数据的模块信息 public class ModuleInfoWithMetadata { public string ModuleName { get; set; } public string DefaultViewName { get; set; } public string DisplayName { get; set; } } // 替换原来的_availableModules为这个类型的集合 private readonly List<ModuleInfoWithMetadata> _availableModules = new(); // 模块发现逻辑 var modulesCatalog = new DirectoryModuleCatalog { ModulePath = @"C:\WhereTheModulesLive" }; modulesCatalog.Initialize(); foreach (var module in modulesCatalog.Modules) { // 通过ModuleType获取模块类的特性 var metadataAttr = module.ModuleType.GetCustomAttribute<ModuleMetadataAttribute>(); if (metadataAttr != null) { _availableModules.Add(new ModuleInfoWithMetadata { ModuleName = module.ModuleName, DefaultViewName = metadataAttr.DefaultViewName, DisplayName = metadataAttr.DisplayName }); } else { // 兼容未标记特性的模块, fallback到原有的约定命名 _availableModules.Add(new ModuleInfoWithMetadata { ModuleName = module.ModuleName, DefaultViewName = module.ModuleName + "View", DisplayName = module.ModuleName }); } }
步骤4:按钮点击时使用元数据导航
现在不需要再拼接字符串,直接用缓存的元数据:
private void LoadAndDisplayModule(ModuleInfoWithMetadata moduleInfo) { _moduleManager.LoadModule(moduleInfo.ModuleName); // 注意:这里不需要手动调用_moduleManager.Run(),LoadModule会自动触发模块初始化 _regionManager.RequestNavigate(RegionNames.ContentRegion, moduleInfo.DefaultViewName); }
方案二:复用Prism的ModuleInfo.CustomProperties
如果你不想额外定义DTO类,可以直接用Prism自带的ModuleInfo对象里的CustomProperties字典来存储元数据,更轻量。
步骤1:模块发现时填充CustomProperties
// 直接缓存ModuleInfo对象即可 private readonly List<ModuleInfo> _availableModules = new(); var modulesCatalog = new DirectoryModuleCatalog { ModulePath = @"C:\WhereTheModulesLive" }; modulesCatalog.Initialize(); foreach (var module in modulesCatalog.Modules) { var metadataAttr = module.ModuleType.GetCustomAttribute<ModuleMetadataAttribute>(); if (metadataAttr != null) { module.CustomProperties["DefaultViewName"] = metadataAttr.DefaultViewName; module.CustomProperties["DisplayName"] = metadataAttr.DisplayName; } else { module.CustomProperties["DefaultViewName"] = module.ModuleName + "View"; module.CustomProperties["DisplayName"] = module.ModuleName; } _availableModules.Add(module); }
步骤2:点击时读取字典信息
private void LoadAndDisplayModule(ModuleInfo module) { _moduleManager.LoadModule(module.ModuleName); var viewName = module.CustomProperties["DefaultViewName"] as string; _regionManager.RequestNavigate(RegionNames.ContentRegion, viewName); }
方案三:元数据服务注册(松耦合的Prism风格)
如果追求最纯正的Prism松耦合设计,让模块自己主动注册元数据,而不是外部反射读取,这个方案更合适。唯一需要注意的是:如果需要在模块未加载前获取按钮显示名称,这个方案需要结合方案一的反射读取(因为模块未加载时无法执行初始化逻辑)。
步骤1:定义元数据服务
public interface IModuleMetadataService { void RegisterMetadata(string moduleName, string defaultViewName, string displayName); ModuleMetadata GetMetadata(string moduleName); } public class ModuleMetadata { public string DefaultViewName { get; set; } public string DisplayName { get; set; } } // 实现服务,注册为单例 public class ModuleMetadataService : IModuleMetadataService { private readonly Dictionary<string, ModuleMetadata> _metadataStore = new(); public void RegisterMetadata(string moduleName, string defaultViewName, string displayName) { if (!_metadataStore.ContainsKey(moduleName)) { _metadataStore[moduleName] = new ModuleMetadata { DefaultViewName = defaultViewName, DisplayName = displayName }; } } public ModuleMetadata GetMetadata(string moduleName) { _metadataStore.TryGetValue(moduleName, out var metadata); // 兼容未注册的模块 return metadata ?? new ModuleMetadata { DefaultViewName = moduleName + "View", DisplayName = moduleName }; } }
步骤2:注册服务到Prism容器
在Bootstrapper的ConfigureContainer方法里注册单例:
protected override void ConfigureContainer(ContainerRegistry containerRegistry) { base.ConfigureContainer(containerRegistry); containerRegistry.RegisterSingleton<IModuleMetadataService, ModuleMetadataService>(); }
步骤3:模块初始化时注册元数据
每个模块在初始化时,通过注入的服务注册自己的元数据:
[Module(OnDemand = true)] public class CustomerModule : IModule { private readonly IModuleMetadataService _metadataService; private readonly IRegionManager _regionManager; public CustomerModule(IModuleMetadataService metadataService, IRegionManager regionManager) { _metadataService = metadataService; _regionManager = regionManager; } public void Initialize() { // 注册元数据 _metadataService.RegisterMetadata(nameof(CustomerModule), "CustomerListView", "客户管理"); // 注册视图 _regionManager.RegisterViewWithRegion(RegionNames.ContentRegion, typeof(CustomerListView)); } }
步骤4:按钮点击时获取元数据
private void LoadAndDisplayModule(string moduleName) { _moduleManager.LoadModule(moduleName); var metadata = _metadataService.GetMetadata(moduleName); _regionManager.RequestNavigate(RegionNames.ContentRegion, metadata.DefaultViewName); }
额外小提示
你现有代码里的_moduleManager.Run();是多余的,Prism的LoadModule方法会自动处理模块的初始化流程,调用Run()反而可能导致重复初始化,直接删掉就行。
内容的提问来源于stack exchange,提问作者Alan Sage
相关产品推荐
相关产品推荐

