(2/7).NET Core 3 WPF MVVM框架 Prism系列之命令
| RyzenAdorer

本文来自转载

原文作者:RyzenAdorer

原文标题:.NET Core 3 WPF MVVM 框架 Prism 系列之命令

原文链接:https://www.cnblogs.com/ryzen/p/12143825.html

本文将介绍如何在.NET Core3 环境下使用 MVVM 框架 Prism 的命令的用法

一.创建 DelegateCommand 命令

我们在上一篇.NET Core 3 WPF MVVM 框架 Prism 系列之数据绑定中知道 prism 实现数据绑定的方式,我们按照标准的写法来实现,我们分别创建 Views 文件夹和 ViewModels 文件夹,将 MainWindow 放在 Views 文件夹下,再在 ViewModels 文件夹下面创建 MainWindowViewModel 类,如下:

xaml 代码如下:

<Window
  x:Class="CommandSample.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/"
  xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
  xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  xmlns:local="clr-namespace:CommandSample"
  mc:Ignorable="d"
  Title="MainWindow"
  Height="350"
  Width="450"
  prism:ViewModelLocator.AutoWireViewModel="True"
>
  <StackPanel>
    <TextBox Margin="10" Text="{Binding CurrentTime}" FontSize="32" />
    <button
      x:Name="mybtn"
      FontSize="30"
      Content="Click Me"
      Margin="10"
      Height="60"
      Command="{Binding GetCurrentTimeCommand}"
    />
    <Viewbox Height="80">
      <CheckBox
        IsChecked="{Binding IsCanExcute}"
        Content="CanExcute"
        Margin="10"
        HorizontalAlignment="Center"
        VerticalAlignment="Center"
      />
    </Viewbox>
  </StackPanel>
</Window>

MainWindowViewModel 类代码如下:

using Prism.Commands;
using Prism.Mvvm;
using System;
using System.Windows.Controls;

namespace CommandSample.ViewModels
{
   public class MainWindowViewModel: BindableBase
    {
        private bool _isCanExcute;
        public bool IsCanExcute
        {
            get { return _isCanExcute; }
            set
            {
                SetProperty(ref _isCanExcute, value);
                GetCurrentTimeCommand.RaiseCanExecuteChanged();
            }
        }

        private string _currentTime;
        public string CurrentTime
        {
            get { return _currentTime; }
            set { SetProperty(ref _currentTime, value); }
        }

        private DelegateCommand _getCurrentTimeCommand;
        public DelegateCommand GetCurrentTimeCommand =>
            _getCurrentTimeCommand ?? (_getCurrentTimeCommand = new DelegateCommand(ExecuteGetCurrentTimeCommand, CanExecuteGetCurrentTimeCommand));

        void ExecuteGetCurrentTimeCommand()
        {
            this.CurrentTime = DateTime.Now.ToString();
        }

        bool CanExecuteGetCurrentTimeCommand()
        {
            return IsCanExcute;
        }
    }
}

运行效果如下:

在代码中,我们通过 using Prism.Mvvm 引入继承 BindableBase,因为我们要用到属性改变通知方法 SetProperty,这在我们上一篇就知道了,再来我们 using Prism.Commands,我们所定义的 DelegateCommand 类型就在该命名空间下,我们知道,ICommand 接口是有三个函数成员的,事件 CanExecuteChanged,一个返回值 bool 的,且带一个参数为 object 的 CanExecute 方法,一个无返回值且带一个参数为 object 的 Execute 方法,很明显我们实现的 GetCurrentTimeCommand 命令就是一个不带参数的命令

还有一个值得注意的是,我们通过 Checkbox 的 IsChecked 绑定了一个 bool 属性 IsCanExcute,且在 CanExecute 方法中 return IsCanExcute,我们都知道 CanExecute 控制着 Execute 方法的是否能够执行,也控制着 Button 的 IsEnable 状态,而在 IsCanExcute 的 set 方法我们增加了一句:

GetCurrentTimeCommand.RaiseCanExecuteChanged();

其实通过 prism 源码我们可以知道 RaiseCanExecuteChanged 方法就是内部调用 ICommand 接口下的 CanExecuteChanged 事件去调用 CanExecute 方法

public void RaiseCanExecuteChanged()
{
    OnCanExecuteChanged();
}

protected virtual void OnCanExecuteChanged()
{
    EventHandler handler = this.CanExecuteChanged;
    if (handler != null)
    {
        if (_synchronizationContext != null && _synchronizationContext != SynchronizationContext.Current)
        {
            _synchronizationContext.Post(delegate
            {
                handler(this, EventArgs.Empty);
            }, null);
        }
        else
        {
            handler(this, EventArgs.Empty);
        }
    }
}

其实上述 prism 还提供了一个更简洁优雅的写法:

 private bool _isCanExcute;
 public bool IsCanExcute
 {
    get { return _isCanExcute; }
    set { SetProperty(ref _isCanExcute, value);}
 }

 private DelegateCommand _getCurrentTimeCommand;
 public DelegateCommand GetCurrentTimeCommand =>
    _getCurrentTimeCommand ?? (_getCurrentTimeCommand = new  DelegateCommand(ExecuteGetCurrentTimeCommand).ObservesCanExecute(()=> IsCanExcute));

 void ExecuteGetCurrentTimeCommand()
 {
    this.CurrentTime = DateTime.Now.ToString();
 }

其中用了 ObservesCanExecute 方法,其实在该方法内部中也是会去调用 RaiseCanExecuteChanged 方法

我们通过上面代码我们可以会引出两个问题:

  • 如何创建带参数的 DelegateCommand?
  • 假如控件不包含依赖属性 Command,我们要用到该控件的事件,如何转为命令?

二. 创建 DelegateCommand 带参命令

在创建带参的命令之前,我们可以来看看 DelegateCommand 的继承链和暴露出来的公共方法,详细的实现可以去看下源码

那么,其实已经很明显了,我们之前创建 DelegateCommand 不是泛型版本,当创建一个泛型版本的DelegateCommand<T>,那么 T 就是我们要传入的命令参数的类型,那么,我们现在可以把触发命令的 Button 本身作为命令参数传入

xaml 代码如下:

<button
  x:Name="mybtn"
  FontSize="30"
  Content="Click Me"
  Margin="10"
  Height="60"
  Command="{Binding GetCurrentTimeCommand}"
  CommandParameter="{Binding RelativeSource={RelativeSource Mode=Self}}"
/>

GetCurrentTimeCommand 命令代码改为如下:

private DelegateCommand<object> _getCurrentTimeCommand;
public DelegateCommand<object> GetCurrentTimeCommand =>
    _getCurrentTimeCommand ?? (_getCurrentTimeCommand = new DelegateCommand<object>(ExecuteGetCurrentTimeCommand).ObservesCanExecute(()=> IsCanExcute));

 void ExecuteGetCurrentTimeCommand(object parameter)
 {
    this.CurrentTime =((Button)parameter)?.Name+ DateTime.Now.ToString();
 }

我们来看看执行效果:

三. 事件转命令

在我们大多数拥有 Command 依赖属性的控件,大多数是由于继承了 ICommandSource 接口,ICommandSource 接口拥有着三个函数成员 ICommand 接口类型属性 Command,object 类型属性 CommandParameter,IInputElement 类型属性 CommandTarget,而基本继承着 ICommandSource 接口这两个基础类的就是 ButtonBase 和 MenuItem,因此像 Button,Checkbox,RadioButton 等继承自 ButtonBase 拥有着 Command 依赖属性,而 MenuItem 也同理。但是我们常用的 Textbox 那些就没有。

现在我们有这种需求,我们要在这个界面基础上新增第二个 Textbox,当 Textbox 的文本变化时,需要将按钮的 Name 和第二个 Textbox 的文本字符串合并更新到第一个 Textbox 上,我们第一直觉肯定会想到用 Textbox 的 TextChanged 事件,那么如何将 TextChanged 转为命令?

首先我们在 xaml 界面引入:

xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"

该程序集 System.Windows.Interactivity dll 是在 Expression Blend SDK 中的,而 Prism 的包也也将其引入包含在内了,因此我们可以直接引入,然后我们新增第二个 Textbox 的代码:

<TextBox
  Margin="10"
  FontSize="32"
  Text="{Binding Foo,UpdateSourceTrigger=PropertyChanged}"
>
  <i:Interaction.Triggers>
    <i:EventTrigger EventName="TextChanged">
      <i:InvokeCommandAction
        Command="{Binding TextChangedCommand}"
        CommandParameter="{Binding ElementName=mybtn}"
      />
    </i:EventTrigger>
  </i:Interaction.Triggers>
</TextBox>

MainWindowViewModel 新增代码:

private string _foo;
public string Foo
{
     get { return _foo; }
     set { SetProperty(ref _foo, value); }
}

private DelegateCommand<object> _textChangedCommand;
public DelegateCommand<object> TextChangedCommand =>
  _textChangedCommand ?? (_textChangedCommand = new DelegateCommand<object>(ExecuteTextChangedCommand));

void ExecuteTextChangedCommand(object parameter)
{
  this.CurrentTime = Foo + ((Button)parameter)?.Name;
}

执行效果如下:

上面我们在 xaml 代码就是添加了对 TextBox 的 TextChanged 事件的 Blend EventTrigger 的侦听,每当触发该事件,InvokeCommandAction 就会去调用 TextChangedCommand 命令

3.1. 将 EventArgs 参数传递给命令#

我们知道,TextChanged 事件是有个 RoutedEventArgs 参数 TextChangedEventArgs,假如我们要拿到该 TextChangedEventArgs 或者是 RoutedEventArgs 参数里面的属性,那么该怎么拿到,我们使用 System.Windows.Interactivity 的 NameSpace 下的 InvokeCommandAction 是不能做到的,这时候我们要用到 prism 自带的 InvokeCommandAction 的 TriggerParameterPath 属性,我们现在有个要求,我们要在第一个 TextBox,显示我们第二个 TextBox 输入的字符串加上触发该事件的控件的名字,那么我们可以用到其父类 RoutedEventArgs 的 Soucre 属性,而激发该事件的控件就是第二个 TextBox

xaml 代码修改如下:

<TextBox
  x:Name="myTextBox"
  Margin="10"
  FontSize="32"
  Text="{Binding Foo,UpdateSourceTrigger=PropertyChanged}"
  TextChanged="TextBox_TextChanged"
>
  <i:Interaction.Triggers>
    <i:EventTrigger EventName="TextChanged">
      <prism:InvokeCommandAction
        Command="{Binding TextChangedCommand}"
        TriggerParameterPath="Source"
      />
    </i:EventTrigger>
  </i:Interaction.Triggers>
</TextBox>

MainWindowViewModel 修改如下:

void ExecuteTextChangedCommand(object parameter)
{
    this.CurrentTime = Foo + ((TextBox)parameter)?.Name;
}

实现效果:

还有一个很有趣的现象,假如上述 xaml 代码将 TriggerParameterPath 去掉,我们其实拿到的是 TextChangedEventArgs

四.实现基于 Task 的命令

首先我们在界面新增一个新的按钮,用来绑定新的基于 Task 的命令,我们将要做的就是点击该按钮后,第一个 Textbox 的在 5 秒后显示"Hello Prism!",且期间 UI 界面不阻塞

xaml 界面新增按钮代码如下:

<button
  x:Name="mybtn1"
  FontSize="30"
  Content="Click Me 1"
  Margin="10"
  Height="60"
  Command="{Binding AsyncCommand}"
/>

MainWindowViewModel 新增代码:

private DelegateCommand _asyncCommand;
  public DelegateCommand AsyncCommand =>
     _asyncCommand ?? (_asyncCommand = new DelegateCommand(ExecuteAsyncCommand));

  async void ExecuteAsyncCommand()
  {
     await ExampleMethodAsync();
  }

  async Task ExampleMethodAsync()
  {
     await Task.Run(()=>
     {
        Thread.Sleep(5000);
        this.CurrentTime = "Hello Prism!";
     } );
  }

也可以更简洁的写法:

 private DelegateCommand _asyncCommand;
 public DelegateCommand AsyncCommand =>
    _asyncCommand ?? (_asyncCommand = new DelegateCommand( async()=>await ExecuteAsyncCommand()));

 Task ExecuteAsyncCommand()
 {
    return Task.Run(() =>
    {
       Thread.Sleep(5000);
       this.CurrentTime = "Hello Prism!";
    });
  }

直接看效果:

五. 创建复合命令

prism 提供 CompositeCommand 类支持复合命令,什么是复合命令,我们可能有这种场景,一个主界面的不同子窗体都有其各自的业务,假如我们可以将上面的例子稍微改下,我们分为三个不同子窗体,三个分别来显示当前年份,月日,时分秒,我们希望在主窗体提供一个按钮,点击后能够使其同时显示,这时候就有一种关系存在了,主窗体按钮依赖于三个子窗体的按钮,而子窗体的按钮不依赖于主窗体的按钮

下面是创建和使用一个 prism 标准复合命令的流程:

  • 创建一个全局的复合命令
  • 通过 IOC 容器注册其为单例
  • 给复合命令注册子命令
  • 绑定复合命令

5.1. 创建一个全局的复合命令

首先,我们创建一个类库项目,新增 ApplicationCommands 类作为全局命令类,代码如下:

public interface IApplicationCommands
{
    CompositeCommand GetCurrentAllTimeCommand { get; }
}

public class ApplicationCommands : IApplicationCommands
{
   private CompositeCommand _getCurrentAllTimeCommand = new CompositeCommand();
   public CompositeCommand GetCurrentAllTimeCommand
   {
        get { return _getCurrentAllTimeCommand; }
   }
}

其中我们创建了 IApplicationCommands 接口,让 ApplicationCommands 实现了该接口,目的是为了下一步通过 IOC 容器注册其为全局的单例接口

5.2. 通过 IOC 容器注册其为单例

我们创建一个新的项目作为主窗体,用来显示子窗体和使用复合命令,关键部分代码如下:

App.cs 代码:

using Prism.Unity;
using Prism.Ioc;
using System.Windows;
using CompositeCommandsSample.Views;
using Prism.Modularity;
using CompositeCommandsCore;

namespace CompositeCommandsSample
{

 public partial class App : PrismApplication
 {
     protected override Window CreateShell()
     {
         return Container.Resolve<MainWindow>();
     }

     //通过IOC容器注册IApplicationCommands为单例
     protected override void RegisterTypes(IContainerRegistry containerRegistry)
     {
        containerRegistry.RegisterSingleton<IApplicationCommands, ApplicationCommands>();
     }

     //注册子窗体模块
     protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog)
     {
        moduleCatalog.AddModule<CommandSample.CommandSampleMoudle>();
     }
  }
}

5.3. 给复合命令注册子命令

我们在之前的 CommandSample 解决方案下面的 Views 文件夹下新增两个 UserControl,分别用来显示月日和时分秒,在其 ViewModels 文件夹下面新增两个 UserControl 的 ViewModel,并且将之前的 MainWindow 也改为 UserControl,大致结构如下图:

关键部分代码:

GetHourTabViewModel.cs:

IApplicationCommands _applicationCommands;

public GetHourTabViewModel(IApplicationCommands applicationCommands)
{
    _applicationCommands = applicationCommands;
    //给复合命令GetCurrentAllTimeCommand注册子命令GetHourCommand
    _applicationCommands.GetCurrentAllTimeCommand.RegisterCommand(GetHourCommand);
}

private DelegateCommand _getHourCommand;
public DelegateCommand GetHourCommand =>
   _getHourCommand ?? (_getHourCommand = new DelegateCommand(ExecuteGetHourCommand).ObservesCanExecute(() => IsCanExcute));

void ExecuteGetHourCommand()
{
   this.CurrentHour = DateTime.Now.ToString("HH:mm:ss");
}

GetMonthDayTabViewModel.cs:

 IApplicationCommands _applicationCommands;

 public GetMonthDayTabViewModel(IApplicationCommands applicationCommands)
 {
     _applicationCommands = applicationCommands;
     //给复合命令GetCurrentAllTimeCommand注册子命令GetMonthCommand
     _applicationCommands.GetCurrentAllTimeCommand.RegisterCommand(GetMonthCommand);
 }

 private DelegateCommand _getMonthCommand;
 public DelegateCommand GetMonthCommand =>
      _getMonthCommand ?? (_getMonthCommand = new DelegateCommand(ExecuteCommandName).ObservesCanExecute(()=>IsCanExcute));

 void ExecuteCommandName()
 {
    this.CurrentMonthDay = DateTime.Now.ToString("MM:dd");
 }

MainWindowViewModel.cs:

IApplicationCommands _applicationCommands;

public MainWindowViewModel(IApplicationCommands applicationCommands)
{
    _applicationCommands = applicationCommands;
    //给复合命令GetCurrentAllTimeCommand注册子命令GetYearCommand
    _applicationCommands.GetCurrentAllTimeCommand.RegisterCommand(GetYearCommand);
}

private DelegateCommand _getYearCommand;
public DelegateCommand GetYearCommand =>
   _getYearCommand ?? (_getYearCommand = new DelegateCommand(ExecuteGetYearCommand).ObservesCanExecute(()=> IsCanExcute));

void ExecuteGetYearCommand()
{
   this.CurrentTime =DateTime.Now.ToString("yyyy");
}

CommandSampleMoudle.cs:

using CommandSample.ViewModels;
using CommandSample.Views;
using Prism.Ioc;
using Prism.Modularity;
using Prism.Regions;

namespace CommandSample
{
  public class CommandSampleMoudle : IModule
  {
    public void OnInitialized(IContainerProvider containerProvider)
    {
       var regionManager = containerProvider.Resolve<IRegionManager>();
       IRegion region= regionManager.Regions["ContentRegion"];

       var mainWindow = containerProvider.Resolve<MainWindow>();
       (mainWindow.DataContext as MainWindowViewModel).Title = "GetYearTab";
       region.Add(mainWindow);

       var getMonthTab = containerProvider.Resolve<GetMonthDayTab>();
       (getMonthTab.DataContext as GetMonthDayTabViewModel).Title = "GetMonthDayTab";
       region.Add(getMonthTab);

       var getHourTab = containerProvider.Resolve<GetHourTab>();
       (getHourTab.DataContext as GetHourTabViewModel).Title = "GetHourTab";
       region.Add(getHourTab);
    }

    public void RegisterTypes(IContainerRegistry containerRegistry)
    {

    }
  }
}

5.4. 绑定复合命令

主窗体 xaml 代码:

<Window
  x:Class="CompositeCommandsSample.Views.MainWindow"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  xmlns:prism="http://prismlibrary.com/"
  xmlns:local="clr-namespace:CompositeCommandsSample"
  mc:Ignorable="d"
  prism:ViewModelLocator.AutoWireViewModel="True"
  Title="MainWindow"
  Height="650"
  Width="800"
>
  <Window.Resources>
    <style TargetType="TabItem">
      <Setter Property="Header" Value="{Binding DataContext.Title}"/>
    </style>
  </Window.Resources>
  <Grid>
    <Grid.RowDefinitions>
      <RowDefinition Height="auto" />
      <RowDefinition Height="*" />
    </Grid.RowDefinitions>
    <button
      Content="GetCurrentTime"
      FontSize="30"
      Margin="10"
      Command="{Binding ApplicationCommands.GetCurrentAllTimeCommand}"
    />
    <TabControl Grid.Row="1" prism:RegionManager.RegionName="ContentRegion" />
  </Grid>
</Window>

MainWindowViewModel.cs:

using CompositeCommandsCore;
using Prism.Mvvm;

namespace CompositeCommandsSample.ViewModels
{
  public  class MainWindowViewModel:BindableBase
  {
    private IApplicationCommands _applicationCommands;
    public IApplicationCommands  ApplicationCommands
    {
       get { return _applicationCommands; }
       set { SetProperty(ref _applicationCommands, value); }
    }

    public MainWindowViewModel(IApplicationCommands applicationCommands)
    {
        this.ApplicationCommands = applicationCommands;
    }
  }
}

最后看看实际的效果如何:

最后,其中复合命令也验证我们一开始说的关系,复合命令依赖于子命令,但子命令不依赖于复合命令,因此,只有当三个子命令的都为可执行的时候才能执行复合命令,其中用到的 prism 模块化的知识,我们下一篇会仔细探讨

网站统计
网站创建
5年
文章分类
7个
文章总计
471篇
文章原创
106篇(22.51%)