mirror of
https://github.com/oliverbooth/X10D
synced 2024-11-09 22:55:42 +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]
|
## 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]])`.
|
||||||
@ -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]])`.
|
- X10D: Added `TextWriter.WriteLineNoAlloc(ulong[, ReadOnlySpan<char>[, IFormatProvider]])`.
|
||||||
|
|
||||||
### 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));
|
||||||
|
}
|
||||||
|
}
|
@ -4,13 +4,14 @@ using System.Collections;
|
|||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityEngine.TestTools;
|
using UnityEngine.TestTools;
|
||||||
|
using Object = UnityEngine.Object;
|
||||||
|
|
||||||
namespace X10D.Unity.Tests
|
namespace X10D.Unity.Tests
|
||||||
{
|
{
|
||||||
public class ComponentTests
|
public class ComponentTests
|
||||||
{
|
{
|
||||||
[UnityTest]
|
[Test]
|
||||||
public IEnumerator GetComponentsInChildrenOnly_ShouldIgnoreParent()
|
public void GetComponentsInChildrenOnly_ShouldIgnoreParent()
|
||||||
{
|
{
|
||||||
var parent = new GameObject();
|
var parent = new GameObject();
|
||||||
var rigidbody = parent.AddComponent<Rigidbody>();
|
var rigidbody = parent.AddComponent<Rigidbody>();
|
||||||
@ -20,10 +21,11 @@ namespace X10D.Unity.Tests
|
|||||||
child.AddComponent<Rigidbody>();
|
child.AddComponent<Rigidbody>();
|
||||||
|
|
||||||
Rigidbody[] components = rigidbody.GetComponentsInChildrenOnly<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));
|
Assert.That(child, Is.EqualTo(components[0].gameObject));
|
||||||
|
|
||||||
yield break;
|
Object.Destroy(parent);
|
||||||
|
Object.Destroy(child);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
|
|
||||||
using System.Collections;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityEngine.TestTools;
|
using Object = UnityEngine.Object;
|
||||||
|
|
||||||
namespace X10D.Unity.Tests
|
namespace X10D.Unity.Tests
|
||||||
{
|
{
|
||||||
public class GameObjectTests
|
public class GameObjectTests
|
||||||
{
|
{
|
||||||
[UnityTest]
|
[Test]
|
||||||
public IEnumerator GetComponentsInChildrenOnly_ShouldIgnoreParent()
|
public void GetComponentsInChildrenOnly_ShouldIgnoreParent()
|
||||||
{
|
{
|
||||||
var parent = new GameObject();
|
var parent = new GameObject();
|
||||||
parent.AddComponent<Rigidbody>();
|
parent.AddComponent<Rigidbody>();
|
||||||
@ -20,14 +20,16 @@ namespace X10D.Unity.Tests
|
|||||||
child.AddComponent<Rigidbody>();
|
child.AddComponent<Rigidbody>();
|
||||||
|
|
||||||
Rigidbody[] components = parent.GetComponentsInChildrenOnly<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));
|
Assert.That(child, Is.EqualTo(components[0].gameObject));
|
||||||
|
|
||||||
yield break;
|
Object.Destroy(parent);
|
||||||
|
Object.Destroy(child);
|
||||||
}
|
}
|
||||||
|
|
||||||
[UnityTest]
|
[Test]
|
||||||
public IEnumerator LookAt_ShouldRotateSameAsTransform()
|
[SuppressMessage("ReSharper", "Unity.InefficientPropertyAccess", Justification = "False positive.")]
|
||||||
|
public void LookAt_ShouldRotateSameAsTransform()
|
||||||
{
|
{
|
||||||
var first = new GameObject {transform = {position = Vector3.zero, rotation = Quaternion.identity}};
|
var first = new GameObject {transform = {position = Vector3.zero, rotation = Quaternion.identity}};
|
||||||
var second = new GameObject {transform = {position = Vector3.right, 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);
|
first.LookAt(Vector3.right);
|
||||||
Assert.That(firstTransform.rotation, Is.EqualTo(expected));
|
Assert.That(firstTransform.rotation, Is.EqualTo(expected));
|
||||||
|
|
||||||
yield break;
|
Object.Destroy(first);
|
||||||
|
Object.Destroy(second);
|
||||||
}
|
}
|
||||||
|
|
||||||
[UnityTest]
|
[Test]
|
||||||
public IEnumerator SetLayerRecursively_ShouldSetLayerRecursively()
|
public void SetLayerRecursively_ShouldSetLayerRecursively()
|
||||||
{
|
{
|
||||||
var parent = new GameObject();
|
var parent = new GameObject();
|
||||||
var child = new GameObject();
|
var child = new GameObject();
|
||||||
@ -82,11 +85,14 @@ namespace X10D.Unity.Tests
|
|||||||
Assert.That(child.layer, Is.EqualTo(layer));
|
Assert.That(child.layer, Is.EqualTo(layer));
|
||||||
Assert.That(grandChild.layer, Is.EqualTo(layer));
|
Assert.That(grandChild.layer, Is.EqualTo(layer));
|
||||||
|
|
||||||
yield break;
|
Object.Destroy(parent);
|
||||||
|
Object.Destroy(child);
|
||||||
|
Object.Destroy(grandChild);
|
||||||
}
|
}
|
||||||
|
|
||||||
[UnityTest]
|
[Test]
|
||||||
public IEnumerator SetParent_ShouldSetParent()
|
[SuppressMessage("ReSharper", "Unity.InefficientPropertyAccess", Justification = "False positive.")]
|
||||||
|
public void SetParent_ShouldSetParent()
|
||||||
{
|
{
|
||||||
var first = new GameObject {transform = {position = Vector3.zero, rotation = Quaternion.identity}};
|
var first = new GameObject {transform = {position = Vector3.zero, rotation = Quaternion.identity}};
|
||||||
var second = new GameObject {transform = {position = Vector3.right, 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);
|
second.SetParent(first);
|
||||||
Assert.That(second.transform.parent, Is.EqualTo(first.transform));
|
Assert.That(second.transform.parent, Is.EqualTo(first.transform));
|
||||||
|
|
||||||
yield break;
|
Object.Destroy(first);
|
||||||
|
Object.Destroy(second);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -70,4 +70,19 @@
|
|||||||
</None>
|
</None>
|
||||||
</ItemGroup>
|
</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>
|
</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>
|
/// <returns>An array <typeparamref name="T" /> representing the child components.</returns>
|
||||||
public static T[] GetComponentsInChildrenOnly<T>(this GameObject gameObject)
|
public static T[] GetComponentsInChildrenOnly<T>(this GameObject gameObject)
|
||||||
{
|
{
|
||||||
|
Transform rootTransform = gameObject.transform;
|
||||||
var components = new List<T>(gameObject.GetComponentsInChildren<T>());
|
var components = new List<T>(gameObject.GetComponentsInChildren<T>());
|
||||||
|
|
||||||
for (var index = 0; index < components.Count; index++)
|
for (var index = 0; index < components.Count; index++)
|
||||||
@ -26,7 +27,7 @@ public static class GameObjectExtensions
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (childComponent.transform.parent != gameObject.transform)
|
if (childComponent.transform == rootTransform)
|
||||||
{
|
{
|
||||||
components.RemoveAt(index);
|
components.RemoveAt(index);
|
||||||
index--;
|
index--;
|
||||||
|
@ -1,9 +1,4 @@
|
|||||||
#if NET5_0_OR_GREATER
|
namespace X10D.Collections;
|
||||||
using System.Runtime.CompilerServices;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
namespace X10D.Collections;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Extension methods for <see cref="Span{T}" /> and <see cref="ReadOnlySpan{T}" />
|
/// 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