Как написать обертку вокруг асинхронного метода? - PullRequest
2 голосов
/ 21 сентября 2011

У меня есть 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.Я нашел этот учебник .

Но это кажется слишком сложным.Я пытаюсь заставить это работать, но я не уверен, что это правильный путь.Возможно, есть более простой и лучший способ?

Ответы [ 2 ]

7 голосов
/ 21 сентября 2011

Все асинхронные операции, требующие подписки на события для получения результатов, просто болезненны. Я думаю, что самый простой способ - абстрагировать обработку событий в несколько хороших методов расширения и использовать стиль передачи продолжения (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`
});

Конечный результат должен быть довольно простым в использовании и прочтении. Надеюсь, это поможет.

0 голосов
/ 21 сентября 2011

Если вы не пишете приложение ASP.NET.

Вы рассматривали возможность использования компонента Background Worker?Для долгосрочных задач, которые не должны связывать пользовательский интерфейс, это простой и понятный способ получить многопоточные возможности.Например, вы можете выполнить обновления пользовательского интерфейса, используя событие ProgressChanged, а фоновый работник и класс фонового рабочего обеспечат, чтобы поток, создавший BW, выполнял события ProcessChanged и WorkComplete.Так что, если вы сделали BW из пользовательского интерфейса и включили его в работу, вы можете безопасно обновить его там.

Вот небольшая статья от MS http://msdn.microsoft.com/en-us/library/cc221403%28v=vs.95%29.aspx

Еще одна действительно хорошая ссылкаhttp://www.albahari.com/threading/part3.aspx#_BackgroundWorker

- edit-- Я посмотрел на ссылку, и он, похоже, делает полную реализацию паттерна Cooporative Cancellation.Это где фоновый поток будет поддерживать отмену изящно, регулярно проверяя переменную и отменяя, если это правда.BW является реализацией этого шаблона.

Если вы хотите что-то действительно простое, вы можете попробовать просто использовать ThreadPool

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();
}
...