一种简单的基于任务的事件异步处理的解决方案

很多时候,为了界面更加友好,我们往往会设计这样一个功能:当用户在界面上输入一段内容时,无需点击确定按钮,后台自动对该输入进行处理,获取输出并显示出来

要实现这个功能,一个最直接的方式是在TextBox的TextChanged事件中增加回调函数,在回调函数中获取返回值,并输出。这么乍看起来是一个比较好的方法,但有经验的程序员就马上会发现这样有几点不足:

  • 用户连输输入时,可能中间过程中的并不是用户的真正的输入,只是输入过程中产生的临时数据,对临时数据进行获取返回值的话,加大了处理负担。
  • TextChanged事件发生在UI线程,当获取返回值的处理函数时间较长的时候,阻塞UI线程,用户界面无响应。

针对这两个问题,我们可以提出如下解决方案:

  • 不实时响应用户的输入,只有当用户停止输入一定时间(500ms)后,才开始进行获取返回值处理。
  • 获取返回值处理采用异步方案,在后台线程中获取返回值,返回值获取完成后,通知UI线程更新界面。

这个方案本身没有什么问题,但这两个步骤都采用了异步操作,而异步操作往往把同步方案变得复杂的多了,带来了一系列要处理的新的问题,常见问题如下:

  1. 如何实现用户的输入延迟通知。
  2. 后台线程异步处理完成时,如何通知UI线程更新界面。
  3. 当某个输入操作的处理时间较长,导致新输入已经产生,但过期的结果可能覆盖新的输入的结果。例如:
    1. 用户先输入了一个google,后台启动线程1获取google的输出结果,但由于方校长发飙了,google被吓住了,一两秒还没有返回值;
    2. 用户等不及了,重新输入了个baidu,线程2很快就返回了baidu的输出结果,
    3. 此时线程1还在运行,再过了会儿,线程1返回了google的结果,重新刷新界面。这个时候,看到的输入时baidu,但界面上显示的结果是google,结果并不准确。
  4. 运行期间的错误如何处理

这几个问题处理起来并不是很复杂,微软在MSDN文章Rx Hands-On Labs中就详述了通过RX解决这个问题的方案(其文档Rx .NET HOL介绍的非常详细,我本来想翻译下的,无奈时间不够,英语也太烂,推荐看本文的朋友读下):

  1. 通过Observable.FromEvent把TextChanged事件封装成IObservable事件源。
  2. 通过ObserveOn扩展函数处理UI线程上的结果更新
  3. 通过Switch函数过滤过期的输出
  4. 通过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中了,非常直观。和同步的方式比起来,主要改动有以下两点:

  1. 将以前的Process函数改名为ProcessAsync,增加了一个CancellationToken参数,用以查询当前输入的任务是否已经取消。
  2. 运行过程中加增加判断当前任务是否已经取消的操作,如果任务已经取消,则放弃本次操作的输出结果(包括异常)。

这种方式用起来要友好的多,为了复用这种方式,我们需要把它封装下,一种方式是:通过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));

完整代码如下,欢迎有需要的朋友使用: 

View Code 
    class Notification<T>
    {
        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函数为如下形式:

View Code
    public static void RegistEventNofity(object obj, string eventName, Action<T, CancellationToken> hanlder)
    {
        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) });
    }
原文地址:https://www.cnblogs.com/TianFang/p/2657439.html