mirror of
https://github.com/oliverbooth/X10D
synced 2024-11-22 14:28:47 +00:00
Merge branch 'develop' into main
This commit is contained in:
commit
b70b704b63
@ -8,10 +8,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
## 4.0.0 - [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- X10D: Added extension methods for `DateOnly`, for parity with `DateTime` and `DateTimeOffset`.
|
||||
- X10D: Added math-related extension methods for `BigInteger`.
|
||||
- X10D: Added `Span<T>.Replace(T, T)`.
|
||||
- X10D: Added `CountDigits` for integer types.
|
||||
- X10D: Added `Progress<T>.OnProgressChanged([T])`;
|
||||
- X10D: Added `TextWriter.WriteNoAlloc(int[, ReadOnlySpan<char>[, IFormatProvider]])`.
|
||||
- X10D: Added `TextWriter.WriteNoAlloc(uint[, ReadOnlySpan<char>[, IFormatProvider]])`.
|
||||
- X10D: Added `TextWriter.WriteNoAlloc(long[, ReadOnlySpan<char>[, IFormatProvider]])`.
|
||||
@ -22,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- X10D: Added `TextWriter.WriteLineNoAlloc(ulong[, ReadOnlySpan<char>[, IFormatProvider]])`.
|
||||
|
||||
### Changed
|
||||
|
||||
- X10D: `DateTime.Age(DateTime)` and `DateTimeOffset.Age(DateTimeOffset)` parameter renamed from `asOf` to `referenceDate`.
|
||||
|
||||
## [3.2.0] - 2023-04-03
|
||||
|
@ -22,6 +22,7 @@
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.4.2"/>
|
||||
<PackageReference Include="NUnit.Analyzers" Version="3.6.1"/>
|
||||
<PackageReference Include="coverlet.collector" Version="3.2.0"/>
|
||||
<PackageReference Include="System.Reactive" Version="5.0.0"/>
|
||||
</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));
|
||||
}
|
||||
}
|
@ -4,13 +4,14 @@ using System.Collections;
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
using UnityEngine.TestTools;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace X10D.Unity.Tests
|
||||
{
|
||||
public class ComponentTests
|
||||
{
|
||||
[UnityTest]
|
||||
public IEnumerator GetComponentsInChildrenOnly_ShouldIgnoreParent()
|
||||
[Test]
|
||||
public void GetComponentsInChildrenOnly_ShouldIgnoreParent()
|
||||
{
|
||||
var parent = new GameObject();
|
||||
var rigidbody = parent.AddComponent<Rigidbody>();
|
||||
@ -20,10 +21,11 @@ namespace X10D.Unity.Tests
|
||||
child.AddComponent<Rigidbody>();
|
||||
|
||||
Rigidbody[] components = rigidbody.GetComponentsInChildrenOnly<Rigidbody>();
|
||||
Assert.That(components.Length, Is.EqualTo(1));
|
||||
Assert.That(components, Has.Length.EqualTo(1));
|
||||
Assert.That(child, Is.EqualTo(components[0].gameObject));
|
||||
|
||||
yield break;
|
||||
Object.Destroy(parent);
|
||||
Object.Destroy(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,16 +1,16 @@
|
||||
#nullable enable
|
||||
|
||||
using System.Collections;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
using UnityEngine.TestTools;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace X10D.Unity.Tests
|
||||
{
|
||||
public class GameObjectTests
|
||||
{
|
||||
[UnityTest]
|
||||
public IEnumerator GetComponentsInChildrenOnly_ShouldIgnoreParent()
|
||||
[Test]
|
||||
public void GetComponentsInChildrenOnly_ShouldIgnoreParent()
|
||||
{
|
||||
var parent = new GameObject();
|
||||
parent.AddComponent<Rigidbody>();
|
||||
@ -20,14 +20,16 @@ namespace X10D.Unity.Tests
|
||||
child.AddComponent<Rigidbody>();
|
||||
|
||||
Rigidbody[] components = parent.GetComponentsInChildrenOnly<Rigidbody>();
|
||||
Assert.That(components.Length, Is.EqualTo(1));
|
||||
Assert.That(components, Has.Length.EqualTo(1));
|
||||
Assert.That(child, Is.EqualTo(components[0].gameObject));
|
||||
|
||||
yield break;
|
||||
Object.Destroy(parent);
|
||||
Object.Destroy(child);
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator LookAt_ShouldRotateSameAsTransform()
|
||||
[Test]
|
||||
[SuppressMessage("ReSharper", "Unity.InefficientPropertyAccess", Justification = "False positive.")]
|
||||
public void LookAt_ShouldRotateSameAsTransform()
|
||||
{
|
||||
var first = new GameObject {transform = {position = Vector3.zero, rotation = Quaternion.identity}};
|
||||
var second = new GameObject {transform = {position = Vector3.right, rotation = Quaternion.identity}};
|
||||
@ -58,11 +60,12 @@ namespace X10D.Unity.Tests
|
||||
first.LookAt(Vector3.right);
|
||||
Assert.That(firstTransform.rotation, Is.EqualTo(expected));
|
||||
|
||||
yield break;
|
||||
Object.Destroy(first);
|
||||
Object.Destroy(second);
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator SetLayerRecursively_ShouldSetLayerRecursively()
|
||||
[Test]
|
||||
public void SetLayerRecursively_ShouldSetLayerRecursively()
|
||||
{
|
||||
var parent = new GameObject();
|
||||
var child = new GameObject();
|
||||
@ -82,11 +85,14 @@ namespace X10D.Unity.Tests
|
||||
Assert.That(child.layer, Is.EqualTo(layer));
|
||||
Assert.That(grandChild.layer, Is.EqualTo(layer));
|
||||
|
||||
yield break;
|
||||
Object.Destroy(parent);
|
||||
Object.Destroy(child);
|
||||
Object.Destroy(grandChild);
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator SetParent_ShouldSetParent()
|
||||
[Test]
|
||||
[SuppressMessage("ReSharper", "Unity.InefficientPropertyAccess", Justification = "False positive.")]
|
||||
public void SetParent_ShouldSetParent()
|
||||
{
|
||||
var first = new GameObject {transform = {position = Vector3.zero, rotation = Quaternion.identity}};
|
||||
var second = new GameObject {transform = {position = Vector3.right, rotation = Quaternion.identity}};
|
||||
@ -103,7 +109,8 @@ namespace X10D.Unity.Tests
|
||||
second.SetParent(first);
|
||||
Assert.That(second.transform.parent, Is.EqualTo(first.transform));
|
||||
|
||||
yield break;
|
||||
Object.Destroy(first);
|
||||
Object.Destroy(second);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -70,4 +70,19 @@
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Update="src\ExceptionMessages.resx">
|
||||
<Generator>ResXFileCodeGenerator</Generator>
|
||||
<LastGenOutput>ExceptionMessages.Designer.cs</LastGenOutput>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Update="src\ExceptionMessages.Designer.cs">
|
||||
<DesignTime>True</DesignTime>
|
||||
<AutoGen>True</AutoGen>
|
||||
<DependentUpon>ExceptionMessages.resx</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
80
X10D.Unity/src/ExceptionMessages.Designer.cs
generated
Normal file
80
X10D.Unity/src/ExceptionMessages.Designer.cs
generated
Normal file
@ -0,0 +1,80 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace X10D.Unity {
|
||||
using System;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// A strongly-typed resource class, for looking up localized strings, etc.
|
||||
/// </summary>
|
||||
// This class was auto-generated by the StronglyTypedResourceBuilder
|
||||
// class via a tool like ResGen or Visual Studio.
|
||||
// To add or remove a member, edit your .ResX file then rerun ResGen
|
||||
// with the /str option, or rebuild your VS project.
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
internal class ExceptionMessages {
|
||||
|
||||
private static global::System.Resources.ResourceManager resourceMan;
|
||||
|
||||
private static global::System.Globalization.CultureInfo resourceCulture;
|
||||
|
||||
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
|
||||
internal ExceptionMessages() {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the cached ResourceManager instance used by this class.
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
internal static global::System.Resources.ResourceManager ResourceManager {
|
||||
get {
|
||||
if (object.ReferenceEquals(resourceMan, null)) {
|
||||
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("X10D.Unity.src.ExceptionMessages", typeof(ExceptionMessages).Assembly);
|
||||
resourceMan = temp;
|
||||
}
|
||||
return resourceMan;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Overrides the current thread's CurrentUICulture property for all
|
||||
/// resource lookups using this strongly typed resource class.
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
internal static global::System.Globalization.CultureInfo Culture {
|
||||
get {
|
||||
return resourceCulture;
|
||||
}
|
||||
set {
|
||||
resourceCulture = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to The game object {0} already has a component of type {1}..
|
||||
/// </summary>
|
||||
internal static string ComponentAlreadyExists {
|
||||
get {
|
||||
return ResourceManager.GetString("ComponentAlreadyExists", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to The game object {0} does not have a component of type {1}..
|
||||
/// </summary>
|
||||
internal static string ComponentDoesNotExist {
|
||||
get {
|
||||
return ResourceManager.GetString("ComponentDoesNotExist", resourceCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
34
X10D.Unity/src/ExceptionMessages.resx
Normal file
34
X10D.Unity/src/ExceptionMessages.resx
Normal file
@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<root>
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema"
|
||||
xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>1.3</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral,
|
||||
PublicKeyToken=b77a5c561934e089
|
||||
</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral,
|
||||
PublicKeyToken=b77a5c561934e089
|
||||
</value>
|
||||
</resheader>
|
||||
|
||||
<data name="ComponentDoesNotExist" xml:space="preserve">
|
||||
<value>The game object {0} does not have a component of type {1}.</value>
|
||||
</data>
|
||||
|
||||
<data name="ComponentAlreadyExists" xml:space="preserve">
|
||||
<value>The game object {0} already has a component of type {1}.</value>
|
||||
</data>
|
||||
</root>
|
@ -15,6 +15,7 @@ public static class GameObjectExtensions
|
||||
/// <returns>An array <typeparamref name="T" /> representing the child components.</returns>
|
||||
public static T[] GetComponentsInChildrenOnly<T>(this GameObject gameObject)
|
||||
{
|
||||
Transform rootTransform = gameObject.transform;
|
||||
var components = new List<T>(gameObject.GetComponentsInChildren<T>());
|
||||
|
||||
for (var index = 0; index < components.Count; index++)
|
||||
@ -26,7 +27,7 @@ public static class GameObjectExtensions
|
||||
continue;
|
||||
}
|
||||
|
||||
if (childComponent.transform.parent != gameObject.transform)
|
||||
if (childComponent.transform == rootTransform)
|
||||
{
|
||||
components.RemoveAt(index);
|
||||
index--;
|
||||
|
@ -1,9 +1,4 @@
|
||||
#if NET5_0_OR_GREATER
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
#endif
|
||||
|
||||
namespace X10D.Collections;
|
||||
namespace X10D.Collections;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="Span{T}" /> and <see cref="ReadOnlySpan{T}" />
|
||||
|
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