在 UWP 中,有一個控件叫 AutoSuggestBox,它的主要成分是一個 TextBox 和 ComboBox。使用它,咱們能夠作一些根據用戶輸入來顯示相關建議輸入的功能,例如百度首頁搜索框那種效果:html
在看這篇文章以前,我建議先看看老周寫的這一篇:http://www.javashuo.com/article/p-mlcrvrlh-cv.html ,先對 AutoSuggestBox 有一個大致的印象,否則下面幹什麼都不知道了。編程
接下來開始咱們的實驗,先準備好百度的接口(這個能夠用瀏覽器的開發者工具抓出來):瀏覽器
public class BaiduService { static BaiduService() { Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); } public async Task<IReadOnlyList<string>> GetSuggestionsAsync(string query) { using (var client = new HttpClient()) { var url = $"http://www.baidu.com/su?wd={HttpUtility.UrlEncode(query)}"; var str = await client.GetStringAsync(url); str = str.Substring(str.IndexOf('{')); str = str.Substring(0, str.LastIndexOf('}') + 1); var jObject = JObject.Parse(str); return jObject["s"].ToObject<string[]>(); } } }
須要引用一下 Newtonsoft.Json 這個包。網絡
靜態構造函數裏我註冊了一下本機的 Encoding,否則會報錯(百度這廝用的是 gbk,而不是常見的 utf-8)。異步
而後開始編寫 Demo 頁面async
XAMLide
<Grid> <Grid Margin="20"> <StackPanel Orientation="Vertical"> <AutoSuggestBox x:Name="AutoSuggestBox" TextChanged="AutoSuggestBox_TextChanged" /> </StackPanel> </Grid> </Grid>
這裏隨便寫了下,反正就是弄了個 AutoSuggestBox,訂閱了一下它的 TextChanged 事件。異步編程
cs代碼:函數
private async void AutoSuggestBox_TextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args) { switch (args.Reason) { case AutoSuggestionBoxTextChangeReason.ProgrammaticChange: case AutoSuggestionBoxTextChangeReason.SuggestionChosen: sender.ItemsSource = null; return; } // User input var query = sender.Text; Debug.WriteLine("get suggestion: " + query); var suggestions = await _baiduService.GetSuggestionsAsync(query); sender.ItemsSource = suggestions; }
觸發的事件參數中有個 Reason 屬性,表面該次事件觸發的緣由。工具
在這裏我若是是程序代碼修改或者用戶選擇了建議項的話,那麼就清除建議項列表。不然就去問百度要一下建議(順便輸出一下,說明觸發了)。
而後就把咱們的 Demo 程序跑起來吧。
看上去工做得仍是蠻正常的嘛。
可是,在這裏我要告訴你,這樣寫,是有一些坑的!
一、
全選,複製,再粘貼,咱們的文字內容是沒有變化纔對的,然而也觸發了一次請求。
二、
若是個人內容爲空,那麼就不該該請求才對的。
三、
在上面的圖中,我 UWP 這三個字母的輸入速度應該是比較快的,那麼 U 那一次就不該該去請求才對。應該以中止輸入一段時間後,纔去進行請求。AutoSuggestBox 控件應該是作了(否則在 UW 時也應該會觸發纔對),但目測時間很是短(可能就 0.1 秒),並且也沒有相關的屬性可以控制這個時長。
四、
由於這個請求是一個異步的網絡請求,因此說很差的話,後發起的請求有可能先返回。按上面的代碼邏輯來講,這樣輸入和建議項就對不上了。
按傳統思路,第 1 點咱們能夠在請求前加個判斷,若是跟上一次相同就不請求。第 2 點加個空字符串判斷便可。第 3 點就麻煩了,真要實現咱們得加個計時之類的方法來作。第 4 點也是很麻煩,我目前想到的是發起請求時給個 token 之類,接收到的時候再對比是不是最新的 token。
但說實話,這麼一整套下來,不麻煩麼?並且代碼量不是一點兩點。
在這裏,我要安利各位,只要你使用 Rx,解決這點小問題徹底不在話下。
Rx 的全稱是 Reactive Extensions,是一種針對異步編程的編程模型。Rx 不只僅在 .Net 下有實現,在 JavaScript、Java 等等平臺都有相關的實現。
概念說完了,繼續實驗。
引用 Rx 的 nuget 包,System.Reactive。
在頁面的構造函數先編寫以下的代碼:
var changed = Observable.FromEventPattern<TypedEventHandler<AutoSuggestBox, AutoSuggestBoxTextChangedEventArgs>, AutoSuggestBox, AutoSuggestBoxTextChangedEventArgs>( handler => AutoSuggestBox.TextChanged += handler, handler => AutoSuggestBox.TextChanged -= handler);
這段代碼以 AutoSuggestBox 的 TextChanged 事件建立一個可監聽的數據源 changed 對象。
接下來,咱們處理第 1 點,須要忽略掉相同的文本內容。
var input = changed .DistinctUntilChanged(temp => temp.Sender.Text);
DistinctUntilChanged 這個擴展方法是 Rx 提供的,若是數據源內容不變,則不會觸發。
而後咱們處理第 3 點,只有中止輸入一段時間後,咱們再去發起請求。
var input = changed .DistinctUntilChanged(temp => temp.Sender.Text) .Throttle(TimeSpan.FromSeconds(1));
這個也很簡單,Rx 提供了 Throttle 方法,傳入須要的時間就能夠了,這裏我設定成中止輸入 1 秒後才觸發。
而後接下來咱們要區分兩種狀況,一個是用戶輸入的,另外一個是非用戶輸入的。
var notUserInput = input .ObserveOnDispatcher() .Where(temp => temp.EventArgs.Reason != AutoSuggestionBoxTextChangeReason.UserInput); var userInput = input .ObserveOnDispatcher() .Where(temp => temp.EventArgs.Reason == AutoSuggestionBoxTextChangeReason.UserInput) .Where(temp => !string.IsNullOrEmpty(temp.Sender.Text));
在用戶輸入的時候,輸入後文本框非空咱們才觸發(第 2 點)。
這裏注意到還有 ObserveOnDispatcher 這個方法的調用,這個調用就是說,接下來個人操做須要在當前線程上進行。Rx 默認是會在另外一個線程上的,在 Where 方法中咱們引用到了 AutoSuggestBox 控件,因此須要調用到該方法。
接下來咱們處理一下 userInput,有了輸入,咱們天然須要輸出,輸出就是建議項:
var userInput = input .ObserveOnDispatcher() .Where(temp => temp.EventArgs.Reason == AutoSuggestionBoxTextChangeReason.UserInput) .Where(temp => !string.IsNullOrEmpty(temp.Sender.Text)) .Select(temp => _baiduService.GetSuggestionsAsync(temp.Sender.Text));
調用百度接口,返回 Task<IReadOnlyList<string>>。同時,咱們對 notUserInput 也處理一下,返回 null,但類型也是 Task<IReadOnlyList<string>>。
var notUserInput = input .ObserveOnDispatcher() .Where(temp => temp.EventArgs.Reason != AutoSuggestionBoxTextChangeReason.UserInput) .Select(temp => Task.FromResult<IReadOnlyList<string>>(null));
如今,咱們把這兩個從新合成爲一個,由於咱們數據源觸發的條件是 TextChanged,而不是由於上面這一大堆東西才進行觸發。
var merge = Observable .Merge(notUserInput, userInput);
最後,咱們能夠監聽這個數據源了,調用 Subscribe 方法(固然還要再 ObserveOnDispatcher 一次):
merge .ObserveOnDispatcher() .Subscribe(suggestions => { AutoSuggestBox.ItemsSource = suggestions; });
這樣更新上去咱們的 AutoSuggestBox 就好了。
慢着,咱們的第 4 點還沒處理呢。這個只須要稍微修改一下就能夠了(Rx 真方便)。
var merge = Observable .Merge(notUserInput, userInput) .Switch();
Switch 方法會將輸出的順序按照輸入的順序來排序,這樣以後,咱們的第 4 點就能解決掉了。
最終下來,咱們解決這麼一系列問題只是寫了這麼點的代碼,若是按傳統的寫法嘛,那不知道寫到何時去了。Rx 萬歲!
雖然 Rx 學習起來難度曲線很是大,可是在解決某些場景,Rx 是很是的有效的。(順帶一提,Angular 就集成了 RxJS,可見 Rx 存在其優點)
參考資料:
DevCamp 2010 Keynote - Rx: Curing your asynchronous programming blues