Prism

此处记录prism框架的方方面面

MIT开源地址

Prism 是一个用于创建模块化和易于维护的应用程序的框架,它主要起到以下作用:

  • 支持 MVVM 模式:提供了一些工具和类来帮助开发人员实现 MVVM 模式,使视图和模型之间的耦合度降低,提高了代码的可维护性和可测试性。
  • 提供依赖注入容器:可以方便地管理应用程序中的依赖关系,使代码更加简洁和易于理解。
  • 支持模块化开发:可以将应用程序拆分成多个模块,每个模块可以独立开发、测试和部署,提高了开发效率和代码的复用性。
  • 提供导航和视图切换功能:可以方便地实现应用程序中的导航和视图切换,使应用程序的用户体验更加流畅。
  • 支持多种平台:除了 WPF 平台,Prism 还支持其他平台,如 UWP、Xamarin.Forms 等,使开发人员可以在不同的平台上使用相同的框架和技术。

Prism在vs2019已经下架了,在vs2022上架

Getting Started | Prism (prismlibrary.com)

教程|720x360

包含如下内容

  • Region(区域管理)
  • Module(模块)
  • View Injection(视图注入)
  • ViewModelLocationProvider(视图模型定位)
  • Command(绑定相关)
  • Event Aggregator (事件聚合器)
  • Navigation(导航)
  • Dialog(对话框)

使用prism只需要在nuget包中引入Prism.WPF和Prism.Dryloc

  • Unity
    Unity 是由 Microsoft 开发的一个轻量级的依赖注入容器,适用于 .NET 应用程序。它支持多种注入方式,包括构造函数注入、属性注入和方法注入。

  • DryIoc

    DryIoc 是一个高性能的依赖注入容器,支持多种生命周期管理和高级功能。它的设计目标是提供更好的性能和更少的内存占用。

Prism的初始化过程

img

image-20240618103848364

PrismApplicationBase其实也是继承Application,App到Application的继承链上隔了一层PrismApplication

vs插件支持

Prism Template Pack

提供了:

  • Blank Project (空白示例项目)

  • Module Project (模块示例项目)

  • 代码片段(用户快速创建属性,命令)

    propp – property (depends on BindableBase)
    cmd - DelegateCommand
    cmdg – DelegateCommand

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>();
//这行代码的作用是确保新创建的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();
//这种方式不是prism支持的通过依赖注入来创建窗口的方式,视图模型的构造函数,prism的导航功能都可能无法正常工作

Region

参考链接

Region作为Prism当中模块化的核心功能,其主要目的是弱化了模块与模块之间的耦合关系。
在普遍的应用程序开发中,界面上的元素及内容往往被固定。

image-20240618104745163

基本使用方法

定义Region的方式有两种,一个是在XAML界面指定,另一种这是代码当中指定。

XAML中指定:

1
<ContentControl prism:RegionManager.RegionName="自定义区域名"/>

代码创建:(这种是初始化的时候没有绑定视图,只是注册视图容器)

1
RegionManager.SetRegionName(指定ContentControl的name,自定义区域名);

代码中给区域注册用户控件 (这种是初始化就绑定了视图)

1
2
3
4
5
6
 public MainWindowViewModel(IRegionManager regionManager)
{
//在ContentRegion区域中显示Views目录下的ContentView内容
regionManager.RegisterViewWithRegion("ContentRegion", typeof(Views.view1));
}
//注意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
//通过下面代码来切换RegionName对应的视图容器中的内容:navigatePath
_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就没有区域适配器

img

Prism提供了许多内置得RegionAdapter

  • ContentControlRegionAdapter
  • ItemsControlRegionAdapter
  • SelectorRegionAdapter
    - ComboBox
    - ListBox
    - Ribbon
    - TabControl

自定义区域适配器

自定义区域适配器是用于定义如何将视图(View)添加到区域(Region)中的机制。区域适配器负责将特定类型的 UI 元素(如 UserControlWindowContentControl 等)与区域关联起来,以便在运行时可以将视图加载到这些区域中

以添加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

除了定义区域,还有以下功能:

  • 维护区域集合

  • 提供对区域的访问

    获得模块区域: var region = regionManager.Regions("区域名")

    这样就可以拿到导航容器

  • 合成视图

  • 区域导航

  • 定义区域

下面贴个例子:

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等都可以独立存在。那么意味着,每个独立的功能我们都可以称之为模块。而往往实际上,我们在一个项目当中,他的结构通常是如下所示:

img

当我们开始考虑划分模块之间的关系的时候,并且采用新的模块化解决方案,结构将变成如下所示:

img

创建模块

实现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
{
/// <summary>
/// 通知模块已被初始化。
/// 执行诸如视图注册或任何其他模块初始化代码之类的操作
/// </summary>
/// <param name="containerProvider"></param>
public void OnInitialized(IContainerProvider containerProvider)
{
//在这里可以进行区域和模块的连接(若有需要的话)
}

/// <summary>
/// 当一个模块被加载到应用程序中时,首先调用RegisterTypes
/// 应用于注册模块实现的任何服务或功能
/// </summary>
/// <param name="containerRegistry"></param>
public void RegisterTypes(IContainerRegistry containerRegistry)
{
//注册视图通过如下方式:
//使用containerProvider找回Prism RegionManage的实例
var regionManager = containerProvider.Resolve<IRegionManager>();
//通过RegionManage注册视图
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)
{
//添加模块A
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文件夹下,当前目录是可执行文件的位置
}
}

需要将模块需要的三个文件放到指定的文件夹(上面是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>();

//不带接口版本
// 通用注册(Transient)
containerRegistry.Register<MyServiceImpl>(); // 每次解析都新建实例
// 单例注册
containerRegistry.RegisterSingleton<MyServiceImpl>(); // 全局单例
// 实例注册
var instance = new MyServiceImpl();
containerRegistry.RegisterInstance(instance); // 注册已有实例
// 作用域注册
containerRegistry.RegisterScoped<MyServiceImpl>(); // 每个作用域内单例
  • 通用注册:每次请求都创建一个新的实例。

  • 单例注册:整个容器中只有一个实例。

  • 实例注册:使用已经创建好的实例。

  • 作用域注册:在每个作用域内是单例,但不同作用域不同实例。

    这种设计模式在Web开发中非常有用,如在一个HTTP请求内,我们希望某些服务(如数据库上下文)在整个请求处理过程中保持同一个实例,以确保事务一致性和性能。同时,不同请求之间又相互隔离,避免数据混淆

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 处理一批数据
    foreach (var batch in dataBatches)
    {
    using (var scope = serviceProvider.CreateScope())
    {
    var processor = scope.ServiceProvider.GetService<IBatchProcessor>();
    processor.Process(batch);
    // 每个批次使用独立的作用域,处理器及其依赖在每个批次内是单例
    // 批次之间完全隔离
    }
    }

以最常用的单例注册为例:

1
2
containerRegistry.RegisterSingleton<IService, MyServiceImpl>();
//这个示例中,通过 containerRegistry 注册了单例实例。其他代码可以通过 IService 接口来使用该单例服务

如何使用该注册的单例

  • 手动Resolve注入
  • 构造函数注入

二者的主要区别:

  • 构造函数注入是编译时声明依赖

    没有会报错

  • Resolve 是运行时按需获取

    没有不会报错,有利于编写模块划分的代码,参考模块划分思想

手动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;
}
}

单例的好处包括

  • 节省资源:只创建一个对象,避免了不必要的资源消耗。
  • 一致性:确保在整个系统中使用的是同一个对象。
  • 易于管理和维护:减少了对象的创建和销毁逻辑。

依赖注入的好处:

  1. 依赖注入:通过接口来获取实例,实现了依赖注入的原则,提高了代码的灵活性和可维护性。
  2. 解耦:使得代码不再直接依赖具体的实现类,而是依赖接口。

依赖注入中的循环引入

在依赖注入中,循环依赖通常发生在以下情况:

  • 构造函数注入:A的构造函数需要B的实例,B的构造函数需要A的实例,导致无法构建任何一个对象。
  • 属性注入或方法注入:虽然可能不会立即导致构造失败,但可能在运行时导致不可预知的行为。

如何避免循环依赖

  1. 重新设计代码结构:检查是否真的需要循环依赖。通常,循环依赖意味着职责划分不清,可以考虑将相互依赖的部分提取到一个新的模块中,或者使用接口抽象来解耦。

  2. 使用接口(面向接口编程):通过接口来解耦具体的实现,使得模块之间依赖于抽象而不是具体实现。这样,即使有循环依赖,也可以通过依赖注入容器来管理生命周期,但最好还是避免循环依赖。

  3. 使用Setter注入或方法注入:如果循环依赖是不可避免的,可以考虑使用Setter注入或方法注入来打破构造函数的循环依赖。但是这种方法只是将问题从编译时转移到了运行时,需要谨慎使用。

    最佳实践:对于必需依赖,优先使用构造函数注入;Setter 注入仅适用于可选依赖或配置参数。

  4. 使用中介者模式或事件驱动:通过引入一个中介者来协调模块之间的交互,或者使用事件驱动架构,使得模块之间不直接相互依赖。

  5. 使用依赖注入容器的高级功能:一些DI容器支持循环依赖(例如,通过属性注入或延迟解析),但这不是推荐的做法,因为它会掩盖设计问题。

总结

循环依赖的根本解决方案是重新设计架构 即在代码中,应该尽量避免循环依赖,保持模块之间的单向依赖关系,而不是技术绕开。主要策略包括:

  1. 引入接口抽象 - 打破具体实现间的直接依赖
  2. 应用设计原则 - 单一职责、依赖倒置等
  3. 使用事件/消息机制 - 替代直接方法调用
  4. 重构大型类 - 拆分为更小、职责更单一的组件
  5. 依赖方向调整 - 确保依赖是单向的

ViewModelLocator

在WPF当中,需要为View与ViewModel建立连接, 我们需要找到View的DataContext

img

wpf中主要是两种建立连接的方式

  • XAML设置

    1
    2
    3
    <UserControl.DataContext>
    <.../>
    </UserControl.DataContext>
  • Code设置 (构造函数注入 或 ViewModelLocator)

    1
    2
    3
    4
    5
    6
    7
    8
    public partial class ViewA : UserControl
    {
    public ViewA()
    {
    InitializeComponent();
    this.DataContext = null; //设定
    }
    }

第三方的MVVM框架, 标准的ViewModelLocator可能如下所示

img

这些方式都可以建立View-ViewModel关系。
但是,这一切并不是Prism想表达的内容, 甚至不建议你按上面的方式去做, 因为这样几乎打破了开发的所有原则。
(我们把View与ViewModel的关系编码的方式通过命名的方式固定了下来, 通过静态类去维护ViewModel的关系…)

在Prism当中, 你可以基于命名约定以及文件夹结构, 便能够轻松的将View/ViewModel建议关联

假设你已经为项目添加Views/ViewModels文件夹。此时, 你的页面为ViewA, 则对应的ViewModel名称为 ViewAViewModel

img img

大致意思为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";
//DelegateCommand如果有第二个参数,可以传入一个方法,通过返回true/false来控制命令是否可执行
SendCommand = new DelegateCommand(() =>
{
Message = "hello world!";
});
}
private string _message;

public string Message
{
get { return _message; }
set { _message = value; RaisePropertyChanged(); }
}

//在Prism当中, 你可以使用DelegateCommand及带参数的Command
public DelegateCommand SendCommand { get; private set; }
//public DelegateCommand<string> SendMessageCommand { 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,它将无法被激活,如下所示:

image-20240619094609395

此图中按下Save All按钮后什么也不执行,因为复合命令被禁用了,假设复合命令被启用,那么将会执行的是Save ASave 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}会同时调用SendCommandSendCommand2

事件聚合器

事件聚合器IEventAggregator

事件聚合器负责接收订阅以及发布消息。订阅者可以接收到发布者发送的内容

功能盘点:

  • 松耦合基于事件通讯
  • 多个发布者和订阅者
  • 微弱的事件
  • 过滤事件
  • 传递参数
  • 取消订阅

AViewModel订阅了一个消息接收的事件, 然后BViewModel当中给指定该事件推送消息,此时AViewModel接收BViewModel推送的内容

image-20240619103421409
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=>
{
//do something
});

事件过滤

在实际的开发过程当中,我们往往会在多个位置订阅一个事件, 但是对于订阅者而言, 他并不需要接收任何消息,事件过滤Filtering Events应运而生

image-20240619103615319

在Prism当中, 我们可以指定为事件指定过滤条件

1
2
3
4
5
6
7
8
eventAggregator.GetEvent<MessageSentEvent>().Subscribe(
arg =>{
//do something
},
ThreadOption.PublisherThread,
false,
//设置条件为token等于“MessageListViewModel” 则接收消息
message => message.token.Equals(nameof(MessageListViewModel)));

关于Subscribe当中的4个参数, 详解:

  1. action: 发布事件时执行的回调委托。
  2. ThreadOption枚举: 指定在哪个线程上接收委托回调。
    • PublisherThread: 在发布事件的线程上调用订阅者的委托回调。
    • BackgroundThread: 在后台线程上调用订阅者的委托回调。
    • UIThread: 在UI线程上调用订阅者的委托回调(通常用于更新UI)
  3. keepSubscriberReferenceAlive: 如果为true,则Prism.Events.PubSubEvent保留对订阅者的引用因此它不会回收垃圾。
    • 长期订阅,需要保持状态的情况下设置为true.(基本上可以这么认为,只有贯穿程序一生的订阅才需要设置为true)
    • 临时订阅,不再需要时要被释放资源的情况下设置为false
  4. 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);
//订阅一个消息回调函数(只接受hello作为回调函数参数的发布信息)
eventAggregator.GetEvent<MessageEvent>().Subscribe(OnMessageReceived, ThreadOption.PublisherThread, false, (x) => { return x == "hello"; });
});
TriggerCommand = new DelegateCommand(() =>
{
//发布消息触发消息回调(hello作为回调函数的参数)
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. 通过委托的方式取消订阅

    1
    2
    var event = IEventAggregator.GetEvent<MessageSentEvent>();
    event.Unsubscribe(OnMessageReceived);
  2. 通过获取订阅者token取消订阅

    1
    2
    3
    var event = eventAggregator.GetEvent<MessageSentEvent>();
    var token = _event.Subscribe(OnMessageReceived);//订阅的时候返回token
    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)
{
//添加别名 "CustomName"
containerRegistry.RegisterForNavigation<ViewA>("CustomName");
//默认名称 "ViewB"
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)
{
//指定ViewModel
containerRegistry.RegisterForNavigation<ViewA, ViewAViewModel>();
//指定ViewModel并且添加别名
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);

//第二种方式
//类似URL地址传递参数
_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中的该方法被触发。

执行流程:

image-20240619143706078

获取导航请求参数

导航中允许我们传递参数, 用于在我们完成导航之前, 进行做对应的逻辑业务处理。这时候, 我们便可以在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, 所以, 它多了一个功能: 允许用户针对导航请求判断是否进行拦截

image-20240619144337205
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);
}
//返回true表示允许导航,返回false表示不允许导航

导航日志

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.xamlViewB.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
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App
{
protected override Window CreateShell()
{
return Container.Resolve<MainWindow>();
}

protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
//此处要注册各种资源
//默认名为ViewA
//containerRegistry.RegisterForNavigation<ViewA>();
//起名方式
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()
{

//传递参数给ViewA
NavigationParameters param = new NavigationParameters();
//参数传递的方式是通过键值对
param.Add("Value", "Hello");
//导航到ViewA(带参数)
//regionManager.RequestNavigate("ContentRegion", nameof(Views.ViewA),param);
//使用字符串导航(带参数)
//regionManager.RequestNavigate("ContentRegion", "PageA",param);
//传递参数的另一种写法(类似html的写法) 顺便加上了获取上下文日志
regionManager.RequestNavigate("ContentRegion", $"PageA?Value=Hello1", arg =>
{
//导航后拿到上下文日志
journal = arg.Context.NavigationService.Journal;
});
}

private void OpenB()
{
//导航到ViewB(第三个参数设置导航完成的时候触发回调函数)
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
{
//继承BindableBase用于实现通知,继承INavigationAware用于实现view导航,可以使用IConfirmNavigationRequest代替INavigationAware,就可以多一个ConfirmNavigationRequest方法用于判断是否允许导航(IConfirmNavigationRequest是继承INavigationAware)
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);
}

/// <summary>
/// 重新创建实例的一个判断,如果已经打开过当前实例的话,如果打开过还返回一个true的话,他会创建一个新的实例覆盖原来的
/// </summary>
/// <param name="navigationContext"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
public bool IsNavigationTarget(NavigationContext navigationContext)
{
return true;
}

/// <summary>
/// 导航离开当前页时触发
/// </summary>
/// <param name="navigationContext"></param>
/// <exception cref="NotImplementedException"></exception>
public void OnNavigatedFrom(NavigationContext navigationContext)
{

}

/// <summary>
/// 导航完成前,接收用户传递的参数以及是否允许导航等控制
/// </summary>
/// <param name="navigationContext"></param>
/// <exception cref="NotImplementedException"></exception>
public void OnNavigatedTo(NavigationContext navigationContext)
{
//接收Value键对应的值(string类型)
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 (注册对话及使用对话)
  • 打开对话框传递参数/关闭对话框返回参数
  • 回调通知对话结果

实现对话框

img

创建对话框,(注意)通常是一组用户控件 ,并且实现 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>();
//注册视图时绑定VM
containerRegistry.RegisterDialog<MessageDialog, MessageDialogViewModel>();
//添加别名
containerRegistry.RegisterDialog<MessageDialog>("DialogName");
}

使用Dialog

使用IDialogService接口 Show/ShowDialog 方法调用对话框

img
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); }
}

//增加IDialogService参数
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");
//显示对话框阳
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); //确保对话框是通过调用RequestClose事件来关闭的,而不是通过直接关闭窗口的方式。这样可以确保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
{
/// <summary>
/// 可配置节流/去重的对话框服务装饰器。
/// - 同名对话框去重:同一时刻只允许一个实例处于打开流程
/// - 节流:同名对话框两次弹出需满足最小时间间隔
/// - 调整方式:
/// 只有一个静态变量控制全局节流间隔:ThrottleInterval(设为 TimeSpan.Zero 表示仅去重不节流)
/// </summary>
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();
// 全局节流间隔(静态变量,代码中直接设置即可;为 Zero 则仅去重)
public static TimeSpan ThrottleInterval = TimeSpan.FromSeconds(1);

public DedupDialogService(IDialogService inner)
{
this.inner = inner;
}

public void Show(string name, IDialogParameters parameters, Action<IDialogResult> callback)
{
// 非模态:保持与原行为一致;如需也节流,可复用 ShowDialog 的控制逻辑
inner.Show(name, parameters, callback);
}

// Prism 新版接口:支持指定 windowName 的非模态展示,保持直通
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);
}

// Prism 新版接口:支持指定 windowName 的模态展示,去重/节流键使用 name+"@"+windowName
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 { }
}
}
}

// 无任何动态配置接口,使用者只需在代码中设置 DedupDialogService.ThrottleInterval
}
}

全局替换IDialogService,加上可配置节流/去重的装饰器替换原有服务

App.xaml.cs中RegisterTypes函数内:

1
2
3
4
5
6
7
8
9
10
11
12
// 全局替换 IDialogService 为可配置节流/去重的装饰器
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 { }