Prism基础教程

1、搭建prism项目

1.1 安装prism模板拓展

打开VS2022拓展选项卡 → 管理拓展 → 搜索prism → 下载Prism Template Pack拓展

安装完成后重启VS2022

1.2 新建项目

1、新建项目,搜索prism

2、选择 Prism Blank App(WPF) 创建项目

3、弹出的提示框选择默认的即可

2、prism项目与原wpf项目的区别

2.1 App.xaml.cs 的改变

namespace PrismStudy
{
    /// <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)
        {

        }
    }
}

我们可以看到App不再继承自Application类,这里的App其实继承自PrismApplication,只不过它作为部分类这里没有继承,我们可以搜索看到它的另一个声明继承自PrismApplication,如下图所示:

2.2 App.xaml 的改变

<prism:PrismApplication x:Class="PrismStudy.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:PrismStudy"
             xmlns:prism="http://prismlibrary.com/" >
    <Application.Resources>
         
    </Application.Resources>
</prism:PrismApplication>

可以看到Application都被PrismApplication取代了。而且没有StartupUri属性的设置,这里prism以及在内部处理过了。如果你在这里添加StartupUri再启动程序会发现直接启动l了两个窗口

3、Prism的类型注册

假设我们现在有个日志记录类,新建一个类,添加如下代码:

using System.Windows;

namespace PrismStudy
{
    public interface ILogger
    {
        void Log(string message);
    }

    public class Logger:ILogger
    {

        int count = 0;

        public void Log(string message)
        {
            count++;
            MessageBox.Show(message + count);
        }
    
    }
}

我们给MainWindow添加一个按钮,并在MainViewModel中编写命令处理时间

再MainWindow.xaml中编写代码如下:

<Window x:Class="PrismStudy.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>
        <Button Width="200" Height="100" Content="点我" FontSize="25" Command="{Binding TestCommand}"/>
    </Grid>
</Window>

对应的ViewModel中编写代码如下:

namespace PrismStudy.ViewModels
{
    public class MainWindowViewModel : BindableBase
    {
        private string _title = "Prism Application";
        public string Title
        {
            get { return _title; }
            set { SetProperty(ref _title, value); }
        }

        ILogger _logger;
        IContainerExtension Container;

        public MainWindowViewModel(IContainerExtension container)
        {
            Container = container;
        }

        private DelegateCommand _TestCommand;
        public DelegateCommand TestCommand =>
            _TestCommand ??= new DelegateCommand(ExecuteTestCommand);

        void ExecuteTestCommand()
        {
            _logger = Container.Resolve<ILogger>();
            _logger.Log("TestCommand 被点击了.");
        }
    }
}

我们修改构造函数,通过 prism 的依赖注入获取Container,然后从Container中获得logger实例。

3.1 Register类型注册

在App.xaml.cs中的RegisterTypes方法中注册我们的Logger类:

        protected override void RegisterTypes(IContainerRegistry containerRegistry)
        {
            containerRegistry.Register<ILogger, Logger>();
        }

这时我们点击按钮发现Count永远是1。那是因为我们每次都获取了一个新的实例。

3.2 RegisterInstance 类型注册

  containerRegistry.RegisterInstance<ILogger>(new Logger());
  containerRegistry.RegisterInstance(typeof(ILogger),new Logger());

RegisterInstance每次返回相同的实例,但是实例在注册时就已存在,生命周期由你管理,以上为两种写法

3.3 RegisterSingleton 类型注册

containerRegistry.RegisterSingleton<ILogger, Logger>();

RegisterSingleton每次返回相同的实例,实例的生命周期由容器管理,用到时才创建实例。

4、Prism命令

小技巧:我们可以通过prism模板中的快捷代码快速生成命令,使用方法:输入cmd然后按两下Tab键

4.1 基本命令

命令代码编写如下:

        private DelegateCommand _TestCommand;
        public DelegateCommand TestCommand =>
            _TestCommand ??= new DelegateCommand(ExecuteTestCommand);

        void ExecuteTestCommand()
        {
            _logger = Container.Resolve<ILogger>();
            _logger.Log("TestCommand 被点击了.");
        }

命令绑定方式如下:

 <Button Width="200" Height="100" 
Content="点我" FontSize="25" 
Command="{Binding TestCommand}"/>

4.2 传递参数

4.2.1 直接传参

控件写法:

        <Button Width="200" Height="100" Content="点我" FontSize="25" 
                Command="{Binding Test2Command}"
                CommandParameter="123"/>

命令写法(输入cmdg按两下Tab快速生成):

 private DelegateCommand<string> _Test2Command;
 public DelegateCommand<string> Test2Command =>
            _Test2Command ?? (_Test2Command = new DelegateCommand<string>(ExecuteTest2Command));

 void ExecuteTest2Command(string parameter)
 {

 }

4.2.2 ItemsControl中传参

我们创建新的命令、类、数据源:

propp 双击Tab可以快速生成属性

private DelegateCommand<Test> _test3Command;
public DelegateCommand<Test> Test3Command =>
    _test3Command ?? (_test3Command = new DelegateCommand<Test>(ExecuteTest3Command));

void ExecuteTest3Command(Test parameter)
{
    MessageBox.Show($"你点击了第 {parameter.Count} 行");
}

private List<Test> testList = new List<Test>()
   {
          new Test(){ Count=1},
          new Test(){ Count=2},
          new Test(){ Count=3},
          new Test(){ Count=4},
          new Test(){ Count=5},
   };
public List<Test> TestList
    {
        get { return testList; }
        set { SetProperty(ref testList, value); }
    }
 public class Test
 {
    public int Count { get; set; }
 }

在MainWindow.xaml中创建ItemsControl并绑定数据源和命令

<StackPanel VerticalAlignment="Center">
    <ItemsControl ItemsSource="{Binding TestList}">
        <ItemsControl.ItemTemplate>
            <DataTemplate>
                <Button
                    Width="300"
                    Height="80"
                    Command="{Binding RelativeSource={RelativeSource AncestorType=ItemsControl},
                    Path=DataContext.Test3Command}"
                    CommandParameter="{Binding .}"
                    Content="{Binding Count}"/>
            </DataTemplate>
         </ItemsControl.ItemTemplate>
    </ItemsControl>
</StackPanel>

这里解释一下命令为什么要这么写:

       首先,在ItemsControl的DataTemplate中我们的程序上下文在Test这个类中,然后这个类中并没有命令去给我们绑定,所以我们需要通过在当前视觉树中向上查找指定类型的祖先元素(这里查找的是ItemsControls),然后通过Path属性到达ItemsControls的上下文中,再找到Test3Command。

        这里的CommandParameter绑定了一个点,表示绑定当前数据项本身,这个点也可以不写,

直接简写为  CommandParameter="{Binding}"

其他AncestorType用法

查找方式语法适用场景
RelativeSource{RelativeSource AncestorType=xxx}在视觉树中向上查找
ElementName{Binding ElementName=xxx}通过名称直接引用
Source{Binding Source={StaticResource xxx}}引用静态资源
DataContext{Binding Property}默认的上下文查找

4.3 案例

现在有个需求,要求输入框有内容时,按钮可以点击,无内容时,按钮被禁用。

通常情况做法:

添加输入框:

<TextBox Height="40" Text="{Binding Value, Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"/>

注意:这里要写上Mode=TwoWay,UpdateSourceTrigger=PropertyChanged,写上之后ViewMoel和视图就进行了双向绑定,任何一方改变另一个也会进行同步,同步的触发源为文本改变时进行同步

添加IsEnable属性绑定到按钮和value值绑定到输入框并在Value的Set逻辑中设置IsEnable的值

        private bool isEnable = false;
        public bool IsEnable
        {
            get { return isEnable; }
            set { SetProperty(ref isEnable, value); }
        }

        private string _value = string.Empty;
        public string Value
        {
            get { return _value; }
            set {
                if (string.IsNullOrEmpty(value))
                {
                    IsEnable = false;
                }
                else
                {
                    IsEnable = true;
                }
                SetProperty(ref _value, value); 
            }
        }

这样逻辑就完成了,但其实我们可以有更好的做法:

不绑定IsEnable,从命令入手:

private DelegateCommand<Test> _test3Command;
public DelegateCommand<Test> Test3Command =>
            _test3Command ?? (_test3Command = new DelegateCommand<Test>(ExecuteTest3Command)
            .ObservesCanExecute(() => IsEnable));

注意:这里使用的是ObservesCanExecute,相比于平时自己写的CanExecute,ObservesCanExecute可以自动更新方法是否可用,不需要CanExecute中手动调用方法RaiseCanExecuteChanged()方法。

5、对话框

5.1 创建对话框

我们在View中按下图所示创建

我们选择Prism UserControl(WPF)并且命名为MessageBox.xaml

生成完成后,我们需要将对话框的ViewModel实现IDialogAware接口

public class MessageBoxViewModel : BindableBase,IDialogAware
{
    public MessageBoxViewModel()
    {

    }

    public string Title => "对话框";

    // 提供了一个事件,触发时表示请求关闭对话框
    public event Action<IDialogResult> RequestClose;

    // 是否要可以销毁对话框
    public bool CanCloseDialog()
    {
        return true;
    }

    // 对话框关闭后执行的方法
    public void OnDialogClosed()
    {
        throw new NotImplementedException();
    }

    // 对话框打开后执行的方法
    public void OnDialogOpened(IDialogParameters parameters)
    {
        throw new NotImplementedException();
    }
}

我们继续在App.xaml.cs中的RegisterTypes方法中注册对话框服务

containerRegistry.RegisterDialog<Views.MessageBox, MessageBoxViewModel>("Msg");

后面的"Msg"是自己起的名字,要求不能重复。

5.2 使用对话框

这里使用一个登录案例进行讲解,首先我们准备号UI界面以及对应的命令和属性进行绑定。

WPF界面:

 <Grid Width="300" Height="200" Background="Gray">
     <Grid.RowDefinitions>
         <RowDefinition Height="0.5*"/>
         <RowDefinition Height="2*"/>
         <RowDefinition Height="2*"/>
         <RowDefinition Height="1*"/>
     </Grid.RowDefinitions>
     <TextBlock Grid.Row="0" Text="{Binding Title}" FontSize="15" HorizontalAlignment="Center" VerticalAlignment="Center"/>
     <StackPanel Grid.Row="1">
         <TextBlock Text="账号:"/>
         <TextBox Text="{Binding Account}"/>
     </StackPanel>
     <StackPanel Grid.Row="2">
         <TextBlock Text="密码:"/>
         <TextBox Text="{Binding Password}"/>
     </StackPanel>
     <StackPanel Grid.Row="3" Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center">
         <Button Content="确认" Margin="0 0 50 0" Command="{Binding ConfirmCommand}"/>
         <Button Content="取消" Command="{Binding CloseCommand}"/>
     </StackPanel>
 </Grid>

对应的ViewModel:

	public class MessageBoxViewModel : BindableBase,IDialogAware
	{
        public MessageBoxViewModel()
        {

        }

        // 账号
        private string _account;
        public string Account
        {
            get { return _account; }
            set { SetProperty(ref _account, value); }
        }

        // 密码
        private string _password;
        public string Password
        {
            get { return _password; }
            set { SetProperty(ref _password, value); }
        }

        private string title;
        public string Title
        {
            get { return title; }
            set { SetProperty(ref title, value); }
        }

        // 提供了一个事件,触发时表示请求关闭对话框
        public event Action<IDialogResult> RequestClose;


        private DelegateCommand _closeCommand;
        public DelegateCommand CloseCommand =>
            _closeCommand ?? (_closeCommand = new DelegateCommand(ExecuteCloseCommand));

        private DelegateCommand _confirmCommand;
        public DelegateCommand ConfirmCommand =>
            _confirmCommand ?? (_confirmCommand = new DelegateCommand(ExecuteConfirmCommand));

        // 确认登录
        void ExecuteConfirmCommand()
        {
            IDialogParameters parameters = new DialogParameters();
            parameters.Add("Account", Account);
            parameters.Add("Password", Password);
            RequestClose.Invoke(new DialogResult(ButtonResult.Yes, parameters));
        }

        // 关闭对话框
        void ExecuteCloseCommand()
        {
            // 加个?防止非空异常
            RequestClose?.Invoke(new DialogResult(ButtonResult.No,null));
        }

        // 是否要可以销毁对话框
        public bool CanCloseDialog()
        {
            return true;
        }

        // 对话框关闭后执行的方法
        public void OnDialogClosed()
        {
            // 清空输入框
            Password = string.Empty;
            Account = string.Empty;
        }

        // 对话框打开后执行的方法
        public void OnDialogOpened(IDialogParameters parameters)
        {
            var data = parameters.GetValue<string>("title");
            Title = data;   
        }
    }

我们在主页面提供一个按钮用来打开对话框,首先在主页面的构造函数中通过依赖注入获得对话框服务

IDialogService Dialog;
public MainWindowViewModel(IDialogService dialogService)
  {
     Dialog = dialogService;
  }

然后通过一下代码来打开对话框,这里我们传入了一个Title参数,用来指定对话框的标题,参数的设置以键值对的形式设置。

注意:这里使用了ShowDialog而不是Show,前者打开的对话框是模态的,必须处理完当前对话框才能进行其他操作,后者打开的对话框为非模态的且可以打开多个。

两个方法接收的参数都是一样的:

第一个参数:要打开的对话框名称(前面注册时设置的)

第二个参数:要传入的参数

第三个参数:对话框传递回来的结果

IDialogParameters parameters = new DialogParameters();
parameters.Add("title", "这是一个对话框");
Dialog.ShowDialog("Msg", parameters, res =>
{
    if(res.Result == ButtonResult.Yes)
    {
        var Account = res.Parameters.GetValue<string>("Account");
        var Password = res.Parameters.GetValue<string>("Password");
        if(Account == null || Password == null)
        {
            return;
        }
        if(Account.Equals("123") && Password.Equals("123"))
        {
            MessageBox.Show("登录成功!");
        }
        else
        {
            MessageBox.Show("登录失败!");
        }
    }
    if(res.Result == ButtonResult.No)
    {
        // 用户关闭了对话框
        MessageBox.Show("用户关闭了对话框");
    }
});

这里关闭对话框只需要触发RequestClose事件即可,BeginInvoke和Invoke却别在于异步调用还是同步调用。这个事件要求接收IDialogResult参数,通过源码可以看到需要指定ButtonResult的值和参数。

对应的简单登录逻辑编写如下:

	public class MessageBoxViewModel : BindableBase,IDialogAware
	{
        public MessageBoxViewModel()
        {

        }

        // 账号
        private string _account;
        public string Account
        {
            get { return _account; }
            set { SetProperty(ref _account, value); }
        }

        // 密码
        private string _password;
        public string Password
        {
            get { return _password; }
            set { SetProperty(ref _password, value); }
        }

        private string title;
        public string Title
        {
            get { return title; }
            set { SetProperty(ref title, value); }
        }

        // 提供了一个事件,触发时表示请求关闭对话框
        public event Action<IDialogResult> RequestClose;


        private DelegateCommand _closeCommand;
        public DelegateCommand CloseCommand =>
            _closeCommand ?? (_closeCommand = new DelegateCommand(ExecuteCloseCommand));

        private DelegateCommand _confirmCommand;
        public DelegateCommand ConfirmCommand =>
            _confirmCommand ?? (_confirmCommand = new DelegateCommand(ExecuteConfirmCommand));

        // 确认登录
        void ExecuteConfirmCommand()
        {
            IDialogParameters parameters = new DialogParameters();
            parameters.Add("Account", Account);
            parameters.Add("Password", Password);
            RequestClose.Invoke(new DialogResult(ButtonResult.Yes, parameters));
        }

        // 关闭对话框
        void ExecuteCloseCommand()
        {
            // 加个?防止非空异常
            RequestClose?.Invoke(new DialogResult(ButtonResult.No,null));
        }

        // 是否要可以销毁对话框
        public bool CanCloseDialog()
        {
            return true;
        }

        // 对话框关闭后执行的方法
        public void OnDialogClosed()
        {
            // 清空输入框
            Password = string.Empty;
            Account = string.Empty;
        }

        // 对话框打开后执行的方法
        public void OnDialogOpened(IDialogParameters parameters)
        {
            var data = parameters.GetValue<string>("title");
            Title = data;   
        }
    }

5.3 样式修改

我们可以发现,我们注册的明明是UserControl,但是为什么弹出的是一个窗口,那是因为:

当你使用 IDialogService.ShowDialog 方法时,Prism 内部会:

  1. 创建一个新的 DialogWindow(继承自 Window)
  2. 将你的 UserControl 作为 Content 放入这个 Window
  3. 显示这个 Window

所以虽然你注册的是 UserControl,但最终显示的是一个完整的 Window。

我们可以在MessageBox.xaml中编写如下代码声明Window窗口的样式,例如我们这里将窗口样式全部清除了,且默认一打开就是最大化

    <prism:Dialog.WindowStyle>
        <Style TargetType="Window">
            <Setter Property="WindowStyle" Value="None"/>
            <Setter Property="WindowState" Value="Maximized"/>
        </Style>
    </prism:Dialog.WindowStyle>

但其实我们还可以先创建一个窗口:

再让这个页面对应的cs文件下的类实现IDialogWindow接口

    /// <summary>
    /// DialogWindowBase.xaml 的交互逻辑
    /// </summary>
    public partial class DialogWindowBase : Window, IDialogWindow
    {
        public DialogWindowBase()
        {
            InitializeComponent();
        }

        public IDialogResult Result { get ; set; }
    }

然后我们在App.xaml.cs中的RegisterTypes方法中注册这个对话框窗口

// 注册对话框基础窗口
containerRegistry.RegisterDialogWindow<DialogWindowBase>();

这样的话所有的对话框的窗口就被我们新建的窗口给替换了,我们可以在新建的窗口中编写一些通用样式。

6、事件聚合器

6.1 案例

这里我们用一个例子来学习,假设我们有一个需求,将我们4.3小节的登录案例进行改造,要求5秒没有登录后自动关闭登录页面,并展示传递的参数

先创建一个关闭事件,尖括号内为需要的参数类型,如果不需要参数可以不写尖括号。

    public class CloseEvent : PubSubEvent<int>
    {

    }

在主页面的ViewModel中通过依赖注入获得事件聚合器

IEventAggregator EventAggregator;
public MainWindowViewModel(IEventAggregator eventAggregator)
{
    EventAggregator = eventAggregator;
}

再打开对话框页面的方法中添加如下代码:

注意:要将打开对话框的方法ShowDialog改成Show,否则模态的对话卡会卡在对话框那一步导致无法往下运行代码。

逻辑为5秒后发布关闭页面的事件,并传递一个参数10(这里传递的参数没有任何意义,只是为了展示如何传递参数)

await Task.Delay(5000);
EventAggregator.GetEvent<CloseEvent>().Publish(10);

在MessageBoxViewModel的构造函数中通过依赖注入获取事件聚合器并订阅关闭页面的事件

        IEventAggregator EventAggregator;
        public MessageBoxViewModel(IEventAggregator eventAggregator)
        {
            EventAggregator = eventAggregator;
            // 订阅关闭窗体的事件
            eventAggregator.GetEvent<CloseEvent>().Subscribe(close);
        }

        void close(int value)
        {
            MessageBox.Show($"成功获取参数{value.ToString()}");
            RequestClose?.Invoke(new DialogResult(ButtonResult.No, null));
        }

6.2 注意事项

在WPF中,只有UI线程可以更新UI元素,如果发布者从UI线程发送事件,则订阅者可以更新UI。但是,如果发布者的线程是后台线程,则订阅者可能无法直接更新UI元素。在这种情况下,订阅者需要使用调用程序类在在UI线程上计划更新,我们可以在订阅事件时添加一个枚举来允许订阅者在UI线程上自动接收事件来提供帮助,代码如下:

添加了”   ThreadOption.UIThread  “

eventAggregator.GetEvent<CloseEvent>().Subscribe(close,ThreadOption.UIThread);

7、导航服务

我们在主页面中添加如下代码:

<ContentControl Grid.Row="4" prism:RegionManager.RegionName="ContentRegion"/>

该代码通过附加属性声明一个导航的区域,该区域被我们命名为“ContentRegion”。

我们在Views文件夹中新建两个UserControl,分别为View1和View2,并生成对应的ViewModel

现在我们假设要实现如下效果:启动项目,将ContentRegion区域导航到视图View1。

将两个页面都进行注册,注意这里使用的是RegisterForNavigation

// 注册为导航视图
containerRegistry.RegisterForNavigation<View1>(nameof(View1));
containerRegistry.RegisterForNavigation<View1>(nameof(View1));

在主页面的ViewModel的构造函数中通过依赖注入获得区域管理器:

IRegionManager RegionManager;
public MainWindowViewModel(IRegionManager regionManager)
{
     RegionManager = regionManager;
     // 第一个参数为要导航的区域名称
     // 第二个参数为要导航到的视图名称
     regionManager.RequestNavigate("ContentRegion","View1");
}

这样就完成了,但是当我们运行的时候发现没有效果,这是为什么呢?

这是因为在Prism项目中采用的是明确的 ViewModel-First 架构,首先实例化的是ViewModel,然后实例化的才是对应的页面,现在在页面还没实例化时就进行导航是无效的!

这里我们可以删除导航的代码,在App.xaml.cs中重写OnStartup方法,使得程序启动后再进行导航

protected override void OnStartup(StartupEventArgs e)
{
    base.OnStartup(e);

    // 第一个参数为要导航的区域名称
    // 第二个参数为要导航到的视图名称
    Container.Resolve<IRegionManager>().RequestNavigate("ContentRegion", "View1");
}

7.1 INavigationAware接口

我们将要导航的页面对应的ViewModel实现INavigationAware接口,并实现对应的方法

        public bool IsNavigationTarget(NavigationContext navigationContext)
        {
            return true;
        }

        public void OnNavigatedFrom(NavigationContext navigationContext)
        {
           
        }

        public void OnNavigatedTo(NavigationContext navigationContext)
        {
            var id =  navigationContext.Parameters.GetValue<int>("id");
        }

OnNavigatedTo:页面导航时触发,相当于Loaded事件

OnNavigatedFrom:页面离开时触发,相当于Unloaded事件

IsNavigationTarget:是否重用现有的视图实例,返回true则服用现有实例,返回false新建实例

7.2 导航传参以及回调

7.2.1 传参

 NavigationParameters keyValuePairs = new NavigationParameters();
 keyValuePairs.Add("id", 123);
 // 第一个参数为要导航的区域名称
 // 第二个参数为要导航到的视图名称
 Container.Resolve<IRegionManager>().RequestNavigate("ContentRegion", "View1", keyValuePairs);

新建NavigationParameters对象,通过Add方法以键值对的形式进行参数添加。

7.2.2 拿参

        public void OnNavigatedTo(NavigationContext navigationContext)
        {
            var id =  navigationContext.Parameters.GetValue<int>("id");
        }

在OnNavigatedTo方法中通过导航上下文拿到参数,尖括号中的为参数的数据类型

7.2.3 导航结束的回调

protected override void OnStartup(StartupEventArgs e)
{
    base.OnStartup(e);

    NavigationParameters keyValuePairs = new NavigationParameters();
    keyValuePairs.Add("id", 123);
    // 第一个参数为要导航的区域名称
    // 第二个参数为要导航到的视图名称
    Container.Resolve<IRegionManager>().RequestNavigate("ContentRegion", "View1", NavigationCompleted, keyValuePairs);
}

private void NavigationCompleted(NavigationResult result)
{

}

result中能拿到导航的一些相关参数

7.3  IRegionMemberLifetime接口

实现这个接口后会要求ViewModel中有一个属性

public bool KeepAlive => false;

该属性控制视图是否保持活跃

当导航离开某个视图时,Prism会根据 KeepAlive 属性的值来决定是否保留该视图实例:

  • KeepAlive = true:视图实例会被保留在内存中
  • KeepAlive = false:视图实例会被销毁

大多数情况下,视图不应该保持活跃,以避免内存泄漏!!!!!!!

7.4 ConfirmNavigationRequest接口

这个接口为我们提供了一个方法,相当于在离开页面时加了放错,防止页面数据还没保存,只有执行continuationCallback委托才能进行页面跳转。

        public void ConfirmNavigationRequest(NavigationContext navigationContext, Action<bool> continuationCallback)
        {
            bool result = false;

            if (MessageBox.Show("是否确定离开?","提示",MessageBoxButton.YesNo) == MessageBoxResult.OK)
            {
                result = true;
            }
            continuationCallback.Invoke(result);
        }

7.5 前进与后退

首先从导航上下文中拿到导航服务并变成全局变量,这里在View1的OnNavigatedTo方法中拿到。

public IRegionNavigationService navigationService;        
public void OnNavigatedTo(NavigationContext navigationContext)
{
    // 从导航上下文中拿到导航服务
    navigationService = navigationContext.NavigationService;
}

下面给出View1和View2的页面代码以及对应的ViewModel

View1页面:

 <Grid>
     <Grid.RowDefinitions>
         <RowDefinition />
         <RowDefinition />
         <RowDefinition />
     </Grid.RowDefinitions>
     <Button
         Grid.Row="0"
         Command="{Binding ToView2}"
         Content="跳转到View2" />
     <Button
         Grid.Row="1"
         Command="{Binding Forward}"
         Content="前进" />
     <Button
         Grid.Row="2"
         Command="{Binding Back}"
         Content="后退" />
 </Grid>

对应的ViewModel:

public class View1ViewModel : BindableBase,INavigationAware,IRegionMemberLifetime,IConfirmNavigationRequest
{
    IRegionManager RegionManager;
    public View1ViewModel(IRegionManager regionManager)
    {
        RegionManager = regionManager;
    }

    public bool KeepAlive => false;

    public IRegionNavigationService navigationService;


    // 前进
    private DelegateCommand _forward;
    public DelegateCommand Forward =>
        _forward ?? (_forward = new DelegateCommand(ExecuteForward));

    void ExecuteForward()
    {
        if (navigationService.Journal.CanGoForward)
        {
            navigationService.Journal.GoForward();
        }
    }

    // 后退
    private DelegateCommand _back;
    public DelegateCommand Back =>
        _back ?? (_back = new DelegateCommand(ExecuteBack));

    void ExecuteBack()
    {
        if (navigationService.Journal.CanGoBack)
        {
            navigationService.Journal.GoBack();
        }
    }

    // 跳转到View2
    private DelegateCommand _toView2;
    public DelegateCommand ToView2 =>
        _toView2 ?? (_toView2 = new DelegateCommand(ExecuteToView2));

    void ExecuteToView2()
    {
        RegionManager.RequestNavigate("ContentRegion", "View2");
    }

    // 导航确认请求
    public void ConfirmNavigationRequest(NavigationContext navigationContext, Action<bool> continuationCallback)
    {
        bool result = false;

        if (MessageBox.Show("是否确定离开?","提示",MessageBoxButton.YesNo) == MessageBoxResult.Yes)
        {
            result = true;
        }
        continuationCallback.Invoke(result);
    }

    public bool IsNavigationTarget(NavigationContext navigationContext)
    {
        return true;
    }

    public void OnNavigatedFrom(NavigationContext navigationContext)
    {
       
    }

    public void OnNavigatedTo(NavigationContext navigationContext)
    {
        var id =  navigationContext.Parameters.GetValue<int>("id");
        // 从导航上下文中拿到导航服务
        navigationService = navigationContext.NavigationService;
    }

View2页面:

 <Grid>
     <Grid.RowDefinitions>
         <RowDefinition />
         <RowDefinition />
         <RowDefinition />
     </Grid.RowDefinitions>
     <Button
         Grid.Row="0"
         Command="{Binding ToView1}"
         Content="跳转到View1" />
     <Button
         Grid.Row="1"
         Command="{Binding Forward}"
         Content="前进" />
     <Button
         Grid.Row="2"
         Command="{Binding Back}"
         Content="后退" />
 </Grid>

对应的ViewModel:

	public class View2ViewModel : BindableBase,INavigationAware
	{
        IRegionManager RegionManager;
        public View2ViewModel(IRegionManager regionManager)
        {
            RegionManager = regionManager;
        }

        public IRegionNavigationService navigationService;

        // 前进
        private DelegateCommand _forward;
        public DelegateCommand Forward =>
            _forward ?? (_forward = new DelegateCommand(ExecuteForward));

        void ExecuteForward()
        {
            if (navigationService.Journal.CanGoForward)
            {
                navigationService.Journal.GoForward();
            }
        }

        // 后退
        private DelegateCommand _back;
        public DelegateCommand Back =>
            _back ?? (_back = new DelegateCommand(ExecuteBack));

        void ExecuteBack()
        {
            if (navigationService.Journal.CanGoBack)
            {
                navigationService.Journal.GoBack();
            }
        }

        // 跳转到View1
        private DelegateCommand _toView1;
        public DelegateCommand ToView1 =>
            _toView1 ?? (_toView1 = new DelegateCommand(ExecuteToView1));

        void ExecuteToView1()
        {
            RegionManager.RequestNavigate("ContentRegion", "View1");
        }

        // 是否重用现有的视图实例,返回true则服用现有实例,返回false新建实例
        public bool IsNavigationTarget(NavigationContext navigationContext)
        {
            return true;
        }

        // 页面离开时触发,相当于Unloaded事件
        public void OnNavigatedFrom(NavigationContext navigationContext)
        {
            
        }

        // 页面导航时触发,相当于Loaded事件
        public void OnNavigatedTo(NavigationContext navigationContext)
        {
           // 从导航上下文中拿到导航服务
           navigationService = navigationContext.NavigationService;
        }
    }

代码中的navigationService.Journal.CanGoForwardnavigationService.Journal.CanGoBack用于判断是否可以前进和后退

7.6 控制导航记录的产生与销毁

我们随便选择一个View对应的ViewModel,实现IJournalAware接口,此时我们需要有一个方法PersistInHistory,当这个方法返回false,则对应的页面不产生导航记录,返回true则产生。

        public bool PersistInHistory()
        {
            return false;
        }

假设页面2我们设置了false以后效果如下:

        页面1导航到页面2,页面2点击后退可以回到页面1,但是页面1点击前进无法回到页面2.

8、View与ViewModel如何对应

在我们View页面,我们一般可以看到如下代码:

prism:ViewModelLocator.AutoWireViewModel="True"

这表示我们框架会自动去找页面对应的ViewModel,一般命名规则如下:View1就去找View1ViewModel,就是在页面名字后面加上ViewModel,如果不想按照这个规则来可以在类型注册时手动指定ViewModel。如下所示:

containerRegistry.RegisterForNavigation<View1,View2ViewModel>(nameof(View1));

以上代码就是将View1绑定到View2ViewModel了。

9、模块加载

9.1 两种加载方式

首先我们按照本文第一节新建一个项目,然后在新项目的解决方案处右键,点击“添加”,选择“新建项目”,我们选择Prism Module然后新建一个Login项目,再按照同样的方法新建一个Home项目

最终结构如下图所示:

我们会发现,作为模块,我们需要实现IModule接口

    public class LoginModule : IModule
    {
        public void OnInitialized(IContainerProvider containerProvider)
        {

        }

        public void RegisterTypes(IContainerRegistry containerRegistry)
        {

        }
    }

接下来,我们进行模块加载。

模块加载有两个方式:1:手动导入,2:目录扫描,不同的方式需要重写不同的方法。

我们在BlankApp1的App.xaml.cs中重写如下方法:

        protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog)
        {
            // 手动引用
            moduleCatalog.AddModule<HomeModule>();
        }

        // 目录扫描
        protected override IModuleCatalog CreateModuleCatalog()
        {
            return new DirectoryModuleCatalog()
            {
                // . 表示当前程序集目录
                ModulePath = ".\\Modules"
            };
        }

作者在这里提前将Home页面背景色改成红色,Login页面改成绿色。

在HomeModule中的OnInitialized方法中添加一个导航方法:

        public void OnInitialized(IContainerProvider containerProvider)
        {
            containerProvider.Resolve<IRegionManager>().RegisterViewWithRegion<HomePage>("ContentRegion");
        }

这样就表示导入的Home模块直接导航到HomePage页面。

补充:这里导航使用的是RegisterViewWithRegion方法,这个方法不需要在RegisterTypes中进行注册Prism会直接实例化 HomePage 类型,不经过容器的注册解析流程。如果HomePage对应的ViewModel的构造函数需要依赖注入服务会错误,所以需要依赖注入还是要使用RequestNavigate方法并进行类型注册。

这样运行时就直接导航到Home页面了(记得先注释掉CreateModuleCatalog方法,还没实现相关逻辑会报错)。

接下来我们来看目录扫描:

我们重新编译三个项目,并在主项目(BlankApp1,下面都称主项目)的生成目录下新建“Modules”文件夹(文件夹名就是我们刚刚代码中写的名字)

然后将Home项目生成的东西放入

下面我们来做一个案例,使用按钮切换的方式来验证Home模块和Login模块是否成功导入:

Home模板我们使用手动导入,Login模块我们使用目录扫描导入。

在BlankApp的MainWindow中编写如下xaml代码:

    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="3*" />
            <ColumnDefinition Width="7*" />
        </Grid.ColumnDefinitions>
        <StackPanel Grid.Column="0" Orientation="Vertical">
            <Button
                Command="{Binding ChangeView}"
                CommandParameter="HomePage"
                Content="HomePage" />
            <Button
                Command="{Binding ChangeView}"
                CommandParameter="LoginPage"
                Content="LoginPage" />
        </StackPanel>
        <ContentControl Grid.Column="1" prism:RegionManager.RegionName="ContentRegion" />
    </Grid>

在对应的ViewModel中编写逻辑:

 private DelegateCommand<string> _changeView;
 public DelegateCommand<string> ChangeView =>
     _changeView ?? (_changeView = new DelegateCommand<string>(ExecuteChangeView));

 void ExecuteChangeView(string viewName)
 {
     RegionManager.RequestNavigate("ContentRegion", viewName);
 }


 IRegionManager RegionManager;
 public MainWindowViewModel(IRegionManager regionManager)
 {
     RegionManager = regionManager;
 }

两个页面都要进行导航注册:

containerRegistry.RegisterForNavigation<HomePage>(nameof(HomePage));
containerRegistry.RegisterForNavigation<LoginPage>(nameof(LoginPage));

运行后我们发现成功导入,并且可以通过按钮切换页面。

9.2 按需加载

我们将上述案例中的HomeModule改造成按需加载

首先为HomeModule添加注解

    [Module(ModuleName = "HomeModule", OnDemand = true)]
    public class HomeModule : IModule
    {
        public void OnInitialized(IContainerProvider containerProvider)
        {
            //containerProvider.Resolve<IRegionManager>().RequestNavigate("ContentRegion", nameof(HomePage));
        }

        public void RegisterTypes(IContainerRegistry containerRegistry)
        {
            containerRegistry.RegisterForNavigation<HomePage>(nameof(HomePage));
        }
    }

我们指定了这个模块名为“HomeModule”,加载方式为需要时加载。

我们在主页面中添加触发器,使得页面加载完成后加载HomeModule模块,使用这个前记得引用命名空间:xmlns:i="http://schemas.microsoft.com/xaml/behaviors"

    <i:Interaction.Triggers>
        <i:EventTrigger EventName="Loaded">
            <i:InvokeCommandAction Command="{Binding LoadModuleCommand}" />
        </i:EventTrigger>
    </i:Interaction.Triggers>

通过在构造函数中使用依赖注入获得模块管理器并编写模块加载代码

// 页面加载完毕后执行的命令
private DelegateCommand _loadModuleCommand;
public DelegateCommand LoadModuleCommand =>
    _loadModuleCommand ?? (_loadModuleCommand = new DelegateCommand(ExecuteLoadModuleCommand));

void ExecuteLoadModuleCommand()
{
    ModuleManager.LoadModule("HomeModule");
}


IRegionManager RegionManager;
IModuleManager ModuleManager;
public MainWindowViewModel(IRegionManager regionManager,IModuleManager module)
{
    RegionManager = regionManager;
    ModuleManager = module;
}

这样子之后我们就能实现模块的按需加载!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值