プログラムを中心とした個人的なメモ用のブログです。 タイトルは迷走中。
内容の保証はできませんのであしからずご了承ください。

2016/10/15

WPF + MVVM の勉強4:コマンドの実装をラムダ式で

update2017/11/17 event_note2016/10/15 14:32

C# も WindowsForms もちょっとしか触ったことがない人が、WPF + MVVM でアプリケーションを作成するために勉強したことをまとめてみる記事4回目です。
間違っているところがあれば指摘していただけると嬉しいです。

前回の記事でコマンドの実装を行いました。 しかし、通常は ICommand インタフェースを実装した DelegateCommandRelayCommand といったヘルパークラスを作成するみたいです。

ExecuteCanExecute はコンストラクタで渡すようにすることで、都度 ICommand インタフェースを実装する必要がなくなります。
ラムダ式を使えばとても簡潔に書けるようになりますね。
コマンドクラスのほうで ViewModel のインスタンスを持たなくてもよいので、結合度も下がります。
というわけで、前回の内容を以下のように変更します。

ヘルパークラスの作成

DelegateCommandRelayCommand という名前にするのが普通らしいですが、個人的に Relay という単語に馴染みがないので、DelegateCommand にします。

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

    /// <summary>
    /// CanExecute の実態を保持します。
    /// </summary>
    private Func<object, bool> canExecute;

    /// <summary>
    /// コンストラクタ
    /// </summary>
    /// <param name="execute">Execute の実体を指定</param>
    /// <param name="canExecute">CanExecute の実体を指定</param>
    public DelegateCommand(Action<object> execute, Func<object, bool> canExecute)
    {
        this.execute = execute;
        this.canExecute = canExecute;
    }

    /// <summary>
    /// コマンドを実行するかどうかに影響するような変更があった場合に発生する
    /// </summary>
    public event EventHandler CanExecuteChanged;

    /// <summary>
    /// CanExecuteChangedイベントを発行する
    /// </summary>
    public void OnCanExecuteChanged()
    {
        CanExecuteChanged?.Invoke(this, EventArgs.Empty);
    }

    /// <summary>
    /// 現在の状態でコマンドが実行可能かどうかを決定するメソッドを定義
    /// </summary>
    /// <param name="parameter">コマンドで使用されたデータ。 コマンドにデータを渡す必要がない場合は、このオブジェクトを null に設定できる。</param>
    /// <returns>コマンドを実行できる場合は true。それ以外の場合は false。</returns>
    public bool CanExecute(object parameter)
    {
        return (canExecute == null ? true : canExecute(parameter));
    }

    /// <summary>
    /// コマンドが起動される際に呼び出すメソッドを定義
    /// </summary>
    /// <param name="parameter">コマンドで使用されたデータ。 コマンドにデータを渡す必要がない場合は、このオブジェクトを null に設定できる。</param>
    public void Execute(object parameter)
    {
        execute?.Invoke(parameter);
    }
}

ViewModel の変更

DelegateCommand を使用して ViewModel を変更します。
前述したとおり、コマンドクラスのほうで ViewModel のインスタンスを持たなくてもよいので、結合度が下がったと思います。

/// <summary>
/// MainWindowに対するViewModel
/// </summary>
class MainWindowViewModel : ViewModelBase
{
    // バインディング対象のプロパティ
    public DelegateCommand Button { get; set; }

    // バインディングされる値を保持するフィールド
    private string sampleText;

    // バインディング対象のプロパティ
    public string SampleText
    {
        get
        {
            return sampleText;
        }
        set
        {
            sampleText = value;

            // 変更をViewに通知する
            OnPropertyChanged(nameof(SampleText));

            // ボタンの無効表示に影響するので、CanExecuteChanged イベントを発行する
            Button?.OnCanExecuteChanged();

            // ラベルの値も連動させる
            SampleLabel = value;
        }
    }

    // バインディングされる値を保持するフィールド
    private string sampleLabel = "";

    // バインディング対象のプロパティ
    public string SampleLabel
    {
        get
        {
            return sampleLabel;
        }
        set
        {
            sampleLabel = value;

            // 変更をViewに通知する
            OnPropertyChanged(nameof(SampleLabel));
        }
    }

    /// <summary>
    /// コンストラクタ
    /// </summary>
    public MainWindowViewModel()
    {
        SampleText = "Sample";
        SampleLabel = "Sample";

        Button = new DelegateCommand(
            x => MessageBox.Show(SampleText),
            x => (SampleText == string.Empty ? false : true)
        );
    }
}