パート1:ReactiveUIの概要:ViewModelのプロパティをポンピングするパート2:ReactiveUIの概要:コレクションプロパティの操作、プロパティ間の依存関係の構築、コレクションの操作に関連するReactiveUIの機能については既に説明しました。 これらは、ReactiveUIを使用した開発の基礎となる基本的なプリミティブの1つです。 別のそのようなプリミティブは、この部分で検討するチームです。 チームは、何らかのイベントに応じて実行されるアクションをカプセル化します。通常、これはユーザーリクエストまたは何らかの追跡された変更です。 ReactiveUIのコマンドを使用して何ができるかを学習し、それらの作業の機能について説明し、ReactiveUIのコマンドがWPFおよびその親fromから精通しているチームとどのように異なるかを調べます。
ただし、コマンドに移る前に、リアクティブプログラミングに関するより広範なトピック、つまりタスク<T>とIObservable <T>の関係、およびホットシーケンスとコールドシーケンスについて説明します。
タスク対 IObservable
したがって、タスク<T>(+ async、await、それだけ)とIObservable <T>の間に類似点を描きましょう。 ReactiveUIでチームと連携する方法を理解することは重要ですが、説明したアプローチはより広範なアプリケーションを持ち、それについて知るのに害はありません。 したがって、
タスク<T>はIObservable <T>です。 しかし、それらは確かに同等ではありません。IObservable<T>ははるかに広範なタスクを解決できます。
なんとなく怪しいですね。 正しくしましょう。 すぐに例を見てみましょう:
Task<string> task = Task.Run(() => { Console.WriteLine(DateTime.Now.ToLongTimeString() + " "); Thread.Sleep(1000); Console.WriteLine(DateTime.Now.ToLongTimeString() + " "); return " "; }); Console.WriteLine(DateTime.Now.ToLongTimeString() + " - "); string result = task.Result; Console.WriteLine(DateTime.Now.ToLongTimeString() + " : " + result);
タスクを作成しました。非同期に実行され、起動後すぐに完了を待たずに他のことを行うことを妨げません。 結果は予測可能です:
18:19:47タスクの結果を待つ前に何かをする
18:19:47長いタスクを開始する
18:19:48長いタスクを完了します
18:19:48結果:長いタスクの結果
最初の2行はすぐに表示され、ラッキーとして別の順序になる場合があります。
そして、IObservable <T>で書き換えます:
IObservable<string> task = Observable.Start(() => { Console.WriteLine(DateTime.Now.ToLongTimeString() + " "); Thread.Sleep(1000); Console.WriteLine(DateTime.Now.ToLongTimeString() + " "); return " "; }); Console.WriteLine(DateTime.Now.ToLongTimeString() + " - "); string result = task.Wait();
違いは2行です:Task <string>の代わりにIObservable <string>、Task.Run()の代わりにObservable.Start()、task.Resultの代わりにtask.Wait()。 作業の結果はまったく同じです。
タスクの完了後にアクションを開始する別の有名なトリックを見てみましょう。
ここでも、実質的に違いはありません。
タスク<T>はIObservable <T>で表すことができ、1つの要素と完了信号を生成します。 このようなアプローチの間に哲学的およびアーキテクチャ上の大きな違いはなく、誰でも使用できます。 async / awaitでも両方の場合に利用できます。非同期メソッドでIObservableから結果を取得する場合、例のように
Wait()メソッドで待機をブロックすることはできませんが、
awaitを使用します。 さらに、これら2つのアプローチを組み合わせて、1つのビューを別のビューに変換し、両方の利点を活用できます。
ホットシーケンスとコールドシーケンス
オブザーバブルシーケンス(オブザーバブル)を使用した作業に関するもう1つの質問について説明します。 コールド(コールド)とホット(ホット)の2つのタイプがあります。 コールドシーケンスは受動的であり、要求に応じて、サブスクライブ時に通知を生成し始めます。 ホットシーケンスはアクティブであり、誰かがサブスクライブしたかどうかに依存しません。通知はとにかく生成されますが、たまに何も送信されません。
タイマーティック、マウス移動イベント、ネットワーク経由のリクエストはホットシーケンスです。 ある時点でそれらにサブスクライブすることにより、現在の通知を受信し始めます。 10人のオブザーバーが購読します-通知は全員に配信されます。 タイマーの例を次に示します。
コールドシーケンスとは、たとえば、データベースへのクエリや、ファイルを1行ずつ読み取ることです。 サブスクリプション時に要求または読み取りが開始され、新しい行が受信されるたびにOnNext()が呼び出されます。 行が終了すると、OnComplete()が呼び出されます。 再サブスクライブすると、すべてが再び繰り返されます。データベース内の新しいクエリまたはファイルを開いて、すべての結果を返し、完了シグナルを返します。
クラシックチーム...
それでは、今日のトピックであるチームに移りましょう。 すでに述べたように、チームは特定のイベントに応じて実行されるアクションをカプセル化します。 このようなイベントは、ユーザーが「保存」ボタンをクリックすることです。 カプセル化されたアクションは、何かを保存する操作になります。 ただし、コマンドは、明示的なユーザーアクションまたは関連する間接イベントへの応答としてだけでなく実行できます。 ユーザーに関係なく、5分ごとに実行されるタイマーからの信号も、同じ「保存」コマンドを開始できます。 また、コマンドは通常、ユーザーが何らかの方法で実行するアクションに対して正確に使用されますが、他のケースでの使用を無視しないでください。
また、コマンドを使用すると、実行が現在使用可能かどうかを確認できます。 たとえば、保存を常に使用できるようにするのではなく、フォームのすべての必須フィールドが入力され、インターフェイスでボタンがアクティブかどうかはコマンドの可用性に依存します。
ICommandチームインターフェイスとは何かを見てみましょう。
public interface ICommand { event EventHandler CanExecuteChanged; bool CanExecute(object parameter); void Execute(object parameter); }
実行は明らかにコマンドを実行します。 パラメーターを渡すことができますが、必要な値がコマンド自体の内部で取得できる場合(たとえば、ViewModelから取得する場合)、この機会を使用しないでください。 さらにその理由を理解します。 しかし、もちろん、パラメーターを渡すことが最も受け入れられるオプションである場合があります。
CanExecuteは、コマンド実行が現在使用可能かどうかを確認します。 パラメーターもあり、ここではすべてがExecuteと同じです。 特定のパラメーター値を使用したCanExecuteが、同じパラメーター値のみを使用したコマンドの実行を許可または禁止することが重要です。他の値については、結果が異なる場合があります。 また、Executeは、アクションを実行する前にCanExecuteをチェックせず、呼び出しコードのタスクであることに注意してください。
CanExecuteChangedイベントは、実行機会のステータスが変化したときに発生するため、CanExecuteを再確認する必要があります。 たとえば、フォーム内のすべてのフィールドに入力して保存できるようになった場合、インターフェイスにボタンを含める必要があります。 コマンドが添付されたボタンは、この方法で認識します。
...そしてそれらの何が問題なのか
最初の問題は、CanExecuteChangedイベントが、実行ステータスが変更されたパラメーター値を示していないことです。 これが、Execute / CanExecuteを呼び出すときのパラメーターの使用を避ける必要がある理由です。パラメーターに関するICommandインターフェイスは特に一貫性がありません。 事後対応型のチームでは、これから見るように、このアプローチはまったくうまくいきませんでした。
2番目の問題-Execute()は、コマンドの完了後にのみ制御を返します。 コマンドが長時間実行されると、ユーザーはハングしたインターフェイスに直面するため、動揺します。
誰がこれを好きになるでしょうか?このプログラムには、コマンド起動ボタン、ログ出力、進行状況バーがあり、通常の状況では常に移動するはずです。 起動時のコマンドは、現在の時刻をログに書き込み、1.5秒間(たとえば、データの読み込み)を実行し、完了時刻を書き込みます。
進行状況バーが停止し、ログはコマンドの最後でのみ更新され、インターフェースがフリーズします。 うまくいかなかった...
状況を保存する方法は? もちろん、別のスレッドで必要なアクションの実行のみを開始し、制御を返すようにコマンドを実装できます。 しかし、その後、別の問題が発生します。ユーザーは、前のボタンが完了する前であっても、もう一度ボタンをクリックしてコマンドを再実行できます。 実装を複雑にしてみましょう。タスクの実行中にmake CanExecuteがfalseを返すようにします。 インターフェースはハングせず、コマンドは数回並行して開始されず、目標を達成しました。 しかし、これはすべて自分の手で行わなければなりません。 また、ReactiveUIチームでは、これらすべてをはじめとする多くのことを既に知っています。
リアクティブチーム
ReactiveCommand <T>に会います。 混同しないでください:ReactiveCommand(ReactiveUI.Legacy名前空間にあり、明らかに非推奨です)という同じ名前の非ジェネリック実装がまだあります。 このジェネリックパラメーターは、パラメーターのタイプではなく、結果のタイプを示しますが、後でこれに戻ります。
CanExecuteに関連するすべてのものから始めることを省略して、すぐにコマンドを作成して実行してください。 通常、new演算子を使用して直接コマンドを作成するのではなく、必要なメソッドを提供する静的なReactiveCommandクラスを使用します。
var command = ReactiveCommand.Create(); command.Subscribe(_ => { Console.WriteLine(DateTime.Now.ToLongTimeString() + " "); Thread.Sleep(1000); Console.WriteLine(DateTime.Now.ToLongTimeString() + " "); }); command.Execute(null); Console.WriteLine(DateTime.Now.ToLongTimeString() + " "); Console.ReadLine();
ReactiveCommand.Create()メソッドは、ReactiveCommand <object>タイプの同期タスクを作成します。 Execute()は、作業の完了後にのみ制御を返します。
19:01:07長いタスクの開始
19:01:08長いタスクを完了します
19:01:08チームを運営した後
後で非同期コマンドを作成する方法を見ていきますが、ここではコマンドを実行する機能の制御について見てみましょう。
コマンドを実行する機能
CanExecuteとその関連機能について説明します。 既に見たもの(CanExecuteメソッドとCanExecuteChangedイベント)に加えて、ReactiveCommandはIsExecutingおよびCanExecuteObservableシーケンスを提供します。
var command = ReactiveCommand.Create(); command.Subscribe(_ => Console.WriteLine(" ")); command.CanExecuteChanged += (o, a) => Console.WriteLine("CanExecuteChanged event: now CanExecute() == {0}", command.CanExecute(null)); command.IsExecuting.Subscribe(isExecuting => Console.WriteLine("IsExecuting: {0}", isExecuting)); command.CanExecuteObservable.Subscribe(canExecute => Console.WriteLine("CanExecuteObservable: {0}", canExecute)); Console.WriteLine(" , ..."); command.Execute(null); Console.WriteLine(" ");
IsExecuting:False
CanExecuteObservable:False
CanExecuteObservable:True
CanExecuteChangedイベント:CanExecute()== Trueになりました
すべてを購読し、チームを運営して...
IsExecuting:True
CanExecuteChangedイベント:現在CanExecute()== False
CanExecuteObservable:False
実行中のコマンド
IsExecuting:False
CanExecuteChangedイベント:CanExecute()== Trueになりました
CanExecuteObservable:True
コマンドを実行した後
サブスクリプションの直後、コマンドが起動される前に何が起こるか、特に注意を払うことはできません。これは初期化です。 実際、サブスクリプションが発生するとすぐに現在の状態が返されます(最初の要素はコールドになり、後続の要素はホットになります)。 また、CanExecuteObservableは最初はfalseに設定されています。 サブスクライブするとき、最初にこの値を提供し、次にチームは可用性を決定するメカニズムを提供しなかったと判断し、デフォルトでコマンドを使用できるようにします。
プログラムの出力から判断すると、コマンドは実行中にすでに使用できません。 これは特に非同期コマンドの場合に当てはまります。この方法では、複数回並列実行されません。 したがって、CanExecute、CanExecuteObservable、およびCanExecuteChangedイベントは、計算に提供するものだけでなく、コマンドが現在実行されているかどうかにも依存します。 IsExecutingは、コマンドが現在実行されているかどうかに関する正確な情報を提供します。これは、たとえば、何らかの進行状況インジケーターを表示するために使用できます。
次に、いつ実行できるかに関する情報をチームに提供しましょう。 これを行うために、ReactiveCommandクラスでコマンドを作成する各メソッドには、
IObservable <bool> canExecuteを受け入れるオーバーロードがあります。 チームはこのシーケンスにサブスクライブし、変更を受信すると実行可能性に関する情報を更新します。 私たちは見ます:
var subject = new Subject<bool>(); var command = ReactiveCommand.Create(subject); command.CanExecuteChanged += (o, a) => Console.WriteLine("CanExecuteChanged event: now CanExecute() == {0}", command.CanExecute(null)); command.CanExecuteObservable.Subscribe(canExecute => Console.WriteLine("CanExecuteObservable: {0}", canExecute)); Console.WriteLine(" "); subject.OnNext(true); Console.WriteLine(" "); subject.OnNext(false); Console.WriteLine(" "); subject.OnNext(false);
ここでの主題は観察可能なものであり、私たちはそれを自分の手で制御し、必要な値をチームに渡します。 少なくとも、これはテスト時に非常に便利です。 私たちはすべてにサブスクライブし、実行を利用可能にしてから、2回アクセスできなくなります。 どのような結果が得られますか?
CanExecuteObservable:False
実行可能にする
CanExecuteChangedイベント:CanExecute()== Trueになりました
CanExecuteObservable:True
実行をアクセス不能にする
CanExecuteChangedイベント:現在CanExecute()== False
CanExecuteObservable:False
もう一度実行にアクセスできないようにする
すべてが期待されているようです。 最初は、実行できません。 その後、チームは私たちが行っている変更に対応し始めます。 ここで、同じアクセシビリティ状態を連続して数回送信すると、チームは再試行を無視することに注意してください。 また、CanExecuteObservableはbool型の値のシーケンスにすぎず、CanExecuteメソッドにパラメーターがあるという事実との非互換性もあります。 ReactiveCommandでは、単に無視されます。
コマンドを呼び出す方法
既にExecute()メソッドを使用したコマンドの呼び出しを見てきました。 他の方法を見てみましょう:
IObservable <T> ExecuteAsync(オブジェクトパラメーター)1つの特徴があります。コマンドは、ExecuteAsync()の結果のサブスクリプションが完了するまで開始されません。 私たちはそれを使用します:
command.ExecuteAsync().Subscribe();
ただし、同期コマンドはこれから非同期になりません。 もちろん、ExecuteAsync()はすぐに制御を返しますが、実行はまだ開始されていません! そして、それを開始するSubscribe()は、コマンドの完了後にのみ制御を返します。 実際、Execute()に相当するものを作成しました。 ただし、これは当然です。ExecuteAsync()はコールドシーケンスを返し、それにサブスクライブすると、長いタスクの実行が開始されるためです。 そして、それは現在のスレッドで実行されます。 これは、サブスクライブする場所を明示的に示すことで修正できますが、
command.ExecuteAsync().SubscribeOn(TaskPoolScheduler.Default).Subscribe();
TPLスケジューラーは、サブスクリプションを完了する責任を負います。 したがって、サブスクリプションはTask.Run()のようなもので行われ、すべてが正常に機能します。 しかし、実際にこれを行うことは価値がなく、この例は可能性の1つだけを示しています。 多くのプランナーがいますが、いつかこのトピックに触れます。
タスク<T> ExecuteAsyncTask(オブジェクトパラメーター)ExecuteAsync()とは異なり、このメソッドはコマンドをすぐに実行します。 私達は試みます:
command.ExecuteAsyncTask();
タスク<>が返されましたが、人生にはまだ幸せがありません。 ExecuteAsyncTask()は、コマンドが完了した後にのみ制御を返し、既に完了したタスクを提供します。 何らかのセットアップ。
InvokeCommand()このメソッドを使用すると、信号がシーケンスに表示されたときにコマンド呼び出しを簡単に構成できます(たとえば、プロパティの変更)。 このようなもの:
this.WhenAnyValue(x => x.FullName).Where(x => !string.IsNullOrEmpty(x)).InvokeCommand(this.Search);
これまでのところ、コマンドを非同期に実行する方法は見つかっていません。 もちろん、ExecuteAsync()メソッドを使用して、サブスクリプションを実行するスケジューラーを割り当てることができますが、これは松葉杖です。 さらに、WPFはこのメソッドを認識していないため、Execute()の呼び出しを続けて電話を切ります。
非同期リアクティブコマンド
同期コマンドは、アクションが迅速に実行されるときに意味があり、物事を複雑にすることは意味がありません。 また、長いタスクには非同期コマンドが必要です。 ここには、ReactiveCommand.CreateAsyncObservable()とReactiveCommand.CreateAsyncTask()の2つのメソッドがあります。 それらの違いは、アクションがどのように表現されるかだけです。 この記事の最初のセクションと非同期タスクの提示方法に戻ります。
CreateAsyncObservableを見てみましょう:
var action = new Action(() => { Console.WriteLine(DateTime.Now.ToLongTimeString() + " "); Thread.Sleep(1000); Console.WriteLine(DateTime.Now.ToLongTimeString() + " "); }); var command = ReactiveCommand.CreateAsyncObservable(_ => Observable.Start(action)); Console.WriteLine(DateTime.Now.ToLongTimeString() + " ..."); command.Execute(42); Console.WriteLine(DateTime.Now.ToLongTimeString() + " "); Console.ReadLine();
2:33:50チームを開始します...
2:33:50コマンド実行後
2:33:50長いタスクの開始
2:33:51長いタスクを完了する
やった! コマンドが完了するまで実行はブロックされなくなり、インターフェイスはハングしません。 ExecuteAsyncとExecuteAsyncTaskの場合、すべてが似ています。ロックはありません。
今createAsyncTask:
var command = ReactiveCommand.CreateAsyncTask(_ => Task.Run(action)); var command = ReactiveCommand.CreateAsyncTask(_ => doSomethingAsync());
説明されている両方のメソッドには、CanExecuteObservableの受け渡しやキャンセル機能をサポートするオーバーロードが多数あります。
さらに、非同期コマンドから結果を返すことができます。 ReactiveCommand <T>からの汎用パラメーターTは、コマンドの結果のタイプにすぎません。
ReactiveCommand<int> command = ReactiveCommand.CreateAsyncTask(_ => Task.Run(() => 42)); var result = await command.ExecuteAsync();
そして、あなたはすぐにそれをどこかに向けることができます:
var command = ReactiveCommand.CreateAsyncTask(_ => Task.Run(() => 42)); command.Subscribe(result => _logger.Log(result)); _answer = command.ToProperty(this, this.Answer);
結果がUIストリームで返されることが保証されています。 したがって、ちなみに、コマンドの実行に対する反応としてSubscribeでいくつかの長いアクションを実行することは禁忌です。 同期コマンドの場合、独自の結果を返すことはできません。コマンドのタイプはReactiveCommand <object>であり、コマンドの実行に使用された値が返されます。
チームが作業するときに発生する例外をキャッチします
コマンドの操作の例外は、特に何らかの種類のデータの読み込みに関しては常に発生する可能性があります。 したがって、それらをキャッチして処理する方法を学習する必要があります。 どこでどのようにそれを行うのですか?
同期コマンド var command = ReactiveCommand.Create(); command.Subscribe(_ => { throw new InvalidOperationException(); }); command.ThrownExceptions.Subscribe(e => Console.WriteLine(e.Message)); command.Execute(null);
すべてのメソッドの呼び出しは同期的であるため、例外がスローされることが予想されます。 しかし、それはそれほど単純ではありません。 Execute()は実際には例外をスローしません。 すべての例外が単純に取り込まれるような方法で実装されます。 他の2つのメソッドは、期待どおりに一度に例外をスローします。
非同期コマンドを使用すると、すべてがはるかに興味深いものになります。ReactiveCommandは、非同期コマンドの実行時に例外がスローされるThrownExceptionのシーケンスを提供します。 ObservableとTaskに基づくチームに違いはありません。 実験用のチームを作成しましょう:
var command = ReactiveCommand.CreateAsyncTask(_ => Task.Run(() => { throw new InvalidOperationException(); })); var command = ReactiveCommand.CreateAsyncObservable(_ => Observable.Start(() => { throw new InvalidOperationException(); })); command.ThrownExceptions.Subscribe(e => Console.WriteLine(e.Message));
そして、コマンドを呼び出すさまざまな方法を試してください:
command.Execute(null);
コマンド自体(たとえば、command.ToProperty(...))を何らかの方法でサブスクライブした場合、例外が発生すると、OnError()コマンドは送信されません。
この例では、例外が「一度」発生する例は奇妙に見えます。 TPLでは、これはキャッチされなかった例外がトレースなしで消えないようにするために必要でした。 すぐに、ThrownExceptionsを介してそれらを転送し、「将来」スローしないようにすることができました。 しかし、これは実装であり、ReactiveUIの次のバージョンでは、この点で何かが変わるようです。
非同期コマンドをキャンセル
長時間実行できるコマンドはキャンセルできます。 これを行うには多くの方法があります。 キャンセルされるまでメッセージをループで表示する非同期コマンドを作成しましょう。
var command = ReactiveCommand.CreateAsyncTask(async (a, t) => { while (!t.IsCancellationRequested) { Console.WriteLine(""); await Task.Delay(300); } Console.WriteLine(""); });
キャンセルする最初の方法は、ExecuteAsync()に基づいて作成されたサブスクリプションをキャンセルすることです。 var disposable = command.ExecuteAsync().Subscribe(); Thread.Sleep(1000); disposable.Dispose();
2番目の方法は、ExecuteAsyncTask()によるトークン転送です。 var source = new CancellationTokenSource(); command.ExecuteAsyncTask(ct: source.Token); Thread.Sleep(1000); source.Cancel();
しかし、Execute()メソッドによって、つまりWPF自体などから呼び出されたときに起動されるコマンドをキャンセルしたい場合はどうでしょうか。これも簡単です。このため、IObservableでTaskをラップし、TakeUntil()メソッドを使用する必要があります。別のコマンドを呼び出してキャンセルする例を示します。 Func<CancellationToken, Task> action = async (ct) => { while (!ct.IsCancellationRequested) { Console.WriteLine(""); await Task.Delay(300); } Console.WriteLine(""); }; IReactiveCommand<object> cancelCommand = null; var runCommand = ReactiveCommand.CreateAsyncObservable(_ => Observable.StartAsync(action).TakeUntil(cancelCommand)); cancelCommand = ReactiveCommand.Create(runCommand.IsExecuting); runCommand.Execute(null); Thread.Sleep(1000); cancelCommand.Execute(null);
コマンドは、cancelCommandシーケンスに次の通知が表示されるまで実行されます。実際、cancelCommandの代わりに、コマンドがなくても、観察されたシーケンスがある場合があります。これらのすべての方法には、微妙な点が1つあります。キャンセルを開始すると、コマンドはすぐに完了して再実行可能と見なされますが、キャンセルトークンがタスクによって無視された場合、一部のアクションは内部で続行できます。チームがキャンセルされた場合、これも検討する価値があります。これは、タスク<T>がまったくないコマンドをキャンセルする場合に特に当てはまります。 Action action = () => { ... }; var runCommand = ReactiveCommand.CreateAsyncObservable(_ => Observable.Start(action).TakeUntil(cancelCommand));
ここでは、実行をキャンセルした後でもアクションが実行され、コマンドを再び呼び出すことができます。これだけがカーテンの後ろで起こり、非常に予期しない結果につながる可能性があります。チーム統合
他のコマンドを呼び出すコマンドを簡単に作成できます。 RefreshAll = ReactiveCommand.CreateCombined(RefreshNotifications, RefreshMessages);
この場合、送信されたセットのすべてのコマンドが実行可能であれば、コマンドは実行可能です。また、別のcanExecuteを渡して、コマンドをいつ実行できるかを示すことができるオーバーロードもあります。この場合、各コマンドを実行する機能と渡されたパラメーターが考慮されます。
チームとの連携の例
コマンドの使用を示す小さな例を書いてみましょう。検索を行います。まあ、条件付き検索。実際、1.5秒間アクティビティをシミュレートし、結果としていくつかのクエリ変更のコレクションを返します。また、検索のキャンセル、新しい検索を開始するときに古い結果をクリアすること、および入力データが変更されたときに自動的に検索を実行する機能もサポートします。ビューモデルを見ます: public class SearchViewModel : ReactiveObject { [Reactive] public string SearchQuery { get; set; } [Reactive] public bool AutoSearch { get; set; } private readonly ObservableAsPropertyHelper<ICollection<string>> _searchResult; public ICollection<string> SearchResult => _searchResult.Value; public IReactiveCommand<ICollection<string>> Search { get; } public IReactiveCommand<object> CancelSearch { get; } public SearchViewModel() { Search = ReactiveCommand.CreateAsyncObservable( this.WhenAnyValue(vm => vm.SearchQuery).Select(q => !string.IsNullOrEmpty(q)), _ => Observable.StartAsync(ct => SearchAsync(SearchQuery, ct)).TakeUntil(CancelSearch) ); CancelSearch = ReactiveCommand.Create(Search.IsExecuting); Observable.Merge( Search, Search.IsExecuting.Where(e => e).Select(_ => new List<string>())) .ToProperty(this, vm => vm.SearchResult, out _searchResult); this.WhenAnyValue(vm => vm.SearchQuery) .Where(x => AutoSearch) .Throttle(TimeSpan.FromSeconds(0.3), RxApp.MainThreadScheduler) .InvokeCommand(Search); } private async Task<ICollection<string>> SearchAsync(string query, CancellationToken token) { await Task.Delay(1500, token); return new List<string>() { query, query.ToUpper(), new string(query.Reverse().ToArray()) }; } }
もちろん、ここには欠陥があります。それらの1つは自動検索です。ユーザーが検索中にクエリを変更した場合、現在の検索は停止されず、最初に完了してから、新しいクエリが検索されます。ただし、これを修正するには数行の問題があります。または、ユーザーがクエリ全体を消去したときに検索結果をクリアしないのは奇妙です。しかし、このような複雑な例は次の部分に任せますが、今のところは既存のロジックに限定します。繰り返しになりますが、一般に、検索エンジンの最も原始的な動作ではなく、非常に簡潔かつ明確に説明されているという事実に注意を向けます。XAMLを見てみましょう。 <Grid DataContext="{Binding ViewModel, ElementName=Window}"> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="*"/> <RowDefinition Height="Auto"/> </Grid.RowDefinitions> <StackPanel> <Label>Search query:</Label> <TextBox Margin="10, 5" Text="{Binding SearchQuery, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" /> </StackPanel> <ListBox Grid.Row="1" ItemsSource="{Binding SearchResult}"/> <Grid Grid.Row="2"> <Grid.ColumnDefinitions> <ColumnDefinition Width="*"></ColumnDefinition> <ColumnDefinition Width="Auto"></ColumnDefinition> </Grid.ColumnDefinitions> <ProgressBar Margin="10" x:Name="SearchExecutingProgressBar" /> <StackPanel Orientation="Horizontal" Grid.Column="1"> <CheckBox VerticalAlignment="Center" IsChecked="{Binding AutoSearch, Mode=TwoWay}">Auto search</CheckBox> <Button Margin="10" Command="{Binding Search}">Run</Button> <Button Margin="10" Command="{Binding CancelSearch}">Cancel</Button> </StackPanel> </Grid> </Grid>
ここでの質問はProgressBarを引き起こす可能性があります。検索プロセスに含めることを望んでいました。ただし、Searchコマンドでは、IsExecutingプロパティはブールではなく、シーケンスであり、XAMLでのバインドは機能しません。したがって、ビューのコンストラクターでバインドします。 public partial class MainWindow : Window { public SearchViewModel ViewModel { get; } public MainWindow() { ViewModel = new SearchViewModel(); InitializeComponent(); this.WhenAnyObservable(w => w.ViewModel.Search.IsExecuting).BindTo(SearchExecutingProgressBar, pb => pb.IsIndeterminate); } }
はい、ReactiveUIはそのようなバインディングをサポートしており、理論的にはこの方法ですべてを行うことができます。しかし、バインディングについては別の機会に話をしますが、今のところ、私は自分がなくてはならないことだけに制限しています。やった!
それは正常に機能し、検索中にインターフェイスがハングせず、キャンセルが動作し、自動検索が動作し、顧客は夢中になり、トリプルプレミアムを提供します。
次のエピソードで
それで、少し要約します。このパートでは、タスク<T>とIObservable <T>の関係を調べました。ホットシーケンスとコールドシーケンスを比較しました。しかし、私たちの主なトピックはチームでした。同期コマンドと非同期コマンドを作成し、それらを異なる方法で呼び出す方法を学びました。それらを実行する機能を有効または無効にする方法、および非同期コマンドでエラーをキャッチする方法を見つけました。さらに、非同期コマンドをキャンセルする方法を見つけました。この部分では、ビューモデルのテストに触れたかったのですが、どういうわけか、チームがそのようなシートに拡張されるとは計算しませんでした。したがって、今回はそうではありません。次のパートでは、バインディングまたはテストとプランナーのいずれかを検討します。切り替えないでください!