ICommand

 ここでは、MVVM パターンの要となるデータバインディング機能を実現するための ICommand インターフェースについて紹介します。 MVVM パターンでは、アプリケーション内部構造を下図のように Model、View、ViewModel の 3 つに大別します。 このとき、View と ViewModel でデータを連携する手段としてデータバインディング機能を用います。

Fig.1 : MVVM パターン概略図

 MVVM パターンでは、ボタンを押すなどの View が受け取ったユーザ指示を ViewModel に通知します。 このとき、ViewModel にその指示を通知するために用いるのが ICommand インターフェースです。 逆に、その指示が実行可能かどうかを判別し、その可否を通知するのも ICommand インターフェースの役割で、 これは ViewModel から View に通知する必要があります。

 ICommand インターフェースのメンバは次のようになります。

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

namespace System.Windows.Input
{
    // 概要:
    //     コマンドを定義します。
    [TypeConverter("System.Windows.Input.CommandConverter, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, Custom=null")]
    [TypeForwardedFrom("PresentationCore, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35")]
    [ValueSerializer("System.Windows.Input.CommandValueSerializer, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, Custom=null")]
    public interface ICommand
    {
        // 概要:
        //     コマンドを実行するかどうかに影響するような変更があった場合に発生します。
        event EventHandler CanExecuteChanged;

        // 概要:
        //     現在の状態でこのコマンドを実行できるかどうかを判断するメソッドを定義します。
        //
        // パラメーター:
        //   parameter:
        //     コマンドで使用されたデータ。 コマンドにデータを渡す必要がない場合は、このオブジェクトを null に設定できます。
        //
        // 戻り値:
        //     このコマンドを実行できる場合は true。それ以外の場合は false。
        bool CanExecute(object parameter);
        //
        // 概要:
        //     コマンドの起動時に呼び出されるメソッドを定義します。
        //
        // パラメーター:
        //   parameter:
        //     コマンドで使用されたデータ。 コマンドにデータを渡す必要がない場合は、このオブジェクトを null に設定できます。
        void Execute(object parameter);
    }
}
Code 1 : ICommand インターフェースのメンバ
Execute() メソッドによってコマンドの実行をおこないます。また、CanExecute() メソッドでコマンドが実行可能かどうかを判別します。 実行可能かどうかに影響する変更があった場合には、CanExecuteChanged イベントを発行する必要があります。

 それでは、ボタンを押すことによって実行されるコマンドを、データバインディング機能を用いて ViewModel 側に記述する方法を紹介します。 コマンドを使用するには、ICommand インターフェースを実装したプロパティが必要になります。 YKToolkit.Controls.dll を利用することで ICommand インターフェースが既に実装されたクラスを扱うことができますが、 ここでは敢えて ICommand インターフェースを自前で実装して、その内部構造を知ってもらおうと思います。

 DelegateCommand クラスという ICommand インターフェースを実装するクラスを次のように定義します。

namespace Section2
{
    using System;
    using System.Windows.Input;

    public class DelegateCommand : ICommand
    {
        /// <summary>
        /// コマンドの実体を保持します。
        /// </summary>
        private Action<object> _execute;

        /// <summary>
        /// コマンドの実行可能判別処理の実態を保持します。
        /// </summary>
        private Func<object, bool> _canExecute;

        /// <summary>
        /// 新しいインスタンスを生成します。
        /// </summary>
        /// <param name="execute">コマンドの実体を指定します。</param>
        public DelegateCommand(Action<object> execute)
            : this(execute, null)
        {
        }

        /// <summary>
        /// 新しいインスタンスを生成します。
        /// </summary>
        /// <param name="execute">コマンドの実体を指定します。</param>
        /// <param name="canExecute">コマンドの実行可能判別処理の実体を指定します。</param>
        public DelegateCommand(Action<object> execute, Func<object, bool> canExecute)
        {
            _execute = execute;
            _canExecute = canExecute;
        }

        /// <summary>
        /// CanExecuteChanged イベントを発行します。
        /// </summary>
        public static void RaiseCanExecuteChanged()
        {
            CommandManager.InvalidateRequerySuggested();
        }

        #region ICommand のメンバ
        /// <summary>
        /// コマンドの実行可能判別処理を実行します。
        /// </summary>
        /// <param name="parameter">コマンドパラメータを指定します。</param>
        /// <returns>コマンドが実行可能であるとき true を返します。</returns>
        public bool CanExecute(object parameter)
        {
            return _canExecute == null ? true : _canExecute(parameter);
        }

        /// <summary>
        /// コマンドの実行可能判別条件が変更されたときに発生します。
        /// </summary>
        public event System.EventHandler CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested += value; }
        }

        /// <summary>
        /// コマンドを実行します。
        /// </summary>
        /// <param name="parameter">コマンドパラメータを指定します。</param>
        public void Execute(object parameter)
        {
            if (_execute != null)
                _execute(parameter);
        }
        #endregion ICommand のメンバ
    }
}
Code 2 : DelegateCommand クラスの定義

 ICommand インターフェースは、実際にコマンドの内容を実施する Execute() メソッドと、 このコマンド自体が実行可能かどうかを判別するための CanExecute() メソッドを実装する必要があります。 コマンドの内容や実行可能かどうかの条件をここで固定させる必要はないため、_execute および _canExecute という private フィールドを用意し、 コマンドの中身や実行可能判別の処理を外部から指定できるようにしています。

 実行可能かどうかに影響するような変更があった場合、RaiseCanExecuteChanged() メソッドを呼び出すことで、 CanExecuteChanged イベントを発行し、その変更を View 側に伝えます。 ここでは、CanExecuteChanged イベントを CommandManager.RequerySuggested イベントに委任しているため、 こちらのイベントを発生させることで CanExecuteChanged イベントを発生させています。 ただし、同じ View(ViewModel)に複数のコマンドが存在し、この RaiseCanExecuteChanged() メソッドを呼び出した場合、 すべてのコマンドに対して再評価がおこなわれるため、各コマンドに対して呼ぶ必要はありません。 このことから、RaiseCanExecuteChanged() メソッドは static なメソッドとして定義しています。

 具体例として、こちらのページで作成した MainViewModel のサンプルコードに、ClearCommand プロパティを次のように追加します。

private DelegateCommand clearCommand;
/// <summary>
/// 文字列をクリアするコマンドを取得します。
/// </summary>
public DelegateCommand ClearCommand
{
    get
    {
        if (clearCommand == null)
            clearCommand = new DelegateCommand(
            _ => Text = string.Empty,
            _ => !string.IsNullOrEmpty(Text)
            );
        return clearCommand;
    }
}
Code 3 : ClearCommand プロパティの定義
get アクセサによるプロパティ値取得タイミングで、private フィールドである clearCommand 変数が null のときのみ DelegateCommand クラスをインスタンス化しています。 コマンドの中身は Text プロパティを空にするという処理で、Text プロパティが既に空である場合は実行不可であるという判別をおこなっています。

 次に、このプロパティを参照するように、MainView を次のように変更します。

<Window x:Class="Section2.Views.MainView"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainView" Height="300" Width="300">
    <StackPanel>
        <TextBox Text="{Binding Text, UpdateSourceTrigger=PropertyChanged}" />
        <TextBlock Text="{Binding Result}" />
        <Button Content="Click me." Command="{Binding ClearCommand}" />
    </StackPanel>
</Window>
Code 4 : データバインディング機能を利用した MainView
コンパイル、実行すると、起動直後は Text プロパティが空であるため、 Button コントロールの Command プロパティの実行可能判別処理によって Button コントロールの IsEnabled プロパティが false となっています。 TextBox コントロールに任意の文字列を入力して Text プロパティに文字列が設定されると、 Button コントロールの実行可能判別処理によって Button コントロールの IsEnabled プロパティが true となり、ボタンを押せるようになります。 また、ボタンを押すと ClearCommand のコマンド実行処理がおこなわれ、Text プロパティが空になるため、TextBox コントロールのテキストが空になります。
(a) テキストを入力するとボタンが有効になる(b) ボタンを押すと TextBox コントロールが空になる
Fig. 2 : ClearCommand プロパティがバインディングされた画面

Designed by CSS.Design Sample