Repository: Tyrrrz/Gress Branch: prime Commit: 39db230235f1 Files: 41 Total size: 80.9 KB Directory structure: gitextract_yhxilno4/ ├── .github/ │ ├── dependabot.yml │ ├── release.yml │ └── workflows/ │ └── main.yml ├── .gitignore ├── Directory.Build.props ├── Directory.Packages.props ├── Gress/ │ ├── Completable/ │ │ ├── AutoResetProgressMuxer.cs │ │ ├── CompletableProgressExtensions.cs │ │ ├── DelegateCompletableProgress.cs │ │ ├── DisposableCompletableProgress.cs │ │ └── ICompletableProgress.cs │ ├── DelegateProgress.cs │ ├── Gress.csproj │ ├── Integrations/ │ │ ├── HttpClientExtensions.cs │ │ ├── HttpContentExtensions.cs │ │ └── StreamExtensions.cs │ ├── Percentage.cs │ ├── ProgressCollector.cs │ ├── ProgressContainer.cs │ ├── ProgressExtensions.cs │ └── ProgressMuxer.cs ├── Gress.Demo/ │ ├── App.axaml │ ├── App.axaml.cs │ ├── Gress.Demo.csproj │ ├── Program.cs │ ├── Readme.md │ ├── ViewModels/ │ │ ├── MainViewModel.cs │ │ └── OperationViewModel.cs │ └── Views/ │ ├── MainWindow.axaml │ └── MainWindow.axaml.cs ├── Gress.Tests/ │ ├── CompositionSpecs.cs │ ├── Gress.Tests.csproj │ ├── IntegrationSpecs.cs │ ├── MuxingSpecs.cs │ ├── TerminalSpecs.cs │ └── xunit.runner.json ├── Gress.slnx ├── License.txt ├── NuGet.config ├── Readme.md └── global.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: github-actions directory: "/" schedule: interval: monthly labels: - enhancement groups: actions: patterns: - "*" - package-ecosystem: nuget directory: "/" schedule: interval: monthly labels: - enhancement groups: nuget: patterns: - "*" ================================================ FILE: .github/release.yml ================================================ changelog: exclude: authors: - dependabot - dependabot[bot] categories: - title: Enhancements labels: - enhancement - title: Bugs labels: - bug - title: Other labels: - "*" ================================================ FILE: .github/workflows/main.yml ================================================ name: main on: workflow_dispatch: inputs: package-version: type: string description: Package version required: false deploy: type: boolean description: Deploy package required: false default: false push: branches: - prime tags: - "*" pull_request: branches: - prime jobs: main: uses: Tyrrrz/.github/.github/workflows/nuget.yml@prime permissions: actions: write contents: write with: deploy: ${{ inputs.deploy || github.ref_type == 'tag' }} package-version: ${{ inputs.package-version || (github.ref_type == 'tag' && github.ref_name) || format('0.0.0-ci-{0}', github.sha) }} secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} NUGET_TOKEN: ${{ secrets.NUGET_TOKEN }} DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} ================================================ FILE: .gitignore ================================================ # User-specific files .vs/ .idea/ *.suo *.user # Build results bin/ obj/ # Test results TestResults/ ================================================ FILE: Directory.Build.props ================================================ 0.0.0-dev Tyrrrz Copyright (C) Oleksii Holub enable true true false false annotations false false $(Company) Progress reporting toolbox progress aggregation muxing transform filter net standard core https://github.com/Tyrrrz/Gress https://github.com/Tyrrrz/Gress/releases MIT ================================================ FILE: Directory.Packages.props ================================================ true ================================================ FILE: Gress/Completable/AutoResetProgressMuxer.cs ================================================ using System; using System.Threading; namespace Gress.Completable; /// /// Aggregates multiple progress updates into a single handler. /// Resets itself once all inputs report completion. /// public partial class AutoResetProgressMuxer(ProgressMuxer muxer) { private readonly Lock _lock = new(); private readonly ProgressMuxer _muxer = muxer; private int _pendingInputs; /// /// Creates a progress handler that reports progress to this muxer. /// Specified weight determines the priority of this handler relative to other /// handlers connected to this muxer. Progress reported on a handler with higher /// weight influences the final progress to a greater degree. /// /// /// Returned progress handler can report completion. Once all linked handlers /// report completion, the progress is reset, and existing handlers are disconnected. /// public ICompletableProgress CreateInput(double weight = 1.0) { using (_lock.EnterScope()) { var item = new Item(this, _muxer.CreateInput(weight)); // Make sure the item was created successfully before incrementing _pendingInputs++; return item; } } } public partial class AutoResetProgressMuxer { private class Item : ICompletableProgress { private readonly AutoResetProgressMuxer _parent; private readonly IProgress _target; public Item(AutoResetProgressMuxer parent, IProgress target) { _parent = parent; _target = target; } public void Report(Percentage value) { using (_parent._lock.EnterScope()) { _target.Report(value); } } public void ReportCompletion() { using (_parent._lock.EnterScope()) { if (--_parent._pendingInputs <= 0) _parent._muxer.Reset(); } } } } ================================================ FILE: Gress/Completable/CompletableProgressExtensions.cs ================================================ using System; namespace Gress.Completable; /// /// Extensions for . /// public static class CompletableProgressExtensions { /// extension(IProgress progress) { /// /// Converts a regular progress handler into a progress handler with explicit completion. /// public ICompletableProgress ToCompletable(Action reportCompletion) => new DelegateCompletableProgress(progress.Report, reportCompletion); } /// extension(ICompletableProgress progress) { /// /// Wraps the specified completable progress handler in a disposable container. /// Disposing the container reports completion on the handler. /// public DisposableCompletableProgress ToDisposable() => new(progress); } /// extension(ProgressMuxer muxer) { /// /// Wraps the muxer in a special adapter that disconnects all inputs from the muxer /// after they all report completion. /// public AutoResetProgressMuxer WithAutoReset() => new(muxer); } } ================================================ FILE: Gress/Completable/DelegateCompletableProgress.cs ================================================ using System; namespace Gress.Completable; /// /// Simple implementation of that uses the provided /// delegates to report progress updates and signal completion. /// public class DelegateCompletableProgress(Action report, Action reportCompletion) : ICompletableProgress { /// public void Report(T value) => report(value); /// public void ReportCompletion() => reportCompletion(); } ================================================ FILE: Gress/Completable/DisposableCompletableProgress.cs ================================================ using System; namespace Gress.Completable; /// /// Convenience wrapper for that reports completion /// on disposal. /// public class DisposableCompletableProgress(ICompletableProgress target) : ICompletableProgress, IDisposable { /// public void Report(T value) => target.Report(value); /// public void ReportCompletion() => target.ReportCompletion(); /// public void Dispose() => ReportCompletion(); } ================================================ FILE: Gress/Completable/ICompletableProgress.cs ================================================ using System; namespace Gress.Completable; /// /// Progress handler with explicit completion feedback. /// public interface ICompletableProgress : IProgress { /// /// Reports overall completion of the operation. /// void ReportCompletion(); } ================================================ FILE: Gress/DelegateProgress.cs ================================================ using System; namespace Gress; /// /// Simple implementation of that uses the provided /// delegate to report progress updates. /// /// /// Unlike , this implementation does not post updates /// through a synchronization context, but rather calls the delegate directly. /// public class DelegateProgress(Action report) : IProgress { /// public void Report(T value) => report(value); } ================================================ FILE: Gress/Gress.csproj ================================================ netstandard2.0;net6.0;net7.0;net10.0 true true true favicon.png true ================================================ FILE: Gress/Integrations/HttpClientExtensions.cs ================================================ using System; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Gress.Integrations; using PowerKit.Extensions; namespace Gress.Integrations; /// /// Provides progress-aware extensions for . /// public static class HttpClientExtensions { /// extension(HttpClient client) { /// /// Sends a GET request and returns the response body as a byte array. /// public async Task GetByteArrayAsync( Uri requestUri, IProgress? progress, CancellationToken cancellationToken = default ) { using var response = await client .GetAsync(requestUri, HttpCompletionOption.ResponseHeadersRead, cancellationToken) .ConfigureAwait(false); response.EnsureSuccessStatusCode(); return await response .Content.ReadAsByteArrayAsync(progress, cancellationToken) .ConfigureAwait(false); } /// /// Sends a GET request and returns the response body as a byte array. /// public async Task GetByteArrayAsync( string requestUri, IProgress? progress, CancellationToken cancellationToken = default ) => await client .GetByteArrayAsync( new Uri(requestUri, UriKind.RelativeOrAbsolute), progress, cancellationToken ) .ConfigureAwait(false); /// /// Sends a GET request and returns the response body as text. /// public async Task GetStringAsync( Uri requestUri, IProgress? progress, CancellationToken cancellationToken = default ) { using var response = await client .GetAsync(requestUri, HttpCompletionOption.ResponseHeadersRead, cancellationToken) .ConfigureAwait(false); response.EnsureSuccessStatusCode(); return await response .Content.ReadAsStringAsync(progress, cancellationToken) .ConfigureAwait(false); } /// /// Sends a GET request and returns the response body as text. /// public async Task GetStringAsync( string requestUri, IProgress? progress, CancellationToken cancellationToken = default ) => await client .GetStringAsync( new Uri(requestUri, UriKind.RelativeOrAbsolute), progress, cancellationToken ) .ConfigureAwait(false); /// /// Sends a GET request and saves the response body to a file. /// public async Task DownloadAsync( Uri requestUri, string filePath, IProgress? progress, CancellationToken cancellationToken = default ) => await client .DownloadAsync(requestUri, filePath, progress?.ToDoubleBased(), cancellationToken) .ConfigureAwait(false); /// /// Sends a GET request and saves the response body to a file. /// public async Task DownloadAsync( string requestUri, string filePath, IProgress? progress, CancellationToken cancellationToken = default ) => await client .DownloadAsync( new Uri(requestUri, UriKind.RelativeOrAbsolute), filePath, progress, cancellationToken ) .ConfigureAwait(false); } } ================================================ FILE: Gress/Integrations/HttpContentExtensions.cs ================================================ using System; using System.IO; using System.Linq; using System.Net.Http; using System.Text; using System.Threading; using System.Threading.Tasks; using PowerKit.Extensions; namespace Gress.Integrations; /// /// Provides progress-aware extensions for . /// public static class HttpContentExtensions { /// extension(HttpContent content) { /// /// Serializes the HTTP content and writes it to the specified stream. /// public async Task CopyToAsync( Stream destination, IProgress? progress, CancellationToken cancellationToken = default ) => await content .CopyToStreamAsync(destination, progress?.ToDoubleBased(), cancellationToken) .ConfigureAwait(false); private async Task ReadAsMemoryStreamAsync( IProgress? progress, CancellationToken cancellationToken = default ) { var destination = new MemoryStream(); await content .CopyToAsync(destination, progress, cancellationToken) .ConfigureAwait(false); destination.Position = 0; return destination; } /// /// Reads the HTTP content and returns it as a byte array. /// public async Task ReadAsByteArrayAsync( IProgress? progress, CancellationToken cancellationToken = default ) { using var buffer = await content .ReadAsMemoryStreamAsync(progress, cancellationToken) .ConfigureAwait(false); return buffer.ToArray(); } /// /// Reads the HTTP content and returns it as a string. /// public async Task ReadAsStringAsync( IProgress? progress, CancellationToken cancellationToken = default ) { var encoding = content .Headers.ContentType?.CharSet?.Pipe(c => Encoding .GetEncodings() .FirstOrDefault(e => string.Equals(e.Name, c, StringComparison.OrdinalIgnoreCase) ) ) ?.GetEncoding() ?? Encoding.UTF8; using var buffer = await content .ReadAsMemoryStreamAsync(progress, cancellationToken) .ConfigureAwait(false); using var reader = new StreamReader(buffer, encoding, true); return await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false); } } } ================================================ FILE: Gress/Integrations/StreamExtensions.cs ================================================ using System; using System.IO; using System.Threading; using System.Threading.Tasks; using Gress.Integrations; using PowerKit.Extensions; namespace Gress.Integrations; /// /// Provides progress-aware extensions for . /// public static class StreamExtensions { /// extension(Stream source) { /// /// Asynchronously copies bytes from the source stream to the destination stream. /// public async Task CopyToAsync( Stream destination, long sourceLength, IProgress? progress, CancellationToken cancellationToken = default ) => await source .CopyToAsync( destination, sourceLength, progress?.ToDoubleBased(), cancellationToken ) .ConfigureAwait(false); /// /// Asynchronously copies bytes from the source stream to the destination stream. /// public async Task CopyToAsync( Stream destination, IProgress? progress, CancellationToken cancellationToken = default ) => await source .CopyToAsync( destination, source.CanSeek ? source.Length : -1, progress, cancellationToken ) .ConfigureAwait(false); } } ================================================ FILE: Gress/Percentage.cs ================================================ using System; namespace Gress; /// /// Unit of progress. /// public readonly partial struct Percentage(double value) { /// /// Percentage value. /// public double Value { get; } = value; /// /// Percentage value in decimal form (e.g. 0.75 for 75%). /// public double Fraction => Value / 100.0; /// /// Formats the value of this instance to a string. /// public string ToString(IFormatProvider? formatProvider) => Fraction.ToString("P1", formatProvider); /// public override string ToString() => ToString(null); } public partial struct Percentage { /// /// Creates a percentage from its value. /// public static Percentage FromValue(double value) => new(value); /// /// Creates a percentage from its value in decimal form (e.g. from 0.75 to 75%). /// public static Percentage FromFraction(double fraction) => FromValue(fraction * 100.0); } public partial struct Percentage : IEquatable, IComparable { /// public int CompareTo(Percentage other) => Value.CompareTo(other.Value); /// public bool Equals(Percentage other) => Value.CompareTo(other.Value) == 0; /// public override bool Equals(object? obj) => obj is Percentage other && Equals(other); /// public override int GetHashCode() => Value.GetHashCode(); /// /// Greater than operator. /// public static bool operator >(Percentage left, Percentage right) => left.CompareTo(right) > 0; /// /// Lesser than operator. /// public static bool operator <(Percentage left, Percentage right) => left.CompareTo(right) < 0; /// /// Equality operator. /// public static bool operator ==(Percentage left, Percentage right) => left.Equals(right); /// /// Inequality operator. /// public static bool operator !=(Percentage left, Percentage right) => !(left == right); } public partial struct Percentage : IFormattable { string IFormattable.ToString(string? format, IFormatProvider? formatProvider) => ToString(formatProvider); } ================================================ FILE: Gress/ProgressCollector.cs ================================================ using System; using System.Collections.Generic; using System.Threading; namespace Gress; /// /// Terminal progress handler that stores all reported progress updates in a collection. /// public class ProgressCollector : IProgress { private readonly Lock _lock = new(); private readonly List _reports = []; /// /// Clears the reported progress updates. /// public void Reset() { using (_lock.EnterScope()) _reports.Clear(); } /// /// Returns the progress updates reported so far. /// public IReadOnlyList GetValues() { using (_lock.EnterScope()) return _reports.ToArray(); } /// public void Report(T value) { using (_lock.EnterScope()) _reports.Add(value); } } ================================================ FILE: Gress/ProgressContainer.cs ================================================ using System; using System.Collections.Generic; using System.ComponentModel; using System.Runtime.CompilerServices; namespace Gress; /// /// Terminal progress handler that records the last reported progress update. /// public partial class ProgressContainer(T initial) : IProgress { /// /// Initializes an instance of . /// /// /// If is a reference type, the initial value of the /// property will be null. /// Consider using the other constructor overload to provide a non-null initial value. /// public ProgressContainer() : this(default!) { } /// /// Last reported progress update. /// /// /// If this property is accessed before any progress has been reported, /// it will evaluate to the initial value provided by the constructor. /// public T Current { get => initial; private set { if (EqualityComparer.Default.Equals(value, initial)) return; initial = value; OnPropertyChanged(); } } /// public void Report(T value) => Current = value; } public partial class ProgressContainer : INotifyPropertyChanged { private event PropertyChangedEventHandler? PropertyChanged; event PropertyChangedEventHandler? INotifyPropertyChanged.PropertyChanged { add => PropertyChanged += value; remove => PropertyChanged -= value; } // Instrumented automatically by Fody // ReSharper disable once UnusedMember.Local private void OnPropertyChanged([CallerMemberName] string? propertyName = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } ================================================ FILE: Gress/ProgressExtensions.cs ================================================ using System; using System.Collections.Generic; using System.Threading; using PowerKit; namespace Gress; /// /// Extensions for . /// public static class ProgressExtensions { /// extension(IProgress progress) { /// /// Projects progress updates into a different shape. /// public IProgress WithTransform(Func map) => new DelegateProgress(p => progress.Report(map(p))); /// /// Projects progress updates into a different shape. /// public IProgress WithTransform(Func map) => progress.WithTransform(map); /// /// Filters progress updates based on the specified predicate. /// public IProgress WithFilter(Func shouldReport) => new DelegateProgress(p => { if (shouldReport(p)) progress.Report(p); }); /// /// Filters out consecutive progress updates with the same value of the specified key. /// public IProgress WithDeduplication( Func getKey, IEqualityComparer? comparer = null ) { var syncRoot = new Lock(); var actualComparer = comparer ?? EqualityComparer.Default; var lastValueCell = new Cell(); return new DelegateProgress(p => { using (syncRoot.EnterScope()) { var value = getKey(p); if ( lastValueCell.TryOpen(out var lastValue) && actualComparer.Equals(lastValue, value) ) { return; } progress.Report(p); lastValueCell.Store(value); } }); } /// /// Filters out consecutive progress updates with the same value. /// public IProgress WithDeduplication(IEqualityComparer? comparer = null) => progress.WithDeduplication(p => p, comparer); /// /// Filters out progress updates that arrive out of order. /// public IProgress WithOrdering(IComparer? comparer = null) { var syncRoot = new Lock(); var actualComparer = comparer ?? Comparer.Default; var lastValueCell = new Cell(); return new DelegateProgress(p => { using (syncRoot.EnterScope()) { if ( lastValueCell.TryOpen(out var lastValue) && actualComparer.Compare(lastValue, p) > 0 ) { return; } progress.Report(p); lastValueCell.Store(p); } }); } /// /// Merges two progress handlers into one. /// public IProgress Merge(IProgress otherProgress) => new DelegateProgress(p => { progress.Report(p); otherProgress.Report(p); }); /// /// Converts the specified progress handler into a -based progress handler. /// public IProgress ToPercentageBased(Func map) => progress.WithTransform(map); } /// extension(IProgress progress) { /// /// Converts the specified -based progress handler into a /// -based progress handler. /// Parameter specifies whether the percentage-based /// progress is reported in its decimal form (true) or in percentage form (false). /// public IProgress ToPercentageBased(bool asFraction = true) => asFraction ? progress.ToPercentageBased(p => p.Fraction) : progress.ToPercentageBased(p => p.Value); } /// extension(IProgress progress) { /// /// Converts the specified -based progress handler into a /// -based progress handler. /// public IProgress ToPercentageBased() => progress.ToPercentageBased(p => (int)p.Value); } /// extension(IProgress progress) { /// /// Converts the specified -based progress handler into a /// -based progress handler. /// Parameter specifies whether the percentage-based /// progress is reported in its decimal form (true) or in percentage form (false). /// public IProgress ToDoubleBased(bool asFraction = true) => asFraction ? progress.WithTransform((double p) => Percentage.FromFraction(p)) : progress.WithTransform((double p) => Percentage.FromValue(p)); /// /// Converts the specified -based progress handler into an /// -based progress handler. /// public IProgress ToInt32Based() => progress.WithTransform((int p) => Percentage.FromValue(p)); /// /// Creates a muxer for the specified progress handler, allowing it to aggregate /// reports from multiple sources. /// public ProgressMuxer CreateMuxer() => new(progress); } /// extension(IReadOnlyList> progresses) { /// /// Merges multiple progress handlers into one. /// public IProgress Merge() => new DelegateProgress(p => { foreach (var progress in progresses) progress.Report(p); }); } } ================================================ FILE: Gress/ProgressMuxer.cs ================================================ using System; using System.Collections.Generic; using System.Threading; namespace Gress; /// /// Aggregates multiple progress updates into a single handler. /// public partial class ProgressMuxer(IProgress target) { private readonly Lock _lock = new(); private readonly HashSet _inputs = []; private bool _anyInputReported; private void ReportAggregatedProgress() { using (_lock.EnterScope()) { var weightedSum = 0.0; var weightedMax = 0.0; foreach (var input in _inputs) { weightedSum += input.Weight * input.Progress.Fraction; weightedMax += input.Weight * 1.0; } target.Report( Percentage.FromFraction(weightedSum != 0 ? weightedSum / weightedMax : 0) ); } } /// /// Creates a progress handler that reports progress to this muxer. /// Specified weight determines the priority of this handler relative to other /// handlers connected to this muxer. Progress reported on a handler with higher /// weight influences the final progress to a greater degree. /// public IProgress CreateInput(double weight = 1.0) { if (weight <= 0) throw new ArgumentOutOfRangeException(nameof(weight), "Weight must be positive."); using (_lock.EnterScope()) { var input = new Input(this, weight); _inputs.Add(input); // Recalculate and report new progress, taking into account the new input. // Don't do it if none of the inputs have reported progress yet, because // we would just be reporting zeroes for each call to this method. if (_anyInputReported) ReportAggregatedProgress(); return input; } } /// /// Disconnects all progress handlers from this muxer. /// public void Reset() { using (_lock.EnterScope()) { _inputs.Clear(); _anyInputReported = false; ReportAggregatedProgress(); } } } public partial class ProgressMuxer { private class Input : IProgress { private readonly ProgressMuxer _parent; public double Weight { get; } public Percentage Progress { get; private set; } public Input(ProgressMuxer parent, double weight) { _parent = parent; Weight = weight; } public void Report(Percentage value) { using (_parent._lock.EnterScope()) { Progress = value; // This input could have been removed from the muxer. // If that's the case, don't do anything. if (!_parent._inputs.Contains(this)) return; _parent.ReportAggregatedProgress(); _parent._anyInputReported = true; } } } } ================================================ FILE: Gress.Demo/App.axaml ================================================ ================================================ FILE: Gress.Demo/App.axaml.cs ================================================ using Avalonia; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; using Gress.Demo.Views; namespace Gress.Demo; public class App : Application { public override void Initialize() { base.Initialize(); AvaloniaXamlLoader.Load(this); } public override void OnFrameworkInitializationCompleted() { if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime) desktopLifetime.MainWindow = new MainWindow(); base.OnFrameworkInitializationCompleted(); } } ================================================ FILE: Gress.Demo/Gress.Demo.csproj ================================================ WinExe net10.0 ../favicon.ico true ================================================ FILE: Gress.Demo/Program.cs ================================================ using System; using Avalonia; namespace Gress.Demo; public static class Program { public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure().UsePlatformDetect().WithInterFont().LogToTrace(); [STAThread] public static int Main(string[] args) => BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); } ================================================ FILE: Gress.Demo/Readme.md ================================================ # Gress Avalonia demo This demo features a simple Avalonia application that lets you start new operations and track their progress. ================================================ FILE: Gress.Demo/ViewModels/MainViewModel.cs ================================================ using System; using System.Collections.ObjectModel; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Gress.Completable; namespace Gress.Demo.ViewModels; public partial class MainViewModel : ObservableObject { private readonly AutoResetProgressMuxer _progressMuxer; public MainViewModel() => _progressMuxer = Progress.CreateMuxer().WithAutoReset(); public ProgressContainer Progress { get; } = new(); public ObservableCollection Operations { get; } = []; // Start an operation that simulates some work and reports progress [RelayCommand(AllowConcurrentExecutions = true)] private async Task PerformWorkAsync(double weight) { using var progress = _progressMuxer.CreateInput(weight).ToDisposable(); var operation = new OperationViewModel(weight); var mergedProgress = progress.Merge(operation.Progress); Operations.Add(operation); for (var i = 1; i <= 100; i++) { // Simulate work await Task.Delay(TimeSpan.FromSeconds(Random.Shared.Next(1, 5) / 10.0)); // Report progress as a value in the 0..100 range mergedProgress.Report(Percentage.FromValue(i)); } Operations.Remove(operation); } } ================================================ FILE: Gress.Demo/ViewModels/OperationViewModel.cs ================================================ using CommunityToolkit.Mvvm.ComponentModel; namespace Gress.Demo.ViewModels; public class OperationViewModel(double weight) : ObservableObject { public double Weight { get; } = weight; public ProgressContainer Progress { get; } = new(); } ================================================ FILE: Gress.Demo/Views/MainWindow.axaml ================================================