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

本文关键字:模式 任务 事件 转换 用于 | 更新日期: 2023-09-27 17:55:39

我想有一个通用的可重用代码段来包装EAP模式作为任务,类似于Task.Factory.FromAsync为APM模式所做的BeginXXX/EndXXX

例如:

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),而无需诉诸反思?

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

使用帮助程序类和类似流利的语法是可能的:

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帮助程序方法,它可以从转换器参数推断类型参数,这意味着你只需要编写一次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);

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

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;
    }
}
我认为

以下版本可能足够令人满意。我确实从 max 的答案中借用了准备正确类型的事件处理程序的想法,但此实现不会显式创建任何其他对象。

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

底层TaskCompletionSource仍然对调用者完全隐藏(因此可以用其他东西替换,例如自定义等待者或自定义承诺):

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) where TEventHandler : Delegate
    {
        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);

更新了简单事件案例的更少样板(这些天我更频繁地使用这个):

public static async Task<TEventArgs> FromEvent<TEventHandler, TEventArgs>(
    Action<TEventHandler> subscribe,
    Action<TEventHandler> unsubscribe,
    CancellationToken token = default,
    bool runContinuationsAsynchronously = true) 
        where TEventHandler : Delegate
        where TEventArgs: EventArgs
{
    var tcs = new TaskCompletionSource<TEventArgs>(runContinuationsAsynchronously ?
        TaskCreationOptions.RunContinuationsAsynchronously :
        TaskCreationOptions.None);
    var handler = new Action<object?, TEventArgs>((_, args) => tcs.TrySetResult(args)); 
    var h = (TEventHandler)Delegate.CreateDelegate(typeof(TEventHandler), handler.Target, handler.Method);
    subscribe(h);
    try
    {
        using (token.Register(() => tcs.TrySetCanceled(), useSynchronizationContext: false))
        {
            return await tcs.Task;
        }
    }
    finally
    {
        unsubscribe(h);
    }
}

用法:

await TaskExt.FromEvent<FormClosedEventHandler, FormClosedEventArgs>(
    h => mainForm.FormClosed += h,
    h => mainForm.FormClosed -= h,
    token);

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

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。代码将处理程序设置为源对象并启动长操作。实际处理程序类型不会泄漏到方法之外。

通过分离这两个关注点(处理事件与启动操作),可以更轻松地创建泛型方法。

这是一个最小使用反射的解决方案,灵感来自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;