技术 C# WPF Prism ZEROKO14 2023-11-24 2026-01-23 此处记录prism框架的方方面面
MIT开源地址
Prism 是一个用于创建模块化和易于维护的应用程序的框架,它主要起到以下作用:
支持 MVVM 模式 :提供了一些工具和类来帮助开发人员实现 MVVM 模式,使视图和模型之间的耦合度降低,提高了代码的可维护性和可测试性。
提供依赖注入容器 :可以方便地管理应用程序中的依赖关系,使代码更加简洁和易于理解。
支持模块化开发 :可以将应用程序拆分成多个模块,每个模块可以独立开发、测试和部署,提高了开发效率和代码的复用性。
提供导航和视图切换功能 :可以方便地实现应用程序中的导航和视图切换,使应用程序的用户体验更加流畅。
支持多种平台 :除了 WPF 平台,Prism 还支持其他平台,如 UWP、Xamarin.Forms 等,使开发人员可以在不同的平台上使用相同的框架和技术。
Prism在vs2019已经下架了,在vs2022上架
Getting Started | Prism (prismlibrary.com)
包含如下内容
Region(区域管理)
Module(模块)
View Injection(视图注入)
ViewModelLocationProvider(视图模型定位)
Command(绑定相关)
Event Aggregator (事件聚合器)
Navigation(导航)
Dialog(对话框)
使用prism只需要在nuget包中引入Prism.WPF和Prism.Dryloc
Prism的初始化过程
PrismApplicationBase其实也是继承Application,App到Application的继承链上隔了一层PrismApplication
vs插件支持 Prism Template Pack
提供了:
Prism也提供了基于Xamarin的项目模板, 因为Prism是一个基于多个平台的框架
ride支持 要在ride中新建prism项目,需要使用安装nuget包:Prism.Templates
然后在ride中新建项目的时候就可以看到有prism框架相关选择了
窗体切换 prism中的窗体显示,使用下面的语法:
1 2 3 4 5 6 7 8 var main = Application.Current.MainWindow;var window=Container.Resolve<MainWindow>();Application.Current.MainWindow = window; window.Show(); main.Close();
需要在App.xaml.cs中通过依赖注入创建视图模型 ,并注册 窗体视图模型,才可以执行视图切换
1 2 3 var mainWindow = new MainWindow();containerRegistry.Register<ViewModels.MainWindowViewModel>();
对比使用下面WPF常规方式显示窗口 ,将丧失导航功能等
1 2 3 var mainWindow = new MainWindow();mainWindow.Show();
Region 参考链接
Region作为Prism当中模块化的核心功能,其主要目的是弱化了模块与模块之间的耦合关系。 在普遍的应用程序开发中,界面上的元素及内容往往被固定。
基本使用方法 定义Region的方式有两种,一个是在XAML界面指定,另一种这是代码当中指定。
XAML中指定:
1 <ContentControl prism:RegionManager.RegionName="自定义区域名"/>
代码创建:(这种是初始化的时候没有绑定视图,只是注册视图容器)
1 RegionManager.SetRegionName(指定ContentControl的name,自定义区域名);
代码中给区域注册用户控件 (这种是初始化就绑定了视图)
1 2 3 4 5 6 public MainWindowViewModel (IRegionManager regionManager ) { regionManager.RegisterViewWithRegion("ContentRegion" , typeof (Views.view1)); }
案例 完全符合MVVM的区域切换实现方式
区域名称的定义和绑定
1 2 3 4 <ContentControl x:Name="MainContentRegion" Grid.Column="1" prism:RegionManager.RegionName="{Binding RegionName}">
区域名称通过 ViewModel 的 RegionName 属性来控制
实现了区域名称的动态绑定,可以在 ViewModel 中随时修改
区域注册的触发时机
1 2 3 4 5 6 <i:Interaction.Triggers> <i:EventTrigger EventName="Loaded"> <prism:InvokeCommandAction Command="{Binding SetRegionCommand}" CommandParameter="{Binding ElementName=MainContentRegion}" /> </i:EventTrigger> </i:Interaction.Triggers>
当ContentControl加载完成时触发,通过Loaded事件确保控件已经完全初始化
1 2 3 4 5 6 7 8 private void SetRegion (ContentControl contentControl ){ if (contentControl != null ) { RegionManager.SetRegionName(contentControl, RegionName); RegionManager.SetRegionManager(contentControl, _regionManager); } }
区域切换
1 2 3 4 5 6 7 8 9 10 11 12 13 14 _regionManager.RequestNavigate(RegionName, navigatePath, NavigationCallback); private void NavigationCallback (NavigationResult result ){ if (result.Result == true ) { Log.Debug($"导航成功: {result.Context.Uri} " ); } else { Log.Error($"导航失败: {result.Error?.Message} " ); } }
区域适配器 RegionAdapter区域适配器
上面能在Grid中能使用prism:RegionManager.RegionName属性是因为官方给Grid提供了区域适配器 ,像StackPanel就没有区域适配器
Prism提供了许多内置得RegionAdapter
ContentControlRegionAdapter
ItemsControlRegionAdapter
SelectorRegionAdapter - ComboBox - ListBox - Ribbon - TabControl
自定义区域适配器 自定义区域适配器 是用于定义如何将视图(View)添加到区域(Region)中的机制。区域适配器负责将特定类型的 UI 元素(如 UserControl、Window、ContentControl 等)与区域关联起来,以便在运行时可以将视图加载到这些区域中
以添加StackPanel区域适配器为例
新建名为StackPanelRegion类
下面针对TabControl
TabControl参考此处
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 class TabControlAdapter : RegionAdapterBase <TabControl > { public TabControlAdapter (IRegionBehaviorFactory regionBehaviorFactory ) : base (regionBehaviorFactory ) { } protected override void Adapt (IRegion region, TabControl regionTarget ) { region.Views.CollectionChanged += (s, e) => { if (e.Action == NotifyCollectionChangedAction.Add) foreach (UserControl item in e.NewItems) { regionTarget.Items.Add(new TabItem { Header = item.Name, Content = item }); } else if (e.Action == NotifyCollectionChangedAction.Remove) foreach (UserControl item in e.OldItems) { var tabTodelete = regionTarget.Items.OfType<TabItem>().FirstOrDefault(n => n.Content == item); regionTarget.Items.Remove(tabTodelete); } }; } protected override IRegion CreateRegion () { return new SingleActiveRegion(); } }
继承RegionAdapterBase<T> T为你要针对的控件
RegionManager 除了定义区域,还有以下功能:
下面贴个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 var tabRegion = _regionManager.Regions[RegionNames.AnalysisTabRegion];if (tabRegion.Views.Any()) return ; var historyJournalView = _containerProvider.Resolve<HistoryJournalView>();var historyDataView = _containerProvider.Resolve<HistoryDataView>();var jobJournalView = _containerProvider.Resolve<JobJournalView>();tabRegion.Add(historyJournalView); tabRegion.Add(historyDataView); tabRegion.Add(jobJournalView); tabRegion.Activate(historyJournalView);
Module 本质上来说,对于一个应用程序而言,特定功能的所有View、Logic.Service等都可以独立存在。那么意味着,每个独立的功能我们都可以称之为模块。而往往实际上,我们在一个项目当中,他的结构通常是如下所示:
当我们开始考虑划分模块之间的关系的时候,并且采用新的模块化解决方案,结构将变成如下所示:
创建模块 实现 IModule接口的类 可以将该包标识为模块
创建Module实际上是将模块独立与类库存在,主程序通过加载类库添加模块。以下步骤: 创建Module
首先, 我们创建一个基于WPF的应用程序, 暂且定义为ModuleA, 接下来为ModuleA定义一个类,并且实现IModule接口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public class ModuleAModule : IModule { public void OnInitialized (IContainerProvider containerProvider ) { } public void RegisterTypes (IContainerRegistry containerRegistry ) { var regionManager = containerProvider.Resolve<IRegionManager>(); regionManager.RegisterViewWithRegion("MyModuleView" , typeof (Views.ThisModuleView)); } }
加载模块 ModuleCatalog保存了应用程序可以使用的模块信息。该目录本质上是ModuleInfo类的集合。每个模块都在一个ModuleInfo类中描述,记录了模块的名称、类型和位置等属性。有几种典型的方法可以用来填充ModuleCatalog ,使其包含ModuleInfo 实例
主程序配置模块目录 配置方式如下:
(代码方式)Code
(配置文件)App.config
(磁盘目录)Disk/Directory
(XAML定义)XAML
(自定义)Custom Register Catalog with PrismApplication Register Modules with Catalog
您应该使用的注册和发现机制取决于您的应用程序需要什么。使用配置文件或XAML文件可以使您的应用程序不需要引用模块。使用目录可以使应用程序发现模块,而无需在文件中指定它们
代码方式 在启动项目当中,添加ModuleA的应用, 打开App.xaml.cs, 重写ConfigureModuleCatalog方法,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public partial class App { protected override Window CreateShell () { return Container.Resolve<MainWindow>(); } protected override void RegisterTypes (IContainerRegistry containerRegistry ) { } protected override void ConfigureModuleCatalog (IModuleCatalog moduleCatalog ) { moduleCatalog.AddModule<ModuleAModule>(); } }
Directory配置模块目录 通过读取根目录Modules文件夹查找模块
1 2 3 4 5 6 7 8 public partial class App { protected override IModuleCatalog CreateModuleCatalog () { return new DirectoryModuleCatalog() { ModulePath=@".\Modules" }; } }
需要将模块需要的三个文件放到指定的文件夹(上面是Modules文件夹中)中,如:Login.deps.json,Login.dll,Login.pdb
这样的方式可以配合类库上右键-属性0-生成-事件-生成后事件中添加如下内容:
1 2 xcopy "$(ProjectDir)\bin\Debug\net6.0-windows\$(ProjectName).dll" "$(SolutionDir)\IonImplantationSystem\bin\Debug\net6.0-windows\Modules\" /Y /S xcopy "$(ProjectDir)\bin\Release\net6.0-windows\$(ProjectName).dll" "$(SolutionDir)\IonImplantationSystem\bin\Release\net6.0-windows\Modules\" /Y /S
这样就成功设置可以很简单的导入模块了
App.Config配置模块目录 1 2 3 4 5 6 7 public partial class App { protected override IModuleCatalog CreateModuleCatalog () { return new ConfigurationModuleCatalog(); } }
然后,为应用程序添加配置文件app.config, 添加以下内容:
1 2 3 4 5 6 7 8 9 10 <configuration> <configSections> <section name="modules" type="Prism.Modularity.ModulesConfigurationSection, Prism.Wpf" /> </configSections> <startup> </startup> <modules> <module assemblyFile="ModuleA.dll" moduleType="ModuleA.ModuleAModule, ModuleA, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" moduleName="ModuleAModule" startupLoaded="True" /> </modules> </configuration>
XAML配置模块目录 修改CreateModuleCatalog方法, 从指定XAML文件读取模块配置
1 2 3 4 5 6 7 public partial class App { protected override IModuleCatalog CreateModuleCatalog () { return new XamlModuleCatalog(new Uri("/Modules;component/ModuleCatalog.xaml" , UriKind.Relative)); } }
创建模块名为ModuleCatalog.xaml文件, 添加模块信息
1 2 3 4 5 6 7 8 9 <m:ModuleCatalog xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:m="clr-namespace:Prism.Modularity;assembly=Prism.Wpf"> <m:ModuleInfo ModuleName="ModuleAModule" ModuleType="ModuleA.ModuleAModule, ModuleA, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" /> </m:ModuleCatalog>
Prism控制何时加载模块
应用程序可以尽快初始化模块,称为“when available”
在应用程序需要它们时初始化,称为“on-demand”
模块划分思想 视图注入 应用程序模块后,每个子模块中的视图可以独立的进行依赖注入 。再使用IRegionManager来实现页面导航。
步骤: 1.利用Region进行导航功能。 2.使用Module将应用程序模块化。 3.将独立模块的视图、服务使用注入到容器当中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class ModuleAModule : IModule { private readonly IRegionManager _regionManager; public ModuleAModule (IRegionManager regionManager ) { _regionManager = regionManager; } public void OnInitialized (IContainerProvider containerProvider ) { _regionManager.RegisterViewWithRegion("ContentRegion" , typeof (ViewA)); } public void RegisterTypes (IContainerRegistry containerRegistry ) { } }
依赖注入 盘点注册方法
Register:这是一种通用的注册方法,用于将具体的实现类与接口或抽象类进行关联。通过这种方式,容器可以在需要时提供相应的实例。
RegisterSingleton:用于将某个实现类注册为单例模式。这意味着在整个应用程序的生命周期中,只会创建一个该实现类的实例,并在需要时进行共享。
RegisterInstance:允许你直接提供一个特定的实例进行注册。这意味着容器将使用你提供的具体实例,而不是创建新的实例。
RegisterScoped:表示注册为作用域范围内的单例。在特定的作用域(例如请求范围)内,会有一个唯一的实例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 containerRegistry.Register<IService, MyServiceImpl>(); containerRegistry.RegisterSingleton<IService, MyServiceImpl>(); var instance = new MyServiceImpl();containerRegistry.RegisterInstance<IService>(instance); containerRegistry.RegisterScoped<IService, MyServiceImpl>(); containerRegistry.Register<MyServiceImpl>(); containerRegistry.RegisterSingleton<MyServiceImpl>(); var instance = new MyServiceImpl();containerRegistry.RegisterInstance(instance); containerRegistry.RegisterScoped<MyServiceImpl>();
以最常用的单例注册 为例:
1 2 containerRegistry.RegisterSingleton<IService, MyServiceImpl>();
如何使用该注册的单例
二者的主要区别:
手动Resolve注入方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class MyViewModel : BindableBase { private readonly IContainerProvider _containerProvider; public MyViewModel (IContainerProvider containerProvider ) { _containerProvider = containerProvider; } public void SomeMethod () { var myService = _containerProvider.Resolve<IMyService>(); myService.DoSomething(); } }
最常用的构造函数注入:
1 2 3 4 5 6 7 8 9 public class MyViewModel : BindableBase { private readonly IMyService _myService; public MyViewModel (IMyService myService ) { _myService = myService; } }
单例的好处包括
节省资源:只创建一个对象,避免了不必要的资源消耗。
一致性:确保在整个系统中使用的是同一个对象。
易于管理和维护:减少了对象的创建和销毁逻辑。
依赖注入的好处:
依赖注入:通过接口来获取实例,实现了依赖注入的原则,提高了代码的灵活性和可维护性。
解耦:使得代码不再直接依赖具体的实现类,而是依赖接口。
依赖注入中的循环引入 在依赖注入中,循环依赖通常发生在以下情况:
构造函数注入:A的构造函数需要B的实例,B的构造函数需要A的实例,导致无法构建任何一个对象。
属性注入或方法注入:虽然可能不会立即导致构造失败,但可能在运行时导致不可预知的行为。
如何避免循环依赖
重新设计代码结构 :检查是否真的需要循环依赖。通常,循环依赖意味着职责划分不清,可以考虑将相互依赖的部分提取到一个新的模块中,或者使用接口抽象来解耦。
使用接口(面向接口编程) :通过接口来解耦具体的实现,使得模块之间依赖于抽象而不是具体实现。这样,即使有循环依赖,也可以通过依赖注入容器来管理生命周期,但最好还是避免循环依赖。
使用Setter注入或方法注入 :如果循环依赖是不可避免的,可以考虑使用Setter注入或方法注入来打破构造函数的循环依赖。但是这种方法只是将问题从编译时转移到了运行时,需要谨慎使用。
最佳实践 :对于必需依赖,优先使用构造函数注入;Setter 注入仅适用于可选依赖或配置参数。
使用中介者模式或事件驱动 :通过引入一个中介者来协调模块之间的交互,或者使用事件驱动架构,使得模块之间不直接相互依赖。
使用依赖注入容器的高级功能 :一些DI容器支持循环依赖(例如,通过属性注入或延迟解析),但这不是推荐的做法,因为它会掩盖设计问题。
总结
循环依赖的根本解决方案是重新设计架构 即在代码中,应该尽量避免循环依赖,保持模块之间的单向依赖关系 ,而不是技术绕开。主要策略包括:
引入接口抽象 - 打破具体实现间的直接依赖
应用设计原则 - 单一职责、依赖倒置等
使用事件/消息机制 - 替代直接方法调用
重构大型类 - 拆分为更小、职责更单一的组件
依赖方向调整 - 确保依赖是单向的
ViewModelLocator 在WPF当中,需要为View与ViewModel建立连接 , 我们需要找到View的DataContext
wpf中主要是两种建立连接的方式
第三方的MVVM框架, 标准的ViewModelLocator可能如下所示
这些方式都可以建立View-ViewModel关系。 但是,这一切并不是Prism想表达的内容, 甚至不建议你按上面的方式去做, 因为这样几乎打破了开发的所有原则。 (我们把View与ViewModel的关系编码的方式通过命名的方式固定了下来 , 通过静态类去维护ViewModel的关系…)
在Prism当中, 你可以基于命名约定以及文件夹结构 , 便能够轻松的将View/ViewModel建议关联
假设你已经为项目添加Views/ViewModels 文件夹。此时, 你的页面为ViewA, 则对应的ViewModel名称为 ViewAViewModel
大致意思为View这个字符串会重叠收纳
注意不仅仅名字遵循约定,并且View要在Views文件夹下,ViewModel要在ViewModels文件夹下
当遵循了命名规范后, 此时还需要在View当中声明,允许当前View自动装配ViewModel
1 2 3 4 5 6 7 <UserControl x:Class="SettingBinding Context.Views.MainPage" 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" > </UserControl>
这种命名约定和文件夹结构也可以在代码中修改:此处略,参考这里
Prism中的MVVM 前面 介绍了Prism中ViewModel如何与View进行连接
Prism与常见的MVVM框架区别 如果你了解WPF当中的ICommand, INotifyPropertyChanged的作用, 就会发现 众多框架都是基于这些进行扩展, 实现其通知、绑定、命令等功能
对于不同的MVVM框架而言, 大体使用上会在声明方式上的差异, 以及特定功能上的差别,如下图
功能↓ / →框架名
Prism
Mvvmlight
Micorosoft.Toolkit.Mvvm
通知
BindableBase
ViewModelBase
ObservableObject
命令
DelegateCommand
RelayCommand
Async/RelayCommand
聚合器
IEventAggregator
IMessenger
IMessenger
模块化
√
×
×
容器
√
×
×
依赖注入
√
×
×
导航
√
×
×
对话
√
×
×
各个框架之间都有各自的通知、绑定、事件聚合器等基础的功能, 而Prsim自带的依赖注入、容器、以及导航会话 等功能, 可以为你提供更加强大的功能
如何在ViewModel实现基础绑定、Command、事件聚合器等操作 实现基础绑定 下面例子既有命令绑定,也有属性绑定
BindableBase 在Prism当中,需要用到绑定的控件的ViewModel需要继承ViewModel,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public class TestViewModel : BindableBase { public TestViewModel () { Message = "hello" ; SendCommand = new DelegateCommand(() => { Message = "hello world!" ; }); } private string _message; public string Message { get { return _message; } set { _message = value ; RaisePropertyChanged(); } } public DelegateCommand SendCommand { get ; private set ; } }
绑定端代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 <!--截取的内容--> xmlns:prism ="http://prismlibrary.com/" mc:Ignorable="d" prism:ViewModelLocator.AutoWireViewModel="True" Title="ViewA" Height="450" Width="800"> <Grid> <StackPanel> <Button Width="100" Height="30" Content="更新TextBlock" Command="{Binding SendCommand}"/> <TextBlock Text="{Binding Message}" FontSize="38" HorizontalAlignment="Center"/> </StackPanel> </Grid>
绑定多命令 复合命令CompositeCommand
对于单个Command而言, 只是触发单个对应的功能, 而复合命令是Prism当中非常强大的功能, CompositeCommand简单来说是一个父命令, 它可以注册N个子命令
相关语法
1 2 3 4 5 public CompositeCommand 复合命令名 { get ; private set ; }复合命令名 = new CompositeCommand(); 复合命令名.RegisterCommand(命令名); 复合命令名.RegisterCommand(命令名2 );
当复合命令被激活,它将触发对所有的子命令, 如果任意一个命令CanExecute=false,它将无法被激活,如下所示:
此图中按下Save All按钮后什么也不执行,因为复合命令被禁用了,假设复合命令被启用,那么将会执行的是Save A和Save C
实例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 public class ViewAViewModel : BindableBase { private string message; public ViewAViewModel () { Message = "hello" ; SendCommand = new DelegateCommand(() => { Message = "hello world!" ; }); SendCommand2=new DelegateCommand(() => { Message += "zeroko14" ; }); SendAll = new CompositeCommand(); SendAll.RegisterCommand(SendCommand); SendAll.RegisterCommand(SendCommand2); } public string Message { get { return message; }set { message = value ; RaisePropertyChanged(); } } public DelegateCommand SendCommand { get ; private set ; } public DelegateCommand SendCommand2 { get ; private set ; } public CompositeCommand SendAll { get ; private set ; } }
最终标题会被设置为hello world!zeroko14 ,Command={Binding SendAll}会同时调用SendCommand和SendCommand2
事件聚合器 事件聚合器IEventAggregator
事件聚合器负责接收订阅以及发布消息。订阅者可以接收到发布者发送的内容
功能盘点:
松耦合基于事件通讯
多个发布者和订阅者
微弱的事件
过滤事件
传递参数
取消订阅
AViewModel订阅了一个消息接收的事件, 然后BViewModel当中给指定该事件推送消息,此时AViewModel接收BViewModel推送的内容
1 2 3 4 5 6 7 8 9 10 11 public class SavedEvent : PubSubEvent <string > { }IEventAggregator.GetEvent<SavedEvent>().Publish("some value" ); IEventAggregator.GetEvent<SavedEvent>().Subscribe(message=> { });
事件过滤 在实际的开发过程当中,我们往往会在多个位置订阅一个事件, 但是对于订阅者而言, 他并不需要接收任何消息,事件过滤Filtering Events应运而生
在Prism当中, 我们可以指定为事件指定过滤条件
1 2 3 4 5 6 7 8 eventAggregator.GetEvent<MessageSentEvent>().Subscribe( arg =>{ }, ThreadOption.PublisherThread, false , message => message.token.Equals(nameof (MessageListViewModel)));
关于Subscribe当中的4个参数, 详解:
action: 发布事件时执行的回调委托。
ThreadOption枚举: 指定在哪个线程上接收委托回调。
PublisherThread: 在发布事件的线程上调用订阅者的委托回调。
BackgroundThread: 在后台线程上调用订阅者的委托回调。
UIThread: 在UI线程上调用订阅者的委托回调(通常用于更新UI)
keepSubscriberReferenceAlive: 如果为true,则Prism.Events.PubSubEvent保留对订阅者的引用因此它不会回收垃圾。
长期订阅,需要保持状态的情况下设置为true.(基本上可以这么认为,只有贯穿程序一生的订阅才需要设置为true)
临时订阅,不再需要时要被释放资源的情况下设置为false
filter: 可选参数,用于筛选事件通知,订阅者可根据筛选条件来决定是否接收特定的事件通知。返回true表示接受通知,返回false表示不接受
实例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 namespace prismTestNew.ViewModels { public class ViewAViewModel : BindableBase { private readonly IEventAggregator eventAggregator; private string message; public ViewAViewModel (IEventAggregator eventAggregator ) { Message = "hello" ; SendCommand = new DelegateCommand(() => { eventAggregator.GetEvent<MessageEvent>().Subscribe(OnMessageReceived, ThreadOption.PublisherThread, false , (x) => { return x == "hello" ; }); }); TriggerCommand = new DelegateCommand(() => { eventAggregator.GetEvent<MessageEvent>().Publish("hello" ); }); this .eventAggregator = eventAggregator; } public void OnMessageReceived (string messageFromMessageEvent ) { Message += messageFromMessageEvent + "\r\n" ; } public string Message { get { return message; } set { message = value ; RaisePropertyChanged(); } } public DelegateCommand SendCommand { get ; private set ; } public DelegateCommand TriggerCommand { get ; private set ; } } public class MessageEvent : PubSubEvent <String >{ } }
视图如下:
1 2 <Button Width="100" Height="30" Content="订阅" Command="{Binding SendCommand}" Margin="10"/> <Button Width="100" Height="30" Content="发布" Command="{Binding TriggerCommand}" Margin="10"/>
点击订阅之后,点击发布,就会执行到自定义的OnMessageReceived函数
取消订阅 为注册的消息取消订阅, Prism提供二种方式取消订阅,如下:
通过委托的方式取消订阅
1 2 var event = IEventAggregator.GetEvent<MessageSentEvent>();event .Unsubscribe(OnMessageReceived);
通过获取订阅者token取消订阅
1 2 3 var event = eventAggregator.GetEvent<MessageSentEvent>();var token = _event.Subscribe(OnMessageReceived);event .Unsubscribe(token);
区域导航 在普遍的业务场景当中, 必不可少的是页面切换, 而Prism就可以使用Navigation功能来进行页面导航
prism实现区域导航,需要满足两点基本条件
注册显示区域 注册视图类型或添加别名, 如果未指定别名,名称默认为当中类型的名称
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class ModuleAModule : IModule { public void OnInitialized (IContainerProvider containerProvider ) { } public void RegisterTypes (IContainerRegistry containerRegistry ) { containerRegistry.RegisterForNavigation<ViewA>("CustomName" ); containerRegistry.RegisterForNavigation<ViewB>(); } }
注册时指定ViewModel或添加别名
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class ModuleAModule : IModule { public void OnInitialized (IContainerProvider containerProvider ) { } public void RegisterTypes (IContainerRegistry containerRegistry ) { containerRegistry.RegisterForNavigation<ViewA, ViewAViewModel>(); containerRegistry.RegisterForNavigation<ViewB, ViewBViewModel>("CustomName" ); } }
使用导航 在某窗口对应的viewModel文件中需要使用导航,要在构造函数参数中接受IRegionManager regionManager,并将该值存入成员属性中,就可以使用regionManager来进行下述操作了
Region的注册以及管理、导航等, 我们可以使用IRegionManager接口,所以,我们现在便可以使用该接口实现导航功能
1 2 IRegionManager regionManager = …; regionManager.RequestNavigate("RegionName" , "ViewName" );
调用了IRegionManager接口的RequestNavigate方法, 并且传递了两个参数:
RegionName: 该参数为注册的区域名称
ViewName: 该参数实际为我们上面注册过的导航页, 字符串类型, 对应的是我们注册页面的nameof
带参数导航 导航页前传递一些参数, 则可以使用NavigationParameters
1 2 3 4 5 6 7 8 9 var param = new NavigationParameters();param.Add("Parameter" , param); _regionManger.RequestNavigate("RegionName" , "ViewName" , param); _regionManger.RequestNavigate("RegionName" , "ViewName?id=1&Name=xiaoming" );
控制导航过程 INavigationAware 该接口包含3个方法, 每个方法中都包含当前导航的上下文, 如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public void OnNavigatedTo (NavigationContext navigationContext ) { } public bool IsNavigationTarget (NavigationContext navigationContext ){ return true ; } public void OnNavigatedFrom (NavigationContext navigationContext ){ }
OnNavigatedTo: 导航完成前, 此处可以传递过来的参数以及是否允许导航等动作的控制。
IsNavigationTarget: 调用以确定此实例是否可以处理导航请求。否则新建实例
OnNavigatedFrom: 当导航离开当前页时, 类似打开A, 再打开B时, A中的该方法被触发。
执行流程:
获取导航请求参数 导航中允许我们传递参数, 用于在我们完成导航之前, 进行做对应的逻辑业务处理。这时候, 我们便可以在OnNavigatedTo方法中通过导航上下文中获取到传递的所有参数
1 2 3 4 5 public void OnNavigatedTo (NavigationContext navigationContext ){ var id = navigationContext.Parameters.GetValue<int >("id" ); var name = navigationContext.Parameters["Name" ].ToString(); }
是否拦截导航请求 IConfirmNavigationRequest接口 :该接口继承于INavigationAware, 所以, 它多了一个功能: 允许用户针对导航请求判断是否进行拦截
1 2 3 4 5 6 7 8 9 10 11 public void ConfirmNavigationRequest (NavigationContext navigationContext, Action<bool > continuationCallback ){ bool result = true ; if (MessageBox.Show("确认导航?" , "温馨提示" , MessageBoxButton.YesNo) == MessageBoxResult.No) result = false ; continuationCallback(result); }
导航日志 Navigation Journal导航日志 , 其实就是对导航系统的一个管理功能, 理论上来说, 我们应该知道我们上一步导航的位置、以及下一步导航的位置, 包括我们导航的历史记录。以便于我们使用导航对应用程序可以灵活的控制。
IRegionNavigationJournal 该接口包含以下功能:
GoBack() : 返回上一页
CanGoBack : 是否可以返回上一页
GoForward(): 返回后一页
CanGoForward : 是否可以返回后一页
完整案例 github上完整代码参考
1 2 3 4 5 6 7 8 9 10 11 12 13 │ ├── App.xaml ├── App.xaml.cs │ ├── Views\ │ ├── MainWindow.xaml │ ├── ViewA.xaml │ └── ViewB.xaml │ └── ViewModels\ ├── MainWindowViewModel.cs ├── ViewAViewModel.cs └── ViewBViewModel.cs
新建两个视图ViewA.xaml和ViewB.xaml,并在App.xaml.cs中注册两个界面
App.xaml.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 using Prism.Ioc;using System.Windows;using testNavigation.Views;namespace testNavigation { public partial class App { protected override Window CreateShell () { return Container.Resolve<MainWindow>(); } protected override void RegisterTypes (IContainerRegistry containerRegistry ) { containerRegistry.RegisterForNavigation<ViewA>("PageA" ); containerRegistry.RegisterForNavigation<ViewB>(); } } }
MainWindowViewModel.cs以及MainWindow.xaml中对视图进行完善并绑定相关command
MainWindowViewModel.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 using Prism.Commands;using Prism.Mvvm;using Prism.Regions;using System.Runtime.CompilerServices;namespace testNavigation.ViewModels { public class MainWindowViewModel : BindableBase { private string _title = "Prism Application" ; public string Title { get { return _title; } set { SetProperty(ref _title, value ); } } private readonly IRegionManager regionManager; IRegionNavigationJournal journal; public DelegateCommand OpenACommand { get ; set ; } public DelegateCommand OpenBCommand { get ; set ; } public DelegateCommand GoBackCommand { get ; set ; } public DelegateCommand GoForwordCommand { get ; set ; } public MainWindowViewModel (IRegionManager regionManager ) { OpenACommand = new DelegateCommand(OpenA); OpenBCommand = new DelegateCommand(OpenB); GoBackCommand = new DelegateCommand(GoBack); GoForwordCommand = new DelegateCommand(GoForword); this .regionManager = regionManager; } private void OpenA () { NavigationParameters param = new NavigationParameters(); param.Add("Value" , "Hello" ); regionManager.RequestNavigate("ContentRegion" , $"PageA?Value=Hello1" , arg => { journal = arg.Context.NavigationService.Journal; }); } private void OpenB () { regionManager.RequestNavigate("ContentRegion" , nameof (Views.ViewB), arg => { journal = arg.Context.NavigationService.Journal; }); } private void GoBack () { journal.GoBack(); } private void GoForword () { journal.GoForward(); } } }
MainWindow.xaml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <Window x:Class="testNavigation.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="350" Width="525" > <Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="*" /> </Grid.RowDefinitions> <StackPanel Grid.Row="0" Orientation="Horizontal"> <Button Content="上一步" Width="auto" Height="auto" Margin="3" Command="{Binding GoBackCommand}"/> <Button Content="下一步" Width="auto" Height="auto" Margin="3" Command="{Binding GoForwordCommand}"/> <Button Content="打开A" Width="auto" Height="auto" Margin="3" Command="{Binding OpenACommand}"/> <Button Content="打开B" Width="auto" Height="auto" Margin="3" Command="{Binding OpenBCommand}"/> </StackPanel> <ContentControl Grid.Row="1" prism:RegionManager.RegionName="ContentRegion" /> </Grid> </Window>
这行代码在 XAML 中定义了一个 TabControl 控件,并使用 Prism 框架的 RegionManager 来将该控件与名为 ContentRegion 的区域关联起来。这种关联允许在该区域内动态加载和管理与 TabControl 相关的视图
ViewAViewModel.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 using Prism.Mvvm;using Prism.Regions;using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Threading.Tasks;using System.Windows;namespace testNavigation.ViewModels { public class ViewAViewModel : BindableBase ,IConfirmNavigationRequest { private string title; public string Title { get { return title; } set { title = value ; } } public void ConfirmNavigationRequest (NavigationContext navigationContext, Action<bool > continuationCallback ) { bool result = true ; if (MessageBox.Show("确认导航" ,"温馨提示" ,MessageBoxButton.YesNo)==MessageBoxResult.No) result = false ; continuationCallback(result); } public bool IsNavigationTarget (NavigationContext navigationContext ) { return true ; } public void OnNavigatedFrom (NavigationContext navigationContext ) { } public void OnNavigatedTo (NavigationContext navigationContext ) { title = navigationContext.Parameters.GetValue<string >("Value" ); } } }
ViewA.xaml 1 2 3 4 5 6 7 8 9 10 11 12 <UserControl x:Class="testNavigation.Views.ViewA" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="clr-namespace:testNavigation.Views" mc:Ignorable="d" d:DesignHeight="450" d:DesignWidth="800"> <Grid Background="Yellow"> <TextBlock Text="{Binding Title}"/> </Grid> </UserControl>
对话服务 Prism提供了一组对话服务, 封装了常用的对话框组件的功能
RegisterDialog/IDialogService (注册对话及使用对话)
打开对话框传递参数/关闭对话框返回参数
回调通知对话结果
实现对话框
创建对话框,(注意)通常是一组用户控件 ,并且实现 IDialogAware(继承他并实现他的必要接口函数,这样才可以使用_dialogService.ShowDialog(xxxxview);找到对应视图文件)
1 2 3 4 5 6 7 8 public interface IDialogAware { string Title { get ; } event Action<IDialogResult> RequestClose; bool CanCloseDialog () ; void OnDialogClosed () ; void OnDialogOpened (IDialogParameters parameters ) ; }
例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 using Prism.Commands;using Prism.Mvvm;using Prism.Services.Dialogs;using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Threading.Tasks;namespace testDialog.ViewModels { public class MsgViewModel : BindableBase ,IDialogAware { public string Title{ set ; get ; } public event Action<IDialogResult> RequestClose; public DelegateCommand OkCommand { get ; set ; } public DelegateCommand CancleCommand { get ; set ; } public MsgViewModel () { OkCommand = new DelegateCommand(() => { DialogParameters param = new DialogParameters(); param.Add("Value" , Title); RequestClose?.Invoke(new DialogResult(ButtonResult.OK, param)); }); CancleCommand = new DelegateCommand(() => { RequestClose?.Invoke(new DialogResult(ButtonResult.No)); }); } public bool CanCloseDialog () { return true ; } public void OnDialogClosed () { } public void OnDialogOpened (IDialogParameters parameters ) { Title = parameters.GetValue<string >("Value" ); } } }
注册对话框 注册对话框 RegisterDialog
1 2 3 4 5 6 7 8 9 10 protected override void RegisterTypes (IContainerRegistry containerRegistry ){ containerRegistry.RegisterDialog<MessageDialog>(); containerRegistry.RegisterDialog<MessageDialog, MessageDialogViewModel>(); containerRegistry.RegisterDialog<MessageDialog>("DialogName" ); }
使用Dialog 使用IDialogService接口 Show/ShowDialog 方法调用对话框
1 2 3 4 5 6 7 8 9 10 11 12 private readonly IDialogService dialogService;private void ShowDialog (){ DialogParameters keys = new DialogParameters(); keys.Add("message" , "Hello,Prism!" ); dialogService.ShowDialog("MessageDialog" , keys, arg => { }); }
调用Show/ShowDialog,我们通过注册时候的名称进行打开, 并且可以传递参数, 以及回调方法(主要用于返回对话框的返回结果)
使用对话框实例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 using Prism.Commands;using Prism.Mvvm;using Prism.Services.Dialogs;using System.Windows;using testDialog.Views;namespace testDialog.ViewModels { public class MainWindowViewModel : BindableBase { public DelegateCommand ShowDialogCommand { get ; private set ; } private readonly IDialogService dialog; private string _title = "Prism Application" ; public string Title { get { return _title; } set { SetProperty(ref _title, value ); } } public MainWindowViewModel (IDialogService dialog ) { ShowDialogCommand = new DelegateCommand(ShowDialog); this .dialog = dialog; } private void ShowDialog () { DialogParameters param = new DialogParameters(); param.Add("Value" , "Hello" ); dialog.ShowDialog("dialogView" ,param, arg => { if (arg.Result == ButtonResult.OK) { MessageBox.Show(arg.Parameters.GetValue<string >("Value" )); } }); } } }
封装Dialog API 对于常用的公共对话框, 我们可以封装成扩展方法, 以便于我们在应用程序的任何位置可以使用到它
1 2 3 4 5 6 7 public static void ShowNotification (this IDialogService dialogService,string message, Action<IDialogResult> callback ){ var p = new DialogParameters();p.Add("message" , message); dialogService.ShowDialog(“NotificationDialog", p, callback); }
实例参考 测试实例参考
弹窗数据传递的核心机制 传入数据给对话框 => [[ 对话框实现 ]](#弹窗 ViewModel 实现 IDialogAware) => 传出数据给父级窗口处理程序
Prism的对话框服务遵循以下流程:
参数传递: 调用弹窗时通过 DialogParameters 向弹窗传递初始参数
结果回调: 弹窗关闭时通过 IDialogResult 返回用户输入的数据
接口实现: 弹窗的 ViewModel 需实现 IDialogAware 接口,处理打开、关闭和数据回传逻辑
传入及传出参数 在父窗口的 ViewModel 中,使用 IDialogService.ShowDialog 方法打开弹窗,并通过 DialogParameters 传递参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 private void OpenDialog (){ DialogParameters parameters = new DialogParameters(); parameters.Add("initialValue" , "默认文本" ); dialogService.ShowDialog("UserInputDialog" , parameters, result => { if (result.Result == ButtonResult.OK) { string userInput = result.Parameters.GetValue<string >("userInput" ); } }); } dialogService.ShowDialog("SelectLinkPointDialogView" , new DialogParameters { { "input" , "测试输入内容111" }, }, r => { if (r.Result == ButtonResult.OK) { var values = r.Parameters.GetValue<string >("output" ); } });
UserInputDialog:弹窗的注册名称。
result:回调参数,包含用户操作结果(确定/取消)和返回的数据
弹窗 ViewModel 实现 IDialogAware 弹窗的 ViewModel 需实现 IDialogAware 接口,处理数据接收与返回
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 public class UserInputDialogViewModel : BindableBase ,IDialogAware { public string Title => "对话框标题" ; public bool CanCloseDialog () { return true ; } public void OnDialogClosed () { } public void OnDialogOpened (IDialogParameters parameters ) { if (parameters.ContainsKey("Value" )) Title = parameters.GetValue<string >("Value" ); } public event Action<IDialogResult> RequestClose; private DelegateCommand<string > _closeDialogCommand; public DelegateCommand<string > CloseDialogCommand => _closeDialogCommand ?? (_closeDialogCommand = new DelegateCommand<string >(ExecuteCloseDialogCommand)); void ExecuteCloseDialogCommand (string parameter ) { var dialogResult = new DialogResult(ButtonResult.OK); dialogResult.Parameters.Add("values" , new Dictionary<string , object >(_inputValues)); RequestClose?.Invoke(dialogResult); } }
实现的对话框需要注册才能使用 在 Prism 的模块或容器中注册弹窗:
1 2 3 4 protected override void RegisterTypes (IContainerRegistry containerRegistry ){ containerRegistry.RegisterDialog<UserInputDialog, UserInputDialogViewModel>("UserInputDialog" ); }
注册时需要指定弹窗的 View 和 ViewModel,并为弹窗命名
参数与返回值的封装 为提高代码可维护性,可封装自定义参数类:
1 2 3 4 5 6 7 8 public class UserInputResult { public string InputText { get ; set ; } public bool IsConfirmed { get ; set ; } } var result = result.Parameters.GetValue<UserInputResult>("result" );
通过强类型对象代替松散键值对,减少硬编码风险
对话框窗口外观设置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 <prism:Dialog.WindowStyle> <Style TargetType="Window"> <!--定义了对话框的启动位置。`CenterScreen` 表示窗口将在屏幕中央启动--> <Setter Property="prism:Dialog.WindowStartupLocation" Value="CenterScreen" /> <!--决定窗口是否在任务栏中显示。`True` 表示该窗口会在任务栏中显示--> <Setter Property="ShowInTaskbar" Value="True" /> <!--定义窗口的大小行为。`WidthAndHeight` 表示窗口的大小将根据其内容自动调整,以适应窗口中显示的内容的宽度和高度--> <Setter Property="SizeToContent" Value="WidthAndHeight" /> <!--指定了窗口的样式。`ToolWindow` 通常用于工具窗口,具有较小的标题栏和不同的外观,适合用于辅助工具或面板--> <Setter Property="WindowStyle" Value="ToolWindow" /> <!--使窗口不可被手动改变大小--> <Setter Property="ResizeMode" Value="NoResize" /> </Style> </prism:Dialog.WindowStyle>
统一防对话框重入处理方式
Prism 的 IDialogService.ShowDialog 并不会为你做“同名对话框单例/去重/节流”。它每次调用都会创建一个新的 DialogWindow 并 ShowDialog()。所谓“重入保护”更多体现在“同一调用栈内已是模态对话框时不会再进同一个对话过程”,但并不阻止你在短时间内从不同代码路径、事件回调或队列中连续多次调用,从而产生大量新窗口。
可以通过以下方式解决对话框的写法问题导致句柄被耗尽的问题:
同名去重+可配置节流 : 创建一个可配置节流的DedupDialogService(支持全局默认、按对话框名设置、以及通过对话框参数覆盖),并在 App.xaml.cs 中注册为全局替换,同时暴露一个配置接口以便在代码任意位置动态调整节流策略
为同名对话框聚合回调,若已在显示则等待该对话框关闭时一次性触发所有回调;若因节流阻止显示,即命中节流时,若存在最近一次结果,立即以该结果回调并清空挂起回调,不再弹窗;若没有最近结果,则保持延时调度,到期只弹一次并统一回调
上面的改动可以做到“一处替换,全局生效”,现有所有 _dialogService.ShowDialog(…) 自动具备去重/节流能力
核心代码 DedupDialogService.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 using Prism.Services.Dialogs;using System;using System.Collections.Concurrent;using System.Windows;using System.Windows.Threading;using UI.Application.Share.Interfaces;namespace UI.Application.Share.Helper { public class DedupDialogService : IDialogService { private readonly IDialogService inner; private readonly ConcurrentDictionary<string , byte > openFlags = new (); private readonly ConcurrentDictionary<string , DateTime> lastShownUtc = new (); private readonly ConcurrentDictionary<string , ConcurrentQueue<Action<IDialogResult>>> callbackQueues = new (); private readonly ConcurrentDictionary<string , DispatcherTimer> delayTimers = new (); private readonly ConcurrentDictionary<string , IDialogResult> lastResults = new (); public static TimeSpan ThrottleInterval = TimeSpan.FromSeconds(1 ); public DedupDialogService (IDialogService inner ) { this .inner = inner; } public void Show (string name, IDialogParameters parameters, Action<IDialogResult> callback ) { inner.Show(name, parameters, callback); } public void Show (string name, IDialogParameters parameters, Action<IDialogResult> callback, string windowName ) { inner.Show(name, parameters, callback, windowName); } public void ShowDialog (string name, IDialogParameters parameters, Action<IDialogResult> callback ) { EnqueueCallback(name, callback); TryShowOrSchedule(name, parameters, windowName: null ); } public void ShowDialog (string name, IDialogParameters parameters, Action<IDialogResult> callback, string windowName ) { string key = string .IsNullOrEmpty(windowName) ? name : ($"{name} @{windowName} " ); EnqueueCallback(key, callback); TryShowOrSchedule(name, parameters, windowName); } private void EnqueueCallback (string key, Action<IDialogResult> callback ) { var queue = callbackQueues.GetOrAdd(key, _ => new ConcurrentQueue<Action<IDialogResult>>()); if (callback != null ) queue.Enqueue(callback); } private void TryShowOrSchedule (string name, IDialogParameters parameters, string windowName ) { string key = string .IsNullOrEmpty(windowName) ? name : ($"{name} @{windowName} " ); if (openFlags.ContainsKey(key)) return ; var now = DateTime.UtcNow; if (ThrottleInterval > TimeSpan.Zero && lastShownUtc.TryGetValue(key, out var last)) { var elapsed = now - last; if (elapsed < ThrottleInterval) { if (lastResults.TryGetValue(key, out var recent)) { DrainCallbacks(key, recent); return ; } var remaining = ThrottleInterval - elapsed; if (!delayTimers.ContainsKey(key)) { var dispatcher = System.Windows.Application.Current?.Dispatcher ?? Dispatcher.CurrentDispatcher; var timer = new DispatcherTimer(DispatcherPriority.Normal, dispatcher) { Interval = remaining }; timer.Tick += (s, e) => { try { var t = (DispatcherTimer)s; t.Stop(); delayTimers.TryRemove(key, out _); ShowNow(name, parameters, windowName); } catch { } }; if (delayTimers.TryAdd(key, timer)) timer.Start(); } return ; } } ShowNow(name, parameters, windowName); } private void ShowNow (string name, IDialogParameters parameters, string windowName ) { string key = string .IsNullOrEmpty(windowName) ? name : ($"{name} @{windowName} " ); if (!openFlags.TryAdd(key, 1 )) return ; lastShownUtc[key] = DateTime.UtcNow; var dispatcher = System.Windows.Application.Current?.Dispatcher ?? Dispatcher.CurrentDispatcher; dispatcher.BeginInvoke(new Action(() => { try { if (windowName == null ) { inner.ShowDialog(name, parameters, r => OnDialogClosed(key, r)); } else { inner.ShowDialog(name, parameters, r => OnDialogClosed(key, r), windowName); } } catch { openFlags.TryRemove(key, out _); throw ; } })); } private void OnDialogClosed (string key, IDialogResult result ) { try { if (result != null ) lastResults[key] = result; if (callbackQueues.TryGetValue(key, out var queue)) { while (queue.TryDequeue(out var cb)) { try { cb?.Invoke(result); } catch { } } } } finally { openFlags.TryRemove(key, out _); } } private void DrainCallbacks (string key, IDialogResult result ) { if (callbackQueues.TryGetValue(key, out var queue)) { while (queue.TryDequeue(out var cb)) { try { cb?.Invoke(result); } catch { } } } } } }
全局替换IDialogService,加上可配置节流/去重的装饰器替换原有服务
App.xaml.cs中RegisterTypes函数内:
1 2 3 4 5 6 7 8 9 10 11 12 try { var container = Prism.Ioc.ContainerLocator.Container; if (container != null ) { var originalDialogService = container.Resolve<IDialogService>(); containerRegistry.RegisterInstance<IDialogService>( new UI.Application.Share.Helper.DedupDialogService(originalDialogService)); } } catch { }