WPF

【C#/WPF実践入門編(8)】コマンドの基礎 ~ICommandによる操作の分離~【MVVMパターン理解の基礎】

Windows Presentation Foundation (WPF) の重要な機能の一つであるコマンドについて学びます。

これまでは、例えばボタンをクリックしたときの処理をUI側のコード(C#コードビハインド)に直接書いていましたが、今回はそれを別クラスへ分離します。

以下の方に役立つ内容となっています。

  • MVVMパターンの準備として、コマンドパターンを習得したい方
  • ICommandの仕組みを理解したい方

ボタンのクリック処理を従来のClickイベントではなく、ICommandを用いて実装することで、役割ごとにクラスをきれいに分離することができます。

この記事では、カウンターアプリを通じて、ICommandインターフェイスの基本的な使い方とボタンの自動有効・無効制御を習得します。

プロ太

前回までで学んだデータバインディングと今回のコマンドを組み合わせることで、MVVMパターンの基礎となる2本柱が完成します!

データバインディングについては以下の記事を参考にしてください。

【C#/WPF実践入門編(5)】データバインディングの基礎①~DataContextとは?~ Windows Presentation Foundation (WPF) の特徴的な機能の一つであるデータバインディングについて学び...
【C#/WPF実践入門編(6)】データバインディングの基礎②~INotifyPropertyChangedとは?~ Windows Presentation Foundation (WPF) のデータバインディングにおいて、データ変更時にUIを自動的...
【C#/WPF実践入門編(7)】データバインディングの基礎③~双方向バインディングの実装~ Windows Presentation Foundation (WPF) のデータバインディングにおいて、UI入力をデータに自動反映...

演習のコード一式はGitHubに置いてあります。

講義:コマンドの仕組み

従来のイベント処理の問題点

従来は以下のように、UIのコードビハインドにイベント処理を直接書いていました。

UIコード(XAML)

...  
<Button Content="+" Click="OnIncrement"/>
...

UIコード(C#コードビハインド)

private void OnIncrement(object sender, RoutedEventArgs e)
{
    Count++;
}

これだと、「UIの表示」と「アプリのロジック(カウンターのインクリメント)」という異なる役割をもつ内容が、すべてUIクラスに書かれてしまっているという問題があります。

コマンドとは?

コマンド」の仕組みは以下のように、「UIコード」と「データやロジックのコード」をきれいに分離し、コードを役割ごとにきれいに整理するための仕組みとなります。

このように整理することで、コードの可読性も上がりますし、「データやロジック」のコードを別の画面や機能で再利用しやすくなります。

プロ美

前回までで学んだ「データバインディング」も、役割でクラスを分離するための仕組みなのかな?

プロ太

その通りです!

データバインディングについて、双方向同期のメリットを中心に説明してきましたが、「UI」と「データ」のコードを分離する役割もあります。

データバインディングとコマンドによって以下のように「UI」と「データやロジック」の完全分離が実現します。以下のようなイメージです。

プロ太

この役割によってクラスを分離する考え方は、MVVMパターンの基礎にもなっています。

まさに、データバインディングとコマンドがMVVMパターン実現の2本柱というわけですね。

ICommandインターフェイスとは?

WPFにおけるコマンドは、ICommandインターフェイスによって実現されます。このインターフェイスは以下の3つのメンバを持ちます。

public interface ICommand
{
    bool CanExecute(object? parameter);
    void Execute(object? parameter); 
    event EventHandler? CanExecuteChanged; 
}

それぞれ以下の役割があります。

  • CanExecute: そのコマンドが現在実行可能かどうかを返す
    (例:カウンターが0の時はマイナスボタンを無効にする)
  • Execute: 実際のコマンド処理を実行
    (例:カウンターをインクリメント・デクリメント)
  • CanExecuteChanged: 実行可否の状態が変わったときに発生するイベント

Microsoft LearnのICommandのリファレンスも参考にしてください。

コマンドとボタンの自動制御

WPFのボタンは、バインドされたコマンドのCanExecuteメソッドの戻り値に応じて、自動的に有効・無効が切り替わります。

例えば、以下のようにデータクラスでICommandプロパティを公開します。

public class Counter : INotifyPropertyChanged
{
    public int Count{
      ...
    }

    public ICommand DecrementCommand { get; }
    
    public Counter()
    {
        // Count > 0の時のみ実行可能なコマンドを作成
        // Executeとして「_ => Count--」を設定
        // CanExecuteとして「_ => Count > 0」を設定
        // (SimpleCommandはICommandを実装している。具体的な実装方法は演習で…)
        DecrementCommand = new SimpleCommand(_ => Count--, _ => Count > 0);
    }
}

そして、事前にDataContextでCounterインスタンスをUIへ紐づけたうえで、XAMLでこのコマンドをバインディングします。

<!-- DecrementCommandのCanExecuteがfalseを返すと、ボタンが自動的に無効化される -->
<Button Content="-" Command="{Binding DecrementCommand}"/>

この仕組みにより、「カウンターが0以下の時はマイナスボタンを押せなくする」といった制御を、UI側のコードを書くことなく実現できます。

演習:カウンターアプリでICommandを実装

作成するアプリの概要

以下の機能を持つカウンターアプリを作成します。

  • カウンター値の表示(データバインディング)
  • プラスボタン:カウンターをインクリメント
  • マイナスボタン:カウンターをデクリメント(0以下で自動無効化)

以下の手順で実装します。

  • 手順1:SimpleCommand.cs: ICommandインターフェイスの実装クラス
  • 手順2:Counter.cs: カウンターのデータとコマンドを管理するクラス
  • 手順3:MainWindow.xamlMainWindows.xaml.cs: UI部分

WPFの新規プロジェクトを作成し、そこへ「SimpleCommand.cs」、「Counter.cs」ファイルをそれぞれ追加しましょう。以下の構成になります。

ソリューションエクスプローラで「WpfApp1」上で右クリックメニューを開き、「追加>新しい項目」で「クラス」を選び追加します。

手順1:SimpleCommand.cs

SimpleCommand.csを以下のように作成しましょう。
(今回、もともとひな型にある不要な名前空間のusingについては削除しています)

これは、カウンターの加算(Increment)・減算(Decrement)のコマンドを作るための汎用的なコマンドクラスです。

using System.Windows.Input;

namespace WpfApp1
{
    public class SimpleCommand : ICommand
    {
        private readonly Action<object?> _execute;
        private readonly Predicate<object?>? _canExecute;

        public SimpleCommand(Action<object?> execute, Predicate<object?>? canExecute = null)
        {
            _execute = execute ?? throw new ArgumentNullException(nameof(execute));
            _canExecute = canExecute;
        }

        public bool CanExecute(object? parameter) => _canExecute?.Invoke(parameter) ?? true;
        public void Execute(object? parameter) => _execute(parameter);

        public event EventHandler? CanExecuteChanged;

        public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
    }
}

このコードのポイントは以下です。

  • コンストラクタで実行処理(execute)と実行可否判定(canExecute)を受け取る
  • CanExecuteは判定関数があれば実行し、なければ常にtrueを返す
  • RaiseCanExecuteChangedメソッドでUI側に実行可否の再評価を要求

手順2:Counter.cs

Counter.csを以下のように作成しましょう。さきほどのSimpleCommandクラスを使って、Increment・Decrementコマンドを作ります。

using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;

namespace WpfApp1
{
    public class Counter : INotifyPropertyChanged
    {
        private int _count;

        public int Count
        {
            get => _count;
            set
            {
                if (_count != value)
                {
                    _count = value;
                    OnPropertyChanged();
                }
            }
        }

        public ICommand IncrementCommand => _incrementCommand;
        public ICommand DecrementCommand => _decrementCommand;

        private readonly SimpleCommand _incrementCommand;
        private readonly SimpleCommand _decrementCommand;

        public Counter()
        {
            _incrementCommand = new SimpleCommand(_ => Count++);
            _decrementCommand = new SimpleCommand(_ => Count--, _ => Count > 0);
        }

        public event PropertyChangedEventHandler? PropertyChanged;

        protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));

            if (propertyName == nameof(Count))
            {
                _decrementCommand.RaiseCanExecuteChanged();
            }
        }
    }
}

このコードのポイントは以下です。

  • 前回学んだINotifyPropertyChangedを実装してデータバインディングに対応
  • IncrementCommand: 常に実行可能なプラスコマンド
  • DecrementCommand: 「Count > 0」の時のみ実行可能なマイナスコマンド
  • Countプロパティ変更時にRaiseCanExecuteChanged()を呼び出し、UI側へ「CanExecute結果が変わった可能性あるよ」と通知

RaiseCanExecuteChanged()を呼び出すと、SimpleCommandクラスで実装した「CanExecuteChanged」を呼び出します。

 public class SimpleCommand : ICommand
    {
       ...
        public event EventHandler? CanExecuteChanged;

        public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
    }

DataContextとCounterを紐づけると、CanExecuteChangedはWPFのUIフレームワーク側で自動で購読します。なので、UIへCanExecuteの状態変更を通知できるわけです。

プロ太

この仕組みは、INotifyPropertyChangedのPropertyChangedイベントと同じですね。

手順3:MainWindow.xaml(と、MainWindow.xaml.cs)

MainWindow.xamlを以下のように修正します。

<Window x:Class="WpfApp1.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:WpfApp1"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <StackPanel Margin="20" VerticalAlignment="Center">
        <!-- カウンター表示 -->
        <TextBlock Text="{Binding Count, StringFormat='Count: {0}'}" 
               FontSize="24" 
               HorizontalAlignment="Center" 
               Margin="0,0,0,20"/>

        <!-- ボタン群 -->
        <StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
            <Button Content="-" 
                Command="{Binding DecrementCommand}" 
                Width="40" Height="30"/>
            <Button Content="+" 
                Command="{Binding IncrementCommand}" 
                Width="40" Height="30"/>
        </StackPanel>

        <!-- 説明テキスト -->
        <TextBlock Text="「-」ボタンは0以下で自動無効化されます" 
               HorizontalAlignment="Center" 
               Margin="0,15,0,0" />
    </StackPanel>
</Window>

ポイントは以下です。

  • 従来のClickイベントの代わりにCommandプロパティを使用
  • Command="{Binding DecrementCommand}"でコマンドをバインディング
  • ボタンの有効・無効制御はWPFが自動的に行う

最後に、もう1点だけUI側のコードビハインド(MainWindow.xaml.cs)でDataContextへCounterインスタンスを紐づけるコードを追記します。

using System.Windows;

namespace WpfApp1
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            DataContext = new Counter(); //ここだけ追加
        }
    }
}

アプリを実行

アプリをデバッグ実行して、以下の動作を確認してみましょう。

プラス・マイナスボタンで値が増減します。加えて、カウンターが0になったとき、マイナスボタンが自動的に無効化されます。

プロ美

マイナスボタンが自動的にグレーアウトするのが面白い!これがコマンドの自動制御機能だね。

プロ太

そうです!CanExecuteメソッドがfalseを返すと、WPFが自動的にボタンを無効化してくれます。

今回実装した「SimpleCommandクラス」は汎用性が高いので、カウンターの加減操作以外にも、様々なコマンドを実装するときに共通して使える部品です。

補足:データバインディングやコマンドをより簡潔に書く方法

今回の記事では、ICommandを自分で実装して「SimpleCommandクラス」を用意しました。

学習目的としては、ICommandの仕組みを理解するのに最適ですが、実務として「毎回このようなボイラープレートコードを書くのは大変そうだな」と感じた方も多いと思います。

そのため、CommunityToolkit.Mvvm(旧Microsoft.Toolkit.Mvvm)やPrismといった外部ライブラリでは、あらかじめ汎用的なコマンドクラスが用意されています。

例として、CommunityToolkit.Mvvmの雰囲気を少しだけみてみましょう。

今回作成した SimpleCommand相当のクラスは、RelayCommandとして用意されているので、それを使うだけでOKです。

public Counter()
{
    ...
    IncrementCommand = new RelayCommand(() => Count++);
    DecrementCommand = new RelayCommand(() => Count--, () => Count > 0);
    ...
}

あと実は、データバインディングについても、より簡潔に記述できます。例えば、プロパティについて以下のような簡潔な記述で行えます。

public partial class Counter : ObservableObject
{
    [ObservableProperty]
    private int count;
...
プロ美

元々はget,setとかOnPropertyChangedの呼び出しとか、こちゃごちゃあったのが、すごくスッキリしてる!

プロ太

CommunityToolkit.Mvvmなど使うとこのようにかなり簡潔に書けます。実務では必須といえます。

ただ、これらの裏側の仕組みを知ることも重要なため、本シリーズでは最初にこれらのライブラリを使わず、WPFの素の実装方法を紹介しています。

CommunityToolkit.Mvvmについては、MVVMパターンまで一通り学習した後に改めて紹介しますので、楽しみにしていてください。

まとめ

本記事では、WPFにおけるコマンド機能の実装について学びました。

コマンドを使ったアプリには以下の特徴と利点があります。

  • 役割の分離:UIコードとアプリロジックを分離し、コードの可読性や再利用性が向上
  • 有効化の自動制御:CanExecuteメソッドによってボタンの有効・無効が自動で切り替わり、UI状態の管理を簡易化

WPFではICommandインターフェイスを実装することで実現できます。

カウンターアプリの演習を通じて、SimpleCommandクラスの作成からボタンのコマンドバインディングまで、実際のアプリへコマンド機能を統合する方法を学びました。

また、データバインディング・コマンドのコードはCommunityToolkit.Mvvmなど、より簡潔な記述にする方法もあることを学びました。

プロ太

データバインディングと今回のコマンドで、MVVMパターンの2本柱が揃いました!次回からいよいよMVVMパターンです。

引き続き、一緒にC# WPFアプリ開発を一緒に学んでいきましょう!

ABOUT ME
プロ太
●仕事:現在は個人事業主(メンター・情報発信等)、大手IT企業で技術者・マネージャ(15年以上)、大学の外部講師、学生時代は学習塾で非常勤講師(約4年間) ●博士(工学)の学位取得 ●高校生の頃に独学で始め、プログラミング歴20年以上 ●言語:C# 、Java、C/C++、Python、JavaScript/TypeScript等 ●分野:Webアプリ、テスト自動化、生成AI、デバッガ、コード解析、ドメイン特化言語

ご依頼・ご相談について

プログラミング学習のご相談、お仕事のご依頼については、
こちらのお問い合わせページをご確認ください。