Шаблон многократного использования для преобразования события в задачу

Я хотел бы иметь универсальный многократно используемый код для упаковки шаблона EAP в качестве задачи, что-то похожее на то, что Task.Factory.FromAsyncделает дляBeginXXX/EndXXX APM шаблон.

Например:

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), не прибегая к размышлениям?

4 ответа

Решение

Это возможно с помощью вспомогательного класса и свободного синтаксиса:

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;
    }
}

Я думаю, что следующая версия может быть достаточно удовлетворительной. Я позаимствовал идею подготовки правильно типизированного обработчика событий из ответа Макса, но эта реализация явно не создает никаких дополнительных объектов.

Как положительный побочный эффект, он позволяет вызывающей стороне отменить или отклонить результат операции (за исключением) на основе аргументов события (например, AsyncCompletedEventArgs.Cancelled, AsyncCompletedEventArgs.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(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);

Преобразование из 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; 

Метод extension создает обработчик и перехватывает 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;
Другие вопросы по тегам