Повторно используемый шаблон для преобразования события в задачу

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

Например:

 private async void Form1_Load(object sender, EventArgs e) { await TaskExt.FromEvent( 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 FromEvent( Action<EventHandler> registerEvent, Action action, Action<EventHandler> unregisterEvent, CancellationToken token) { var tcs = new TaskCompletionSource(); EventHandler 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 ), не прибегая к размышлению?

Это возможно со вспомогательным classом и свободно-синтаксическим синтаксисом:

 public static class TaskExt { public static EAPTask> FromEvent() { var tcs = new TaskCompletionSource(); var handler = new EventHandler((s, e) => tcs.TrySetResult(e)); return new EAPTask>(tcs, handler); } } public sealed class EAPTask where TEventHandler : class { private readonly TaskCompletionSource _completionSource; private readonly TEventHandler _eventHandler; public EAPTask( TaskCompletionSource completionSource, TEventHandler eventHandler) { _completionSource = completionSource; _eventHandler = eventHandler; } public EAPTask WithHandlerConversion( Converter converter) where TOtherEventHandler : class { return new EAPTask( _completionSource, converter(_eventHandler)); } public async Task Start( Action subscribe, Action action, Action unsubscribe, CancellationToken cancellationToken) { subscribe(_eventHandler); try { using(cancellationToken.Register(() => _completionSource.SetCanceled())) { action(); return await _completionSource.Task; } } finally { unsubscribe(_eventHandler); } } } 

Теперь у вас есть вспомогательный метод WithHandlerConversion , который может вывести параметр типа из аргумента преобразователя, что означает, что вам нужно писать WebBrowserDocumentCompletedEventHandler только один раз. Применение:

 await TaskExt .FromEvent() .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 { ///  /// Extension Method that converts a typical EventArgs Event into an awaitable Task ///  /// The type of the EventArgs (must inherit from EventArgs) /// the object that has the event /// optional Function that triggers the event /// the name of the event -> use nameof to be safe, eg nameof(button.Click)  /// an optional Cancellation Token ///  public static async Task EventAsync(this object objectWithEvent, Action trigger, string eventName, CancellationToken ct = default) where TEventArgs : EventArgs { var completionSource = new TaskCompletionSource(ct); var eventInfo = objectWithEvent.GetType().GetEvent(eventName); var delegateDef = new UniversalEventDelegate(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 EventAsync(this object objectWithEvent, string eventName, CancellationToken ct = default) where TEventArgs : EventArgs => EventAsync(objectWithEvent, null, eventName, ct); private delegate void UniversalEventDelegate(object sender, TEventArgs e) where TEventArgs : EventArgs; } } 

Преобразование из EAP в задачи не так просто, главным образом потому, что вам приходится обрабатывать исключения как при вызове метода long-running, так и при обработке события.

Библиотека ParallelExtensionsExtras содержит метод расширения EAPCommon.HandleCompletion (TaskCompletionSource tcs, AsyncCompletedEventArgs e, Func getResult, Action unregisterHandler), чтобы упростить преобразование. Метод обрабатывает подписку / отмену подписки на событие. Он также не пытается запустить долговременную операцию

Используя этот метод, библиотека реализует асинхронные версии SmtpClient, WebClient и PingClient.

Следующий метод показывает общую модель использования:

  private static Task SendTaskCore(Ping ping, object userToken, Action> 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(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. Ваш код задает обработчик исходному объекту и запускает длительную операцию. Фактический тип обработчика не течет вне метода.

Разделяя две проблемы (обработка события и начало операции), проще создать общий метод.

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

Как положительный побочный эффект, он позволяет вызывающему абоненту отменить или отклонить результат операции (с исключением), основанный на аргументах события (например, AsyncCompletedEventArgs.Cancelled , AsyncCompletedEventArgs.Error ).

Основной TaskCompletionSource по-прежнему полностью скрыт от вызывающего (поэтому его можно заменить чем-то другим, например пользовательским awaiter или пользовательским promiseм ):

 private async void Form1_Load(object sender, EventArgs e) { await TaskExt.FromEvent( 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 FromEvent( Func, Action, Action, TEventHandler> getHandler, Action subscribe, Action unsubscribe, Action, Action, Action> initiate, CancellationToken token = default(CancellationToken)) where TEventHandler : class { var tcs = new TaskCompletionSource(); Action complete = args => tcs.TrySetResult(args); Action cancel = () => tcs.TrySetCanceled(); Action 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( (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); 
  • Как можно удалить все обработчики событий события «Click» кнопки «Button»?
  • Глобальные события в угловых
  • Как отправлять события в C #
  • ActionListener для конкретного текста внутри JTextArea?
  • jquery Event.stopPropagation (), похоже, не работает
  • Что означает «Захват мыши» в WPF?
  • Как проверить, была ли клавиша нажата клавишей со стрелкой в ​​Java KeyListener?
  • Пользовательские события в jQuery?
  • .NET Events - Что такое отправитель объекта и EventArgs?
  • Событие Click не работает с динамически сгенерированными элементами
  • Каковы различия между делегатами и событиями?
  • Давайте будем гением компьютера.