< Summary

Information
Class: MyNet.UI.Loading.ProgresserBusyExtensions
Assembly: MyNet.UI
File(s): https://raw.githubusercontent.com/sandre58/MyNet/85372080fe102cd9ee155ceab49ae000e7f66103/src/MyNet.UI/Loading/Extensions/ProgresserBusyExtensions.cs
Tag: 323_28699572109
Line coverage
0%
Covered lines: 0
Uncovered lines: 34
Coverable lines: 34
Total lines: 107
Line coverage: 0%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
RunWithProgressAsync(...)100%210%
RunWithProgressAsync(...)100%210%
RunBridgedAsync()100%210%

File(s)

https://raw.githubusercontent.com/sandre58/MyNet/85372080fe102cd9ee155ceab49ae000e7f66103/src/MyNet.UI/Loading/Extensions/ProgresserBusyExtensions.cs

#LineLine coverage
 1// -----------------------------------------------------------------------
 2// <copyright file="ProgresserBusyExtensions.cs" company="Stéphane ANDRE">
 3// Copyright (c) Stéphane ANDRE. All rights reserved.
 4// </copyright>
 5// -----------------------------------------------------------------------
 6
 7using System;
 8using System.Threading;
 9using System.Threading.Tasks;
 10using MyNet.UI.Loading.Models;
 11using MyNet.Utilities.Progress;
 12
 13#pragma warning disable IDE0130
 14namespace MyNet.UI.Loading;
 15#pragma warning restore IDE0130
 16
 17/// <summary>
 18/// Bridges the hierarchical progress API (<see cref="IProgresser"/>) onto the busy system:
 19/// a <see cref="ProgressionBusy"/> scope is opened for the duration of the operation and kept in
 20/// sync with every <see cref="ProgressReport{T}"/> emitted by the progresser (value, breadcrumb
 21/// message, cancellation availability and cancel action).
 22/// </summary>
 23public static class ProgresserBusyExtensions
 24{
 25    extension(IBusyService busyService)
 26    {
 27        /// <summary>
 28        /// Runs <paramref name="operation"/> inside a <see cref="ProgressionBusy"/> scope while mirroring
 29        /// the progress reported through <paramref name="progresser"/>. The operation receives the same
 30        /// <paramref name="progresser"/> so it can open steps with <c>Begin</c>/<c>StartStep</c>.
 31        /// </summary>
 32        /// <param name="progresser">The progresser whose reports drive the busy presentation.</param>
 33        /// <param name="operation">The asynchronous work to execute.</param>
 34        /// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
 35        public Task RunWithProgressAsync(
 36            IProgresser progresser,
 37            Func<IProgresser, CancellationToken, Task> operation,
 38            CancellationToken cancellationToken = default)
 39        {
 040            ArgumentNullException.ThrowIfNull(progresser);
 041            ArgumentNullException.ThrowIfNull(operation);
 42
 043            return busyService.RunAsync<ProgressionBusy>(
 044                (busy, token) => RunBridgedAsync(progresser, busy, () => operation(progresser, token), token),
 045                cancellationToken);
 46        }
 47
 48        /// <summary>
 49        /// Runs <paramref name="operation"/> inside a <see cref="ProgressionBusy"/> scope, mirroring the
 50        /// progress reported through a freshly created <see cref="Progresser"/> handed to the operation.
 51        /// </summary>
 52        /// <param name="operation">The asynchronous work to execute.</param>
 53        /// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
 54        public Task RunWithProgressAsync(
 55            Func<IProgresser, CancellationToken, Task> operation,
 56            CancellationToken cancellationToken = default)
 057            => busyService.RunWithProgressAsync(new Progresser(), operation, cancellationToken);
 58    }
 59
 60    private static async Task RunBridgedAsync(
 61        IProgresser progresser,
 62        ProgressionBusy busy,
 63        Func<Task> operation,
 64        CancellationToken token)
 65    {
 066        ProgressReport<ProgressMessage>? lastReport = null;
 067        string? lastMessage = null;
 68
 69        // A cancel request coming from the busy UI (its CancellationToken) is forwarded to the
 70        // cancel action carried by the latest progress report, so the progress session stops too.
 071        await using var registration = token.Register(() => lastReport?.CancelAction?.Invoke()).ConfigureAwait(false);
 72
 73        // Progress<T> marshals callbacks to the synchronization context captured here, so the busy
 74        // model (and its bound UI) is always updated on the originating thread.
 075        var sink = new Progress<ProgressReport<ProgressMessage>>(report =>
 076        {
 077            lastReport = report;
 078            busy.CanCancel = report.CanCancel;
 079            busy.Value = report.Progress;
 080
 081            var message = report.Messages.Count > 0 ? report.Messages[^1].ToString() : null;
 082            if (string.IsNullOrWhiteSpace(message))
 083                return;
 084
 085            busy.Title = message;
 086
 087            // Only append a breadcrumb entry when the innermost message actually changes,
 088            // otherwise repeated reports for the same step would flood the history.
 089            if (!string.Equals(message, lastMessage, StringComparison.Ordinal))
 090            {
 091                busy.Messages.Add(message);
 092                lastMessage = message;
 093            }
 094        });
 95
 096        progresser.Subscribe(sink);
 97        try
 98        {
 099            await operation().ConfigureAwait(false);
 0100        }
 101        finally
 102        {
 0103            progresser.Unsubscribe(sink);
 104        }
 0105    }
 106}
 107