WPF prism
Prism
Prism.Dryloc 包
安装 Nuget 包 - Prism.DryIoc
1. 修改 App.xaml
修改 App.xaml 文件,添加 prism 命名空间, 继承由 Application → PrismApplication,删除默认启动 url, StartupUri=“MainWindow.xaml”
<dryioc:PrismApplicationx:Class="PrismClass.App"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"🔴xmlns:dryioc="http://prismlibrary.com/"xmlns:local="clr-namespace:PrismClass"><dryioc:PrismApplication.Resources /></dryioc:PrismApplication>
2. 修改App.xaml.cs
打开 App.xaml.cs 文件, 继承由 Application → PrismApplication(注意这里一定要编译一下,不然会报分部类继承类不一致的错误) , 如下所示。实现基类两个抽象方法:CreateShell( ) 与 RegisterTypes( ) 。
- CreateShell:该方法返回了一个 Window 类型的窗口, 其实就是返回应用程序的主窗口。
- RegisterTypes:该方法用于在 Prism 初始化过程中, 我们定义自身需要的一些注册类型, 以便于在 Prism 中可以使用。说白了,就是依赖注入,可以注入需要的服务等。
public partial class App : PrismApplication
{protected override Window CreateShell() //返回应用程序的主窗口{return Container.Resolve<MainWindow>();}protected override void RegisterTypes(IContainerRegistry containerRegistry) //依赖注入{throw new NotImplementedException();}
}
Prism Template Pack 扩展
Prism Template Pack 提供了哪些?
- Blank Project 空项目
- Module Project 模块示例项目
- 代码片段(用户快速创建属性、命令)
- propp-property(depends on BindableBase)
- cmd-DelegateCommand
- cmdg-DelegateCommand
安装完成后,再次打开 Visual Studio,将会看到 Prism Template Pack 提供了多种项目模板,用于快速构建基于 Prism 的应用程序
-
完整带Damo
-
模块
-
空项目
内置代码片段
这个生产力工具也内置了大量代码片段,例如:
-
propp - Property, 有一个后端字段,该字段依赖于 BindableBase 类。
private string _fieldName; public string PropertyName { get { return _fieldName; } set { SetProperty(ref _fieldName, value); } }
-
cmd - 创建一个带有执行方法的委托命令属性。
private DelegateCommand _fieldName; public DelegateCommand CommandName =>_fieldName ?? (_fieldName = new DelegateCommand(ExecuteCommandName));void ExecuteCommandName() {}
-
cmdfull - 创建一个具有“执行”和“能否执行”方法的委托命令属性
private DelegateCommand _fieldName; public DelegateCommand CommandName =>_fieldName ?? (_fieldName = new DelegateCommand(ExecuteCommandName));void ExecuteCommandName() {}bool CanExecuteCommandName() {return true; }
-
cmdg - 创建一个带参数的委托命令属性
private DelegateCommand<string> _fieldName; public DelegateCommand<string> CommandName =>_fieldName ?? (_fieldName = new DelegateCommand<string>(ExecuteCommandName));void ExecuteCommandName(string parameter) {}
-
cmdgfull - 创建一个具有“执行”和“能否执行”方法的泛型委托命令属性
private DelegateCommand<string> _fieldName; public DelegateCommand<string> CommandName =>_fieldName ?? (_fieldName = new DelegateCommand<string>(ExecuteCommandName, CanExecuteCommandName));void ExecuteCommandName(string parameter) {}bool CanExecuteCommandName(string parameter) {return true; }
Prism MVVM
本框架和其它二个常用的MVVM框架之间的区别:
功能↓ / →框架名 | Prism | Mvvmlight | CommunityToolkit.MVVM |
---|---|---|---|
通知 | BindableBase | ViewModelBase | ObservableObject |
命令 | DelegateCommand | RelayCommand | Async/RelayCommand |
聚合器 | IEventAggregator | IMessenger | IMessenger |
模块化 | √ | × | × |
容器 | √ | × | × |
依赖注入 | √ | × | × |
导航 | √ | × | × |
对话 | √ | × | × |
Views 和 ViewModels 的绑定
在 MVVM 中,Prism 提供了 Views 和 ViewModels 的绑定的几种方式:
方法一:基于约定
两个个文件夹 Views 和 ViewModels
-
Views 中是 xxx.xaml
-
ViewModels 是 xxxViewModel.cs
-
注意要在 Xaml 中开启自动绑定:
prism:ViewModelLocator.AutoWireViewModel="True
方法二:重写映射规则
重写 ConfigureViewModelLocator 方法,用于配置 ViewModel 定位器
protected override void ConfigureViewModelLocator()
{// 调用基类的 ConfigureViewModelLocator 方法,保留默认的配置行为base.ConfigureViewModelLocator();// 设置默认的 View 类型到 ViewModel 类型的解析方式ViewModelLocationProvider.SetDefaultViewTypeToViewModelTypeResolver((viewType) =>{// 获取视图类型的完整名称(包括命名空间)var viewName = viewType.FullName;// 获取视图所在程序集的完整名称var viewAssemblyName = viewType.GetTypeInfo().Assembly.FullName;// 构造对应的 ViewModel 类型名称,假设约定为视图类名加后缀 "VM",并与视图同在一个程序集var viewModelName = $"{viewName}VM, {viewAssemblyName}";// 根据构造的 ViewModel 名称获取对应的类型对象return Type.GetType(viewModelName);});
}
方法三:手动指定
手动指定 View 和 ViewModel 关系
protected override void ConfigureViewModelLocator()
{base.ConfigureViewModelLocator();//1️⃣ type / typeViewModelLocationProvider.Register(typeof(MainWindow).ToString(), typeof(CustomViewModel));//2️⃣ type / factoryViewModelLocationProvider.Register(typeof(MainWindow).ToString(), () => Container.Resolve<CustomViewModel>());//3️⃣ generic factoryViewModelLocationProvider.Register<MainWindow>(() => Container.Resolve<CustomViewModel>());//4️⃣ generic typeViewModelLocationProvider.Register<MainWindow, CustomViewModel>();
}
BindableBase 通知
属性通知
在 Prism 中, 继承 BindableBase 可以实现属性的变化通知,变化通知可以设置 SetProperty() 这个可以通知其他属性,带有这个方法重载或 RaisePropertyChanged() 我们可以看到 SetProperty() 方法有两个参数,会判断 _title 和 value 是否相等,如果不相等就进行赋值操作并触发 OnPropertyChanged 事件。
namespace PrismCollection.ViewModels
{public class MainWindowViewModel : BindableBase //支持通知需要继承 BindableBase{private string _title = "Prism Application";public string Title{get { return _title; }set { SetProperty(ref _title, value); } //SetProperty()通知}private string name;public string Name{get { return name; }set{name = value;RaisePropertyChanged(); //相当于原版 OnPropertyChanged()}}public MainWindowViewModel(){}}
}
📍 RaisePropertyChanged() 和 SetProperty() 的区别?
- RaisePropertyChanged():手动触发属性变更通知,相当于原版 OnPropertyChanged()
- 通知绑定系统某个属性的值发生了变化。
- 你需要手动传入属性名。
- SetProperty() :简化属性设置和通知变更
- 自动比较新旧值(避免重复通知)。
- 如果值有变化,则赋值并调用
RaisePropertyChanged()
。- 可以添加额外的回调(如值改变后的操作)。
数据验证
在属性变化的时候还会涉及到数据验证的问题,Prism 提供了 ErrorContainer 以便管理及通知数据验证的错误消息,如果想要使用,ViewModel 类不要直接继承 BindableBase,而是抽离出基类 DomainObject,让其实现 INotifyDataErrorInfo 的接口和 BindableBase 类,代码如下:
使用时就让 ViewModel 继承下面的实现类 DomainObject
1️⃣ 创建基类
实现 INotifyDataErrorInfo 的接口和 BindableBase 类
//带属性验证的基类
public class DomainObject : BindableBase, INotifyDataErrorInfo //继承BindableBase类和INotifyDataErrorInfo接口
{public ErrorsContainer<string> _errorsContainer;protected ErrorsContainer<string> ErrorsContainer{get{if (_errorsContainer == null)_errorsContainer = new ErrorsContainer<string>(s => OnErrorsChanged(s));return _errorsContainer;}}public void OnErrorsChanged(string propertyName){ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));}//实现 INotifyDataErrorInfo 接口public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;public IEnumerable GetErrors(string propertyName){return ErrorsContainer.GetErrors(propertyName);}public bool HasErrors //实现 bool HasErrors { get; } 属性{get { return ErrorsContainer.HasErrors; }}
}
2️⃣ViewModel 继承上面创建的类
在 ViewModel 中通过 ErrorsContainer.SetErrors 输出错误消息 和 ErrorContainer.ClearErrors 清空错误 管理数据验证的错误消息了,代码如下:
namespace PrismCollection.ViewModels
{public class ErrorContainerMockViewModel:DomainObject //继承上面自己实现的基类 DomainObject{private int age; public int Age{get { return age; }set { SetProperty(ref age, value);if (age < 0)//参数1. 属性名 参数2. 错误信息ErrorsContainer.SetErrors(nameof(Age), new[] { "年龄不能小于0" });elseErrorsContainer.ClearErrors(nameof(Age));}}}
}
nameof :是 C# 6.0 引入的一个关键字,用于获取变量、属性、方法、类等成员的名称(字符串),不会因重命名而失效
3️⃣ 在 XAML 中绑定
<TextBoxWidth="200"Height="30"Text="{Binding Age, UpdateSourceTrigger=PropertyChanged}" ToolTip="{Binding (Validation.Errors)[0].ErrorContent, RelativeSource={RelativeSource Self}}" />
DelegateCommand 命令
在 Prism 当中,可以使用 DelegateCommand 即带参数的 Command。
注意 XAML 中传参的方式为 CommandParameter,如果是属性则直接 CommandParameter="{Binding Property}"
,如果是绑定到控件属性上则要写成下面这种方式,指定元素名称和路径值。
装完 Prism Template Pack 扩展后可以直接使用 cmd 快速生成 DelegateCommand 属性。
命令快捷键:
-
cmd:只有一个逻辑方法
-
cmdfull: 有一个逻辑方法和一个是否执行方法
-
cmdg:只有一个带参数的逻辑方法
-
cmdgfull :有一个带参数的逻辑方法和一个是否执行方法
1️⃣ ViewModel .cs 中实现命令
DelegateCommand 用于命令,其也有泛型 DelegateCommand,本质是一个委托,当命令触发的时候,委托调用方法执行。
using System.Windows;
using Prism.Commands;
using Prism.Mvvm;namespace PrismCollection.ViewModels
{public class MainWindowViewModel : BindableBase{//构造函数public MainWindowViewModel(){ClickBtnCommand = new DelegateCommand(ClickBtnMethod);ClickBtnCommandByPara = new DelegateCommand<string>(ClickBtnMethodByPara);}//1️⃣ 不带参数命令public DelegateCommand ClickBtnCommand { get; set; }private void ClickBtnMethod(){MessageBox.Show("I can click btn");}//2️⃣ 带参数命令public DelegateCommand<string> ClickBtnCommandByPara { get; set; }private void ClickBtnMethodByPara(string obj){MessageBox.Show(obj);}}
}
2️⃣ View .Xaml 中绑定命令
<Button Content="{Binding Title}"/><TextBox Name="textBox" Text="你好"/><Button Content="ClickCommand"Command="{Binding ClickBtnCommand}"/><Button Content="ClickCommandByPara"Command="{Binding ClickBtnCommandByPara}"CommandParameter="{Binding ElementName=textBox,Path=Text}"/>
⏺ 带属性校验的命令:
public MainWindowViewModel(IEventAggregator eventAggregator,IRegionManager regionManager)
{// 如果标题发生变化ClickBtnCommand = new DelegateCommand(ClickBtnMethod, CanExecuteFromTitleChange).ObservesProperty(()=>Title); //当 Title 属性发生变化时,自动调用 RaiseCanExecuteChanged(),刷新按钮是否可用
}// 不带参数命令
public DelegateCommand ClickBtnCommand { get; set; }private void ClickBtnMethod()
{MessageBox.Show("你好");
}private bool CanExecuteFromTitleChange()
{if(Title == "Albert"){return true;}return false;
}
ObservesProperty( ):是 Prism 框架中
DelegateCommand
的一个扩展方法,用于自动监听某个属性的变化,从而自动触发命令的CanExecute
逻辑刷新。
CompositeCommand 多路命令
对于单个 Command 而言, 只是触发单个对应的功能, 而复合命令是 Prism 当中非常强大的功能, CompositeCommand 简单来说是一个父命令, 它可以注册 N 个子命令, 如下所示:
当父命令被激活, 它将触发对所有的子命令, 如果任意一个命令 CanExecute=false 它将无法被激活,如下所示:
注意代码中复合命令 CompositeCommandByBtn.RegisterCommand(xxx)
using System.Windows;
using Prism.Commands;
using Prism.Mvvm;namespace PrismCollection.ViewModels
{public class MainWindowViewModel : BindableBase{//构造函数public MainWindowViewModel(){ClickBtnCommand = new DelegateCommand(ClickBtnMethod);ClickBtnCommandByPara = new DelegateCommand<string>(ClickBtnMethodByPara);// 注册复合命令CompositeCommandByBtn = new CompositeCommand();CompositeCommandByBtn.RegisterCommand(ClickBtnCommand);CompositeCommandByBtn.RegisterCommand(ClickBtnCommandByPara);}// 不带参数命令public DelegateCommand ClickBtnCommand { get; set; }// 带参数命令public DelegateCommand<string> ClickBtnCommandByPara { get; set; }// 多路命令public CompositeCommand CompositeCommandByBtn { get; private set; }private void ClickBtnMethodByPara(string obj){MessageBox.Show(obj);}private void ClickBtnMethod(){MessageBox.Show("你好");}}
}
IEventAggregator 事件聚合器
事件聚合器什么意思?
相信大家一定都使用过聊天软件,这就是事件聚合器。当你在一个视图 A 中输入文字点击发送之后,另外一个视图 B 会接收到这个消息,并将文字输出到屏幕上,而这个时候,视图 A 并不关心谁将收到信息,只管提交,视图 B 也不管是谁发来的消息,只管接收,并显示。这个其实就是订阅发布,通过 DelegateCommand 来实现事件的订阅发布。
1️⃣ 消息订阅体
定义一个事件类,这边选择的是用字典来发送消息:
public class MessageEvent: PubSubEvent<Dictionary<string,string>> //要继承 PubSubEvent<> 里面页可以选择其它类型
{
}
2️⃣ 订阅事件和发布事件(先订阅再发布)
关于 Subscribe 当中的4个参数, 详解:
- action: 发布事件时执行的委托。
- ThreadOption 枚举: 指定在哪个线程上接收委托回调,有三种选择 PublisherThread(与发布者保持在同一线程上)、UIThread(在 UI 线程上执行)、BackgroundThread(在后台线程上执行)
- keepSubscriberReferenceAlive: 如果为 true,则 Prism.Events.PubSubEvent 保留对订阅者的引用因此它不会收集垃圾,用完必须要取消订阅。
- filter: 进行筛选以评估订阅者是否应接收事件。
//创建事件聚合器字段
private readonly IEventAggregator _eventAggregator;
private string _textLook = string.Empty;/*1️⃣ SubscribeCommand 订阅命令***********/
public DelegateCommand SubscribeCommand { get; private set; }void SubscribeMessage(){this.UnsubscribeMessage(); //先取消对事件的订阅,防止重复注册或内存泄露_eventAggregator.GetEvent<MessageEvent>().Subscribe(OnMessageReceived); //注册上面定义的事件}void OnMessageReceived(Dictionary<string, string> dicMsg) //收到事件后执行的方法{// 处理接收到的消息//把当前时间和 dicMsg["TextLook"] 的内容,以字符串的形式追加到 TextLook 属性中TextLook += $"{DateTime.Now} Subscribe : {dicMsg["TextLook"]} \r\n";}/*2️⃣ UnSubscribeCommand 取消订阅命令***********/
public DelegateCommand UnsubscribeCommand { get; private set; }void UnsubscribeMessage(){_eventAggregator.GetEvent<MessageEvent>().Unsubscribe(OnMessageReceived);}/*3️⃣ PublishCommand 发布命令***********/
public DelegateCommand<Dictionary<string, string>> PublishCommand { get; private set; }void PublishMessage(Dictionary<string,string> dicMsg){dicMsg = new Dictionary<string,string>();dicMsg.Add("TextLook", "AlbertZhao");_eventAggregator.GetEvent<MessageEvent>().Publish(dicMsg);}/*4️⃣ FilterCommand 带条件过滤的订阅***********/
public DelegateCommand FilterCommand { get; private set; }
private void Filter(){//先取消对事件的订阅,防止重复注册或内存泄露_eventAggregator.GetEvent<MessageEvent>().Unsubscribe(OnMessageReceived);//过滤事件//参数1. 收到事件后执行的方法(自己写的) //参数2. 指定在哪个线程调用回调(在发布线程中执行)//参数3. 是否保持强引用(不保持强引用,让GC可以清理)//参数4. 过滤器,只让满足条件的消息触发回调_eventAggregator.GetEvent<MessageEvent>().Subscribe(OnMessageReceived, ThreadOption.PublisherThread, false, dicMsg =>{if (dicMsg["TextLook"].Equals("Hello")) return true;else{TextLook += $"{DateTime.Now} : Filter data :{dicMsg["TextLook"]} \r\n";return false;}});}
📌实例项目功能结构概览:
命令 方法 功能 SubscribeCommand
SubscribeMessage
普通订阅消息 UnsubscribeCommand
UnsubscribeMessage
取消订阅 PublishCommand
PublishMessage
发布消息 FilterCommand
Filter
带条件过滤的订阅
- _eventAggregator.GetEvent<MessageEvent>().Subscribe(OnMessageReceived):订阅
- _eventAggregator.GetEvent<MessageEvent>().Unsubscribe(OnMessageReceived):取消订阅
- _eventAggregator.GetEvent<MessageEvent>() .Publish(dicMsg):发布
- _eventAggregator.GetEvent<MessageEvent>().Subscribe():过滤
📌在 “4” 过滤订阅 中几个参数说明:
参数名 类型 含义 默认值 action
Action<TPayload>
接收到事件时执行的回调方法 必填 threadOption
ThreadOption
指定在哪个线程调用回调 PublisherThread
keepSubscriberReferenceAlive
bool
是否保持强引用(防止被GC) false
filter
Predicate<TPayload>
过滤器,只让满足条件的消息触发回调 null
下面是示例中所使用的值解释:
参数 值 说明 action
OnMessageReceived
收到事件后执行的方法 threadOption
PublisherThread
在发布线程中执行(UI操作时推荐用 UIThread
)keepSubscriberReferenceAlive
false
不保持强引用,让GC可以清理(推荐) filter
dicMsg => ...
只有 TextLook == "Hello"
的消息才触发OnMessageReceived
,否则被拦截
3️⃣ 构造函数注入 IEventAggregator
//构造函数
public MainWindowViewModel(IEventAggregator eventAggregator)
{this._eventAggregator = eventAggregator; //注入聚合器字段PublishCommand = new DelegateCommand<Dictionary<string, string>>(PublishMessage);//发布SubscribeCommand = new DelegateCommand(SubscribeMessage); // 订阅UnsubscribeCommand = new DelegateCommand(UnsubscribeMessage); //取消订阅FilterCommand = new DelegateCommand(Filter); //过滤订阅
}
注意其中还有消息过滤的事件订阅 FilterCommand,用于过滤消息。
Module
Modules 是能够独立开发、测试、部署的功能单元,Modules 可以被设计成实现特定业务逻辑的模块(如 Profile Management),也可以被设计成实现通用基础设施或服务的模块(如 Logging、Exception Management)。既然 Modules 能够独立开发、测试、部署,那么如何告诉Shell(我们的宿主程序)去 Load 哪些 Module,以怎样的形式 Load 这些 Module 呢?Prism 为此提供了一个叫 ModuleCatalog 的东西。他用这个来管理 Module。所以在 App 启动之初,需要创建配置一个 ModuleCatalog。
-
通过 Prism Template Package 创建 Prism Module 项目或者直接创建 Wpf 项目,引入 Prism 包,并删除 App.xaml,然后将 outputType 改为 Class Library。
-
新建 Views 文件夹,新建一个用户控件 ViewA,新增一个类 ModuleAModule.cs 实现 IModule 接口(每一个 Module 类都要实现这个接口,而每一个 Module 都要有这样一个类来对 Module 里的资源统一管理)
using Prism.Ioc;
using Prism.Modularity;
using Prism.Regions;
using PrismCollectionModuleA.Views;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace PrismCollectionModuleA
{public class ModuleA : IModule{public void OnInitialized(IContainerProvider containerProvider){var regionManager = containerProvider.Resolve<RegionManager>();regionManager.RegisterViewWithRegion("RegionPage", typeof(ModuleViewA));}public void RegisterTypes(IContainerRegistry containerRegistry){}}
}
- 在主程序中加载模块官方 Demo 中有五种常见方式 https://github.com/PrismLibrary/Prism-Samples-Wpf
(1)AppConfig 方式
在项目中添加 App.config 配置文件,注意这边 moduleType 是名称空间.类名形式。重写 App.xaml.cs CreateModuleCatalog() 方法。注意拷贝模块 dll 到主项目生成目录下
<?xml version="1.0" encoding="utf-8"?>
<configuration><configSections><section name="modules" type="Prism.Modularity.ModulesConfigurationSection, Prism.Wpf" /></configSections><startup></startup><modules><module assemblyFile="PrismCollectionModuleA.dll" moduleType="PrismCollectionModuleA.ModuleA, PrismCollectionModuleA, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" moduleName="ModuleA" startupLoaded="True" /></modules>
</configuration>
//App.xaml.cs
protected override IModuleCatalog CreateModuleCatalog()
{return new ConfigurationModuleCatalog();
}
(2)目录方式:主项目重写 CreateModuleCatalog 方法,创建一个 Modules 文件夹,里面拷贝生成好的 dll。
protected override IModuleCatalog CreateModuleCatalog()
{return new DirectoryModuleCatalog() { ModulePath = @".\Modules" };
}
(3)代码方式:主项目引用模块项目,重写 ConfigureModuleCatalog 方法,将模块添加进去。
protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog)
{moduleCatalog.AddModule<PrismCollectionModuleA.ModuleA>();
}
(4)手动加载方式:主项目引用模块项目,重写 ConfigureModuleCatalog 方法
protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog)
{var moduleAType = typeof(ModuleA);moduleCatalog.AddModule(new ModuleInfo(){ModuleName = ModuleA.Name,ModuleType = ModuleA.AssemblyQualifiedName,InitializationMode = InitializationMode.OnDemand});
}
(5)Xaml 方式:不推荐,不作介绍,直接查看官方源码
Region 区域
什么是区域?
在理解这个之前, 首先需要了解一下, 在最常见的开发模式当中, 我们去设计某个页面的时候, 实际上界面元素在设计的时候已经被固定。
举个简单的例子,当我们去设计如下页面, 它包含 Header、Menu、Content 内容。我们可以为这个页面设计一些元素, 例如:
-
Menu 可以放置 ListBox
-
Content 可以放置一个 ContentControl
-
Header 可以放置一些 ToolBar
这就导致了页面多的时候难以统一管理,就引出了 Prism 中 Region 的概念,演变为下图:
RegionManager 功能主要有维护区域集合、提供对区域的访问、合成视图、区域导航、定义区域。
定义 Region
区域(Region)就是 View 的占位容器,你可以将其他 View 动态地插入这个容器中,而不需要在 XAML 中提前写死。
在 XAML 代码中引入名称空间xmlns:prism="http://prismlibrary.com/"
,定义一个 ContentControl,官方实现了 ContentControlRegionAdapter.cs 内容控件适配器,所以可以为其指定区域,这里用的设计模式为适配器模式。
区域名称 RegionManager.RegionName=“xxx”
定义区域
可以使用前端或者后端指定区域
-
前端定义
- prism:RegionManager.RegionName=“RegionPage”
-
后端代码定义
- RegionManager.SetRegionName(RegionPage, “RegionPage”);
1️⃣ 前端代码定义区域
<Window x:Class="PrismCollection.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><ContentControl Name="RegionPage"prism:RegionManager.RegionName="RegionPage"/></Grid>
</Window>
2️⃣ 后端代码定义区域:
using Prism.Regions;
using System.Windows;namespace PrismCollection.Views
{/// <summary>/// Interaction logic for MainWindow.xaml/// </summary>public partial class MainWindow : Window{public MainWindow(){InitializeComponent();//参数1. 你要标记为区域的控件 参数2. 给这个区域起的名字RegionManager.SetRegionName(RegionPage, "RegionPage");}}
}
定义视图和区域的关系
我们有两种方式来定义视图和区域的关系,一种是视图发现,一种是视图注入,下面将介绍两种方式。
1️⃣ 视图发现
视图发现 ViewDiscovery
- 创建一个用户自定义控件叫做 RegionPageB,
- 在 MainWindowViewModel 中进行区域注入,使用容器注入 IRegionManager
- RegisterViewWithRegion 将我们的视图和区域进行关联起来
IRegionManager 接口包含一个只读属性 Regions,是 Region 的集合(这个集合是从 xaml 中获取的,也就是我们定义的那些),RegionManager 的实例会使用他们,并将 view 注册给他们。
namespace PrismCollection.ViewModels
{public class MainWindowViewModel : BindableBase{private readonly IRegionManager regionManager; //声明一个字段来接收 Region 集合public MainWindowViewModel(IRegionManager regionManager){// 在区域中注册视图this.regionManager = regionManager;//参数1. 区域名称 参数2. 你要注册到该区域的视图this.regionManager.RegisterViewWithRegion("RegionPage", typeof(RegionPageB));}}
}
📌
RegisterViewWithRegion
和RequestNavigate
区别
方法 作用 显示多个视图? 备注 RegisterViewWithRegion
注册一个默认视图,自动显示 ❌ 通常只用于初始加载 RequestNavigate
动态导航切换视图 ✅(配合 INavigationAware
)更灵活,推荐用于菜单切换
2️⃣ 视图注入
视图注入 View Injection:实际上还是注入 IRegionManager,通过容器 IContainerExtension 来获取 view,通过 RegionManager 来获取 region,最后在 region 中加入 view。
-
先在 App.xaml.cs 中注册两个视图 ChangeRegionToA 和 ChangeRegionToB:
protected override void RegisterTypes(IContainerRegistry containerRegistry) {// 注册 RegionPageAcontainerRegistry.Register<RegionPageA>();// 如果有 RegionPageB,也一并注册containerRegistry.Register<RegionPageB>(); }
-
然后再 view.cs 中注入并激活:
public partial class MainWindow : Window{private readonly IContainerExtension _container;private readonly IRegionManager _regionManager;public DelegateCommand ChangeRegionToA { get; private set; }public DelegateCommand ChangeRegionToB { get; private set; }public MainWindow(IContainerExtension container, IRegionManager regionManager){InitializeComponent();_container = container;_regionManager = regionManager;}ChangeRegionToA = new DelegateCommand(() =>{//获取名为 "RegionPage" 的区域容器var region = _regionManager.Regions["RegionPage"];//获取注册的类型实例 RegionPageAvar view = _container.Resolve<RegionPageA>();if (!region.Views.Contains(view))region.Add(view); //把 视图 添加到 区域中//激活视图 a,切换显示当前视图region.Activate(view);});ChangeRegionToB = new DelegateCommand(() =>{var view = containerExtension.Resolve<RegionPageB>();var region = regionManager.Regions["RegionPage"];region.Add(view);//激活视图 b,切换显示当前视图region.Activate(view);});}
IContainerExtension:依赖注入容器的扩展封装接口,它用于在运行时解析服务 / 视图 / ViewModel常用方法:
方法 用法说明 Resolve<T>()
获取注册的类型实例 Register<TFrom, TTo>()
注册服务接口和实现 RegisterInstance<T>(T instance)
注册已存在的实例 IsRegistered<T>()
判断是否已注册
视图激活
private void Button_Click(object sender, RoutedEventArgs e)
{//activate view a_region.Activate(_viewA);
}private void Button_Click_1(object sender, RoutedEventArgs e)
{//deactivate view a_region.Deactivate(_viewA);
}
导航 Navigation
导航基础
导航核心api:
名称 | 说明 |
---|---|
Region (区域) | 页面中用来承载视图的容器(如 ContentControl) |
RegionManager | 管理区域和导航的工具 |
RequestNavigate | 请求区域跳转到某个视图 |
RegisterForNavigation | 将视图注册为可导航的组件 |
IContainerRegistry | 依赖注入容器注册接口 |
Prism 官方实现的导航方式示例:
- 注册区域,按照上面所述注册,比如区域名为 RegionPage.
- 编写前台界面并绑定到后台 Command 上如下文代码:
<Windowx:Class="PrismCollection.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/"Title="{Binding Title}"Width="525"Height="350"prism:ViewModelLocator.AutoWireViewModel="True"><Grid><Grid.ColumnDefinitions><ColumnDefinition Width="40*" /><ColumnDefinition Width="67*" /></Grid.ColumnDefinitions><!--定义区域--><ContentControl Name="RegionPage"Grid.Column="1"prism:RegionManager.RegionName="RegionPage" /> <StackPanel><Button Command="{Binding NavigationACommand}" CommandParameter="NavigationA"Content="NavigationA"/><Button Command="{Binding NavigationBCommand}" CommandParameter="NavigationB"Content="NavigationB"/></StackPanel></Grid>
</Window>
- 在 App.xmal.cs 中注册导航
namespace PrismCollection
{public partial class App: PrismApplication{/*设置主窗口 *********/protected override Window CreateShell(){return Container.Resolve<MainWindow>();}/*注册可导航视图 *********/protected override void RegisterTypes(IContainerRegistry containerRegistry){// 注册两个导航containerRegistry.RegisterForNavigation<NavigationA>();containerRegistry.RegisterForNavigation<NavigationB>();} /*模块系统 *********/protected override IModuleCatalog CreateModuleCatalog(){return new ConfigurationModuleCatalog();}}
}
- CreateShell():设置主窗口
- 这是应用启动时的入口,告诉 Prism 使用哪个窗口作为主界面。
MainWindow
需要在容器中注册(Prism 会自动处理主窗体),这里你只需要确保MainWindow.xaml
定义好区域。- RegisterTypes():注册导航视图
- 注册
NavigationA.xaml
和NavigationB.xaml
为可导航视图。- 注册后你可以使用
RequestNavigate("RegionName", "NavigationA")
来切换页面。- 这里的
"NavigationA"
默认就是类名- CreateModuleCatalog():模块系统
- 表示你打算使用模块(Module)功能,可以加载独立功能模块(DLL)。
- 如果暂时没有使用模块系统,可以用默认的
return base.CreateModuleCatalog();
- IContainerRegistry:依赖注入容器注册接口
- Register()注册类型Transient(每次新实例)
- RegisterSingleton()注册单例Singleton(全局单例)
- RegisterInstance()注册实例Singleton(使用提供的实例)
- RegisterForNavigation()注册导航视图根据框架管理
- RegisterDialog()注册对话框根据框架管理
- 在 MainViewModel 中绑定带参数命令来切换导航。
//导航到 A 页面命令
private DelegateCommand<string> navigationACommand;
public DelegateCommand<string> NavigationACommand =>
navigationACommand ?? (navigationACommand = new DelegateCommand<string>(ExecuteNavigationACommand));void ExecuteNavigationACommand(string parameter){if (!string.IsNullOrEmpty(parameter)){//参数1. 目标区域名 参数2. 要导航的页面名 参数3. 回调方法,用于处理导航成功或失败的情况this.regionManager.RequestNavigate("NavigationPage", parameter, NavigationCompelted);} }//导航到 B 页面命令
private DelegateCommand<string> navigationBCommand;
public DelegateCommand<string> NavigationBCommand =>navigationBCommand ?? (navigationBCommand = new DelegateCommand<string>(ExecuteNavigationBCommand));void ExecuteNavigationBCommand(string parameter){if (!string.IsNullOrEmpty(parameter)){this.regionManager.RequestNavigate("NavigationPage", parameter, NavigationCompelted);}}//导航回调处理
//NavigationResult 包含导航是否成功的结果。
//若找不到页5面或绑定失败,则 Result 为 false。
private void NavigationCompelted(NavigationResult result){if (result.Result == true){MessageBox.Show($"Success-{result.Context.Uri.ToString()}");}else{MessageBox.Show($"Failure-{result.Context.Uri.ToString()}");}}
名称 内容 RequestNavigate(region, viewName)
请求导航 NavigationResult
回调判断是否导航成功 CommandParameter
在按钮中传递 "NavigationA"
、"NavigationB"
导航前后回调 INavigationAware(导航传参)
我们经常在两个界面切换的时候需要做一些逻辑处理,比如保存当前用户填写的一些信息,这时候就要用到 INavigationAware 接口来处理了。
📌 INavigationAware 源码:
public interface INavigationAware : Object
{Void OnNavigatedTo(NavigationContext navigationContext);Boolean IsNavigationTarget(NavigationContext navigationContext);Void OnNavigatedFrom(NavigationContext navigationContext);
}
此接口有三个方法,分别是:
- OnNavigatedFrom:导航之前触发,一般用于保存该页面的数据
- OnNavigatedTo:导航后目的页面触发,一般用于初始化或者接受上页面的传递参数
- IsNavigationTarget:True 则重用该 View 实例,Flase 则每一次导航到该页面都会实例化一次。
导航基础传递参数:
public class MainWindowViewModel
{private readonly IRegionManager regionManager;public MainWindowViewModel(IRegionManager regionManager){this.regionManager = regionManager;}private DelegateCommand<string> navigationACommand;public DelegateCommand<string> NavigationACommand =>navigationACommand ?? (navigationACommand = new DelegateCommand<string>(ExecuteNavigationACommand));void ExecuteNavigationACommand(string parameter){if (!string.IsNullOrEmpty(parameter)){var param = new NavigationParameters(); // 创建导航参数对象param.Add("OpenA","Tuling"); // 添加参数 key = OpenA, value = Tuling//参数1. 导航到的页面 参数2. 目标区域 参数3. 要传递的参数this.regionManager.RequestNavigate("NavigationPage", "PageA", param); // 发起导航并传参} }
}
this.regionManager.RequestNavigate(“NavigationPage”, “PageA”, param);
其中的参数:
- 参数1. 目标区域是
"NavigationPage"
(你在 XAML 中指定的RegionName="NavigationPage"
的控件);- 参数2. 请求导航到名为
"PageA"
的页面;- 参数3. 附带参数
param
(类型是NavigationParameters
)一起传过去。
导航页面 A
public class LoginMainContentViewModel : BindableBase, INavigationAware
{private readonly IRegionManager _regionManager;private DelegateCommand _createAccountCommand;public LoginMainContentViewModel(IRegionManager regionManager){_regionManager = regionManager;}public DelegateCommand CreateAccountCommand =>_createAccountCommand ?? (_createAccountCommand = new DelegateCommand(ExecuteCreateAccountCommand));private void ExecuteCreateAccountCommand(){Navigate("CreateAccount");}private void Navigate(string navigatePath){if (navigatePath != null)_regionManager.RequestNavigate(RegionNames.LoginContentRegion, navigatePath);}// True 则重用该 View 实例,False 则每一次导航到该页面都会实例化一次。public bool IsNavigationTarget(NavigationContext navigationContext){return true;}// 导航离开当前页时触发public void OnNavigatedFrom(NavigationContext navigationContext){MessageBox.Show("退出了LoginMainContent");}// 导航完成后,接收用户传递的参数public void OnNavigatedTo(NavigationContext navigationContext){//获取键为 "OpenA" 的字符串参数值//GetValue 是 NavigationParameters 类的一个泛型方法,用于从导航参数中获取指定类型的值var test = navigationContext.Parameters.GetValue<string>("OpenA");}
}
以上代码的基本流程为:
NavigationContext
包含了导航相关的信息,比如:
Parameters
:导航时传递的参数NavigationService
:导航服务Uri
:导航的目标URI
导航页面 B
// ViewModel
public class CreateAccountViewModel : BindableBase, INavigationAware
{private readonly IRegionManager _regionManager;private DelegateCommand _loginMainContentCommand;public CreateAccountViewModel(IRegionManager regionManager){_regionManager = regionManager;}public DelegateCommand LoginMainContentCommand =>_loginMainContentCommand ?? (_loginMainContentCommand = new DelegateCommand(ExecuteLoginMainContentCommand));private void ExecuteLoginMainContentCommand(){Navigate("LoginMainContent");}private void Navigate(string navigatePath){if (navigatePath != null)_regionManager.RequestNavigate(RegionNames.LoginContentRegion, navigatePath);}public bool IsNavigationTarget(NavigationContext navigationContext){return true;}public void OnNavigatedFrom(NavigationContext navigationContext){MessageBox.Show("退出了CreateAccount");}public void OnNavigatedTo(NavigationContext navigationContext){MessageBox.Show("从LoginMainContent导航到CreateAccount");}
}
导航询问是否允许
IConfirmNavigationRequest 这个接口继承自 INavigationAware,里面有一个导航前是否询问的方法。
IConfirmNavigationRequest 接口:
- 在导航发生前进行拦截
- 允许用户确认或取消导航操作
- 必须调用
continuationCallback(bool)
来告知框架是否继续导航
//导航前询问,由 Prism 框架自动调用
//当用户尝试 离开当前页面 时,框架会检查当前ViewModel是否实现了 IConfirmNavigationRequest
//如果实现了,就会自动调用这个方法
public void ConfirmNavigationRequest(NavigationContext navigationContext, Action<bool> continuationCallback)
{var result = false;if (MessageBox.Show("是否需要导航到LoginMainContent页面?", "温馨提示",MessageBoxButton.YesNo) ==MessageBoxResult.Yes){result = true;}continuationCallback(result);
}
导航间传递参数
当然我们也可以通过导航前询问传参到下一处都是 OK 的,本质上还是依托 navigationContext
public class CreateAccountViewModel : IConfirmNavigationRequest, INavigationAware
{private readonly IRegionManager _regionManager;public User CurrentUser { get; set; }public string RegisteredLoginId { get; set; }public bool IsUseRequest { get; set; }public CreateAccountViewModel(IRegionManager regionManager){_regionManager = regionManager;}public void ConfirmNavigationRequest(NavigationContext navigationContext, Action<bool> continuationCallback){if (!string.IsNullOrEmpty(RegisteredLoginId) && this.IsUseRequest){var result = MessageBox.Show("是否需要用当前注册的用户登录?", "Navigate?", MessageBoxButton.YesNo);if (result == MessageBoxResult.Yes){// 取消当前导航continuationCallback(false);// 手动导航并带参(MainRegion 是你在 Shell 界面里注册的区域名)var parameters = new NavigationParameters{{ "loginId", RegisteredLoginId }};_regionManager.RequestNavigate("MainRegion", "LoginMainContent", parameters);return;}}// 正常继续原有导航continuationCallback(true);}public void OnNavigatedTo(NavigationContext navigationContext){MessageBox.Show("从CreateAccount导航到LoginMainContent");var loginId = navigationContext.Parameters["loginId"] as string;if (loginId != null){//创建一个新的 User 实例,并设置它的 LoginId 属性为导航参数中接收到的 loginId,//然后赋值给 ViewModel 的 CurrentUser 属性。this.CurrentUser = new User() { LoginId = loginId };}}public bool IsNavigationTarget(NavigationContext navigationContext) => true;public void OnNavigatedFrom(NavigationContext navigationContext) { }
}
导航日志
IRegionNavigationJournal
//ViewModelA 代码
private readonly IRegionManager _regionManager;
private IRegionNavigationJournal _journal; // 用于记录导航历史public ViewModelA(IRegionManager regionManager)
{_regionManager = regionManager;
}regionManager.RequestNavigate("ContentRegion","ViewA",arg=>{journal = arg.Context.NavigationService.Journal;
});regionManager.RequestNavigate("ContentRegion","ViewB",arg=>{journal = arg.Context.NavigationService.Journal;
});//
IRegionNavigationJournal _journal;private DelegateCommand _goBackCommand;
public DelegateCommand GoBackCommand =>_goBackCommand ?? (_goBackCommand = new DelegateCommand(ExecuteGoBackCommand));void ExecuteGoBackCommand()
{_journal.GoBack();
}
如果不打算将页面在导航过程中不加入导航日志,可以通过实现 IJournalAware 并从 PersistInHistory 返回 false
public class LoginMainContentViewModel : IJournalAware
{public bool PersistInHistory() => false;
}
对话框
在 Prism 中,通过一个 IDialogAware 接口来实现对话框服务。
public interface IDialogAware
{bool CanCloseDialog();void OnDialogClosed();void OnDialogOpened(IDialogParameters parameters);string Title { get; set; }event Action<IDialogResult> RequestClose;
}
- CanCloseDialog() 函数是决定窗体是否关闭
- OnDialogClosed() 函数是窗体关闭时触发,触发条件取决于CanCloseDialog() 函数
- OnDialogOpened() 函数时窗体打开时触发,比窗体Loaded事件早触发
- Title 为窗体的标题
- RequestClose 为关闭事件,可由此控制窗体的关闭
-
弹出框前后端代码
<!--xaml--> <Button Content="{Binding DialogTitle}" FontSize="30" />
//View.cs namespace PrismCollection.ViewModels {public class DialogAViewModel : Albert_BindableBase, IDialogAware{public string Title => "弹框提醒";public event Action<IDialogResult> RequestClose;// 允许关闭当前窗口public bool CanCloseDialog(){return true;}public void OnDialogClosed(){}public void OnDialogOpened(IDialogParameters parameters){DialogTitle = parameters.GetValue<string>("message");}private string dialogTitle;public string DialogTitle{get { return dialogTitle; }set { SetProperty(ref dialogTitle, value); }}} }
-
App.cs 注册对话框
using Prism.DryIoc; using Prism.Ioc; using Prism.Modularity; using Prism.Regions; using PrismCollection.ViewModels; using PrismCollection.Views; using System.Windows;namespace PrismCollection {/// <summary>/// Interaction logic for App.xaml/// </summary>public partial class App: PrismApplication{protected override Window CreateShell(){return Container.Resolve<MainWindow>();}/// <summary>/// 用于导航/// </summary>/// <param name="containerRegistry"></param>protected override void RegisterTypes(IContainerRegistry containerRegistry){// 注册两个导航containerRegistry.RegisterForNavigation<NavigationA>();containerRegistry.RegisterForNavigation<NavigationB>();// 注册区域Container.Resolve<RegionManager>().RegisterViewWithRegion("RegionPage", typeof(RegionPageA));// 注册弹窗containerRegistry.RegisterDialog<DialogA, DialogAViewModel>("Albert_Dialog");}protected override IModuleCatalog CreateModuleCatalog(){return new ConfigurationModuleCatalog();}} }
-
使用对话框服务,构造函数注入 IDialogService 服务
private IDialogService _dialogService;private DelegateCommand dialogCommand; public DelegateCommand DialogCommand =>dialogCommand ?? (dialogCommand = new DelegateCommand(ExecuteDialogCommand));void ExecuteDialogCommand() {// 第二个参数可以传参_dialogService.ShowDialog("Albert_Dialog",null,arg=>{if(arg.Result == ButtonResult.OK){}}); }public MainWindowViewModel(IDialogService dialogService,IEventAggregator eventAggregator,IRegionManager regionManager,IContainerExtension containerExtension) {this._dialogService = dialogService; }
IDialogService 中有两个方法:
//源码 public interface IDialogService : Object {Void Show(String name, IDialogParameters parameters, Action<IDialogResult> callback);Void ShowDialog(String name, IDialogParameters parameters, Action<IDialogResult> callback); }
我们可以发现 Show 和 ShowDialog 函数都是一样形参,无非就是使用场景不一样
- name:所要调用对话框 view 的名字,当注册别名时,只能使用别名来调用
- parameters:IDialogParameters 接口类型参数,传入的提示消息,通常是 $“message={xxxx}” 格式,然后在 ViewModel 中 OnDialogOpened 方法通过IDialogParameters 接口的 GetValue 函数来获取
- callback:用于传入无返回值回调函数
有一些常见我们需要自定义对话框窗体,可以按照下面样式进行修改:
<prism:Dialog.WindowStyle><Style TargetType="Window"><Setter Property="prism:Dialog.WindowStartupLocation" Value="CenterScreen" /><Setter Property="ShowInTaskbar" Value="False"/><Setter Property="SizeToContent" Value="WidthAndHeight"/><Setter Property="WindowStyle" Value="None"/></Style></prism:Dialog.WindowStyle>