Add ObservableCollections.R3
This commit is contained in:
parent
bcf250c631
commit
320012d840
@ -23,6 +23,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tools", "tools", "{7133A3F7
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PostBuildUtility", "tools\PostBuildUtility\PostBuildUtility.csproj", "{29E3967D-89E9-494F-B1E6-9706B8F1CD57}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ObservableCollections.R3", "src\ObservableCollections.R3\ObservableCollections.R3.csproj", "{D5950521-C5B3-4B92-834E-3B12CDDD8DD6}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ObservableCollections.R3.Tests", "tests\ObservableCollections.R3.Tests\ObservableCollections.R3.Tests.csproj", "{1205F414-EE6D-49C6-9500-3E62E2120EAF}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@ -53,6 +57,14 @@ Global
|
||||
{29E3967D-89E9-494F-B1E6-9706B8F1CD57}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{29E3967D-89E9-494F-B1E6-9706B8F1CD57}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{29E3967D-89E9-494F-B1E6-9706B8F1CD57}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{D5950521-C5B3-4B92-834E-3B12CDDD8DD6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{D5950521-C5B3-4B92-834E-3B12CDDD8DD6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D5950521-C5B3-4B92-834E-3B12CDDD8DD6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{D5950521-C5B3-4B92-834E-3B12CDDD8DD6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{1205F414-EE6D-49C6-9500-3E62E2120EAF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{1205F414-EE6D-49C6-9500-3E62E2120EAF}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{1205F414-EE6D-49C6-9500-3E62E2120EAF}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{1205F414-EE6D-49C6-9500-3E62E2120EAF}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@ -64,6 +76,8 @@ Global
|
||||
{7E10EF01-24DC-4346-8A18-F791BB5252A7} = {FD836539-75F1-4707-BCFF-751B95DAE19C}
|
||||
{B84027E4-9B39-4FB6-B888-C55CE4C79152} = {B6D0425C-7902-4EFB-B0EA-99F164C20835}
|
||||
{29E3967D-89E9-494F-B1E6-9706B8F1CD57} = {7133A3F7-B398-4DE0-8295-0F1ECFCC4CE4}
|
||||
{D5950521-C5B3-4B92-834E-3B12CDDD8DD6} = {8F60DC54-F617-4841-8C79-6B0137500D1C}
|
||||
{1205F414-EE6D-49C6-9500-3E62E2120EAF} = {B6D0425C-7902-4EFB-B0EA-99F164C20835}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {4156A725-69F4-469F-9BBB-0EE9921CA83E}
|
||||
|
216
src/ObservableCollections.R3/ObservableCollectionR3Extensions.cs
Normal file
216
src/ObservableCollections.R3/ObservableCollectionR3Extensions.cs
Normal file
@ -0,0 +1,216 @@
|
||||
using System;
|
||||
using System.Collections.Specialized;
|
||||
using System.Threading;
|
||||
using R3;
|
||||
|
||||
namespace ObservableCollections.R3;
|
||||
|
||||
public readonly record struct CollectionAddEvent<T>(int Index, T Value);
|
||||
public readonly record struct CollectionRemoveEvent<T>(int Index, T Value);
|
||||
public readonly record struct CollectionReplaceEvent<T>(int Index, T OldValue, T NewValue);
|
||||
public readonly record struct CollectionMoveEvent<T>(int OldIndex, int NewIndex, T Value);
|
||||
|
||||
public static class ObservableCollectionR3Extensions
|
||||
{
|
||||
public static Observable<CollectionAddEvent<T>> ObserveAdd<T>(this IObservableCollection<T> source, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return new ObservableCollectionAdd<T>(source, cancellationToken);
|
||||
}
|
||||
|
||||
public static Observable<CollectionRemoveEvent<T>> ObserveRemove<T>(this IObservableCollection<T> source, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return new ObservableCollectionRemove<T>(source, cancellationToken);
|
||||
}
|
||||
|
||||
public static Observable<CollectionReplaceEvent<T>> ObserveReplace<T>(this IObservableCollection<T> source, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return new ObservableCollectionReplace<T>(source, cancellationToken);
|
||||
}
|
||||
|
||||
public static Observable<CollectionMoveEvent<T>> ObserveMove<T>(this IObservableCollection<T> source, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return new ObservableCollectionMove<T>(source, cancellationToken);
|
||||
}
|
||||
|
||||
public static Observable<Unit> ObserveReset<T>(this IObservableCollection<T> source, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return new ObservableCollectionReset<T>(source, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
sealed class ObservableCollectionAdd<T>(IObservableCollection<T> collection, CancellationToken cancellationToken)
|
||||
: Observable<CollectionAddEvent<T>>
|
||||
{
|
||||
protected override IDisposable SubscribeCore(Observer<CollectionAddEvent<T>> observer)
|
||||
{
|
||||
return new _ObservableCollectionAdd(collection, observer, cancellationToken);
|
||||
}
|
||||
|
||||
sealed class _ObservableCollectionAdd(
|
||||
IObservableCollection<T> collection,
|
||||
Observer<CollectionAddEvent<T>> observer,
|
||||
CancellationToken cancellationToken)
|
||||
: ObservableCollectionObserverBase<T, CollectionAddEvent<T>>(collection, observer, cancellationToken)
|
||||
{
|
||||
protected override void Handler(in NotifyCollectionChangedEventArgs<T> eventArgs)
|
||||
{
|
||||
if (eventArgs.Action == NotifyCollectionChangedAction.Add)
|
||||
{
|
||||
if (eventArgs.IsSingleItem)
|
||||
{
|
||||
observer.OnNext(new CollectionAddEvent<T>(eventArgs.NewStartingIndex, eventArgs.NewItem));
|
||||
}
|
||||
else
|
||||
{
|
||||
var i = eventArgs.NewStartingIndex;
|
||||
foreach (var item in eventArgs.NewItems)
|
||||
{
|
||||
observer.OnNext(new CollectionAddEvent<T>(i++, item));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class ObservableCollectionRemove<T>(IObservableCollection<T> collection, CancellationToken cancellationToken)
|
||||
: Observable<CollectionRemoveEvent<T>>
|
||||
{
|
||||
protected override IDisposable SubscribeCore(Observer<CollectionRemoveEvent<T>> observer)
|
||||
{
|
||||
return new _ObservableCollectionRemove(collection, observer, cancellationToken);
|
||||
}
|
||||
|
||||
sealed class _ObservableCollectionRemove(
|
||||
IObservableCollection<T> collection,
|
||||
Observer<CollectionRemoveEvent<T>> observer,
|
||||
CancellationToken cancellationToken)
|
||||
: ObservableCollectionObserverBase<T, CollectionRemoveEvent<T>>(collection, observer, cancellationToken)
|
||||
{
|
||||
protected override void Handler(in NotifyCollectionChangedEventArgs<T> eventArgs)
|
||||
{
|
||||
if (eventArgs.Action == NotifyCollectionChangedAction.Remove)
|
||||
{
|
||||
if (eventArgs.IsSingleItem)
|
||||
{
|
||||
observer.OnNext(new CollectionRemoveEvent<T>(eventArgs.OldStartingIndex, eventArgs.OldItem));
|
||||
}
|
||||
else
|
||||
{
|
||||
var i = eventArgs.OldStartingIndex;
|
||||
foreach (var item in eventArgs.OldItems)
|
||||
{
|
||||
observer.OnNext(new CollectionRemoveEvent<T>(i++, item));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class ObservableCollectionReplace<T>(IObservableCollection<T> collection, CancellationToken cancellationToken)
|
||||
: Observable<CollectionReplaceEvent<T>>
|
||||
{
|
||||
protected override IDisposable SubscribeCore(Observer<CollectionReplaceEvent<T>> observer)
|
||||
{
|
||||
return new _ObservableCollectionReplace(collection, observer, cancellationToken);
|
||||
}
|
||||
|
||||
sealed class _ObservableCollectionReplace(
|
||||
IObservableCollection<T> collection,
|
||||
Observer<CollectionReplaceEvent<T>> observer,
|
||||
CancellationToken cancellationToken)
|
||||
: ObservableCollectionObserverBase<T, CollectionReplaceEvent<T>>(collection, observer, cancellationToken)
|
||||
{
|
||||
protected override void Handler(in NotifyCollectionChangedEventArgs<T> eventArgs)
|
||||
{
|
||||
if (eventArgs.Action == NotifyCollectionChangedAction.Replace)
|
||||
{
|
||||
observer.OnNext(new CollectionReplaceEvent<T>(eventArgs.NewStartingIndex, eventArgs.OldItem, eventArgs.NewItem));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class ObservableCollectionMove<T>(IObservableCollection<T> collection, CancellationToken cancellationToken)
|
||||
: Observable<CollectionMoveEvent<T>>
|
||||
{
|
||||
protected override IDisposable SubscribeCore(Observer<CollectionMoveEvent<T>> observer)
|
||||
{
|
||||
return new _ObservableCollectionMove(collection, observer, cancellationToken);
|
||||
}
|
||||
|
||||
sealed class _ObservableCollectionMove(
|
||||
IObservableCollection<T> collection,
|
||||
Observer<CollectionMoveEvent<T>> observer,
|
||||
CancellationToken cancellationToken)
|
||||
: ObservableCollectionObserverBase<T, CollectionMoveEvent<T>>(collection, observer, cancellationToken)
|
||||
{
|
||||
protected override void Handler(in NotifyCollectionChangedEventArgs<T> eventArgs)
|
||||
{
|
||||
if (eventArgs.Action == NotifyCollectionChangedAction.Move)
|
||||
{
|
||||
observer.OnNext(new CollectionMoveEvent<T>(eventArgs.OldStartingIndex, eventArgs.NewStartingIndex, eventArgs.NewItem));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class ObservableCollectionReset<T>(IObservableCollection<T> collection, CancellationToken cancellationToken)
|
||||
: Observable<Unit>
|
||||
{
|
||||
protected override IDisposable SubscribeCore(Observer<Unit> observer)
|
||||
{
|
||||
return new _ObservableCollectionReset(collection, observer, cancellationToken);
|
||||
}
|
||||
|
||||
sealed class _ObservableCollectionReset(
|
||||
IObservableCollection<T> collection,
|
||||
Observer<Unit> observer,
|
||||
CancellationToken cancellationToken)
|
||||
: ObservableCollectionObserverBase<T, Unit>(collection, observer, cancellationToken)
|
||||
{
|
||||
protected override void Handler(in NotifyCollectionChangedEventArgs<T> eventArgs)
|
||||
{
|
||||
if (eventArgs.Action == NotifyCollectionChangedAction.Reset)
|
||||
{
|
||||
observer.OnNext(Unit.Default);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract class ObservableCollectionObserverBase<T, TEvent> : IDisposable
|
||||
{
|
||||
protected readonly IObservableCollection<T> collection;
|
||||
protected readonly Observer<TEvent> observer;
|
||||
readonly CancellationTokenRegistration cancellationTokenRegistration;
|
||||
readonly NotifyCollectionChangedEventHandler<T> handlerDelegate;
|
||||
|
||||
public ObservableCollectionObserverBase(IObservableCollection<T> collection, Observer<TEvent> observer, CancellationToken cancellationToken)
|
||||
{
|
||||
this.collection = collection;
|
||||
this.observer = observer;
|
||||
this.handlerDelegate = Handler;
|
||||
|
||||
collection.CollectionChanged += handlerDelegate;
|
||||
|
||||
if (cancellationToken.CanBeCanceled)
|
||||
{
|
||||
cancellationTokenRegistration = cancellationToken.UnsafeRegister(static state =>
|
||||
{
|
||||
var s = (ObservableCollectionObserverBase<T, TEvent>)state!;
|
||||
s.observer.OnCompleted();
|
||||
s.Dispose();
|
||||
}, this);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
collection.CollectionChanged -= handlerDelegate;
|
||||
cancellationTokenRegistration.Dispose();
|
||||
}
|
||||
|
||||
protected abstract void Handler(in NotifyCollectionChangedEventArgs<T> eventArgs);
|
||||
}
|
22
src/ObservableCollections.R3/ObservableCollections.R3.csproj
Normal file
22
src/ObservableCollections.R3/ObservableCollections.R3.csproj
Normal file
@ -0,0 +1,22 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;netstandard2.1;net6.0;net8.0</TargetFrameworks>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>12</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="PolySharp" Version="1.14.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="R3" Version="0.1.18" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ObservableCollections\ObservableCollections.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
13
src/ObservableCollections.R3/Shims.cs
Normal file
13
src/ObservableCollections.R3/Shims.cs
Normal file
@ -0,0 +1,13 @@
|
||||
#if NETSTANDARD2_0 || NETSTANDARD2_1
|
||||
|
||||
namespace System.Threading;
|
||||
|
||||
internal static class CancellationTokenExtensions
|
||||
{
|
||||
public static CancellationTokenRegistration UnsafeRegister(this CancellationToken cancellationToken, Action<object?> callback, object? state)
|
||||
{
|
||||
return cancellationToken.Register(callback, state, useSynchronizationContext: false);
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
@ -0,0 +1,135 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using R3;
|
||||
|
||||
namespace ObservableCollections.R3.Tests;
|
||||
|
||||
public class ObservableCollectionExtensionsTest
|
||||
{
|
||||
[Fact]
|
||||
public void ObserveAdd()
|
||||
{
|
||||
var events = new List<CollectionAddEvent<int>>();
|
||||
|
||||
var collection = new ObservableList<int>();
|
||||
|
||||
var subscription = collection.ObserveAdd().Subscribe(ev => events.Add(ev));
|
||||
collection.Add(10);
|
||||
collection.Add(50);
|
||||
collection.Add(30);
|
||||
|
||||
events.Count.Should().Be(3);
|
||||
events[0].Index.Should().Be(0);
|
||||
events[0].Value.Should().Be(10);
|
||||
events[1].Index.Should().Be(1);
|
||||
events[1].Value.Should().Be(50);
|
||||
events[2].Index.Should().Be(2);
|
||||
events[2].Value.Should().Be(30);
|
||||
|
||||
subscription.Dispose();
|
||||
|
||||
collection.Add(100);
|
||||
events.Count.Should().Be(3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ObserveAdd_CancellationToken()
|
||||
{
|
||||
var cts = new CancellationTokenSource();
|
||||
var events = new List<CollectionAddEvent<int>>();
|
||||
var result = default(Result?);
|
||||
|
||||
var collection = new ObservableList<int>();
|
||||
|
||||
var subscription = collection.ObserveAdd(cts.Token).Subscribe(ev => events.Add(ev), x => result = x);
|
||||
collection.Add(10);
|
||||
collection.Add(50);
|
||||
collection.Add(30);
|
||||
|
||||
events.Count.Should().Be(3);
|
||||
|
||||
cts.Cancel();
|
||||
|
||||
result.HasValue.Should().BeTrue();
|
||||
|
||||
subscription.Dispose();
|
||||
|
||||
collection.Add(100);
|
||||
events.Count.Should().Be(3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ObserveRemove()
|
||||
{
|
||||
var events = new List<CollectionRemoveEvent<int>>();
|
||||
var collection = new ObservableList<int>([111, 222, 333]);
|
||||
var cts = new CancellationTokenSource();
|
||||
var result = default(Result?);
|
||||
|
||||
var subscription = collection.ObserveRemove(cts.Token).Subscribe(ev => events.Add(ev), x => result = x);
|
||||
collection.RemoveAt(1);
|
||||
|
||||
events.Count.Should().Be(1);
|
||||
events[0].Index.Should().Be(1);
|
||||
events[0].Value.Should().Be(222);
|
||||
|
||||
cts.Cancel();
|
||||
result.HasValue.Should().BeTrue();
|
||||
|
||||
subscription.Dispose();
|
||||
|
||||
collection.RemoveAt(0);
|
||||
events.Count.Should().Be(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ObserveReplace()
|
||||
{
|
||||
var events = new List<CollectionReplaceEvent<int>>();
|
||||
var collection = new ObservableList<int>([111, 222, 333]);
|
||||
var cts = new CancellationTokenSource();
|
||||
var result = default(Result?);
|
||||
|
||||
var subscription = collection.ObserveReplace(cts.Token).Subscribe(ev => events.Add(ev), x => result = x);
|
||||
collection[1] = 999;
|
||||
|
||||
events.Count.Should().Be(1);
|
||||
events[0].Index.Should().Be(1);
|
||||
events[0].OldValue.Should().Be(222);
|
||||
events[0].NewValue.Should().Be(999);
|
||||
|
||||
cts.Cancel();
|
||||
result.HasValue.Should().BeTrue();
|
||||
|
||||
subscription.Dispose();
|
||||
|
||||
collection[1] = 444;
|
||||
events.Count.Should().Be(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ObserveMove()
|
||||
{
|
||||
var events = new List<CollectionMoveEvent<int>>();
|
||||
var collection = new ObservableList<int>([111, 222, 333]);
|
||||
var cts = new CancellationTokenSource();
|
||||
var result = default(Result?);
|
||||
|
||||
var subscription = collection.ObserveMove(cts.Token).Subscribe(ev => events.Add(ev), x => result = x);
|
||||
|
||||
collection.Move(1, 2);
|
||||
|
||||
events.Count.Should().Be(1);
|
||||
events[0].OldIndex.Should().Be(1);
|
||||
events[0].NewIndex.Should().Be(2);
|
||||
events[0].Value.Should().Be(222);
|
||||
|
||||
cts.Cancel();
|
||||
result.HasValue.Should().BeTrue();
|
||||
|
||||
subscription.Dispose();
|
||||
|
||||
collection.Move(1, 2);
|
||||
events.Count.Should().Be(1);
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" Version="6.12.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0"/>
|
||||
<PackageReference Include="R3" Version="0.1.18" />
|
||||
<PackageReference Include="xunit" Version="2.4.2"/>
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\ObservableCollections.R3\ObservableCollections.R3.csproj" />
|
||||
<ProjectReference Include="..\..\src\ObservableCollections\ObservableCollections.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
2
tests/ObservableCollections.R3.Tests/_GlobalUsings.cs
Normal file
2
tests/ObservableCollections.R3.Tests/_GlobalUsings.cs
Normal file
@ -0,0 +1,2 @@
|
||||
global using Xunit;
|
||||
global using FluentAssertions;
|
Loading…
x
Reference in New Issue
Block a user