// 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); } } } }