将事件转换为任务的可重用模式

时间:2014-04-01 10:53:13

标签: c# .net events generics async-await

我想为wrapping EAP pattern as task提供一个通用的可重复使用的代码,类似于Task.Factory.FromAsyncBeginXXX/EndXXX APM pattern的代码。

E.g:

private async void Form1_Load(object sender, EventArgs e)
{
    await TaskExt.FromEvent<EventArgs>(
        handler => this.webBrowser.DocumentCompleted += 
            new WebBrowserDocumentCompletedEventHandler(handler),
        () => this.webBrowser.Navigate("about:blank"),
        handler => this.webBrowser.DocumentCompleted -= 
            new WebBrowserDocumentCompletedEventHandler(handler),
        CancellationToken.None);

    this.webBrowser.Document.InvokeScript("setTimeout", 
        new[] { "document.body.style.backgroundColor = 'yellow'", "1" });
}

到目前为止,它看起来像这样:

public static class TaskExt
{
    public static async Task<TEventArgs> FromEvent<TEventArgs>(
        Action<EventHandler<TEventArgs>> registerEvent,
        Action action,
        Action<EventHandler<TEventArgs>> unregisterEvent,
        CancellationToken token)
    {
        var tcs = new TaskCompletionSource<TEventArgs>();

        EventHandler<TEventArgs> handler = (sender, args) =>
            tcs.TrySetResult(args);

        registerEvent(handler);
        try
        {
            using (token.Register(() => tcs.SetCanceled()))
            {
                action();
                return await tcs.Task;
            }
        }
        finally
        {
            unregisterEvent(handler);
        }
    }
}

是否有可能提出类似的内容,但要求我输入WebBrowserDocumentCompletedEventHandler两次(registerEvent / unregisterEvent),求助于反思?

5 个答案:

答案 0 :(得分:8)

可以使用辅助类和流畅的语法:

public static class TaskExt
{
    public static EAPTask<TEventArgs, EventHandler<TEventArgs>> FromEvent<TEventArgs>()
    {
        var tcs = new TaskCompletionSource<TEventArgs>();
        var handler = new EventHandler<TEventArgs>((s, e) => tcs.TrySetResult(e));
        return new EAPTask<TEventArgs, EventHandler<TEventArgs>>(tcs, handler);
    }
}


public sealed class EAPTask<TEventArgs, TEventHandler>
    where TEventHandler : class
{
    private readonly TaskCompletionSource<TEventArgs> _completionSource;
    private readonly TEventHandler _eventHandler;

    public EAPTask(
        TaskCompletionSource<TEventArgs> completionSource,
        TEventHandler eventHandler)
    {
        _completionSource = completionSource;
        _eventHandler = eventHandler;
    }

    public EAPTask<TEventArgs, TOtherEventHandler> WithHandlerConversion<TOtherEventHandler>(
        Converter<TEventHandler, TOtherEventHandler> converter)
        where TOtherEventHandler : class
    {
        return new EAPTask<TEventArgs, TOtherEventHandler>(
            _completionSource, converter(_eventHandler));
    }

    public async Task<TEventArgs> Start(
        Action<TEventHandler> subscribe,
        Action action,
        Action<TEventHandler> unsubscribe,
        CancellationToken cancellationToken)
    {
        subscribe(_eventHandler);
        try
        {
            using(cancellationToken.Register(() => _completionSource.SetCanceled()))
            {
                action();
                return await _completionSource.Task;
            }
        }
        finally
        {
            unsubscribe(_eventHandler);
        }
    }
}

现在你有一个WithHandlerConversion辅助方法,它可以从converter参数中推断出类型参数,这意味着你只需要写一次WebBrowserDocumentCompletedEventHandler。 用法:

await TaskExt
    .FromEvent<WebBrowserDocumentCompletedEventArgs>()
    .WithHandlerConversion(handler => new WebBrowserDocumentCompletedEventHandler(handler))
    .Start(
        handler => this.webBrowser.DocumentCompleted += handler,
        () => this.webBrowser.Navigate(@"about:blank"),
        handler => this.webBrowser.DocumentCompleted -= handler,
        CancellationToken.None);

答案 1 :(得分:4)

我有一个(使用明智的)更短的解决方案。我将首先向您展示使用情况,然后为您提供实现此目的的代码(自由使用) 用法例如:

await button.EventAsync(nameof(button.Click));

或:

var specialEventArgs = await busniessObject.EventAsync(nameof(busniessObject.CustomerCreated));

或需要以某种方式触发的事件:

var serviceResult = await service.EventAsync(()=> service.Start, nameof(service.Completed));

实现这一目标的神奇之处(请注意它的C#7.1语法,但可以通过添加几行轻松转换回较低语言版本):

using System;
using System.Threading;
using System.Threading.Tasks;

namespace SpacemonsterIndustries.Core
{
    public static class EventExtensions
    {
        /// <summary>
        /// Extension Method that converts a typical EventArgs Event into an awaitable Task 
        /// </summary>
        /// <typeparam name="TEventArgs">The type of the EventArgs (must inherit from EventArgs)</typeparam>
        /// <param name="objectWithEvent">the object that has the event</param>
        /// <param name="trigger">optional Function that triggers the event</param>
        /// <param name="eventName">the name of the event -> use nameof to be safe, e.g. nameof(button.Click) </param>
        /// <param name="ct">an optional Cancellation Token</param>
        /// <returns></returns>
        public static async Task<TEventArgs> EventAsync<TEventArgs>(this object objectWithEvent, Action trigger, string eventName, CancellationToken ct = default)
            where TEventArgs : EventArgs
        {
            var completionSource = new TaskCompletionSource<TEventArgs>(ct);
            var eventInfo = objectWithEvent.GetType().GetEvent(eventName);
            var delegateDef = new UniversalEventDelegate<TEventArgs>(Handler);
            var handlerAsDelegate = Delegate.CreateDelegate(eventInfo.EventHandlerType, delegateDef.Target, delegateDef.Method);

            eventInfo.AddEventHandler(objectWithEvent, handlerAsDelegate);

            trigger?.Invoke();

            var result = await completionSource.Task;

            eventInfo.RemoveEventHandler(objectWithEvent, handlerAsDelegate); 

            return result;

            void Handler(object sender, TEventArgs e) => completionSource.SetResult(e);
        }

        public static Task<TEventArgs> EventAsync<TEventArgs>(this object objectWithEvent, string eventName, CancellationToken ct = default) where TEventArgs : EventArgs
            => EventAsync<TEventArgs>(objectWithEvent, null, eventName, ct);

        private delegate void UniversalEventDelegate<in TEventArgs>(object sender, TEventArgs e) where TEventArgs : EventArgs;
    }
}

答案 2 :(得分:3)

从EAP转换为Tasks并不是那么简单,主要是因为在调用长时间运行的方法和处理事件时都必须处理异常。

ParallelExtensionsExtras库包含EAPCommon.HandleCompletion(TaskCompletionSource tcs, AsyncCompletedEventArgs e, Func getResult, Action unregisterHandler)扩展方法,可以使转换更容易。该方法处理来自事件的订阅/取消订阅。它也没有尝试启动长时间运行的操作

使用此方法,库实现了SmtpClient,WebClient和PingClient的异步版本。

以下方法显示了一般使用模式:

    private static Task<PingReply> SendTaskCore(Ping ping, object userToken, Action<TaskCompletionSource<PingReply>> sendAsync) 
    { 
        // Validate we're being used with a real smtpClient.  The rest of the arg validation 
        // will happen in the call to sendAsync. 
        if (ping == null) throw new ArgumentNullException("ping"); 

        // Create a TaskCompletionSource to represent the operation 
        var tcs = new TaskCompletionSource<PingReply>(userToken); 

        // Register a handler that will transfer completion results to the TCS Task 
        PingCompletedEventHandler handler = null; 
        handler = (sender, e) => EAPCommon.HandleCompletion(tcs, e, () => e.Reply, () => ping.PingCompleted -= handler); 
        ping.PingCompleted += handler; 

        // Try to start the async operation.  If starting it fails (due to parameter validation) 
        // unregister the handler before allowing the exception to propagate. 
        try 
        { 
            sendAsync(tcs); 
        } 
        catch(Exception exc) 
        { 
            ping.PingCompleted -= handler; 
            tcs.TrySetException(exc); 
        } 

        // Return the task to represent the asynchronous operation 
        return tcs.Task; 
    } 

与您的代码的主要区别在于:

// Register a handler that will transfer completion results to the TCS Task 
PingCompletedEventHandler handler = null; 
handler = (sender, e) => EAPCommon.HandleCompletion(tcs, e, () => e.Reply, 
          () => ping.PingCompleted -= handler); 
ping.PingCompleted += handler; 

扩展方法创建处理程序并挂钩tcs。您的代码将处理程序设置为源对象并启动长操作。实际的处理程序类型不会泄漏到方法之外。

通过分离两个问题(处理事件与开始操作),可以更容易地创建通用方法。

答案 3 :(得分:2)

我认为以下版本可能足够令人满意。我确实借用了从max's answer准备正确类型的事件处理程序的想法,但是这个实现并没有明确地创建任何其他对象。

作为一个积极的副作用,它允许调用者根据事件的参数(如AsyncCompletedEventArgs.CancelledAsyncCompletedEventArgs.Error取消或拒绝操作的结果(例外) )。

基础TaskCompletionSource仍然完全隐藏在调用方之外(因此可以将其替换为其他内容,例如a custom awaitera custom promise):

private async void Form1_Load(object sender, EventArgs e)
{
    await TaskExt.FromEvent<WebBrowserDocumentCompletedEventHandler, EventArgs>(
        getHandler: (completeAction, cancelAction, rejectAction) => 
            (eventSource, eventArgs) => completeAction(eventArgs),
        subscribe: eventHandler => 
            this.webBrowser.DocumentCompleted += eventHandler,
        unsubscribe: eventHandler => 
            this.webBrowser.DocumentCompleted -= eventHandler,
        initiate: (completeAction, cancelAction, rejectAction) =>
            this.webBrowser.Navigate("about:blank"),
        token: CancellationToken.None);

    this.webBrowser.Document.InvokeScript("setTimeout", 
        new[] { "document.body.style.backgroundColor = 'yellow'", "1" });
}

public static class TaskExt
{
    public static async Task<TEventArgs> FromEvent<TEventHandler, TEventArgs>(
        Func<Action<TEventArgs>, Action, Action<Exception>, TEventHandler> getHandler,
        Action<TEventHandler> subscribe,
        Action<TEventHandler> unsubscribe,
        Action<Action<TEventArgs>, Action, Action<Exception>> initiate,
        CancellationToken token = default(CancellationToken)) where TEventHandler : class
    {
        var tcs = new TaskCompletionSource<TEventArgs>();

        Action<TEventArgs> complete = args => tcs.TrySetResult(args);
        Action cancel = () => tcs.TrySetCanceled();
        Action<Exception> reject = ex => tcs.TrySetException(ex);

        TEventHandler handler = getHandler(complete, cancel, reject);

        subscribe(handler);
        try
        {
            using (token.Register(() => tcs.TrySetCanceled(),
                useSynchronizationContext: false))
            {
                initiate(complete, cancel, reject);
                return await tcs.Task;
            }
        }
        finally
        {
            unsubscribe(handler);
        }
    }
}

<小时/> 这实际上可用于等待任何回调,而不仅仅是事件处理程序,例如:

var mre = new ManualResetEvent(false);
RegisteredWaitHandle rwh = null;

await TaskExt.FromEvent<WaitOrTimerCallback, bool>(
    (complete, cancel, reject) => 
        (state, timeout) => { if (!timeout) complete(true); else cancel(); },
    callback => 
        rwh = ThreadPool.RegisterWaitForSingleObject(mre, callback, null, 1000, true),
    callback => 
        rwh.Unregister(mre),
    (complete, cancel, reject) => 
        ThreadPool.QueueUserWorkItem(state => { Thread.Sleep(500); mre.Set(); }),
    CancellationToken.None);

答案 4 :(得分:1)

这是一种受Observable.FromEvent方法(反应性扩展)的启发而最大限度地减少反射的解决方案。

public static Task<TEventArgs> TaskFromEvent<TDelegate, TEventArgs>(
    Action<TDelegate> addHandler, Action<TDelegate> removeHandler)
    where TDelegate : Delegate where TEventArgs : EventArgs
{
    var tcs = new TaskCompletionSource<TEventArgs>();
    TDelegate specificHandler = null;
    Action<object, TEventArgs> handler = (sender, e) =>
    {
        removeHandler(specificHandler);
        handler = null;
        tcs.SetResult(e);
        tcs = null;
    };
    var invokeMethodInfo = typeof(Action<object, TEventArgs>).GetMethod("Invoke");
    specificHandler = (TDelegate)invokeMethodInfo
        .CreateDelegate(typeof(TDelegate), handler);
    addHandler(specificHandler);
    return tcs.Task;
}

用法示例:

var documentCompletedAsync = TaskFromEvent<
    WebBrowserDocumentCompletedEventHandler,
    WebBrowserDocumentCompletedEventArgs>(
    handler => webBrowser.DocumentCompleted += handler,
    handler => webBrowser.DocumentCompleted -= handler);
webBrowser.Navigate("about:blank");
var url = (await documentCompletedAsync).Url;