A reusable pattern to convert event into task

后端 未结 5 800
陌清茗
陌清茗 2020-11-27 06:58

I\'d like to have a generic reusable piece of code for wrapping EAP pattern as task, something similar to what Task.Factory.FromAsync does for BeginXXX/EndXXX APM pattern.

5条回答
  •  暖寄归人
    2020-11-27 07:42

    I think the following version might be satisfactory enough. I did borrow the idea of preparing a correctly typed event handler from max's answer, but this implementation doesn't create any additional object explicitly.

    As a positive side effect, it allows the caller to cancel or reject the result of the operation (with an exception), based upon the event's arguments (like AsyncCompletedEventArgs.Cancelled, AsyncCompletedEventArgs.Error).

    The underlying TaskCompletionSource is still completely hidden from the caller (so it could be replaced with something else, e.g. a custom awaiter or a custom 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) where TEventHandler : Delegate
        {
            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);
            }
        }
    }
    

    This actually can be used to await any callback, not just event handlers, e.g.:
    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);
    

提交回复
热议问题