MVVMパターンでのコマンド処理の実装方法について調べてみた
RelayCommandについて調べた背景
スタンドアロン版のソフトをC#で開発するとき、MVVMデザインパターンで開発するのが一般的であるとしりました。今回は、MVVMパターンを利用するために、メソッドをxamlにバインドする方法について調べたのでまとめておきます。
結論
順番が前後しますが、先に結論です。
MVVMパターンでコマンドを扱う方法です。
RelayCommand.csを作成
下記のクラスを丸コピでOKです。
このRelayCommandクラスは、WPF(Windows Presentation Foundation)アプリケーションでコマンド処理を行うための便利なユーティリティクラスです。
RelayCommandクラスを使用することで、ViewModel内でコマンドを簡単に定義し、Viewとの間でコマンドの実行や実行可能性の判定をスムーズに行うことができます。
public class RelayCommand : ICommand
{
private Action<object> _execute; // 実行するアクション
private Func<object, bool> _canExecute; // 実行可能性を判定する条件
private Stack<Action<object>> _undoStack = new Stack<Action<object>>(); // Undo 用のスタック
private Stack<Action<object>> _redoStack = new Stack<Action<object>>(); // Redo 用のスタック
// コンストラクタ
public RelayCommand(Action<object> execute, Func<object, bool> canExecute = null)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}
// ICommand インターフェースのイベント
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; } // イベントハンドラーの追加
remove { CommandManager.RequerySuggested -= value; } // イベントハンドラーの削除
}
// 実行可能性の判定
public bool CanExecute(object parameter)
{
return _canExecute == null || _canExecute(parameter);
}
// アクションの実行
public void Execute(object parameter)
{
_execute(parameter);
}
// CanExecuteChanged イベントの発火
public void RaiseCanExecuteChanged()
{
CommandManager.InvalidateRequerySuggested();
}
// 実行するアクションの設定
public void SetExecute(Action<object> execute)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
}
// 実行可能性を判定する条件の設定
public void SetCanExecute(Func<object, bool> canExecute)
{
_canExecute = canExecute;
}
// アクションと実行可能性をクリア
public void Clear()
{
_execute = null;
_canExecute = null;
}
// Undo 用のアクションをスタックに追加
public void ExecuteWithUndo(object parameter)
{
_execute(parameter); // 実行
_undoStack.Push((param) => _execute(param)); // Undo 用のアクションをスタックに追加
_redoStack.Clear(); // Redo スタックをクリア
}
// Undo 操作
public void Undo(object parameter)
{
if (_undoStack.Count > 0)
{
var undoAction = _undoStack.Pop(); // スタックからアクションを取り出す
undoAction(parameter); // アクションを実行
_redoStack.Push(undoAction); // Redo スタックに追加
}
}
// Redo 操作
public void Redo(object parameter)
{
if (_redoStack.Count > 0)
{
var redoAction = _redoStack.Pop(); // スタックからアクションを取り出す
redoAction(parameter); // アクションを実行
_undoStack.Push(redoAction); // Undo スタックに追加
}
}
// CanExecuteChanged イベントのハンドラーを追加
public void AddCanExecuteChangedHandler(EventHandler handler)
{
CommandManager.RequerySuggested += handler;
}
// CanExecuteChanged イベントのハンドラーを削除
public void RemoveCanExecuteChangedHandler(EventHandler handler)
{
CommandManager.RequerySuggested -= handler;
}
}
MainWindow.xaml
<Window x:Class="RelayCommandTest.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:local="clr-namespace:RelayCommandTest"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<StackPanel Margin="20">
<Button Content="Execute" Command="{Binding ExecuteCommand}"/>
<Button Content="Undo" Command="{Binding UndoCommand}"/>
<Button Content="Redo" Command="{Binding RedoCommand}"/>
<Button Content="Clear" Command="{Binding ClearCommand}"/>
<Button Content="RaiseCanExecuteChanged" Command="{Binding RaiseCanExecuteChangedCommand}"/>
</StackPanel>
</Grid>
</Window>
DataContextの指定はコードビハインドでしています。
xamlで指定する場合は、下記を追加してください。
<Window.DataContext>
<local:MainViewModel/>
</Window.DataContext>
MainWindow.xaml.csで行う場合は下記を追加
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new MainViewModel();
}
}
これで、MainViewModelとバインドできます。
まれに、ビルドしないとxamlでエラーが発生してしまいます。
逆にエラーが発生してもxamlで原因がある場合は、試しにビルドしてみると解消されたりします。
MainViewModel.cs
public class MainViewModel
{
public ICommand ExecuteCommand { get; }
public ICommand UndoCommand { get; }
public ICommand RedoCommand { get; }
public ICommand ClearCommand { get; }
public ICommand RaiseCanExecuteChangedCommand { get; }
public MainViewModel()
{
// ExecuteCommand に実行するアクションと実行可能性条件を設定
ExecuteCommand = new RelayCommand(
execute: Execute,
canExecute: parameter => true // いつでも実行可能
);
// UndoCommand に Undo 操作を実行するアクションを設定
UndoCommand = new RelayCommand(
execute: Undo,
canExecute: parameter => _undoStack.Count > 0 // Undo スタックが空でない場合に実行可能
);
// RedoCommand に Redo 操作を実行するアクションを設定
RedoCommand = new RelayCommand(
execute: Redo,
canExecute: parameter => _redoStack.Count > 0 // Redo スタックが空でない場合に実行可能
);
// ClearCommand にスタックをクリアするアクションを設定
ClearCommand = new RelayCommand(
execute: Clear,
canExecute: parameter => _undoStack.Count > 0 || _redoStack.Count > 0 // Undo スタックまたは Redo スタックに要素がある場合に実行可能
);
// RaiseCanExecuteChangedCommand に RaiseCanExecuteChanged を実行するアクションを設定
RaiseCanExecuteChangedCommand = new RelayCommand(
execute: RaiseCanExecuteChanged
);
}
private void Execute(object parameter)
{
Console.WriteLine("Command executed.");
// Undo スタックに実行したアクションを追加
_undoStack.Push(() => Console.WriteLine("Undo executed."));
}
private void Undo(object parameter)
{
// Undo スタックから直近のアクションを取り出し、実行
if (_undoStack.Count > 0)
{
var undoAction = _undoStack.Pop();
undoAction();
_redoStack.Push(undoAction);
}
}
private void Redo(object parameter)
{
// Redo スタックから直近のアクションを取り出し、実行
if (_redoStack.Count > 0)
{
var redoAction = _redoStack.Pop();
redoAction();
_undoStack.Push(redoAction);
}
}
private void Clear(object parameter)
{
// Undo スタックと Redo スタックをクリア
_undoStack.Clear();
_redoStack.Clear();
}
// RaiseCanExecuteChanged ボタンの実行アクション
private void RaiseCanExecuteChanged(object parameter)
{
// 各コマンドの実行可能性を再評価
((RelayCommand)ExecuteCommand).RaiseCanExecuteChanged();
((RelayCommand)UndoCommand).RaiseCanExecuteChanged();
((RelayCommand)RedoCommand).RaiseCanExecuteChanged();
((RelayCommand)ClearCommand).RaiseCanExecuteChanged();
((RelayCommand)RaiseCanExecuteChangedCommand).RaiseCanExecuteChanged();
}
private readonly Stack<Action> _undoStack = new Stack<Action>();
private readonly Stack<Action> _redoStack = new Stack<Action>();
}
- ICommandの変数を宣言
public ICommand ExecuteCommand { get; }
- コンストラクタで、ExecuteCommandを初期化します。
// ExecuteCommand に実行するアクションと実行可能性条件を設定
ExecuteCommand = new RelayCommand(
execute: Execute,
canExecute: parameter => true // いつでも実行可能
);
- Executeメソッドを作成
private void Execute(object parameter)
{
Console.WriteLine("Command executed.");
// Undo スタックに実行したアクションを追加
_undoStack.Push(() => Console.WriteLine("Undo executed."));
}
UIのイメージ
①Executeボタンを押すと処理が実行。
②UndoボタンとClearボタンを押せるようになる。
③Undoボタンを押すとRedoボタンが押せるようになる。
まとめ③
この記事では、MVVMパターンでのコマンド処理の実装方法について具体的なコードを作成して勉強したので、紹介させていただきました。RelayCommandを使用することで、ViewModel内でコマンドを簡単に定義し、Viewとの間でコマンドの実行や実行可能性の判定をスムーズに行うことができます。また、MainViewModel内でのコマンドの定義やXAMLでのバインディング方法など、具体的な手順を備忘録としてまとめておきました。
また、Undo/Redo機能の実装方法も紹介しました。この機能を活用することで、ユーザーが直前の操作を取り消したり、やり直したりすることができます。
最後に、これらの手法を活用することで、WPFアプリケーションの開発を効率化し、保守性を高めることができるようです。ほかのプログラムを作成するときも今回のコードを参考に効率よく開発していきたいと思います。