3

I've got an async discover function which discovers devices in the local network which I call on a button click. I'm sending a broadcast message to all devices and listen for responses for 5 seconds using a CancellationTokenSource. After the token has expired I'm returning an IEnumerable of parsed responses to my WPF model.

I'd like return incoming responses directly (and stop listening after 5 seconds) so that I can show discovered devices instantly in the UI instead of showing them all after 5 seconds.

This is my code:

public async Task<IEnumerable<IDevice>> Discover()
{
    var client = new MyClient();
    var responseData = await GetResponseData(client);
    return this.ParseResponseData(responseData);
}

private IEnumerable<IDevice> ParseResponseData(List<DeviceResponseData> responseData)
{
    foreach (var data in responseData)
    {
        yield return DeviceFactory.Create(data);
    }
}

private static async Task<List<DeviceResponseData>> GetResponseData(MyClient client,
    int timeout = 5000)
{
    var cancellationToken = new CancellationTokenSource(timeout);
    var data = new List<DeviceResponseData>();

    // ... prepare message and send it
    await client.SendAsync(message, new CancellationToken());

    try
    {
        while (!cancellationToken.IsCancellationRequested)
        {
            // Wait indefinitely until any message is received.
            var response = await client.ReceiveAsync(cancellationToken.Token);

            data.Add(new DeviceResponseData(/* ... */ response));
        }
    }
    catch (TaskCanceledException e)
    {

    }

    return data;
}
Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
Brian O
  • 41
  • 3
  • Do you mean return after one message, or get results as they come in? – TheGeneral Mar 26 '19 at 23:49
  • The latter. Sending results to my UI as they come in. – Brian O Mar 26 '19 at 23:53
  • I wonder do you realise just how broad a question this is. Essentially you are asking how to design an asynchronous UX. That's not just a book, it's a whole career. The short answer is _You use binding, callbacks, promises and events on a shared state model_. – Peter Wone Mar 27 '19 at 00:01

2 Answers2

1

If you want to show results as they come in, there are many ways of achieving this, like decoupled messages, events, etc.

However, you could just use a simple Action

private static async Task<List<DeviceResponseData>> GetResponseData(MyClient client, Action<DeviceResponseData> update, int timeout = 5000)
{
   var cancellationToken = new CancellationTokenSource(timeout);
   ...
   while (!cancellationToken.IsCancellationRequested)
   {
      // Wait indefinitely until any message is received.
      var response = await client.ReceiveAsync(cancellationToken.Token);

      var result = new DeviceResponseData( /* ... */ response);

      data.Add(result);
      update(result);

   }
   ...
}

usage

var allResults =  await GetResponseData(client,data => UdpateUI(data), timeout);

Note : because this the Async Await Pattern, you wont have to marshal the result form the Action back to the UI Context, if that's where this was called from.

halfer
  • 19,824
  • 17
  • 99
  • 186
TheGeneral
  • 79,002
  • 9
  • 103
  • 141
0

If you have upgraded to C# 8, you have the option of returning an IAsyncEnumerable. This mechanism can propagate a stream of DeviceResponseData objects, that can be consumed as soon as they are produced. Here is the producer method, that is implemented as an iterator (contains yield statements).

private static async IAsyncEnumerable<DeviceResponseData> GetResponseDataStream(
    MyClient client, int timeout = 5000)
{
    using CancellationTokenSource cts = new CancellationTokenSource(timeout);

    // Prepare message...
    try
    {
        await client.SendAsync(message, cts.Token).ConfigureAwait(false);
    }
    catch (OperationCanceledException) { throw new TimeoutException(); }

    while (!cts.IsCancellationRequested)
    {
        Response response;
        try
        {
            // Wait until any response is received.
            response = await client.ReceiveAsync(cts.Token).ConfigureAwait(false);
        }
        catch (OperationCanceledException) { yield break; }

        yield return new DeviceResponseData(/* ... */ response);
    }
}

And here is how it can be consumed:

await foreach (DeviceResponseData data in GetResponseDataStream(client))
{
    // Do something with data
}

The best part is that you don't need to add synchronization code (Dispatcher.Invoke etc) every time you receive a DeviceResponseData object. The async/await machinery restores the captured synchronization context automatically for you (unless you tell it to do otherwise, by using the ConfigureAwait method).

Important: Make sure that you dispose the CancellationTokenSources you create.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104