Reputation: 41
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;
}
Upvotes: 3
Views: 284
Reputation: 43495
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 CancellationTokenSource
s you create.
Upvotes: 0
Reputation: 81493
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.
Upvotes: 1