(2/7).net core 3 wpf mvvm框架 prism系列之命令

(2/7).net core 3 wpf mvvm框架 prism系列之命令

如何在.net core3環境下使用mvvm框架prism的命令的用法

最后更新 2023/6/10 下午11:41
RyzenAdorer
预计阅读 13 分钟
分类
Blazor
专题
wpf mvvm框架 prism系列
标签
.NET C# Blazor WPF Prism

本文來自轉載

原文作者: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 模塊化的知識,我們下一篇會仔細探討

Keep Exploring

延伸阅读

更多文章