【问题标题】:How to write a wrapper around an asynchronous method?如何围绕异步方法编写包装器?
【发布时间】:2011-09-20 23:56:41
【问题描述】:

我有一个我希望我的应用程序访问的 JSON API。所以我写了一个方法。

public List<Books> GetBooks()
{
  var webclient = new WebClient();
  var jsonOutput = webclient.DownloadString(
                         new Uri("http://someplace.com/books.json")
                             );

  return ParseJSON(jsonOutput);//Some synchronous parsing method 
}

现在我需要将 DonwloadString 更改为 DownloadStringAsync。 我找到了这个tutorial

但这似乎太复杂了。我试图让这个工作,但我不确定这是否是正确的方法。也许有更简单更好的方法?

【问题讨论】:

  • 你想把什么包裹在什么周围?
  • 嗯,最后我想要一个名为 GetBooksAsync() 的异步方法,我也会有 GetBooksCompleted。

标签: c# events windows-phone-7 asynchronous webclient


【解决方案1】:

所有需要您订阅事件才能获得结果的异步操作都非常痛苦。我认为最简单的方法是将事件处理抽象为一些不错的扩展方法,并使用连续传递样式(CPS)来处理结果。

所以,首先要创建一个下载字符串的扩展方法:

public static void DownloadString(this Uri uri, Action<string> action)
{
    if (uri == null) throw new ArgumentNullException("uri");
    if (action == null) throw new ArgumentNullException("action");

    var webclient = new WebClient();

    DownloadStringCompletedEventHandler handler = null;
    handler = (s, e) =>
    {
        var result = e.Result;
        webclient.DownloadStringCompleted -= handler;
        webclient.Dispose();
        action(result);
    };

    webclient.DownloadStringCompleted += handler;
    webclient.DownloadStringAsync(uri);
}

此方法隐藏了 WebClient 的创建、所有的事件处理以及之后的处置和取消订阅以进行清理。

它是这样使用的:

var uri = new Uri("http://someplace.com/books.json");
uri.DownloadString(t =>
{
    // Do something with the string
});

现在这可以用来创建GetBooks 方法。这里是:

public void GetBooks(Uri uri, Action<List<Books>> action)
{
    if (action == null) throw new ArgumentNullException("action");
    uri.DownloadString(t =>
    {
        var books = ParseJSON(t);
        action(books);
    });
}

它是这样使用的:

this.GetBooks(new Uri("http://someplace.com/books.json"), books =>
{
    // Do something with `List<Books> books`
});

应该简洁明了。

现在,您可能希望通过几种方式扩展它。

您可以创建具有此签名的 ParseJSON 重载:

void ParseJSON(string text, Action<List<Books>> action)

然后你可以完全取消 GetBooks 方法,只写这个:

var uri = new Uri("http://someplace.com/books.json");
uri.DownloadString(t => ParseJSON(t, books =>
{
    // Do something with `List<Books> books`
    // `string t` is also in scope here
}));

现在,您拥有了一套简洁流畅、可组合的操作。作为奖励,下载的字符串t也在范围内,因此您可以轻松记录它或在需要时进行一些其他处理。

您可能还需要处理异常,这些可以像这样添加:

public static void DownloadString(
    this Uri uri,
    Action<string> action,
    Action<Exception> exception)
{
    if (uri == null) throw new ArgumentNullException("uri");
    if (action == null) throw new ArgumentNullException("action");

    var webclient = (WebClient)null;

    Action<Action> catcher = body =>
    {
        try
        {   
            body();
        }
        catch (Exception ex)
        {
            ex.Data["uri"] = uri;
            if (exception != null)
            {
                exception(ex);
            }
        }
        finally
        {
            if (webclient != null)
            {
                webclient.Dispose();
            }
        }
    };

    var handler = (DownloadStringCompletedEventHandler)null;        
    handler = (s, e) =>
    {
        var result = (string)null;
        catcher(() =>
        {   
            result = e.Result;
            webclient.DownloadStringCompleted -= handler;
        });
        action(result);
    };

    catcher(() =>
    {   
        webclient = new WebClient();
        webclient.DownloadStringCompleted += handler;
        webclient.DownloadStringAsync(uri);
    });
}

然后您可以将非错误处理DownloadString 扩展方法替换为:

public static void DownloadString(this Uri uri, Action<string> action)
{
    uri.DownloadString(action, null);
}

然后要使用错误处理方法,您可以这样做:

var uri = new Uri("http://someplace.com/books.json");
uri.DownloadString(t => ParseJSON(t, books =>
{
    // Do something with `List<Books> books`
}), ex =>
{
    // Do something with `Exception ex`
});

最终结果应该非常易于使用和阅读。我希望这会有所帮助。

【讨论】:

  • 虽然我不一定同意“要求您订阅事件才能获得结果的异步操作很痛苦”,但这是一个构造得非常好的解决方案。干得好。
  • 干得好!虽然最后我选择了一种更简单的方法。
【解决方案2】:

假设您不是在编写 ASP.NET 应用程序。

您是否考虑过使用 Background Worker 组件?对于不应该占用 UI 的长时间运行的任务,它是一种获得多线程功能的干净且简单的方法。例如,您可以使用 ProgressChanged 事件对 UI 执行更新,并且后台工作人员和后台工作人员类将确保创建 BW 的线程是执行 ProcessChanged 和 WorkComplete 事件的线程。因此,如果您从 UI 制作 BW 并将其设置为工作,那么您可以从那里安全地更新 UI。

这是来自 MS http://msdn.microsoft.com/en-us/library/cc221403%28v=vs.95%29.aspx 的快速文章

另一个非常好的链接http://www.albahari.com/threading/part3.aspx#_BackgroundWorker

--编辑-- 我查看了链接,他似乎正在做的是合作取消模式的完整实现。这是后台线程将通过例行检查变量并在其为真时取消来优雅地支持取消的地方。 BW 就是这种模式的一种实现。

如果你想要一些非常简单的东西,那么你可以尝试只使用线程池

ThreadPool.QueueUserWorkItem(DoWork);
public void DoWork(){
    //Just remember that this code happens in a seperate thread so don't update 
    //the UI. It will throw an exception. You would need to call 
    //Form.BeginInvoke(UpdateFunction) in order to update the UI 
    DoSomethingInteresting();
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-06-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多