Prism 框架
Prism 框架
Prism 是什么
- Prism框架是WPF对MVVM的实现。
- Prism是一个用于在 WPF、Xamarin Form、Uno 平台和 WinUI 中构建松散耦合、可维护和可测试的 XAML 应用程序框架。
- Prism提供了一组设计模式的实现,这些模式有助于编写结构良好和可维护的XAML应用程序,包括MVVM、依赖注入、命令、EventAggregator等。
- Prism.Core:实现MVVM的核心功能,属于一个与平台无关的项目;
- Prism.Wpf:包含了DialogService、Region、Module、Navigation,其他的一些WPF的功能;
- Prism.Unity:IOC容器
- Prism.DryIoc:IOC容器
- Prism.Ninject
快速入门
创建一个基于.NetFramework 或.NetCore的WPF应用程序
为当前应用程序添加NuGet源, 打开NuGet管理器,安装Prism.DryIoc
- 在Prism提供的VusualStudio Template Pack当中, 默认支持选择两种类型的容器:DryIoc 和 Unity
修改App.xaml文件,添加prism命名空间, 继承由Application改为PrismApplication
<!-- 需要删除 StartupUri="MainWindow.xaml" 否则会启动两个窗口 --> <prism:PrismApplication x:Class="PrismDemo.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:PrismDemo" xmlns:prism="http://prismlibrary.com/"> </prism:PrismApplication>
// 继承于PrismApplication类, 需要实现其中的两个抽象方法。 CreateShell 与 RegisterTypes public partial class App : PrismApplication { // 该方法返回了一个Window类型的窗口, 其实就是返回应用程序的主窗口。 protected override Window CreateShell() { // Container 是基类当中的一个容器属性,可用于解析对象的依赖项注入容器 return Container.Resolve<MainWindow>(); } // 该方法用于在Prism初始化过程中, 我们定义自身需要的一些注册类型, 以便于在Prism中可以使用。 protected override void RegisterTypes(IContainerRegistry containerRegistry) { } }
Prism Template Pack
- 创建 Blank Project (空白示例项目)
- 创建 Module Project (模块示例项目)
- 创建代码段:
- propp : 属性,具有依赖于BindableBase的支持字段
- cmd : 使用Execute方法创建DelegateCommand属性
- cmdfull : 使用Execute和CanExecute方法创建DelegateCommand属性
- cmdg : 创建通用DelegateCommand属性
- cmdgfull : 使用Execute和CanExecute方法创建通用DelegateCommand属性
Prism 的初始化过程
依赖注入 Dependency Injection
- Prism 始终围绕着依赖注入构建对象
注册类型
跟 ASP.NET Core 依赖注册方式类似(Transients, Singletons, and Scoped Services)
Transient :瞬态,每次创建新对象
containerRegistry.Register<IBarService, BarService>();
Singleton :单例,首次调用时创建具有全局生命周期的对象
containerRegistry.RegisterSingleton<IBarService, BarService>();
Scoped :作用域,在作用域范围内共享同一个实例对象
containerRegistry.RegisterScoped<IBarService, BarService>();
RegisterInstance :注册实例,以单例模式注册手动创建的实例对象
containerRegistry.RegisterInstance<IFoo>(new FooImplementation());
RegisterMany :注册多接口实例,从任意接口注入都可以获得实例对象
containerRegistry.RegisterMany<TestService>(new[] { typeof(ITestService), typeof(ITest2Service) });
检查是否已经注册:
containerRegistry.IsRegistered<ISomeService>()
异常处理(Prism 8)
Prism 容器会捕捉所有容器异常并抛出 ContainerResolutionException
protected virtual void LoadModuleCompleted(IModuleInfo moduleInfo, Exception error, bool isHandled) { if (error != null && error is ContainerResolutionException cre) { var errors = cre.GetErrors(); foreach((var type, var ex) in errors) { Console.WriteLine($"Error with: {type.FullName}"); Console.WriteLine($"{ex.GetType().Name}: {ex.Message}"); } } } /* Error with: MyProject.Services.IServiceIForgotToRegister ContainerResolutionException: No Registration was found in the container for the specified type */
ContainerLocator(Prism 8)
- 可以继承 PrismApplicationBase 实现第三方的IOC容器管控各种组件注入和生命周期
- 还可以重写 CreateContainerExtension() 方法实现容器扩展
Prism 使用 IServiceCollection
安装对应容器的扩展包就可以把 .net core 自带的 IServiceCollection 中注入的组件注入到由 Prism 提供的容器(Unity或者DryIoc容器)
# 根据使用的容器选择对应的扩展依赖包 Install-Package Prism.Unity.Extensions Install-Package Prism.DryIoc.Extensions
对于其他第三方容器,可以实现 IContainerExtension 接口来扩展容器
ViewModelLocator
ViewModelLocator 用于导入视图的 DataContext
自动导入 AutoWireViewModel
<Window x:Class="Demo.Views.MainWindow" ... xmlns:prism="http://prismlibrary.com/" prism:ViewModelLocator.AutoWireViewModel="False">
WPF 中 region navigation and IDialogService 默认绑定 DataContext
默认命名约定
- View 和 ViewModel 位于同一个程序集
- View页面需要放在Views文件夹下,并且命名为 [Name] 或者 [Name]View
- ViewModel需要放在ViewModels文件夹下, 命名为 [Name]ViewModel, 并且需要继承 Bindablebase基类
修改默认命名约定
protected override void ConfigureViewModelLocator() { base.ConfigureViewModelLocator(); ViewModelLocationProvider.SetDefaultViewTypeToViewModelTypeResolver((viewType) => { var viewName = viewType.FullName.Replace(".ViewModels.", ".CustomNamespace."); var viewAssemblyName = viewType.GetTypeInfo().Assembly.FullName; var viewModelName = $"{viewName}ViewModel, {viewAssemblyName}"; return Type.GetType(viewModelName); }); }
为不符合命名约定的 View 手动注册 ViewModel
// 多种重载方式注册 ViewModelLocationProvider.Register(typeof(MainWindow).ToString(), typeof(CustomViewModel)); ViewModelLocationProvider.Register(typeof(MainWindow).ToString(), () => Container.Resolve<CustomViewModel>()); ViewModelLocationProvider.Register<MainWindow>(() => Container.Resolve<CustomViewModel>()); ViewModelLocationProvider.Register<MainWindow, CustomViewModel>();
手动注册 ViewModel 比自动注册速度快,因为自动注册会依赖反射,手动注册则直接绑定类型。
控制视图模型的解析方式
ViewModelLocationProvider.SetDefaultViewModelFactory()
属性绑定通知
private string name;
public string Name
{
get{return name;}
set{
// SetProperty方法是Bindablebase类中的,这个类实现了INotifyPropertyChanged基类。
// Bindablebase实现了事件通知的功能。当属性修改之后,相应的前台属性也会修改
SetProperty(ref name,value);
//也可以直接调用 RaisePropertyChanged();
}
}
Commands(命令)
Wpf 绑定命令
DelegateCommand
使用 DelegateCommand 类,该类实现了 ICommand 接口
public DelegateCommand Clicl_Command { get; private set; } = new DelegateCommand( Action action );
<TextBlock Text = "Test" x:Name = "text_box"/> <Button Content = "Click" Command="{Binding Click_Command}" CommandParameter="{Binding ElementName = text_box}" />
组合命令
组合命令 CompositeCommand,包含 DelegateCommand 子命令列表
创建命令:可以使用静态类或者依赖注入方式使用
public static CompositeCommand SaveCommand = new CompositeCommand();
向组合命令中注册子命令
var UpdateCommand = new DelegateCommand(Update); ApplicationCommands.SaveCommand.RegisterCommand(UpdateCommand);
执行命令
<!-- 使用 ViewModel 命令属性 --> <Button Content="Save" Command="{Binding ApplicationCommands.SaveCommand}"/> <!-- 使用静态方法 --> <Button Content="Save" Command="{x:Static local:ApplicationCommands.SaveCommand}" />
UnregisterCommand :由于子命令注册会使用 CompositeCommand,因此改对象不能自动回收,需要手动回收
// 不在使用组合命令时,比如 View / ViewModel 被卸载时手动取消注册 public void Destroy() { _applicationCommands.UnregisterCommand(UpdateCommand); }
子页面执行组合命令中的子命令
子页面 ViewModel 需要实现 IActiveAvare 接口
DelegateCommand 中实现了 IActiveAvare ,CompositeCommand 会根据子命令的 IsActive 和 IsActiveChanged 来调用命令
public class TabViewModel : BindableBase, IActiveAware { /* 其他代码略 */ public DelegateCommand UpdateCommand { get; private set; } private void OnIsActiveChanged() { UpdateCommand.IsActive = IsActive; //set the command as active IsActiveChanged?.Invoke(this, new EventArgs()); //invoke the event for all listeners } }
Region(区域)
概念
- 将页面显示的区域划分称N个 Region,每个 Region 将变成了动态分配区域。
- RegionManager 功能:
- 维护区域集合
- 提供对区域的访问
- 合成视图
- 区域导航
- 定义区域
定义 Region
使用 XAML 方式:
<Grid> <ContentControl prism:RegionManager.RegionName="ContentRegion" /> </Grid>
使用代码方式
<Grid> <ContentContrl x:Name="Content"/> </Grid>
public MainWindow(IRegionManager regionManager) { // 为界面控件定义区域 RegionManager.SetRegionName(Content,"ContentRegion"); // 为指定区域注册页面 regionManager.RegisterViewWithRegion("ContentRegion",typyof(Content)) }
为区域注册页面时,需要先在 Views 下创建页面
RegionAdapter
Prism 提供了许多内置的 RegionAdapter:
- ContentControlRegionAdapter
- ItemsControlRegionAdapter
- SelectorRegionAdapter ( ComboBox 、ListBox 、Ribbon 、TabControl )
非内置 RegionAdapter 的控件想要实现控件作用域 Region 必须创建自定义 RegionAdapter
自定义 RegionAdapter 步骤:
创建一个类, 然后继承于RegionAdapterBase
重写其中的CreateRegion方法,返回一个IRegion接口,可以返回 SingleActiveRegion 、AllActiveRegion 、Region 三种区域类型
重写其中的Adapt方法
在 PrismApplication 中重写 ConfigureRegionAdapterMappings 方法注册自定义适配器
// 创建自定义 RegionAdapter public class StackPanelRegionAdapter : RegionAdapterBase<StackPanel> { public StackPanelRegionAdapter(IRegionBehaviorFactory regionBehaviorFactory) : base(regionBehaviorFactory) { } protected override void Adapt(IRegion region, StackPanel regionTarget) { // 实现代码 } protected override IRegion CreateRegion() =>new Region(); }
// 注册自定义适配器 protected override void ConfigureRegionAdapterMappings(RegionAdapterMappings regionAdapterMappings) { base.ConfigureRegionAdapterMappings(regionAdapterMappings); // Container 用于解析对象的依赖注入容器 // Resolve 解析给定的 System.Type regionAdapterMappings.RegisterMapping(typeof(StackPanel),Container.Resolve<StackPanelRegionAdapter>()); }
Module(模块)
概念
模块是可独立开发、测试和(可选)部署的功能包。
模块可用于表示特定的业务相关功能(例如,配置文件管理),并封装实现该功能所需的所有视图、服务和数据模型。
项目包含了一个启动页, 并且在启动页当中划分好了对应的区域,可以动态的加载应用程序模块, 为指定的Region动态分配内容。
创建模块
创建一个 WPF类库项目,引用 Prism.DryIoc 包,模块项目按正常项目编写 MVVM
定义一个类, 并且实现 IModule 接口
namespace ModuleA { public class ModuleAProfile : IModule { public void OnInitialized(IContainerProvider containerProvider) { //throw new NotImplementedException(); } public void RegisterTypes(IContainerRegistry containerRegistry) { // 将页面 ViewA 注册到模块中 containerRegistry.RegisterForNavigation<ViewA>(); } } }
添加模块
添加模块的方法:
- (代码方式)Code
- (配置文件)App.config
- (磁盘目录)Disk/Directory
- (XAML定义)XAML
- (自定义)Custom
代码添加模块
- 添加 ModuleA.dll 的项目引用,属于静态添加模块
- 在 App.xaml.cs 中重写 ConfigureModuleCatalog 方法
protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog) { // 直接添加模块A moduleCatalog.AddModule<ModuleAProfile>(); }
- 在窗口中使用外部模块的页面 ViewA
public partial class MainWindow : Window { public MainWindow(IRegionManager regionManager) { InitializeComponent(); regionManager.RegisterViewWithRegion("ContentRegion", typeof(ModuleA.Views.ViewA)); } }
Directory 配置模块目录
- 将模块生成的 .dll 文件放到 \bin\Debug\net6.0-windows\Modules\ 中(只要能从主程序文件夹定位到 Modules 文件夹即可)
- 在 App.xaml.cs 中重写 CreateModuleCatalog 方法,自动读取根目录Modules文件夹中的模块
protected override IModuleCatalog CreateModuleCatalog() { return new DirectoryModuleCatalog() { ModulePath = @".\modules" }; }
- 在 MainWindowViewModel 中设置命令用于页面跳转
public class MainWindowViewModel : BindableBase { private readonly IRegionManager regionManager; public object OpenCommand { get; } public MainWindowViewModel(IRegionManager regionManager) { this.regionManager = regionManager; // 通过传入的模块页面名称修改前端页面区域的显示 OpenCommand = new DelegateCommand<string>((obj) => regionManager.Regions["ContentRegion"].RequestNavigate(obj)); } }
<StackPanel Orientation="Vertical"> <!-- 传入 ViewA 用于 Content 区域跳转 --> <Button Content="ModuleA" Command="{Binding OpenCommand}" CommandParameter="ViewA"/> <StackPanel Orientation="Horizontal"> <ContentControl prism:RegionManager.RegionName="CatalogRegion"/> <ContentControl prism:RegionManager.RegionName="ContentRegion" /> </StackPanel> </StackPanel>
- 由于 Dictionary 方式没有引用 .dll 文件,因此不能在代码中使用外部模块的类,只能通过页面事件或命令跳转页面
App.Config 配置模块目录
- 在 App.xaml.cs 中重写 CreateModuleCatalog 方法
protected override IModuleCatalog CreateModuleCatalog() { return new ConfigurationModuleCatalog(); }
- 项目根目录(与App.xaml 同级)中添加配置文件 app.config
<?xml version="1.0" encoding="utf-8" ?> <configuration> <configSections> <section name="modules" type="Prism.Modularity.ModulesConfigurationSection, Prism.Wpf" /> </configSections> <startup> </startup> <modules> <module assemblyFile=".\Modules\ModuleA.dll" moduleType="ModuleA.ModuleAProfile, ModuleA, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" moduleName="ModuleA" startupLoaded="True" /> </modules> </configuration>
XAML 配置模块目录
- 在 App.xaml.cs 中重写 CreateModuleCatalog 方法
protected override IModuleCatalog CreateModuleCatalog() { return new XamlModuleCatalog(new Uri("/Modules;component/ModuleCatalog.xaml", UriKind.Relative)); }
- 创建 ModuleCatalog.xaml 文件, 添加模块信息
<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="ModuleA" ModuleType="ModuleA.ModuleAProfile, ModuleA, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" /> </m:ModuleCatalog>
Navigation(导航)
注册导航
将页面注册为可导航的页面
public void RegisterTypes(IContainerRegistry containerRegistry) { // 将页面 ViewA 注册为导航,并将 ViewA 与 ViewAViewModel 绑定 containerRegistry.RegisterForNavigation<ViewA,ViewAViewModel>(); // 在 .xaml 中进行配置后可以实现并将 ViewA 与 ViewAViewModel 自动绑定 // containerRegistry.RegisterForNavigation<ViewA>(); // 在 <UserControl> 中添加 prism:ViewModelLocator.AutoWireViewModel = "True" // ViewAViewModel 需要放在 ViewModels 文件夹下,并且命名必须是 “页面名+ViewModel” // 可以传入参数指定别名 }
控制导航页面
注册为导航的页面需要实现 INavigationAware 或
internal class ViewAViewModel : BindableBase, INavigationAware { public bool IsNavigationTarget(NavigationContext navigationContext) { // 每次重新导航的时候是否重用已有的实例 return true; } public void OnNavigatedFrom(NavigationContext navigationContext) { // 跳转到其它页面时将会调用 } public void OnNavigatedTo(NavigationContext navigationContext) { // 从其它页面跳转过来的时候调用 } }
也可以继承 IConfirmNavigationRequest, 该接口继承自 INavigationAware
// IConfirmNavigationReques 提供的确认是否进行跳转的方法 public void ConfirmNavigationRequest(NavigationContext navigationContext, Action<bool> continuationCallback) { if (MessageBox.Show("确认跳转?", "提示", MessageBoxButton.YesNo) == MessageBoxResult.Yes) continuationCallback(true); }
INavigationAware 执行流程
导航参数 NavigationContext
发送导航时携带 NavigationParameters
void Open(string obj) { NavigationParameters keys = new NavigationParameters(); keys.Add("title", "Hello"); regionManager.Regions["ContentRegion"].RequestNavigate(obj, keys); }
通过 NavigationContext 中获取到传递的所有参数
public void OnNavigatedTo(NavigationContext navigationContext) { // 从其它页面跳转过来的时候调用 if (navigationContext.Parameters.ContainsKey("title")) { this.Title = navigationContext.Parameters.GetValue<string>("title"); } }
导航日志 IRengionNavigationJournal
通过导航回调函数获取上下文并获得导航日志
void Open(string obj) { NavigationParameters keys = new NavigationParameters(); keys.Add("title", "Hello"); regionManager.Regions["ContentRegion"].RequestNavigate(obj,callback=> { if ((bool)callback.Result) journal = callback.Context.NavigationService.Journal; }, keys); }
调用 GoBack、GoForward 方法
GoBackCommand = new DelegateCommand(() => { if (journal.CanGoBack) journal.GoBack(); }); GoForwardCommand = new DelegateCommand(() => { if (journal.CanGoForward) journal.GoForward(); });
Dialog(对话服务)
概念
Prism 提供了一组对话服务, 封装了常用的对话框组件的功能:
RegisterDialog/IDialogService (注册对话及使用对话)
打开对话框传递参数/关闭对话框返回参数
回调通知对话结果
快速入门
创建用户控件并实现 IDialogAware
internal class NotifyDialogViewModel : BindableBase, IDialogAware { public event Action<IDialogResult> RequestClose; public bool CanCloseDialog() { // 能否关闭对话框 return true; } public void OnDialogClosed() { // 关闭对话框时调用 } public void OnDialogOpened(IDialogParameters parameters) { // 打开对话框时调用,获取传入的参数 Title = parameters.GetValue<string>("title"); Content = parameters.GetValue<string>( "content"); } }
在 Dialog 所在的模块中注册对话框 RegisterDialog
// ModuleAProfile public void RegisterTypes(IContainerRegistry containerRegistry) { containerRegistry.RegisterDialog<NotifyDialog,NotifyDialogViewModel>(); }
调用对话框:注入 IDialogService 接口并调用其 Show/ShowDialog 方法
// MainWindowModel public MainWindowViewModel(IRegionManager regionManager,IDialogService dialogService) { this.regionManager = regionManager; this.dialogService = dialogService; // 打开弹窗 OpenDialogCommand = new DelegateCommand<string>(OpenDialog); } void OpenDialog(string obj) { DialogParameters pairs = new DialogParameters(); pairs.Add("title", "测试弹窗"); pairs.Add("content", "弹窗内容"); dialogService.ShowDialog(obj, pairs, callback =>{//回调方法用于接受参数}); }
弹窗结果返回及参数传递
// NotifyDialogViewModel // 绑定页面按钮命令 YesBtnCommand = new DelegateCommand(() => { // 定义传递参数 DialogParameters pairs = new DialogParameters(); pairs.Add("value", "Hello"); // 关闭窗口并返回 Yes 和参数 RequestClose?.Invoke(new DialogResult(ButtonResult.Yes, pairs)); }); // MainWindowModel // 打开弹窗 void OpenDialog(string obj) { // 向 Dialog 传递参数 DialogParameters pairs = new DialogParameters(); pairs.Add("title", "测试弹窗"); pairs.Add("content", "弹窗内容"); dialogService.ShowDialog(obj, pairs, callback => { // 接受 Dialog 返回的参数 if (callback.Result == ButtonResult.Yes) string msg = callback.Parameters.GetValue<string>("value"); }); }
封装成扩展方法
用于实现打开, 传递参数, 接收到指定的返回结果的功能
// DialogExtention.cs public static class DialogExtention { // 扩展方法第一个参数需要有 this 修饰 public static void ShowNotify(this IDialogService dialogService, string message,Action<IDialogResult> callback) { var pairs = new DialogParameters(); pairs.Add("message", message); // 调用自定义的通知对话框 dialogService.ShowDialog("NotifyDialog",pairs, callback); } }
// MainWindowModel 直接调用扩展方法 void OpenDialog(string obj) { dialogService.ShowNotify("Test Extention Function.", callback =>{ if (callback.Result == ButtonResult.Yes) { string msg = callback.Parameters.GetValue<string>("value"); }}); }
PubSubEvent(发布订阅)
快速入门
创建消息类并继承 PubSubEvent
// string 指定传递的消息的类型
internal class MessageEvent:PubSubEvent<string> { }
通过构造函数注入 IEventAggregator 接口,用于发布或订阅事件
public MainWindowViewModel(IEventAggregator aggregator){ }
订阅消息
aggregator.GetEvent<MessageEvent>().Subscribe(MessageSub);
void MessageSub(string msg)
{
MessageBox.Show(msg);
}
发布消息
aggregator.GetEvent<MessageEvent>().Publish("Hello");
取消订阅
aggregator.GetEvent<MessageEvent>().Unsubscribe(MessageSub);