视频地址:www.bilibili.com/video/av543…
1 创建Prism程序
1.1 从WPF项目变更
-
创建WPF项目
-
安装Nuget组件
Install-Package Prism.DryIoc -
修改启动类App继承自PrismApplication,并重写方法
public partial class App : PrismApplication { protected override Window CreateShell() { // 使用Prism提供的容器注入MainWindow窗口 return Container.Resolve<MainWindow>(); } protected override void RegisterTypes(IContainerRegistry containerRegistry) { // 用于将其他类型注入IoC容器中 } } -
修改App.xaml
<prism:PrismApplication x:Class="prism_wpf.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:prism_wpf" xmlns:prism="http://prismlibrary.com/" > <Application.Resources> </Application.Resources> </prism:PrismApplication>- 添加命名空间
xmlns:prism="http://prismlibrary.com/" - 修改根标签为
prism:PrismApplication - 移除
StartupUri,(Prism从CreateShell()启动)
- 添加命名空间
1.2 从模板创建项目
- 安装拓展(扩展 - 管理扩展) Prism Template Pack
- 启动时,使用 Prsim Blank App 创建项目,创建时会提示要使用的容器类型,选择DryIoc
2 Region(区域)
传统应用程序开发中,视图中的组件基本是固定的。使用Region后,可以通过代码动态变更Region标记的组件。
2.1 使用内置的Region
-
Prism对一些组件已经适配了Region,可通过指定RegionName来作为Region使用,可通过2种方式指定:
- 在XAML中,通过
RegionManager.RegionName属性指定
<Window x:Class="BlankApp1.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="Test" Height="350" Width="525" > <Grid> <ContentControl prism:RegionManager.RegionName="ContentRegion" /> </Grid> </Window>- 在代码中,通过
RegionManager.SetRegionName指定:(需要先给组件增加x:Name="ctr"来获取该组件)
public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); RegionManager.SetRegionName(ctr, "ContentRegion"); } } - 在XAML中,通过
-
创建用户控件(WPF) ViewA
<UserControl x:Class="BlankApp1.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:BlankApp1.Views" mc:Ignorable="d" d:DesignHeight="450" d:DesignWidth="800"> <Grid Background="Yellow"> <TextBlock HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="60">Prism!</TextBlock> </Grid> </UserControl> -
修改MainWindow的ViewModel
public class MainWindowViewModel : BindableBase { private readonly IRegionManager regionManager; public MainWindowViewModel(IRegionManager regionManager) { this.regionManager = regionManager; regionManager.RegisterViewWithRegion("ContentRegion", typeof(ViewA)); } }- 增加IRegionManager字段,其会通过构造函数,由Prism提供的IoC容器注入
- 调用
RegisterViewWithRegion方法,可将指定View实例化到Region中
2.2 RegionManager的用途
-
维护区域集合
-
提供对区域的访问
var regions = regionManager.Regions["ContentRegion"]; -
合成视图
-
区域导航
-
定义区域
RegionManager.SetRegionName(ctr, "ContentRegion");
2.3 RegionAdapter
Region通过适配器实现对组件内容的替换,而Prism官方实现了以下组件的适配器:
- ContentControlRegionAdapter
- ItemsControlRegionAdapter
- SelectorRegionAdapter
- ComboBox
- ListBox
- Ribbon
- TabControl
因此我们可以直接在上述组件中定义Region,但若在其他组件上定义Region时就会引发异常
但我们可以实现自己的RegionAdapter,从而可以在其他组件中定义Region,以StackPanel为例:
-
创建StackPanelRegionAdapter
using Prism.Regions; using System.Windows; using System.Windows.Controls; namespace BlankApp1; public class StackPanelRegionAdapter : RegionAdapterBase<StackPanel> { public StackPanelRegionAdapter(IRegionBehaviorFactory regionBehaviorFactory) : base(regionBehaviorFactory) { } // 编写将组件注入到Region组件的逻辑 protected override void Adapt(IRegion region, StackPanel regionTarget) { // 检测Region中视图的变化 region.Views.CollectionChanged += (s, e) => { // 如果是新增事件 if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add) { // 将新增元素添加到Region组件中 foreach (FrameworkElement item in e.NewItems) { regionTarget.Children.Add(item); } } }; } // 创建Region区域 protected override IRegion CreateRegion() { return new Region(); } } -
在App中注册RegionAdapter
public partial class App { protected override Window CreateShell() { return Container.Resolve<MainWindow>(); } protected override void RegisterTypes(IContainerRegistry containerRegistry) { } // 重写该方法并注册 protected override void ConfigureRegionAdapterMappings(RegionAdapterMappings regionAdapterMappings) { base.ConfigureRegionAdapterMappings(regionAdapterMappings); regionAdapterMappings.RegisterMapping<StackPanel, StackPanelRegionAdapter>(); } }
3 Module(模块化)
Prism模块化允许界面在不同项目中,甚至是不同解决方案里,在主项目中,通过Region切换显示不同模块中的视图。
3.1 模块开发
创建模块的2种方式:
- 在同一解决方案中创建新项目
- 通过Prism Module WPF模板创建
创建模块:每个独立模块项目需要在根目录创建IModule接口的实现类
public class ModuleAModule : IModule
{
public void OnInitialized(IContainerProvider containerProvider)
{
// 通过IoC容器获取IRegionManager,并将模块中的视图注入到Region中
var regionManager = containerProvider.Resolve<IRegionManager>();
regionManager.RegisterViewWithRegion("ContentRegion", typeof(ViewA));
}
public void RegisterTypes(IContainerRegistry containerRegistry)
{
}
}
3.2 在主项目中引入模块
通过在主项目的App.xaml.cs中重写CreateModuleCatalog*()方法引入模块
-
通过配置文件方式引入
这种方式引入模块无需关联项目,因为它是通过dll引入的
public partial class App : PrismApplication { protected override Window CreateShell() { return Container.Resolve<MainWindow>(); } protected override void RegisterTypes(IContainerRegistry containerRegistry) { } protected override IModuleCatalog CreateModuleCatalog() { return new ConfigurationModuleCatalog(); } }此时,Prism会从根目录下的
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="ModuleA.dll" moduleType="ModuleA.ModuleAModule, ModuleA, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" moduleName="ModuleAModule" startupLoaded="True" /> </modules> </configuration> -
通过代码引入
需要在主项目中关联模块项目
protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog) { moduleCatalog.AddModule<ModuleA.ModuleAModule>(); } -
通过路径引入
protected override IModuleCatalog CreateModuleCatalog() { return new DirectoryModuleCatalog() { ModulePath = @".\Modules" }; } -
手动引入
需要在主项目中关联模块项目
Module注册到Prism会以
ModuleInfo的形式存在,包含了Module的相关信息protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog) { var moduleAType = typeof(ModuleAModule); moduleCatalog.AddModule(new ModuleInfo() { // Module的标识符,相当于Module的ID ModuleName = moduleAType.Name, // Module的AssemblyQualifiedName ModuleType = moduleAType.AssemblyQualifiedName, // 初始化模式: OnDemand - 在调用模块时时初始化,WhenAvailable - 程序启动时初始化 InitializationMode = InitializationMode.OnDemand }); } -
通过XAML引入
需要在主项目中关联模块项目
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="ModuleAModule" ModuleType="ModuleA.ModuleAModule, ModuleA, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" /> </m:ModuleCatalog>
3.3 视图注入
每个子模块可以先注册不显示。再使用IRegionManager实现导航。
- 在模块配置类中重写
RegisterTypes方法,将视图注测到导航中 - 通过
RequestNavigate()将Region导航至指定视图
public class Module1Module : IModule
{
public void OnInitialized(IContainerProvider containerProvider)
{
var regionManager = containerProvider.Resolve<IRegionManager>();
// 导航到指定View有2种方式,任选其一:
// 1.先从Regions中获取指定Region,在调用其RequestNavigate方法
regionManager.Regions["ContentRegion"].RequestNavigate("ViewA");
// 2.直接调用IRegionManager的RequestNavigate方法
regionManager.RequestNavigate("ContentRegion", "ViewA");
}
public void RegisterTypes(IContainerRegistry containerRegistry)
{
containerRegistry.RegisterForNavigation<ViewA>();
}
}
4 MVVM
常见的MVVM框架当中基本都围绕了ICommand、INotifyPropertyChanged封装实现绑定、通知等功能。而对于不同的框架,大部分只是写法不同,实现的功能都一样。下图列举了几个常见的框架的区别。
| 功能 | Prism | Mvvmlight | Micorosoft.Toolkit.Mvvm |
|---|---|---|---|
| 通知 | BindableBase | ViewModelBase | ObservableObject |
| 命令 | DelegateCommand | RelayCommand | Async/RelayCommand |
| 聚合器 | IEventAggregator | IMessenger | IMessenger |
| 模块化 | √ | x | x |
| 容器 | √ | x | x |
| 依赖注入 | √ | x | x |
| 导航 | √ | x | x |
| 对话 | √ | x | x |
4.1 ViewModel
Prism可以自动找到ViewModel与View关联,无需手动指定DataContext,但其必须遵循如下约定:
- View必须放在根目录
Views下 - ViewModel必须放在根目录
ViewModels目录下 - ViewModel的类名必须是
{View名称}ViewModel格式 - View根节点增加属性
prism:ViewModelLocator.AutoWireViewModel="True"
-
创建继承BindableBase的ViewAViewModel
public class ViewAViewModel : BindableBase { private string title; public DelegateCommand OpenCommand { get; set; } public ViewAViewModel() { Title = "Hello!"; OpenCommand = new DelegateCommand(() => Title = "Prism"); } public string Title { get { return title; } set { title = value; RaisePropertyChanged(); } } } -
在ViewA根节点上增加属性
ViewModelLocator.AutoWireViewModel<UserControl x:Class="BlankApp1.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:BlankApp1.Views" xmlns:prism="http://prismlibrary.com/" prism:ViewModelLocator.AutoWireViewModel="True" mc:Ignorable="d" d:DesignHeight="450" d:DesignWidth="800"> <Grid> <StackPanel> <Button Command="{Binding OpenCommand}" Content="UpdateText" /> <TextBlock FontSize="38" Text="{Binding Title}" /> </StackPanel> </Grid> </UserControl>
4.2 复合命令
通过CompositeCommand,可一次触发多个命令
public class ViewAViewModel : BindableBase
{
private string title;
public DelegateCommand OpenCommand1 { get; private set; }
public DelegateCommand OpenCommand2 { get; private set; }
public CompositeCommand OpenAll { get; set; }
public ViewAViewModel()
{
Title = "Hello!!!!";
OpenCommand1 = new DelegateCommand(() => Title += " Prism1");
OpenCommand2 = new DelegateCommand(() => Title += " Prism2");
OpenAll = new CompositeCommand();
OpenAll.RegisterCommand(OpenCommand1);
OpenAll.RegisterCommand(OpenCommand2);
}
public string Title
{
get { return title; }
set { title = value; RaisePropertyChanged(); }
}
}
<Grid>
<StackPanel>
<Button Command="{Binding OpenAll}" Content="UpdateText" />
<TextBlock FontSize="38" Text="{Binding Title}" />
</StackPanel>
</Grid>
4.3 事件聚合器
通过依赖注入IEventAggregator可以进行事件的订阅发布
public ViewBViewModel(IEventAggregator eventAggregator)
{
// 订阅事件
eventAggregator.GetEvent<MyCustomEvent>().Subscribe(OnReceiveMessage);
// 过滤事件
eventAggregator.GetEvent<MyCustomEvent>().Subscribe(
msg => Title += msg + "\r\n",
ThreadOption.PublisherThread,
false,
msg => msg.Equals("Hello"));
// 取消订阅
eventAggregator.GetEvent<MyCustomEvent>().Unsubscribe(OnReceiveMessage);
// 发布事件
eventAggregator.GetEvent<MyCustomEvent>().Publish("Hello");
}
public void OnReceiveMessage(string msg) {
...
}
Subscribe的4个参数:
- action:发布事件时执行的委托
- ThreadOption枚举:指定在哪个线程上接收委托回调
- keepSubscriberReferenceAlive:如果为true,则Prism.Events.PubSubEvent保留对订阅者的引用因此它不会被垃圾回收
- flter:进行筛选以评估订阅者是否应接收事件
5 Navigation(导航)
5.1 使用导航
-
在App.xaml.cs中注册导航组件
public partial class App { protected override Window CreateShell() { return Container.Resolve<MainWindow>(); } protected override void RegisterTypes(IContainerRegistry containerRegistry) { containerRegistry.RegisterForNavigation<ViewA>(); // 注册为别名 containerRegistry.RegisterForNavigation<ViewA>(“PageA”); containerRegistry.RegisterForNavigation<ViewB>(); // 不使用自动关联的方式手动关联ViewModel containerRegistry.RegisterForNavigation<ViewB, ViewBViewModel>(); } } -
导航到目标视图
regionManager.RequestNavigate("ContentRegion", "ViewA");
5.2 带参导航
导航组件的ViewModel可以实现INavigationAware来处理导航过程
public class ViewBViewModel : BindableBase, INavigationAware
{
private string title;
public string Title
{
get { return title; }
set { title = value; RaisePropertyChanged(); }
}
// 是否重用当前视图,返回true表示重用,false创建新的实例
public bool IsNavigationTarget(NavigationContext navigationContext)
{
return true;
}
// 导航离开当前页面时触发
public void OnNavigatedFrom(NavigationContext navigationContext)
{
}
// 导航到当前页面前触发,接收传入的参数以及控制是否允许导航
public void OnNavigatedTo(NavigationContext navigationContext)
{
// 获取导航传过来的参数
Title = navigationContext.Parameters.GetValue<string>("Value");
}
}
INavigationAware执行流程:
graph LR
A[RequestNavigate]
B[OnNavigatedFrom]
C[IsNavigationTarget]
D[Resolve View]
E[OnNavigatedTo]
F[Navigate Complete]
A-->B-->C-->D-->E-->F
切换导航时,可以通过第三个参数,或以URI方式传递参数
// 方法1
NavigationParameters param = new NavigationParameters();
param.Add("Value", "Hello");
regionManager.RequestNavigate("ContentRegion", "PageA", param);
// 方法2
regionManager.RequestNavigate("ContentRegion", $"PageA?Value=Hello");
5.3 拦截导航请求
可在离开当前视图时并导航到其他页面时进行确认。
修改INavigationAware为IConfirmNavigationRequest接口
public interface IConfirmNavigationRequest : INavigationAware
{
void ConfirmNavigationRequest(NavigationContext navigationContext, Action<bool> continuationCallback);
}
它继承自INavigationAware,并增加了一个新方法。它会在离开当前视图时触发,通过continuationCallback判断是否可以继续导航。
public class ViewBViewModel : BindableBase, IConfirmNavigationRequest
{
public void ConfirmNavigationRequest(NavigationContext navigationContext, Action<bool> continuationCallback)
{
if (MessageBox.Show("确认导航?", "温馨提示", MessageBoxButton.YesNo) == MessageBoxResult.No)
{
// 取消导航,留在当前视图
continuationCallback(false);
return;
}
// 继续导航到下一视图,离开当前视图
continuationCallback(true);
}
...
}
IConfirmNavigationRequest执行流程:
graph LR
A[RequestNavigate]
B[ConfirmNavigationRequest]
C[OnNavigatedFrom]
D[Continue Navigation Process]
A-->B-->C-->D
5.4 导航日志
即IRegionNavigationJournal接口,可以控制导航返回上一页、下一页。该接口包含以下功能:
- GoBack():返回上一页
- CanGoBack:是否可以返回上一页
- GoForward():返回后一页
- CanGoForward:是否可以返回后一页
可以通过以下2种方式获取该接口实例:
-
通过RegionManager获取
var journal = regionManager.Regions["ContentRegion"].NavigationService.Journal; -
通过NavigationContext获取
regionManager.RequestNavigate("ContentRegion", "ViewB", arg => { var journal = r.Context.NavigationService.Journal; });这个重载第三个参数为回调函数,会有一个NavigationResult入参,从中可以获取到NavigationContext
6 Dialog(对话服务)
Prism提供了一组对话服务(IDialogService),封装了常用对话框组件的功能:
- 对话框组件的注册
- 弹出对话框
- 向对话框传递参数
- 接收对话框的返回值
6.1 创建对话框组件
-
创建对话框的View,和常规组件一样
<UserControl x:Class="BlankApp1.Views.InputDialog" 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:BlankApp1.Views" mc:Ignorable="d" d:DesignHeight="450" d:DesignWidth="800"> <Grid Background="White"> <Grid.RowDefinitions> <RowDefinition Height="auto" /> <RowDefinition/> <RowDefinition Height="auto" /> </Grid.RowDefinitions> <TextBlock VerticalAlignment="center" FontSize="22" Text="编辑" /> <TextBox Grid.Row="1" Margin="5" HorizontalAlignment="Stretch" VerticalAlignment="Center" FontSize="39" Text="{Binding Content}" /> <StackPanel Grid.Row="2" HorizontalAlignment="Right" Orientation="Horizontal"> <Button Width="100" Height="35" Margin="10" FontSize="22" Command="{Binding SaveCommand}" Content="确认" /> <Button Width="100" Height="35" Margin="10" FontSize="22" Command="{Binding CancelCommand}" Content="取消" /> </StackPanel> </Grid> </UserControl> -
创建对话框的ViewModel,需要额外实现
IDialogAware接口public interface IDialogAware { string Title { get; } event Action<IDialogResult> RequestClose; bool CanCloseDialog(); void OnDialogClosed(); void OnDialogOpened(IDialogParameters parameters); }public class InputDialogViewModel : BindableBase, IDialogAware { private string content; public string Content { get { return content; } set { content = value; RaisePropertyChanged(); } } public DelegateCommand saveCommand { get; set; } public DelegateCommand cancelCommand { get; set; } // 标题,用不上的话就直接放这里 public string Title { get; set; } // 事件,调用时会被Prism监听到从而关闭窗口 public event Action<IDialogResult> RequestClose; // 是否允许关闭当前窗口 public bool CanCloseDialog() { return true; } // 关闭Dialog时触发 public void OnDialogClosed() { } // 打开Dialog时触发,可接收创建时传入的参数 public void OnDialogOpened(IDialogParameters parameters) { } } -
注册对话框(3种方式,和注册导航一样)
public partial class App { protected override Window CreateShell() { return Container.Resolve<MainWindow>(); } protected override void RegisterTypes(IContainerRegistry containerRegistry) { containerRegistry.RegisterDialog<InputDialog>(); // 起别名 containerRegistry.RegisterDialog<InputDialog>(“Input”); // 手动关联ViewModel containerRegistry.RegisterDialog<InputDialog, InputDialogViewModel>(); } } -
打开对话框
IDialogService dialog = _; // 通过依赖注入获取 // 非模态对话框,弹出后允许和其他窗口交互 dialog.Show("InputDialog"); // 模态对话框,弹出后不允许和其他窗口交互 dialog.ShowDialog("InputDialog");
6.2 向对话框传参
-
打开对话框时,通过第二个参数传入
DialogParameters param = new DialogParameters(); param.Add("Value", "Hello"); dialog.ShowDialog("InputDialog", param ,arg => { }); -
在对话框ViewModel中获取
public void OnDialogOpened(IDialogParameters parameters) { var value = parameters.GetValue<string>("Value"); MessageBox.Show("传入参数:" + value); }
6.3 接收对话框结果
-
打开对话框时,通过第三个参数传入回调接收结果
dialog.Show("InputDialog", param ,arg => { if (arg.Result == ButtonResult.OK) { var value = arg.Parameters.GetValue<string>("Value"); } }); -
在对话框ViewModel中,调用RequestClose.Invoke,该方法需要传入一个DialogResult参数,最终会返回给外层窗口。
public DelegateCommand saveCommand { get; set; } public DelegateCommand cancelCommand { get; set; } public InputDialogViewModel() { saveCommand = new DelegateCommand(() => { DialogParameters param = new DialogParameters(); param.Add("Value", Content); RequestClose.Invoke(new DialogResult(ButtonResult.OK, param)); }); cancelCommand = new DelegateCommand(() => { RequestClose.Invoke(new DialogResult(ButtonResult.No)); }); }