// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.Contracts;
using System.Threading;
using System.Threading.Tasks;
namespace CommunityToolkit.Mvvm.Messaging.Messages;
///
/// A for request messages that can receive multiple replies, which can either be used directly or through derived classes.
///
/// The type of request to make.
public class AsyncCollectionRequestMessage : IAsyncEnumerable
{
///
/// The collection of received replies. We accept both instance, representing already running
/// operations that can be executed in parallel, or instances, which can be used so that multiple
/// asynchronous operations are only started sequentially from and do not overlap in time.
///
private readonly List<(Task?, Func>?)> responses = new();
///
/// The instance used to link the token passed to
/// and the one passed to all subscribers to the message.
///
private readonly CancellationTokenSource cancellationTokenSource = new();
///
/// Gets the instance that will be linked to the
/// one used to asynchronously enumerate the received responses. This can be used to cancel asynchronous
/// replies that are still being processed, if no new items are needed from this request message.
/// Consider the following example, where we define a message to retrieve the currently opened documents:
///
/// public class OpenDocumentsRequestMessage : AsyncCollectionRequestMessage<XmlDocument> { }
///
/// We can then request and enumerate the results like so:
///
/// await foreach (var document in Messenger.Default.Send<OpenDocumentsRequestMessage>())
/// {
/// // Process each document here...
/// }
///
/// If we also want to control the cancellation of the token passed to each subscriber to the message,
/// we can do so by passing a token we control to the returned message before starting the enumeration
/// ().
/// The previous snippet with this additional change looks as follows:
///
/// await foreach (var document in Messenger.Default.Send<OpenDocumentsRequestMessage>().WithCancellation(cts.Token))
/// {
/// // Process each document here...
/// }
///
/// When no more new items are needed (or for any other reason depending on the situation), the token
/// passed to the enumerator can be canceled (by calling ),
/// and that will also notify the remaining tasks in the request message. The token exposed by the message
/// itself will automatically be linked and canceled with the one passed to the enumerator.
///
public CancellationToken CancellationToken => this.cancellationTokenSource.Token;
///
/// Replies to the current request message.
///
/// The response to use to reply to the request message.
public void Reply(T response)
{
Reply(Task.FromResult(response));
}
///
/// Replies to the current request message.
///
/// The response to use to reply to the request message.
/// Thrown if is .
public void Reply(Task response)
{
ArgumentNullException.ThrowIfNull(response);
this.responses.Add((response, null));
}
///
/// Replies to the current request message.
///
/// The response to use to reply to the request message.
/// Thrown if is .
public void Reply(Func> response)
{
ArgumentNullException.ThrowIfNull(response);
this.responses.Add((null, response));
}
///
/// Gets the collection of received response items.
///
/// A value to stop the operation.
/// The collection of received response items.
[Pure]
public async Task> GetResponsesAsync(CancellationToken cancellationToken = default)
{
if (cancellationToken.CanBeCanceled)
{
_ = cancellationToken.Register(this.cancellationTokenSource.Cancel);
}
List results = new(this.responses.Count);
await foreach (T? response in this.WithCancellation(cancellationToken).ConfigureAwait(false))
{
results.Add(response);
}
return results;
}
///
[Pure]
[EditorBrowsable(EditorBrowsableState.Never)]
public async IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default)
{
if (cancellationToken.CanBeCanceled)
{
_ = cancellationToken.Register(this.cancellationTokenSource.Cancel);
}
foreach ((Task? task, Func>? func) in this.responses)
{
if (cancellationToken.IsCancellationRequested)
{
yield break;
}
if (task is not null)
{
yield return await task.ConfigureAwait(false);
}
else
{
yield return await func!(cancellationToken).ConfigureAwait(false);
}
}
}
}