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