具有已完成事件的异步方法

本文关键字:异步方法 事件 已完成 | 更新日期: 2023-09-27 17:58:53

我使用.net 4.0,并试图找出如何使用异步方法等待DocumentCompleted事件完成并返回值。我的原始代码如上所述,在这种情况下,我如何将其转换为异步/等待模型?

private class BrowserWindow
    {
        private bool webBrowserReady = false;
        public string content = "";

        public void Navigate(string url)
        {
            xxx browser = new xxx();
            browser.DocumentCompleted += new EventHandler(wb_DocumentCompleted);
            webBrowserReady = false;
            browser.CreateControl();
            if (browser.IsHandleCreated)
                browser.Navigate(url);

            while (!webBrowserReady)
            {
                //Application.DoEvents(); >> replace it with async/await 
            }
        }
        private void wb_DocumentCompleted(object sender, EventArgs e)
        {
            try
            {
               ...
                  webBrowserReady = true;
                  content = browser.Document.Body.InnerHtml;
            }
            catch
            {
            }
        }
        public delegate string AsyncMethodCaller(string url);
    }

具有已完成事件的异步方法

因此,我们需要一个在DocumentCompleted事件触发时返回任务的方法。任何时候,对于给定的事件,您都可以创建这样的方法:

public static Task WhenDocumentCompleted(this WebBrowser browser)
{
    var tcs = new TaskCompletionSource<bool>();
    browser.DocumentCompleted += (s, args) => tcs.SetResult(true);
    return tcs.Task;
}

一旦你有了,你可以使用:

await browser.WhenDocumentCompleted();

@Servy有我想要的天才答案,但它不太适用于我的用例。当事件被多次引发时,我发现了错误,因为事件处理程序试图在后续事件调用的TaskCompletionSource上设置结果。

我从两个方面加强了他的回答。第一种方法是在第一次处理DocumentCompleted事件后取消订阅。

public static Task WhenDocumentCompleted(this WebBrowser browser)
{
    var tcs = new TaskCompletionSource<bool>();
    browser.DocumentCompleted += DocumentCompletedHandler;
    return tcs.Task;
    void DocumentCompletedHandler(object sender, EventArgs e)
    {
        browser.DocumentCompleted -= DocumentCompletedHandler;
        tcs.SetResult(true);
    }
}

注意,我在这里使用了一个本地函数来捕获TaskCompletionSource实例,它至少需要C#7.0。

第二个增强功能是添加超时。我的特定用例是在单元测试中,我想让等待我的特定事件具有确定性,而不是在出现问题时无限期地等待。

我选择使用计时器,并将其设置为只启动一次,然后在不再需要计时器时停止计时器。或者,可以在这里使用CancellationToken来管理TaskCompletionSource,但我觉得这需要维护人员更多地了解它们的用法,并且计时器更容易被普遍理解。

public static Task WhenDocumentCompleted(this WebBrowser browser, int timeoutInMilliseconds = 500)
{
    var tcs = new TaskCompletionSource<bool>();
    var timeoutTimer = new System.Timers.Timer(timeoutInMilliseconds);
    timeoutTimer.AutoReset = false;
    timeoutTimer.Elapsed += (s,e) => tcs.TrySetCanceled();
    timeoutTimer.Start();
    browser.DocumentCompleted += DocumentCompletedHandler;
    return tcs.Task;
    void DocumentCompletedHandler(object sender, EventArgs e)
    {
        timeoutTimer.Stop();
        browser.DocumentCompleted  -= DocumentCompletedHandler;
        tcs.TrySetResult(true);
    }
}

注意,为了确保代码是线程安全的,我在这里采取了更多的防御措施,并使用了Try...函数。这确保了即使在发生边缘情况交错执行时,也不会出现设置结果的错误。