銀の弾丸

プログラミングに関して、いろいろ書き残していければと思っております。

WPFのMVVMでコマンドをバインディングする利点

f:id:takamints:20160924175447p:plain
photo credit: Storm Trooper at Oxford via photopin (license)

MVVMパターンでは、ボタンを押した時の処理などは、コマンドにバインディングしましょうということらしく、 従来の「Clickイベントをコードビハインドで受けて、、、」というのは嫌われるらしい。

しかし理由なく「MVVMでなきゃダメ!」と言われても納得しがたい。 「なぜ?」にはちゃんと答えて欲しいですよね。

ということで、この際きちんと理解しようと頑張りました。

いくつになってもお勉強です。

WHY JAPANESE PEOPLE!? [DVD]
WHY JAPANESE PEOPLE!? [DVD]
posted with amazlet at 16.09.24
アニプレックス (2015-06-24)
売り上げランキング: 10,600
C#実践開発手法 (マイクロソフト公式解説書)
Gary McLean Hall
日経BP
売り上げランキング: 152,501
目次

コマンドの実装例

具体的に、コマンドを利用するには

  1. 実行したい処理を、ICommandインターフェースを実装したコマンドクラスに記述して、
  2. ビューモデルから、そのインスタンスをプロパティで公開して、
  3. ボタンコントロールのCommand属性からバインディングする

と、これで幸せになれるらしい。

以降、以下の順に実装例を掲載してます。

  1. コマンドクラス:SampleCommand.cs
  2. ビューモデル MainWindowViewModel.cs
  3. ビュー:MainWindow.xaml
  4. ビューのコードビハインド:MainWindow.xaml.cs(おまけ)

コマンド:SampleCommand.cs

押してから5秒間、押せなくなるボタンです。 やってることは無意味ですが、 最低限ICommandインターフェースの全機能を使おうとして長くなってしまいました。

using System;
using System.ComponentModel;
using System.Threading;
using System.Windows.Input;

namespace StudyDotNet.Commands
{
    public class SampleCommand : ICommand
    {
        /// <summary>
        ///忙しいフラグ。忙しい時は何もできません。
        /// </summary>
        private bool _isBusy = false;
        
        /// <summary>
        /// 忙しいフラグのプロパティ。
        /// コマンドが実行可能かどうかに関連するプロパティなので。
        /// 代入されたらCanExecuteChangedイベントを投げる。
        /// </summary>
        public bool IsBusy
        {
            get { return _isBusy; }
            set
            {
                _isBusy = value;
                CanExecuteChanged?.Invoke(this, new EventArgs());
            }
        }

        /// <summary>
        /// 以下でタイマー使っているので、
        /// UIスレッドで画面更新するために必要
        /// </summary>
        private AsyncOperation _uiThreadOperation =
            AsyncOperationManager.CreateOperation(null);
        //
        // 以降 ICommand インターフェースの実装
        //

        public event EventHandler CanExecuteChanged;

        public bool CanExecute(object parameter)
        {
            //忙しくない時だけコマンド実行できる
            Console.WriteLine("実行可否を調べられてる。"
                + (!IsBusy?"お仕事できます":"今無理です"));
            return !IsBusy;
        }

        public void Execute(object parameter)
        {
            //忙しいフラグON
            //一定時間後には暇になる。
            Console.WriteLine("忙しくなるぞー");
            IsBusy = true;
            Timer _busyTimer = new Timer( timerParam => {
                _uiThreadOperation.Post(updatePropParam => {
                    IsBusy = false;
                    Console.WriteLine("暇になった。");
                }, null);
            }, null, 5000, Timeout.Infinite);
        }
    }
}
  • CanExecuteChangedイベント - コマンドを実行可否の状態変化時に発行するイベント。UIオブジェクトではこのイベントによって、画面表示状態を変更する。なので別スレッドから投げるときは、AsyncOperationでUIスレッドへActionをPostしなくてはなりませんね。
  • CanExecuteプロパティ - コマンドが実行可能な状況ではtrue、実行できない状況ならfalseを返すプロパティです。
  • Executeメソッド - コマンドの処理本体です。

ビューモデル MainWindowViewModel.cs

ビューモデルは非常にシンプル。上のコマンドクラスのインスタンスをパブリックプロパティとして持ってるだけ。 「この画面にはモデルを操作するための、こういう名前のコマンドがありますよ」と。そして「何をするかはコマンドを見て頂戴」というところでしょうか。

using StudyDotNet.Commands;

namespace StudyDotNet.ViewModels
{
    class MainWindowViewModel
    {
        public SampleCommand SampleCommand
        { get; private set; } = new SampleCommand();
    }
}

このプロパティのsetterからは、INotifyPropertyChangedインターフェースを実装して、PropertyChangedイベントを発行すべきだと思い込んでいましたが、なくても動いているようです(ここ、ちょっと理解が曖昧)。

ビュー:MainWindow.xaml

ビューからは ButtonのCommand属性からビューモデルのコマンドにバインディングButtonCommand="{Binding SampleCommand}"の部分ですね。

<Window x:Class="StudyDotNet.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:StudyDotNet"
        xmlns:vm="clr-namespace:StudyDotNet.ViewModels"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
    <Window.DataContext>
        <vm:MainWindowViewModel/>
    </Window.DataContext>
    <Grid>
        <Button x:Name="button" Content="Button"
                Command="{Binding SampleCommand}"
                HorizontalAlignment="Left" Margin="50,50,0,0"
                VerticalAlignment="Top" Width="75"/>
    </Grid>
</Window>

ビューのコードビハインド:MainWindow.xaml.cs(おまけ)

ちなみにMainWindowのコードビハインドは、何も触っていません。

using System.Windows;

namespace StudyDotNet
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }
    }
}

コマンドを使う利点

処理をコマンドクラスに記述することで得られる利点は、以下のようなものかと推測します。他にもたくさんありそな気もする。

  1. コードビハインド(*.xaml.cs)に何も書く必要がない。
  2. コマンドの単体テストで、ビューやビューモデルが不要です。

一言でいえば、コマンドの独立性が保たれて、ビューモデルがシンプルに保てます。疎結合はいつでも正義。

コマンドの実行可否がコマンド自体から得られます(CanExecuteプロパティ)し、UIオブジェクトに直結しているので、別途IsEnabledプロパティでバインディングしなくても良いのです。複数の条件が絡み合ったUIオブジェクトが複数ある場合、ビューモデルにIs〇〇ButtonEnabledみたいなプロパティがゴロゴロ沸いてきて煩雑になりやすいですからね。

コマンド処理のアンチパターン

上に書いたように、コマンドには明らかなメリットがありますが、一発で台無しにする方法があります。

それは、コマンドの処理内で昔ながらのメッセージボックスやダイアログを直接モーダル表示することです。

お手軽なので、ついついエラーの表示や問い合わせ等で使いがちですが、よろしくない。

なぜかというとユニットテストが自動ではなくなってしまいます。例えばユーザーがOKボタンを押さないとテストが進まないという事態になります。あとモーダルダイアログとして表示するためにはメインウィンドウが必要なのですが、コマンドの処理の中でメインウィンドウを参照するのもコードの独立性が破れて嫌われます。

コマンド処理内でユーザー入力が必要な場合は、MVVMでの「メッセンジャーパターン」という機構で回避できます。 ユニットテストでは簡単に応答を返すスタブに差し替えられます。 この「メッセンジャーパターン」については、以下のページに詳しく書いていますので是非。

takamints.hatenablog.jp

いくつになってもお勉強

設計思想的には古くからあるDocument-ViewやMVCなどを最新技術で発展させたもののようですね。

古くはVC++MFCアプリケーションでメニュー項目などを更新するUpdateUIなどと同じ便利さ加減と思います。便利なんだけどひと手間必要というのも同じですね。当時、あの機構をまともに理解している人が少なくて、往生した覚えがありますわって話が古くて伝わってない?

なにより、「どんな利点があるのか」という観点から、腰を据えて理解できて非常によかった。

フタを開ければかつて知ったる設計思想的な気分でもありました。

やっぱり、いくつになってもお勉強ですねー。


関連記事:

takamints.hatenablog.jp