mirror of
https://github.com/oliverbooth/X10D
synced 2024-11-22 13:48:47 +00:00
feat: add Progress<T>.OnProgressChanged
Provides a mechanism to wrap the ProgressChanged event of a Progress<T> as an IObservable<T>.
This commit is contained in:
parent
a4a1d3b13a
commit
f6847315a1
@ -8,10 +8,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
## 4.0.0 - [Unreleased]
|
## 4.0.0 - [Unreleased]
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- X10D: Added extension methods for `DateOnly`, for parity with `DateTime` and `DateTimeOffset`.
|
- X10D: Added extension methods for `DateOnly`, for parity with `DateTime` and `DateTimeOffset`.
|
||||||
- X10D: Added math-related extension methods for `BigInteger`.
|
- X10D: Added math-related extension methods for `BigInteger`.
|
||||||
- X10D: Added `Span<T>.Replace(T, T)`.
|
- X10D: Added `Span<T>.Replace(T, T)`.
|
||||||
- X10D: Added `CountDigits` for integer types.
|
- X10D: Added `CountDigits` for integer types.
|
||||||
|
- X10D: Added `Progress<T>.OnProgressChanged([T])`;
|
||||||
- X10D: Added `TextWriter.WriteNoAlloc(int[, ReadOnlySpan<char>[, IFormatProvider]])`.
|
- X10D: Added `TextWriter.WriteNoAlloc(int[, ReadOnlySpan<char>[, IFormatProvider]])`.
|
||||||
- X10D: Added `TextWriter.WriteNoAlloc(uint[, ReadOnlySpan<char>[, IFormatProvider]])`.
|
- X10D: Added `TextWriter.WriteNoAlloc(uint[, ReadOnlySpan<char>[, IFormatProvider]])`.
|
||||||
- X10D: Added `TextWriter.WriteNoAlloc(long[, ReadOnlySpan<char>[, IFormatProvider]])`.
|
- X10D: Added `TextWriter.WriteNoAlloc(long[, ReadOnlySpan<char>[, IFormatProvider]])`.
|
||||||
@ -24,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- X10D.Unity: Added `GameObject.CopyComponent<T>(GameObject)` and `GameObject.MoveComponent<T>(GameObject)`.
|
- X10D.Unity: Added `GameObject.CopyComponent<T>(GameObject)` and `GameObject.MoveComponent<T>(GameObject)`.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- X10D: `DateTime.Age(DateTime)` and `DateTimeOffset.Age(DateTimeOffset)` parameter renamed from `asOf` to `referenceDate`.
|
- X10D: `DateTime.Age(DateTime)` and `DateTimeOffset.Age(DateTimeOffset)` parameter renamed from `asOf` to `referenceDate`.
|
||||||
|
|
||||||
## [3.2.0] - 2023-04-03
|
## [3.2.0] - 2023-04-03
|
||||||
|
@ -22,6 +22,7 @@
|
|||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.4.2"/>
|
<PackageReference Include="NUnit3TestAdapter" Version="4.4.2"/>
|
||||||
<PackageReference Include="NUnit.Analyzers" Version="3.6.1"/>
|
<PackageReference Include="NUnit.Analyzers" Version="3.6.1"/>
|
||||||
<PackageReference Include="coverlet.collector" Version="3.2.0"/>
|
<PackageReference Include="coverlet.collector" Version="3.2.0"/>
|
||||||
|
<PackageReference Include="System.Reactive" Version="5.0.0"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
67
X10D.Tests/src/Reactive/ProgressTests.cs
Normal file
67
X10D.Tests/src/Reactive/ProgressTests.cs
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
using NUnit.Framework;
|
||||||
|
using X10D.Reactive;
|
||||||
|
|
||||||
|
namespace X10D.Tests.Reactive;
|
||||||
|
|
||||||
|
[TestFixture]
|
||||||
|
public class ProgressTests
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void OnProgressChanged_ShouldCallCompletionDelegate_GivenCompletionValue()
|
||||||
|
{
|
||||||
|
var subscriberWasCalled = false;
|
||||||
|
var completionWasCalled = false;
|
||||||
|
|
||||||
|
var progress = new Progress<float>();
|
||||||
|
progress.OnProgressChanged(1.0f).Subscribe(_ => subscriberWasCalled = true, () => completionWasCalled = true);
|
||||||
|
|
||||||
|
((IProgress<float>)progress).Report(0.5f);
|
||||||
|
((IProgress<float>)progress).Report(1.0f);
|
||||||
|
|
||||||
|
Thread.Sleep(1000);
|
||||||
|
Assert.That(subscriberWasCalled);
|
||||||
|
Assert.That(completionWasCalled);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void OnProgressChanged_ShouldCallSubscribers_OnProgressChanged()
|
||||||
|
{
|
||||||
|
var subscriberWasCalled = false;
|
||||||
|
|
||||||
|
var progress = new Progress<float>();
|
||||||
|
progress.OnProgressChanged().Subscribe(_ => subscriberWasCalled = true);
|
||||||
|
|
||||||
|
((IProgress<float>)progress).Report(0.5f);
|
||||||
|
|
||||||
|
Thread.Sleep(1000);
|
||||||
|
Assert.That(subscriberWasCalled);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void OnProgressChanged_ShouldCallSubscribers_OnProgressChanged_GivenCompletionValue()
|
||||||
|
{
|
||||||
|
var subscriberWasCalled = false;
|
||||||
|
|
||||||
|
var progress = new Progress<float>();
|
||||||
|
progress.OnProgressChanged(1.0f).Subscribe(_ => subscriberWasCalled = true);
|
||||||
|
|
||||||
|
((IProgress<float>)progress).Report(0.5f);
|
||||||
|
|
||||||
|
Thread.Sleep(1000);
|
||||||
|
Assert.That(subscriberWasCalled);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void OnProgressChanged_ShouldThrowArgumentNullException_GivenNullProgress()
|
||||||
|
{
|
||||||
|
Progress<float> progress = null!;
|
||||||
|
Assert.Throws<ArgumentNullException>(() => progress.OnProgressChanged());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void OnProgressChanged_ShouldThrowArgumentNullException_GivenNullProgressAndCompletionValue()
|
||||||
|
{
|
||||||
|
Progress<float> progress = null!;
|
||||||
|
Assert.Throws<ArgumentNullException>(() => progress.OnProgressChanged(1.0f));
|
||||||
|
}
|
||||||
|
}
|
48
X10D/src/Reactive/ObservableDisposer.cs
Normal file
48
X10D/src/Reactive/ObservableDisposer.cs
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
namespace X10D.Reactive;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a disposable that removes an observer from a collection of observers.
|
||||||
|
/// </summary>
|
||||||
|
internal readonly struct ObservableDisposer<T> : IDisposable
|
||||||
|
{
|
||||||
|
private readonly HashSet<IObserver<T>> _observers;
|
||||||
|
private readonly IObserver<T> _observer;
|
||||||
|
private readonly Action? _additionalAction;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="ObservableDisposer{T}" /> struct.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="observers">A collection of observers from which to remove the specified observer.</param>
|
||||||
|
/// <param name="observer">The observer to remove from the collection.</param>
|
||||||
|
/// <param name="additionalAction">The additional action to run on dispose.</param>
|
||||||
|
public ObservableDisposer(HashSet<IObserver<T>> observers, IObserver<T> observer, Action? additionalAction)
|
||||||
|
{
|
||||||
|
#if NET6_0_OR_GREATER
|
||||||
|
ArgumentNullException.ThrowIfNull(observers);
|
||||||
|
ArgumentNullException.ThrowIfNull(observer);
|
||||||
|
#else
|
||||||
|
if (observers is null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(observers));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (observer is null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(observer));
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
_observers = observers;
|
||||||
|
_observer = observer;
|
||||||
|
_additionalAction = additionalAction;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes the observer from the collection of observers.
|
||||||
|
/// </summary>
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_observers.Remove(_observer);
|
||||||
|
_additionalAction?.Invoke();
|
||||||
|
}
|
||||||
|
}
|
97
X10D/src/Reactive/ProgressExtensions.cs
Normal file
97
X10D/src/Reactive/ProgressExtensions.cs
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
namespace X10D.Reactive;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides extension methods for <see cref="Progress{T}" />.
|
||||||
|
/// </summary>
|
||||||
|
public static class ProgressExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Wraps the <see cref="Progress{T}.ProgressChanged" /> event of the current <see cref="Progress{T}" /> in an
|
||||||
|
/// <see cref="IObservable{T}" /> object.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="progress">The progress whose <see cref="Progress{T}.ProgressChanged" /> event to wrap.</param>
|
||||||
|
/// <typeparam name="T">The type of progress update value.</typeparam>
|
||||||
|
/// <returns>
|
||||||
|
/// An <see cref="IObservable{T}" /> object that wraps the <see cref="Progress{T}.ProgressChanged" /> event of the current
|
||||||
|
/// <see cref="Progress{T}" />.
|
||||||
|
/// </returns>
|
||||||
|
/// <exception cref="ArgumentNullException"><paramref name="progress" /> is <see langword="null" />.</exception>
|
||||||
|
public static IObservable<T> OnProgressChanged<T>(this Progress<T> progress)
|
||||||
|
{
|
||||||
|
#if NET6_0_OR_GREATER
|
||||||
|
ArgumentNullException.ThrowIfNull(progress);
|
||||||
|
#else
|
||||||
|
if (progress is null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(progress));
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
var progressObservable = new ProgressObservable<T>();
|
||||||
|
|
||||||
|
void ProgressChangedHandler(object? sender, T args)
|
||||||
|
{
|
||||||
|
IObserver<T>[] observers = progressObservable.Observers;
|
||||||
|
|
||||||
|
for (var index = 0; index < observers.Length; index++)
|
||||||
|
{
|
||||||
|
observers[index].OnNext(args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
progress.ProgressChanged += ProgressChangedHandler;
|
||||||
|
progressObservable.OnDispose = () => progress.ProgressChanged -= ProgressChangedHandler;
|
||||||
|
|
||||||
|
return progressObservable;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Wraps the <see cref="Progress{T}.ProgressChanged" /> event of the current <see cref="Progress{T}" /> in an
|
||||||
|
/// <see cref="IObservable{T}" /> object, and completes the observable when the progress reaches the specified value.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="progress">The progress whose <see cref="Progress{T}.ProgressChanged" /> event to wrap.</param>
|
||||||
|
/// <param name="completeValue">The value that indicates completion.</param>
|
||||||
|
/// <typeparam name="T">The type of progress update value.</typeparam>
|
||||||
|
/// <returns>
|
||||||
|
/// An <see cref="IObservable{T}" /> object that wraps the <see cref="Progress{T}.ProgressChanged" /> event of the current
|
||||||
|
/// <see cref="Progress{T}" />.
|
||||||
|
/// </returns>
|
||||||
|
/// <exception cref="ArgumentNullException"><paramref name="progress" /> is <see langword="null" />.</exception>
|
||||||
|
public static IObservable<T> OnProgressChanged<T>(this Progress<T> progress, T completeValue)
|
||||||
|
{
|
||||||
|
#if NET6_0_OR_GREATER
|
||||||
|
ArgumentNullException.ThrowIfNull(progress);
|
||||||
|
#else
|
||||||
|
if (progress is null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(progress));
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
var progressObservable = new ProgressObservable<T>();
|
||||||
|
var comparer = EqualityComparer<T>.Default;
|
||||||
|
|
||||||
|
void ProgressChangedHandler(object? sender, T args)
|
||||||
|
{
|
||||||
|
IObserver<T>[] observers = progressObservable.Observers;
|
||||||
|
|
||||||
|
for (var index = 0; index < observers.Length; index++)
|
||||||
|
{
|
||||||
|
observers[index].OnNext(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (comparer.Equals(args, completeValue))
|
||||||
|
{
|
||||||
|
for (var index = 0; index < observers.Length; index++)
|
||||||
|
{
|
||||||
|
observers[index].OnCompleted();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
progress.ProgressChanged += ProgressChangedHandler;
|
||||||
|
progressObservable.OnDispose = () => progress.ProgressChanged -= ProgressChangedHandler;
|
||||||
|
|
||||||
|
return progressObservable;
|
||||||
|
}
|
||||||
|
}
|
40
X10D/src/Reactive/ProgressObservable.cs
Normal file
40
X10D/src/Reactive/ProgressObservable.cs
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
namespace X10D.Reactive;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a concrete implementation of <see cref="IObservable{T}" /> that tracks progress of a <see cref="Progress{T}"/>.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class ProgressObservable<T> : IObservable<T>
|
||||||
|
{
|
||||||
|
private readonly HashSet<IObserver<T>> _observers = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the observers.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The observers.</value>
|
||||||
|
public IObserver<T>[] Observers
|
||||||
|
{
|
||||||
|
get => _observers.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal Action? OnDispose { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Subscribes the specified observer to the progress tracker.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="observer">The observer.</param>
|
||||||
|
/// <returns>An object which can be disposed to unsubscribe from progress tracking.</returns>
|
||||||
|
public IDisposable Subscribe(IObserver<T> observer)
|
||||||
|
{
|
||||||
|
#if NET6_0_OR_GREATER
|
||||||
|
ArgumentNullException.ThrowIfNull(observer);
|
||||||
|
#else
|
||||||
|
if (observer is null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(observer));
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
_observers.Add(observer);
|
||||||
|
return new ObservableDisposer<T>(_observers, observer, OnDispose);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user