本文は転載記事です
原文作者: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 メソッドの 3 つのメンバーがあります。明らかに、実装した GetCurrentTimeCommand コマンドはパラメーターを持たないコマンドです。
もう 1 つ注目すべき点は、Checkbox の IsChecked に bool プロパティ IsCanExcute をバインドし、CanExecute メソッドで return IsCanExcute としていることです。CanExecute は Execute メソッドの実行可否を制御し、Button の IsEnable 状態も制御します。IsCanExcute の set メソッドでは次の 1 行を追加しています。
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 メソッドが呼び出されます。
上記のコードから、次の 2 つの疑問が生じます。
- パラメーターを持つ 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 の 3 つのメンバーを持っています。ICommandSource インターフェイスを継承している基本クラスは ButtonBase と MenuItem です。そのため、Button、Checkbox、RadioButton などは ButtonBase を継承して Command 依存関係プロパティを持っています。MenuItem も同様です。しかし、TextBox などにはありません。
ここで次のような要件があるとします。このインターフェースに 2 つ目の Textbox を追加し、Textbox のテキストが変更されたときに、ボタンの名前と 2 つ目の Textbox のテキスト文字列を結合して 1 つ目の Textbox に更新したいとします。まず Textbox の TextChanged イベントを思い浮かべますが、この TextChanged イベントをコマンドに変換するにはどうすればよいでしょうか?
まず、xaml に次の名前空間を追加します。
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
このアセンブリ System.Windows.Interactivity.dll は Expression Blend SDK に含まれています。Prism のパッケージにも含まれているため、直接追加できます。次に、2 つ目の 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 名前空間の InvokeCommandAction ではできません。その場合は、prism 独自の InvokeCommandAction の TriggerParameterPath プロパティを使用します。ここで、1 つ目の TextBox に、2 つ目の TextBox に入力された文字列とイベントを発生させたコントロールの名前を表示したいとします。RoutedEventArgs の Source プロパティを使用すると、イベントを発生させたコントロール(つまり 2 つ目の 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 クラスを提供しており、複合コマンドをサポートしています。複合コマンドとは何でしょうか?メイン画面の異なる子フォームがそれぞれのビジネスロジックを持つシナリオがあります。例えば、上の例を少し変更して、3 つの子フォームに分け、それぞれ現在の年、月日、時分秒を表示するとします。メインフォームにボタンがあり、それをクリックすると同時に表示させたいとします。この場合、メインフォームのボタンは 3 つの子フォームのボタンに依存しますが、子フォームのボタンはメインフォームのボタンに依存しないという関係があります。
以下は、標準的な 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 フォルダに、月日と時分秒を表示するための 2 つの UserControl を追加し、ViewModels フォルダにもそれぞれの 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;
}
}
}
最後に実際の効果を見てみましょう。

最後に、複合コマンドは最初に説明した関係を検証しています。複合コマンドは子コマンドに依存しますが、子コマンドは複合コマンドに依存しません。そのため、3 つの子コマンドすべてが実行可能な場合にのみ複合コマンドが実行されます。ここで使用した prism のモジュール化の知識については、次の記事で詳しく説明します。