バックグランド検索をするWPF TextBox

以前WPFで入力補完が効くTextBoxを作成してみました。似たところで、入力の度にバックグランドで非同期タスクにより検索を走らせる処理を考えてみます。

 

form

簡単にはこんなところでしょうか。

MainWindow.xaml.cs

<Window x:Class="BackgroundSearchTextBox.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="174" Width="334">
    <Grid>
        <Label Content="検索ワード" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top"/>
        <TextBox x:Name="SearchWordTextBox" HorizontalAlignment="Left" Height="23" Margin="95,10,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="201"/>
        <Label Content="検索結果" HorizontalAlignment="Left" Margin="10,43,0,0" VerticalAlignment="Top"/>
        <TextBlock x:Name="SearchResultTextBlock" HorizontalAlignment="Left" Margin="95,43,0,0" TextWrapping="Wrap" Text="Enter Keyword" VerticalAlignment="Top" Height="91" Width="201"/>
    </Grid>
</Window>

MainWindow.xaml

namespace BackgroundSearchTextBox
{
    using System;
    using System.Linq;
    using System.Threading.Tasks;
    using System.Windows;

    public partial class MainWindow : Window
    {
        private string[] candidates = new string[]{
            "apple",
            "banana",
            "orange",
            "grape",
            "pineapple",
            "mango"
        };

        public MainWindow()
        {
            InitializeComponent();

            // Rxなし
            SearchWordTextBox.TextChanged += (s, e) =>
            {
                Task.Factory.StartNew(
                    search =>
                    {
                        string searchWord = search as string;
                        if (String.IsNullOrEmpty(searchWord)) return "Enter Keyword.";
                        var candidatesQuery = candidates.Where(cand => cand.StartsWith(searchWord));
                        return candidatesQuery.IsEmpty() ? "No Result." : candidatesQuery.Aggregate((a, b) => a + Environment.NewLine + b);
                    },
                    SearchWordTextBox.Text)
                .ContinueWith(t => SearchWordTextBox.Dispatcher.Invoke(() => SearchResultTextBlock.Text = t.Result));
            };

        }
    }
}

これを動かすにはNugetパッケージインストーラから

PM> install-package ix-main

としてInteractive Extensionsをインストールしておく必要があります。

上記はイマイチ。文字入力の度にタスクを起動させていますが、頻繁な文字入力の度にタスクを生成してたら結構オーバーヘッドが大きいです。そこでRxを使ってみます。

 

MainWindow.xaml.cs

namespace BackgroundSearchTextBox
{
    using System;
    using System.Linq;
    using System.Windows;
    using System.Windows.Controls;
    using System.Reactive.Linq;
    using System.Reactive.Concurrency;

    public partial class MainWindow : Window
    {
        private string[] candidates = new string[]{
            "apple",
            "banana",
            "orange",
            "grape",
            "pineapple",
            "mango"
        };

        public MainWindow()
        {
            InitializeComponent();

            // Rxあり
            Observable.FromEvent<TextChangedEventHandler, TextChangedEventArgs>(
                action => (s, e) => action(e),
                handler => SearchWordTextBox.TextChanged += handler,
                handler => SearchWordTextBox.TextChanged -= handler)
                .Select(_ => SearchWordTextBox.Text)
                .Throttle(TimeSpan.FromMilliseconds(100))
                .Select(searchWord =>
                    {
                        if (String.IsNullOrEmpty(searchWord)) return "Enter Keyword.";
                        var candidatesQuery = candidates.Where(cand => cand.StartsWith(searchWord));
                        return candidatesQuery.IsEmpty() ? "No Result." : candidatesQuery.Aggregate((a, b) => a + Environment.NewLine + b);
                    }
                )
                .ObserveOn(new DispatcherScheduler(App.Current.Dispatcher))
                .Subscribe(t => SearchResultTextBlock.Text = t);

        }

    }
}

Rxあり版を動かすためにはNugetパッケージマネージャーから

PM> install-package rx-main
PM> install-package rx-wpf

としてReactive Extensions(Rx)のインストールも必要です。Rxを使うことで、

  • ちゃんとイベントリッスン解除ができる
  • 100ms間値が落ち着くのを待つことができる
  • 全体的に、特にスレッド行き来する処理の記述が容易&見やすい

と言えますね。まだまだ学習が必要だ・・。

広告

ReactiveUI WinFormsをかじる

WPFと言えばMVVMですが実際の現場でWPFの浸透度はどうでしょうか?WinXPのサポートが切れたことで 『.NET3.0のインストールは30分も掛かってユーザに迷惑だから.NET2.0で開発する』 などと古臭い主張をする保守派への反論材料が揃いだした昨今ではまだ、WindowsFormsで新規開発しているほうがまだ多いと感じます。

Windows Formsで『MVC(おそらく正確にはMVP)で開発する』と意気込んだプロジェクトの何%が目標通りの設計、メンテナンス性が実現できているのか疑問です。Frameworkサポートとしては弱いWindows FormsでMVVMを実現させるライブラリの紹介です。

 

ReactiveUI WinForms

まずWindowsFormsアプリのプロジェクトを作成した状態で、Nugetパッケージマネージャで

PM> install-package reactiveui-winforms

としてReactiveUI WinFormsをインストールしておきます。VS2010ではダメだったのですが、VS2013ではokでした、VS2012以降などの制限があるかもしれません。

サンプルFormは以下の構成とします。

form

ReactiveUI-WinFormsではFormをただのViewとして扱い、ViewModelは以下のように別クラスに定義します。

 SampleViewModel.cs

namespace ReactiveUiForWinFormsTest
{
    using System;
    using ReactiveUI;

    public class SampleViewModel : ReactiveObject
    {
        private string inputString1;
        public string InputString1 {
            get { return inputString1; }
            set { this.RaiseAndSetIfChanged(ref inputString1, value); }
        }

        private string inputString2 = "input2の初期値です";
        public string InputString2 {
            get { return inputString2; }
            set { this.RaiseAndSetIfChanged(ref inputString2, value); }
        }

        private DateTime currentDateTime = DateTime.Now;
        public DateTime CurrentDateTime {
            get { return currentDateTime; }
            set { this.RaiseAndSetIfChanged(ref currentDateTime, value); }
        }

        // 初期化コマンド
        public ReactiveCommand<object> Initialize{ get; set;}

        public SampleViewModel()
        {
            // 初期化コマンドの作成
            Initialize = ReactiveCommand.Create(this.WhenAny(vm => vm.InputString1, str => true)); // =常時有効
            Initialize.Subscribe(_ =>
            {
                InputString1 = "初期値1";
                InputString2 = "初期値2";
                CurrentDateTime = new DateTime(2020, 1, 1);
            }
            );
        }

        public override string ToString(){
            return "1:" + InputString1 + Environment.NewLine +
                   "2:" + InputString2 + Environment.NewLine +
                   CurrentDateTime.ToString();
        }
    }
}

MVVMに従い、Viewに対する操作は全てViewModelに定義します。ViewModelの基本クラスとしてReactiveUI.ReactiveObjectクラスから派生させます。

データ更新は通常INotifyPropertyChangedを実装しますが、ReactiveObjectクラスが実装するRaiseAndSetIfChangedメソッドの呼び出しでフィールドへの値のセット及びINotifyPropertyChangedのイベント発砲の処理を行います。thisを明示しないとこのメソッドが出てこず苦労しました;

データ操作はReactiveCommandとして公開します。ReactiveExtensionsを使い、いつ、どんな処理を行うかを定義します。上の例では常時有効にしていますが、これをプロパティ状態に応じてtrue/falseが切り替わるようにするとこの後でバインドするボタンの有効状態が変わります。WPFではおなじみのICommandを実装しているのでCanExecute/Executeを実装しているのでWindowsFormsであっても有効状態も同期する利点がでています。

これをバインドする先のViewはこんな感じになります。

 SampleViewForm.cs

namespace ReactiveUiForWinFormsTest
{
    using System.Windows.Forms;
    using ReactiveUI;

    public partial class SampleViewForm : Form, IViewFor<SampleViewModel>
    {
        public SampleViewForm(SampleViewModel viewModel)
        {
            InitializeComponent();

            ViewModel = viewModel;
            this.Bind(ViewModel, vm => vm.InputString1, v => v.input1TextBox.Text);
            this.Bind(ViewModel, vm => vm.InputString2, v => v.input2TextBox.Text);
            this.Bind(ViewModel, vm => vm.CurrentDateTime, v => v.currentDateTimePicker.Value);
            this.BindCommand(ViewModel, vm => vm.Initialize, v => v.executeButton);
            this.closeButton.Click += (_, __) => this.Close();
        }

        public SampleViewModel ViewModel{
            get;set;
        }
        object IViewFor.ViewModel{
            get { return ViewModel; }
            set { ViewModel = value as SampleViewModel; }
        }
    }
}

ViewはViewModelに依存するので、依存先のViewModelを指定したIViewFor<TViewModel>から派生させます。CloseだけはViewで処理していますが、他は全てViewCommandの処理とバインドさせておりViewが処理を担うことをなくしています。ここでのFormは前述の通りただのViewなのでViewModelはConstructor Injectionで渡してもらうのが妥当でしょう。

蛇足ですがこれを呼び出すのはこんな感じ。

Program.cs

namespace ReactiveUiForWinFormsTest
{
    using System;
    using System.Windows.Forms;
    static class Program
    {
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);

            var viewModel = new SampleViewModel();
            Application.Run(new SampleViewForm(viewModel));
            MessageBox.Show(viewModel.ToString());
        }
    }
}

実行結果はご想像の通り、ちゃんとViewModelとViewのバインドはとれています。

binding__

上の例では単純にBindでバインドしただけですが、WPFでのバインド似でOneWayBindがあったり、バインド時の引数にIValueConverterみたいなものがあったり、かなり使えそうな印象です。

Interactive Extensionsを自分で実装する – 7.Do

InteractiveExtensions(Ix)を自分で実装するシリーズ、第7回はDoです。

DoのシグネチャはReactiveExtensionsのSubscribeに似ています。たくさんオーバーロードがあります。

public static System.Collections.Generic.IEnumerable<TSource> Do<TSource>(this System.Collections.Generic.IEnumerable<TSource> source, System.Action<TSource> onNext);
public static System.Collections.Generic.IEnumerable<TSource> Do<TSource>(this System.Collections.Generic.IEnumerable<TSource> source, System.Action<TSource> onNext, System.Action onCompleted);
public static System.Collections.Generic.IEnumerable<TSource> Do<TSource>(this System.Collections.Generic.IEnumerable<TSource> source, System.Action<TSource> onNext, System.Action<Exception> onError);
public static System.Collections.Generic.IEnumerable<TSource> Do<TSource>(this System.Collections.Generic.IEnumerable<TSource> source, System.Action<TSource> onNext, System.Action<Exception> onError, System.Action onCompleted);
public static System.Collections.Generic.IEnumerable<TSource> Do<TSource>(this System.Collections.Generic.IEnumerable<TSource> source, System.IObserver<TSource> observer)

使用例1

static IEnumerable<int> GetDoTestSequence()
{
    yield return 2;
    yield return 4;
    yield return 3;
    yield return 1;
}
static void DoTest()
{
    var sequence = GetDoTestSequence();
    var doAttachedSequence = sequence.Do(
            i => Console.WriteLine("do:{0}", i),
            e => Console.WriteLine("error:{0}", e.Message),
            () => Console.WriteLine("completed!")
    );
    try{
        foreach (var item in doAttachedSequence){
            Console.WriteLine("foreach:{0}", item);
        }
    }
    catch (Exception e){
        Console.WriteLine("Exception Handled in Main: {0}", e.Message);
    }
}

do_1
Doはシーケンスの間に挟む処理をonNextで指定します。また、例外発生時にはonError, 列挙完了時にはonCompletedが呼ばれます。たくさんオーバーロードがあったのは適宜省略しやすいように用意してくれているだけですね。注意する点としては、初回のonNextは1つめのアイテム列挙に呼ばれること。

続いて、onErrorを使った使用例2

static IEnumerable<int> GetDoTestSequenceWithException()
{
    yield return 2;
    yield return 4;
    yield return 3;
    throw new Exception("Test Exception");
    yield return 1;
}
static void DoTestWithException()
{
    var sequence = GetDoTestSequenceWithException();
    var doAttachedSequence = sequence.Do(
            i => Console.WriteLine("do:{0}", i),
            e => Console.WriteLine("error:{0}", e.Message),
            () => Console.WriteLine("completed!")
    );
    try{
        foreach (var item in doAttachedSequence){
            Console.WriteLine("foreach:{0}", item);
        }
    }
    catch (Exception e){
        Console.WriteLine("Exception Handled in Main: {0}", e.Message);
    }
}

do_2
予想通りではないかと思います。onErrorで例外を一旦キャッチしますが、例外は列挙元にそのまま渡されます。また、ReactiveExtensionsのSubsribeと違う点として、onError発生時のonCompletedの呼ばれ方が挙げられます。ReactiveExtensionsのSubscribeはonErrorが発生してもonCompletedが呼ばれますが、InteractiveExtensionsのDoではonErrorが呼ばれる場合onCompletedが呼ばれません。

話は変わりますが、マイコンを使った組込み機器では処理が正常に動作していることを確認するためWDT(Watch Dog Timer)というペリフェラルがあります。これは定期的に番犬をなだめてやる処理を事前に設定しておくことで、万一一定期間番犬をなだめる処理が入らなかったらシステムが意図通り動作できていないことだと検出し緊急処理(システム停止とか)するために用いられるものです。この番犬をなだめる処理はKickdogと言い、システム依存ですが1~100msに1回実行したりします。今回のDoメソッドはそのKickdogにも使えそうです。C#が使えれば…

さぁ、では実装です。(Let’s Implement It!!)

Do…対象シーケンスの列挙中、また例外発生・完了時にも処理を割り込ませたシーケンスを返す。

namespace EmulateInteractiveExtensions
{
    public static class EmulateIxExtensions
    {
        public static System.Collections.Generic.IEnumerable<TSource> Do<TSource>(this System.Collections.Generic.IEnumerable<TSource> source, System.Action<TSource> onNext){
            return source.Do(onNext, () => {});
        }
        public static System.Collections.Generic.IEnumerable<TSource> Do<TSource>(this System.Collections.Generic.IEnumerable<TSource> source, System.Action<TSource> onNext, System.Action onCompleted){
            return source.Do(onNext, e => {}, onCompleted);
        }
        public static System.Collections.Generic.IEnumerable<TSource> Do<TSource>(this System.Collections.Generic.IEnumerable<TSource> source, System.Action<TSource> onNext, System.Action<Exception> onError){
            return source.Do(onNext, onError, () => {});
        }
        public static System.Collections.Generic.IEnumerable<TSource> Do<TSource>(this System.Collections.Generic.IEnumerable<TSource> source, System.IObserver<TSource> observer){
            if (observer == null){
                throw new ArgumentNullException();
            }
            return source.Do(
                new Action<TSource>(observer.OnNext),
                new Action<Exception>(observer.OnError),
                new Action(observer.OnCompleted)
            );
        }
        public static System.Collections.Generic.IEnumerable<TSource> Do<TSource>(this System.Collections.Generic.IEnumerable<TSource> source, System.Action<TSource> onNext, System.Action<Exception> onError, System.Action onCompleted){
            if (source == null || onNext == null || onCompleted == null){
                throw new ArgumentNullException();
            }

            var enumerator = source.GetEnumerator();
            bool exist = false;
            do
            {
                try
                {
                    exist = enumerator.MoveNext();
                }
                catch (Exception e)
                {
                    onError(e);
                    throw;
                }
                if (exist)
                {
                    onNext(enumerator.Current);
                    yield return enumerator.Current;
                }
            } while (exist);
            onCompleted();
        }
    }
}

たくさんオーバーライドがありますが、結局最後の1つで全て代用できます。やっていることは、対象のシーケンス列挙中に1アイテム列挙前にonNextを呼び、例外の発生確認をしつつ発生したらonErrorを呼んでキャッチ&再スローしつつ、列挙完了時にonCompletedを呼んでいます。

ちょっとGetEnumeratorやMoveNextなど生のIEnumeratorインターフェースメンバを使った実装でした。コンパイルは通りませんが、意味的には以下のようにも読みかえることができます。

Doのほぼ等価メソッド

public static System.Collections.Generic.IEnumerable<TSource> Do<TSource>(this System.Collections.Generic.IEnumerable<TSource> source, System.Action<TSource> onNext, System.Action<Exception> onError, System.Action onCompleted)
{
    // nullチェックは省略
    try
    {
        foreach (var item in source)
        {
            onNext(item);
            yield return item;
        }
    }
    catch (Exception e)
    {
        onError(e);
        throw;
    }
    onCompleted();
}

この場合「try-catch内でyield returnが使えない」というコンパイルエラーになります、yield return も「try-finally内なら」使えるんですけどね。

Reactive Extensionsの入門の入門から、必殺技を繰り出すまで

ReactiveExtensionsの学習にはかずきさんのブログがよくまとまっていると感じました。ReactiveExtensions(以下Rx)は非同期処理でその真価を発すると推測していますが、イベント向けのFromEventというファクトリメソッドで遊んでみます。

まず、Observable.FromEventメソッドのシグネチャは次のようになっています。

 Observable.FromEvent

public static IObservable<TEventArgs> FromEvent<TDelegate, TEventArgs>(
	Func<Action<TEventArgs>, TDelegate> conversion,
	Action<TDelegate> addHandler,
	Action<TDelegate> removeHandler
)

「ややこしー、特に第一引数は一体何モノ?」と思ったのが私の第一印象でした。WPFでの実装例は下のような感じになります。

    public static class RxEventHelper
    {
        public static IObservable<MouseButtonEventArgs> MouseDownAsObservable(this MainWindow window)
        {
            return Observable.FromEvent<MouseButtonEventHandler, MouseButtonEventArgs>
                (
                    handler => (sender, e) => handler(e),
                    handler => window.MouseDown += handler,
                    handler => window.MouseDown -= handler
                );
        }
    }

第2,3引数はイベントへのハンドラの追加・削除なのでいいとして、第1引数ちゃんと理解できていますか?3つのラムダ式ではhandlerを引数にしていますが、第1引数のそれと第2,3引数のそれとでは型が違います。第1引数は =>演算子が続いているので切れ目がパッと理解できなかったのですが、切れ目はここです。

fromeventについて

つまり下のように書き換えられます。こうするとわかりやすいかも。

    public static class RxEventHelper
    {
        public static IObservable<MouseButtonEventArgs> MouseDownAsObservable(this MainWindow window)
        {
            return Observable.FromEvent<MouseButtonEventHandler, MouseButtonEventArgs>
                (
                    mouseAction => {
                        MouseButtonEventHandler handler = (sender, e) =>
                        {
                            mouseAction(e);
                        };
                        return handler;
                    },
                    handler => window.MouseDown += handler,
                    handler => window.MouseDown -= handler
                );
        }
    }

上のコードで第1引数内のhandlerと、第2,3引数内のhandlerの型は同じにしています。このhanlderデリゲート変数は省略でき、省略表記すると良く見るソースになるわけです。

さて、”シーケンス”を処理するイベントを構成して真価を発揮するRxですが、実務でイベントをシーケンス処理したい好例は思いつきませんでしたが、Rxでできることを初めて知ったとき、私の頭にひらめいた「アレ」を作ってみたいと思います。何をしたいかはコードをみればわかってきます。以下WPFの例ですが、WindowsFormsでもイベントの型が変わるだけでやることは何も変わりません。まず先ほど作っていたマウス関連イベントを拡張メソッドとして作成します。(※拡張メソッドである必要はありませんし、今回は有難みもほとんどありません;)

 RxEventHelper.cs

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

    public static class RxEventHelper
    {
        public static IObservable<MouseButtonEventArgs> MouseDownAsObservable(this UIElement window)
        {
            return Observable.FromEvent<MouseButtonEventHandler, MouseButtonEventArgs>
                (
                    mouseAction => {
                        MouseButtonEventHandler handler = (sender, e) =>
                        {
                            mouseAction(e);
                        };
                        return handler;
                    },
                    handler => window.MouseDown += handler,
                    handler => window.MouseDown -= handler
                );
        }

        public static IObservable<MouseEventArgs> MouseMoveAsObservable(this UIElement window)
        {
            return Observable.FromEvent<MouseEventHandler, MouseEventArgs>
                (
                    mouseAction => (s, e) => mouseAction(e),
                    handler => window.MouseMove += handler,
                    handler => window.MouseMove -= handler
                );
        }

        public static IObservable<MouseButtonEventArgs> MouseUpAsObservable(this UIElement window)
        {
            return Observable.FromEvent<MouseButtonEventHandler, MouseButtonEventArgs>
                (
                    mouseAction => (s, e) => mouseAction(e),
                    handler => window.MouseUp += handler,
                    handler => window.MouseUp -= handler
                );
        }
    }

MouseDown, MouseUpで同じようにかけますが、先ほどの通り意識して見比べられるようにしておきました、対象クラスはUIElementにしていれば問題ないでしょう。WindowsFormsならFormかControlにしておけばよさそうですね。今回はWPFなのでイベント処理を行うのはMainWindowです、とはいえXAMLは編集不要です。デフォルトのGridが張り付けられたままでいいです。c#のソースは以下のようにしてみます。

MainWindow.xaml.cs

namespace RxTest
{
    using System;
    using System.Windows;
    using System.Reactive.Linq;

    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        private Point prev = new Point(0,0);
        private IObservable<Point> dragOperation;

        public MainWindow()
        {
            InitializeComponent();
            RegisterXXXCommand();
        }

        public void RegisterXXXCommand()
        {
            dragOperation = this
                .MouseMoveAsObservable()
                .SkipUntil(this.MouseDownAsObservable().Do(_ => this.CaptureMouse()))
                .TakeUntil(this.MouseUpAsObservable().Do(_ => this.ReleaseMouseCapture()))
                .Select(e => e.GetPosition(null));
                //.Where(p => { prev = p; return (prev.X < p.X && prev.Y > p.Y); });

            Subscrbe();
        }

        private void Subscrbe()
        {
            dragOperation.Subscribe(p => { }, () =>
            {
                Subscrbe();
                hadouken();
            });
        }

        private void hadouken(){
            // 体内のエネルギーを自らの両手の中に..
        }

    }
}

MouseDown~MouseUpはRxの例で見かけたことがあるかと思います。そこで使っているRepeatをここでは使っていません。なぜでしょうか? まずここでやりたいことはMouseDown~MouseMove~MouseUpの一連の操作が行われた場合に1度だけ処理をしたいのです。しかし、MouseMoveAsOvservableではマウス移動のたびにメッセージが飛んできます。途中フィルタリングはできても、完了まで待機というのは難しいものと思われます(今の私にはわかりませんでした)。

そのため一連の処理が完了したOnCompletedで”必殺技”を放つようにしました。必殺技とは言え何発も打てないとダメなので、dragOperationの最後にRepeat()を付けたいところですがRepeat()をするとOnCompletedは呼ばれなくなります。「Repeatを使わずに繰り返してSubscribeしたい」、じゃあどうしようということでOnCompletedで再びSubscribeしている訳です。コード上再起っぽく見えますが、再帰呼び出しにはなっていません。また、イレギュラーなことにOnCompletedで処理をすることだけが目的なので、普通最もよく使うはずのOnNextでは何もしたくありません。これは必須引数になっていますので、p=>{} として何もしないAction<Point>を定義しています。

コマンド解釈のところは本来はもっと頑張る必要がありそうです。とりあえず前後関係を把握して[下]・[右下]・[右]の順に位置が移動するということは、前後関係からいえばX,Yはこう移動するはずで…と想定できると思います。さらにリアルなことを言えばタイミング制約もあるでしょうが、ここまでで。。。