在 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