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アプリケーションの開発を効率化し、保守性を高めることができるようです。ほかのプログラムを作成するときも今回のコードを参考に効率よく開発していきたいと思います。

返信を残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です