WPFでReactiveProperty入門 ~Bing画像検索ビューアを作る (検索バーの実装)

前回 WPF+ReactivePropertyでBing画像検索ビューアを作るの続きです。

今回は、検索バー(検索ワードの入力と、検索の実行を行う部分)の実装を通して、ReactiveProperty の基本的な使い方を確認してみたいと思います。

ここで期待される動作は以下の通りです。

  1. テキストボックスにキーワードを入力して[検索]ボタンをクリックすると検索が開始される
  2. 検索実行中に[キャンセル]で検索を中止できる。
  3. [検索]ボタンは「テキストボックスが空ではなくて」「検索実行中ではない」場合のみ押せる
  4. [キャンセル]ボタンは、「検索実行中」の場合のみ押せる
  5. [検索]ボタンの代わりに、「Enter」キーを押すことでも検索を実行できる

これを踏まえて、実装を見てみましょう。まずはViewModelから。

ViewModel

ViewModelでは、ViewにバインドするReactiveProperty と ReactiveCommand を定義します。
これらは MainWindowViewModel クラスのコンストラクタ内で初期化を行っています。

検索ワード

まずは、TextBoxに入力された検索ワードを格納する ReactiveProperty を定義します。

SearchWord = new ReactiveProperty<string>("");
ReactiveProperty のコンストラクタについて

以下の省略可能な2つの引数を受け付けます

  • 第1引数でDefault値を設定
  • 第2引数は ReactivePropertyMode で細かな挙動を指定
    • DistinctUntilChanged : 同じ値は続けて通さない
    • RaiseLatestValueOnSubscribe : Subscribe 時に最新の値を流す。値が無ければDefault値を流す
    • 指定しなければどちらも有効になる (DistinctUntilChanged | RaiseLatestValueOnSubscribe)

検索コマンド

次に、[検索]を実行する ReactiveCommand、「SearchCommand」 の定義を見てみます。

//バックグラウンド処理の実行状態とか、処理の進捗状態とかを通知するオブジェクト
var progress = new ProgressNotifier();
//[検索]コマンド
// 検索ボックスに文字が入っていて、検索実行中でない場合に実行可能
this.SearchCommand = new[] {
  SearchWord.Select(x => string.IsNullOrWhiteSpace(x)),
  progress.IsProcessingObservable.StartWith(false)
}
.CombineLatestValuesAreAllFalse()
.ToReactiveCommand();
this.SearchCommand.Subscribe(_ =>
{
  //検索実行 & サムネイル画像のダウンロード
  webImageStore.Clear();
  webImageStore.DownloadWebImage(SearchWord.Value, progress);
}).AddTo(disposables);
ReactiveCommandの初期化について
  • IObservable<bool>ToReactiveCommand() で ReactiveCommand に変換しています。
  • このIObservable<bool> が ICommand でいうところの CanExecute() の役割を担っています。
    (これがFalseを返す間は、コマンドが実行されず、バインド先のコントロールもグレーアウトした状態になります)

ここでは、検索ワードが空以外で、検索が実行中でない場合にコマンドが実行可能になるようにしています。

this.SearchCommand = new[] {
  //検索ワードが 空または空白のみのときTrueを流す
  SearchWord.Select(x => string.IsNullOrWhiteSpace(x)),
  //検索実行中になったらTrueを流すIO<bool>
  progress.IsProcessingObservable.StartWith(false)
}
//配列中のIO<bool>について、流れてくる最新の値がすべてFalse になっていたら Trueを流す
.CombineLatestValuesAreAllFalse()
//...
progress.IsProcessingObservable.StartWith(false) の部分について。
  • ここで、検索実行中になったらTrueを、実行中でなくなったらFalse流すIO<bool> を取得しています。
  • 取得元(ProgressNotifier という自作のクラス)では、内部でReactive.Bindings.Notifiers.CountNotifier を使用して処理の実行状態の変化を監視しています。

キャンセル コマンド

[キャンセル]実行時のコマンドも、検索コマンドと同様に初期化を行います。
検索実行中の場合のみ コマンドが実行可能になります。

var isProcessing = progress.IsProcessingObservable.StartWith(false)
//[キャンセル]コマンド
//検索中の場合のみ実行可能
this.CancelCommand = isProcessing.ToReactiveCommand();
this.CancelCommand.Subscribe(_ =>
{
  webImageStore.Cancel();
}).AddTo(disposables);

View

続いて、Viewを見てみましょう。
ここまでに定義したReactiveProperty、ReactiveCommand をViewでバインドします。

<!-- 検索バー -->
<StackPanel Orientation="Horizontal" DockPanel.Dock="Top">
  <TextBox Margin="4,2" Padding="2" Width="240"
    Text="{Binding SearchWord.Value,Mode=OneWayToSource,UpdateSourceTrigger=PropertyChanged}">
    <i:Interaction.Triggers>
      <i:EventTrigger EventName="KeyDown">
        <Interactivity:EventToReactiveCommand Command="{Binding SearchCommand}">
          <conv:ReturnKeyDownConverter/>
        </Interactivity:EventToReactiveCommand>
      </i:EventTrigger>
    </i:Interaction.Triggers>
  </TextBox>
  <Button Padding="8,2" Margin="4,2" Command="{Binding SearchCommand}">検索</Button>
  <Button Padding="8,2" Margin="8,2" Command="{Binding CancelCommand}">キャンセル</Button>
</StackPanel>

このViewの全体像は、こちら MainWindow.xaml を参照ください

  • TextBox の Text に VieModelのSearchWord をバインド
    • ReactiveProperty をバインドする場合、.Value を忘れずに
  • [検索]ボタンに ViewModelの SearchCommand を バインド
  • [キャンセル]ボタンに ViewModel の CancelComannd をバインド

ここまでは、特に問題ないかと思います。
残るは <i:Interraction.Triggers>... の部分ですが、

ViewのイベントをReactiveCommand に変換する EventToReactiveCommand

冒頭に挙げた、期待される動作の最後に、以下の要件がありました。

  • [検索]ボタンの代わりに、「Enter」キーを押すことでも検索を実行できる。

これを実現するために、EventToReactiveCommand を使用しています。
ViewのイベントをReactiveCommandに変換してくれる優れものです。

使い方としては、

  • TextBox の Triggers に KeyDownイベントのEventTriggerとしてEventToReactiveCommandを登録し
  • KeyDownイベントハンドラをReactiveCommandに変換するためコンバータを定義して
  • EventToReactiveCommandの Command プロパティにReactiveCommandをバインドする

という流れになります。

「KeyDownイベントハンドラをReactiveCommandに変換するためコンバータ」は、以下の様に定義しました。

using Reactive.Bindings.Interactivity;
using System;
using System.Reactive.Linq;
using System.Windows.Input;

namespace ReactiveBingViewer.Converters
{
    public class ReturnKeyDownConverter : ReactiveConverter<KeyEventArgs, object>
    {
        protected override IObservable<object> OnConvert(IObservable<KeyEventArgs> source)
        {
            return source.Where(arg => arg.Key == Key.Return)
                .Select<KeyEventArgs,object>(_ => null);
        }
    }
}

OnConvertにはKetDownのイベント引数 KeyEventArgs がIObservable<T>として流れてきます。
この値を見て、Enter(Return)キーが押された場合のみReactiveCommandに値を流すようにしています。

まとめ

これで、検索バーの処理をReactivePropertyで実装することが出来ました。

次回は、Model部分の実装についても少し見てみたいと思います。