CanExecuteChangedイベント
これが最後になります。コマンドのCanExecuteプロパティが変化したときには、CanExecuteChangedイベントを発火させねばなりません。コマンドがバインドされているコントロールは、このイベントを受け取ると、コマンドのCanExecuteプロパティを読み取って自身の表示(実行可能か否か)を変更します。
CanExecuteプロパティは外部から直接変更できませんが、Numberプロパティを使って間接的に変更できます。これも、前述の戦略2.ですね。テストコードは、PropertyChangedイベントと同じような方法で書けます(今度は発火するイベントが1種類だけなので、より簡単です)。次の製品コードは、膨れ上がってしまったNumberプロパティのsetterをリファクタリングした後のものです。
// GoNextCommand.CanExecuteChangedイベントを実装するためのテストコード
bool isCanExecuteChanged01Fired;
void cmd_CanExecuteChanged01(object sender, System.EventArgs e)
{
isCanExecuteChanged01Fired = true;
}
[TestMethod]
[Timeout(100)]
public async Task GoNextCommand_CanExecuteChangedTest01_trueからfalse()
{
FizzBuzzViewModel vm = new FizzBuzzViewModel();
// GoNextCommand.CanExecute は既定で true
vm.GoNextCommand.CanExecuteChanged += cmd_CanExecuteChanged01;
vm.Number = "x"; // これで CanExecute が false になったので、発火するはず
while (!isCanExecuteChanged01Fired)
await Task.Delay(10);
// → Numberのsetterに this.GoNextCommand.RaiseCanExecuteChanged(); を追加
}
[TestMethod]
public async Task GoNextCommand_CanExecuteChangedTest02_trueからtrue()
{
FizzBuzzViewModel vm = new FizzBuzzViewModel();
// GoNextCommand.CanExecute は既定で true
vm.GoNextCommand.CanExecuteChanged += cmd_CanExecuteChanged01;
vm.Number = "2"; // CanExecute は true のままので、発火しないはず
// 十分な時間を待機
await Task.Delay(100);
Assert.IsFalse(isCanExecuteChanged01Fired);
// → NumberのsetterにgoNextローカル変数を追加
// メンバー変数_canGoNextに直接代入せず、いったんgoNextで受け、
// true/falseが変化したときだけCanExecuteChangedを発火させるように。
}
// GoPreviousCommand.CanExecuteChangedイベントを実装するためのテストコード
[TestMethod]
[Timeout(100)]
public async Task GoPreviousCommand_CanExecuteChangedTest01_falseからtrue()
{
FizzBuzzViewModel vm = new FizzBuzzViewModel();
// GoPreviousCommand.CanExecute は既定で false
vm.GoPreviousCommand.CanExecuteChanged += cmd_CanExecuteChanged01;
vm.Number = "2"; // これで CanExecute が true になったので、発火するはず
while (!isCanExecuteChanged01Fired)
await Task.Delay(10);
// → Numberのsetterに this.GoPreviousCommand.RaiseCanExecuteChanged(); を追加
}
[TestMethod]
public async Task GoPreviousCommand_CanExecuteChangedTest02_falseからfalse()
{
FizzBuzzViewModel vm = new FizzBuzzViewModel();
// GoPreviousCommand.CanExecute は既定で false
vm.GoPreviousCommand.CanExecuteChanged += cmd_CanExecuteChanged01;
vm.Number = "x"; // CanExecute は false のままので、発火しないはず
// 十分な時間を待機
await Task.Delay(100);
Assert.IsFalse(isCanExecuteChanged01Fired);
// → NumberのsetterにgoPreviousローカル変数を追加
// メンバー変数_canGoPreviousに直接代入せず、いったんgoPreviousで受け、
// true/falseが変化したときだけCanExecuteChangedを発火させるように。
}
public string Number
{
get { return _number; }
set
{
SetProperty<string>(ref _number, value);
SetProperty<string>(ref _word, GetWord(), "Word");
SetCanExecute();
}
}
// (…略…)
private void SetCanExecute()
{
int n = GetNumber();
SetCanGoNext(n);
SetCanGoPrevious(n);
}
#region GoNextCommand
public Common.RelayCommand GoNextCommand { get; private set; }
private bool _canGoNext = true;
private void SetCanGoNext(int number)
{
bool goNext = (0 < number) && (number < int.MaxValue);
if (_canGoNext != goNext)
{
_canGoNext = goNext;
this.GoNextCommand.RaiseCanExecuteChanged();
}
}
#endregion
#region GoPreviousCommand
public Common.RelayCommand GoPreviousCommand { get; private set; }
private bool _canGoPrevious = false;
private void SetCanGoPrevious(int number)
{
bool goPrevious = (1 < number);
if (_canGoPrevious != goPrevious)
{
_canGoPrevious = goPrevious;
this.GoPreviousCommand.RaiseCanExecuteChanged();
}
}
#endregion
これでViewModelは完成です。出来上がったFizzBuzzViewModelクラスの全体は、サンプルソースをダウンロードしてご覧ください。
画面にバインドする
出来上がったViewModelを画面と結び付けましょう。次のコードのようになります。
<UserControl
(…略…)
>
<!-- ViewModelのインスタンス -->
<UserControl.Resources>
<local:FizzBuzzViewModel x:Key="FizzBuzzViewModel" />
</UserControl.Resources>
<!-- ViewModelのインスタンスをDataContextに設定 -->
<Grid (…略…)
DataContext="{StaticResource FizzBuzzViewModel}"
>
<!-- (…略…) -->
<!-- 戻るボタンのCommandプロパティには、ViewModelのGoPreviousCommandをバインド -->
<AppBarButton Icon="Previous" IsCompact="True"
Command="{Binding GoPreviousCommand}"/>
<!-- (…略…) -->
<!-- 数字のTextBoxのTextプロパティには、ViewModelのNumberプロパティを双方向バインド -->
<TextBox Text="{Binding Number, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
MinWidth="60" TextAlignment="Right" />
<!-- (…略…) -->
<!-- 進むボタンのCommandプロパティには、ViewModelのGoNextCommandをバインド -->
<AppBarButton Icon="Next" IsCompact="True"
Command="{Binding GoNextCommand}" />
<!-- (…略…) -->
<!-- 結果のTextBlockのTextプロパティには、ViewModelのWordプロパティを一方向バインド -->
<TextBlock FontSize="60" Margin="10,10,0,0"
Text="{Binding Word}" />
<!-- (…略…) -->
</Grid>
</UserControl>
どのコントロールにも名前が付いていないこと、また、イベントも定義されていないことにご注目ください。それは、コードビハインドからコントロールにアクセスしていないことを示しています(実際、サンプルコードの「UI」フォルダーにある「FizzBuzzControl.xaml.cs」ファイルを見てもらうと、コンストラクター以外は何もありません)。
しかしこれで、期待通りに動作します。数字を書き換えると結果が変わります。ボタンをタップすると、数字が変わるとともに、結果も変化します。数字によって、ボタンの実行可/不可も変化します。
このように、ViewModelを作って画面からきれいに分離できることが分かります。そして、ViewModelはUIを持っていないので、TDDで作るのに適しています(もっとも、コマンドを実装するために書いたテストの数を考えると、コマンドを実装する必要があるのか(イベントハンドラーで構わないのでは)と思う人もいるでしょう)。
まとめ
状態を持っているクラスの例として、ユニバーサルWindowsアプリのViewModelをテストファーストで作りました。状態と無関係である単純なメソッドと違って、状態(および、状態に依存するパーツ)は単独で作れないことが多いので、テストファーストする戦略を考える必要がありました。
まず状態と作りたいパーツの依存関係を明確にすると、理解しやすくなります。そして、依存関係の少ないものから複雑なものという順序で作っていくとよいでしょう。
スペック(外部設計)の観点から、状態をpublicにしてしまうのは最後の手段です。その前に、次の戦略を試してみます。
- スペック上、状態へのアクセスが必要な場合:すなおにテストが書けるはずです
- スペック上、状態の変化が別に観測可能な場合:変更と観測のペアをまとめてテストファーストします
- スペック上、状態の変化が観測できない場合:「プローブ」の挿入を検討します
