很多时候,为了界面更加友好,我们往往会设计这样一个功能:当用户在界面上输入一段内容时,无需点击确定按钮,后台自动对该输入进行处理,获取输出并显示出来。
要实现这个功能,一个最直接的方式是在TextBox的TextChanged事件中增加回调函数,在回调函数中获取返回值,并输出。这么乍看起来是一个比较好的方法,但有经验的程序员就马上会发现这样有几点不足:
针对这两个问题,我们可以提出如下解决方案:
这个方案本身没有什么问题,但这两个步骤都采用了异步操作,而异步操作往往把同步方案变得复杂的多了,带来了一系列要处理的新的问题,常见问题如下:
这几个问题处理起来并不是很复杂,微软在MSDN文章Rx Hands-On Labs中就详述了通过RX解决这个问题的方案(其文档Rx .NET HOL介绍的非常详细,我本来想翻译下的,无奈时间不够,英语也太烂,推荐看本文的朋友读下):
该文档中还列举了其它几个问题的解决方案,由于较多,这里就不一一列举了。这种方式比较完善,但增加了一大堆回调函数和扩展函数,用起来不是很友好,对RX框架不熟悉的程序员来说也容易出错。
在.Net 4.5中,引入了基于任务的异步编程模型,可以将以同步编程的方式实现异步处理。例如,对于前面的需求,我们可以以如下的方式实现:
CancellationTokenSource currentCancelTokenSource = new CancellationTokenSource();
input.TextChanged += (s, e) =>
{
currentCancelTokenSource.Cancel(); //取消当前正在执行的任务
currentCancelTokenSource.Dispose();
currentCancelTokenSource = new CancellationTokenSource();
ProcessAsync(input.Text, currentCancelTokenSource.Token);
};
async void ProcessAsync(string input, CancellationToken cancel)
{
await Task.Delay(500); //延迟500ms执行
if (cancel.IsCancellationRequested) //新的输入已经产生,任务取消
return;
try
{
var outputContent = await GetOutputAsync(input);
if (cancel.IsCancellationRequested) //本轮任务处理时间较长,后续的新的任务已经开始了。使用新任务的结果,本轮任务结果放弃。
return;
output.Text = outputContent;
}
catch (Exception)
{
if (cancel.IsCancellationRequested)
return;
output.Text = "运行出错";
}
}
这种方式下,前面的几个问题处理全部集中在一个函数ProcessAsync中了,非常直观。和同步的方式比起来,主要改动有以下两点:
这种方式用起来要友好的多,为了复用这种方式,我们需要把它封装下,一种方式是:通过Observable.FromEvent把TextChanged事件封装成IObservable事件源,然后通过我前文的IObservable的两个简单的扩展函数在Subscribe扩展函数中注册ProcessAsync回调。
这种方式下,需要安装RX库,如果不想装RX库,可以使用我下面的这个轻量级的封装。
class Notification<T>
{
Action<T, CancellationToken> hanlder;
CancellationTokenSource currentCancelTokenSource = new CancellationTokenSource();
public Notification(Action<T, CancellationToken> hanlder)
{
this.hanlder = hanlder;
}
public void Notify(T value)
{
currentCancelTokenSource.Cancel(); //取消当前正在执行的任务
currentCancelTokenSource.Dispose();
currentCancelTokenSource = new CancellationTokenSource();
hanlder(value, currentCancelTokenSource.Token);
}
}
这个类使用比较简单:
var notify = new Notification<string>(ProcessAsync);
input.TextChanged += (s, e) => notify.Notify(input.Text);
当然,这个没有Observable.FromEvent那么友好,因此,我仿照Observable.FromEvent的功能增加了一个函数:
//TODO 暂时没有考虑UnRegist,如果要支持UnRegist,把返回值改成IDisposable
public static void RegistEventNofity(object obj, string eventName, Action<T, CancellationToken> hanlder)
{
EventInfo evt = obj.GetType().GetEvent(eventName, BindingFlags.Instance | BindingFlags.Public);
var notify = new Notification<T>(hanlder);
var eventInvoker = new EventPatten(arg => notify.Notify((T)arg));
var method = typeof(EventPatten).GetMethod("EventOccured", BindingFlags.NonPublic | BindingFlags.Instance);
evt.AddEventHandler(obj, Delegate.CreateDelegate(evt.EventHandlerType, eventInvoker, method));
}
class EventPatten
{
Action<object> hanlder;
internal EventPatten(Action<object> hanlder) { this.hanlder = hanlder; }
void EventOccured(object sender, object args) { hanlder(args); }
}
现在用起来就友好些了:
Notification<TextChangedEventArgs>.RegistEventNofity(input, "TextChanged", (args, cancel) => ProcessAsync(input.Text, cancel));
完整代码如下,欢迎有需要的朋友使用:
另外,由于WinRT的反射机制有所改变,如果要在WinRT环境下使用这个程序需要修改RegistEventNofity函数为如下形式: