Преобразование API на основе обратного вызова делегата в асинхронный - PullRequest
0 голосов
/ 10 января 2019

Мне нужно использовать библиотеку, API которой выглядит примерно так:

public void Connect();

...

public delegate void ConnectResultDelegate(bool succeeded, string msg);
public ConnectResultDelegate ConnectResultHandler;

После вызова метода Connect() будет вызван делегат обратного вызова ConnectResultHandler.

API предоставляет другие методы, которые работают аналогичным образом «запрос-ответ»; Я предполагаю, что причина для делегатов заключается в том, что методы взаимодействуют с внешним аппаратным устройством, и ответ (вызов делегата) может не произойти в течение многих миллисекунд.

Я надеялся, что смогу обернуть API каким-либо образом, который позволил бы мне использовать его более «последовательным» способом, который больше похож на async / await, в соответствии с:

void DoSomething()
{
    _library.Connect();
    // Wait for notification that this has completed
    // Do something with the response passed to the delegate callback

    _library.Configure(...);
    // Wait for notification that this has completed
    // Do something with the response
    ..etc..
}

Мысли? Рефакторинг самой библиотеки не вариант.

Существует один или два аналогичных вопроса SO, но они отличаются тем, что их делегаты передаются методам, а не отдельными свойствами, что упрощает перенос в задаче.

1 Ответ

0 голосов
/ 10 января 2019

Существует много ответов, которые показывают, как преобразовать события или асинхронные операции начала / конца в задачи. Этот код, тем не менее, не соответствует соглашениям ни одной из моделей. Это похоже на основанную на событиях асинхронную модель EAP без использования события. Если бы вы искали преобразования событий в задачи, вы бы нашли много ответов. Тем не менее, делегаты не используются для асинхронных операций, так как до EAP существовало соглашение о модели асинхронного программирования (APM) или Begin/End.

.

Процесс процесса все тот же. Это описано в Взаимодействие с другими асинхронными шаблонами и типами . Во всех случаях TaskCompletionSource используется для создания Задачи, сигнализирующей о завершении операции.

Когда класс следует соглашениям APM, можно использовать метод TaskFactory.FromAsync для преобразования пары Beging/End в задачу. FromAsync использует TaskCompletionSource под прикрытием для возврата Задачи, которая сигнализируется при вызове обратного вызова. Пример документа Interop для этого: Stream.BeginRead:

public static Task<int> ReadAsync(this Stream stream, 
                              byte[] buffer, int offset, 
                              int count)
{
    if (stream == null) 
       throw new ArgumentNullException("stream");

    return Task<int>.Factory.FromAsync(stream.BeginRead, 
                                   stream.EndRead, buffer, 
                                   offset, count, null);
}

Использование делегатов аналогично использованию событий, что также показано в статье interop . Адаптированный к вопросу, он будет выглядеть примерно так:

public Task<bool> ConnectAsync(ThatService service)
{
    if (service==null) 
        throw new ArgumentNullException(nameof(service));

    var tcs=new TaskCompletionSource<bool>();

    service.ConnectResultHandler=(ok,msg)=>
    {
        if(ok)
        {
            tcs.TrySetResult(true);
        }
        else
        {
            tcs.TrySetException(new Exception(msg));
        }
    };

    return tcs.Task;
}

Это позволит вам использовать ConnectAsync в методе async, например:

public async Task MyMethod()
{
    ...
    var ok=await ConnectAsync(_service);
    ...

}

Если msg содержит данные об успехе, вы можете изменить ConnectAsync на:

public Task<string> ConnectAsync(ThatService service)
{
    if (service==null) 
        throw new ArgumentNullException(nameof(service));

    var tcs=new TaskCompletionSource<string>();

    service.ConnectResultHandler=(ok,msg)=>
    {
        if(ok)
        {
            tcs.TrySetResult(msg);
        }
        else
        {
            tcs.TrySetException(new Exception(msg));
        }
    };

    return tcs.Task;
}

Вы можете изменить ConnectAsync на метод расширения , который позволит вам использовать его, как если бы он был методом вашего класса обслуживания:

public static class MyServiceExtensions 
{
    public static Task<string> ConnectAsync(this ThatService service)
    {
        //Same as before
    }
}

И используйте это:

public async Task MyMethod()
{
    ...
    var msg=await _service.ConnectAsync();
    ...
}
...