很多时候,为了界面更加友好,我们往往会设计这样一个功能:当用户在界面上输入一段内容时,无需点击确定按钮,后台自动对该输入进行处理,获取输出并显示出来。
要实现这个功能,一个最直接的方式是在TextBox的TextChanged事件中增加回调函数,在回调函数中获取返回值,并输出。这么乍看起来是一个比较好的方法,但有经验的程序员就马上会发现这样有几点不足:
-
用户连输输入时,可能中间过程中的并不是用户的真正的输入,只是输入过程中产生的临时数据,对临时数据进行获取返回值的话,加大了处理负担。
-
TextChanged事件发生在UI线程,当获取返回值的处理函数时间较长的时候,阻塞UI线程,用户界面无响应。
针对这两个问题,我们可以提出如下解决方案:
-
不实时响应用户的输入,只有当用户停止输入一定时间(500ms)后,才开始进行获取返回值处理。
-
获取返回值处理采用异步方案,在后台线程中获取返回值,返回值获取完成后,通知UI线程更新界面。
这个方案本身没有什么问题,但这两个步骤都采用了异步操作,而异步操作往往把同步方案变得复杂的多了,带来了一系列要处理的新的问题,常见问题如下:
-
如何实现用户的输入延迟通知。
-
后台线程异步处理完成时,如何通知UI线程更新界面。
-
当某个输入操作的处理时间较长,导致新输入已经产生,但过期的结果可能覆盖新的输入的结果。例如:
-
用户先输入了一个google,后台启动线程1获取google的输出结果,但由于方校长发飙了,google被吓住了,一两秒还没有返回值;
-
用户等不及了,重新输入了个baidu,线程2很快就返回了baidu的输出结果,
-
此时线程1还在运行,再过了会儿,线程1返回了google的结果,重新刷新界面。这个时候,看到的输入时baidu,但界面上显示的结果是google,结果并不准确。
-
-
运行期间的错误如何处理
这几个问题处理起来并不是很复杂,微软在MSDN文章Rx Hands-On Labs中就详述了通过RX解决这个问题的方案(其文档Rx .NET HOL介绍的非常详细,我本来想翻译下的,无奈时间不够,英语也太烂,推荐看本文的朋友读下):
-
通过Observable.FromEvent把TextChanged事件封装成IObservable事件源。
-
通过ObserveOn扩展函数处理UI线程上的结果更新
-
通过Switch函数过滤过期的输出
-
通过OnError函数处理异常
该文档中还列举了其它几个问题的解决方案,由于较多,这里就不一一列举了。这种方式比较完善,但增加了一大堆回调函数和扩展函数,用起来不是很友好,对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中了,非常直观。和同步的方式比起来,主要改动有以下两点:
-
将以前的Process函数改名为ProcessAsync,增加了一个CancellationToken参数,用以查询当前输入的任务是否已经取消。
-
运行过程中加增加判断当前任务是否已经取消的操作,如果任务已经取消,则放弃本次操作的输出结果(包括异常)。
这种方式用起来要友好的多,为了复用这种方式,我们需要把它封装下,一种方式是:通过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));
完整代码如下,欢迎有需要的朋友使用:

{
Action<T, CancellationToken> hanlder;
CancellationTokenSource current = new CancellationTokenSource();
public Notification(Action<T, CancellationToken> hanlder)
{
this.hanlder = hanlder;
}
public void Notify(T value)
{
current.Cancel(); //取消当前正在执行的任务
current.Dispose();
current = new CancellationTokenSource();
hanlder(value, current.Token);
}
//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); }
}
另外,由于WinRT的反射机制有所改变,如果要在WinRT环境下使用这个程序需要修改RegistEventNofity函数为如下形式:

{
var evt = obj.GetType().GetRuntimeEvent(eventName);
var notify = new Notification<T>(hanlder);
var eventInvoker = new EventPatten(arg => notify.Notify((T)arg));
var method = eventInvoker.GetType().GetRuntimeMethods().First(i => i.Name == "EventOccured");;
var delegateType = evt.AddMethod.GetParameters()[0].ParameterType;
evt.AddMethod.Invoke(obj, new object[] { method.CreateDelegate(delegateType, eventInvoker) });
}