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

2016/09/18

WPF + MVVM の勉強3:コマンドを実装する

update2017/11/17 event_note2016/09/18 13:38

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

前回までの記事で、データバインディングとプロパティの変更を通知する仕組みについて勉強してきました。

今回は、新たにボタンを追加し、ボタンを押すとテキストボックスの中身を表示するようにしてみます。
具体的には、ボタンを押したときに実行されるコマンドクラスを作成して ViewModel でインスタンス化し、データバインディングにより View に結びつける、ということになるのだと思います。

作成するサンプルプログラム

前回までに作成したプログラムにボタンを追加し、ボタンを押すとテキストボックスの中身をメッセージボックスで表示します。
テキストボックスに何も入力されていない場合はボタンを無効にしておきます。

コマンドの実装

WPF + MVVM でコマンドの仕組みを使う際には ICommand インターフェイスを実装したクラスを作成するそうです。

/// <summary>
/// コマンドの実装
/// </summary>
class ButtonCommand : ICommand
{
    private MainWindowViewModel MainWindow;

    /// <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 (MainWindow?.SampleText == string.Empty ? false : true);
    }

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

    /// <summary>
    /// コンストラクタ
    /// </summary>
    /// <param name="viewmodel">ViewModelのインスタンスへの参照</param>
    public ButtonCommand(MainWindowViewModel mainWindow)
    {
        MainWindow = mainWindow;
    }
}

ViewModel へのコマンドの追加

Button に対するコマンドクラスを作成したので、ViewModel に実装します。

/// <summary>
/// MainWindowに対するViewModel
/// </summary>
class MainWindowViewModel : ViewModelBase
{
    // バインディング対象のプロパティ
    public ButtonCommand 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 ButtonCommand(this);
    }
}

データバインディング

XAML に Button を追加し、データバインディングにより、ViewModel の Button プロパティとバインドします。
コードビハインドは最初のときのままで変更ありません。

<Window x:Class="WpfApplication1.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:WpfApplication1"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <TextBox x:Name="textBox" HorizontalAlignment="Left" Height="23" Margin="10,10,0,0" TextWrapping="Wrap" Text="{Binding SampleText, UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Top" Width="120"/>
        <Label x:Name="label" Content="{Binding SampleLabel}" HorizontalAlignment="Left" Margin="10,38,0,0" VerticalAlignment="Top"/>
        <Button x:Name="button" Command="{Binding Button}" Content="Button" HorizontalAlignment="Left" Margin="135,13,0,0" VerticalAlignment="Top" Width="75"/>
    </Grid>
</Window>

CanExecuteChanged イベントの発行

今回一番悩んだのがここでした。
上記のコマンドを実装して、Button コントロールにバインドしただけでは Button が無効表示にならなかったのです。

いろいろ調べてみると、どうやら ViewModel の PropertyChanged イベントが発生した場合などに CanExecuteChanged イベントを発行する必要があるようです。
以下の場所で CanExecuteChanged イベントを発行しています。

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

当たり前と言えば当たり前なのですが、WPF 初心者の私はかなり悩みました。
確かに、データバインディングにより Button とコマンドは関連付けられていますが、TextBox と Button の関連付けはどこにもされていないので、ViewModel の PropertyChanged イベントが発生したタイミングで明示的に CanExecuteChanged イベントを発行する必要があるということなのでしょう。

改善すべき点

MainWindowViewModel クラスと ButtonCommand クラスが互いにインスタンスを保持しているので結合度が強すぎますね。 普通は ICommand インタフェースを実装した DelegateCommandRelayCommand といったヘルパークラスを作成するみたいです。 次回はここらへんを改善していきたいと思います。

その他に気になること

ICommand インタフェースの実装は ViewModel で合っているのかどうかが気になります。
また、メッセージボックスの表示を ViewModel で行っていますが、MVVM 的にこれは正しいのでしょうか?
メッセージボックスなんだから View の責務のような気がしてます。

参考URL