Merge pull request #21 from Cysharp/hadashiA/r3

Add ObservableCollections.R3
This commit is contained in:
Yoshifumi Kawai 2024-02-15 10:41:40 +09:00 committed by GitHub
commit ae98b69690
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 515 additions and 0 deletions

View File

@ -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}

View File

@ -0,0 +1,264 @@
using System;
using System.Collections.Specialized;
using System.Threading;
using R3;
namespace ObservableCollections;
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);
}
public static Observable<int> ObserveCountChanged<T>(this IObservableCollection<T> source, bool notifyCurrentCount = false, CancellationToken cancellationToken = default)
{
return new ObservableCollectionCountChanged<T>(source, notifyCurrentCount, 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);
}
}
}
}
sealed class ObservableCollectionCountChanged<T>(IObservableCollection<T> collection, bool notifyCurrentCount, CancellationToken cancellationToken)
: Observable<int>
{
protected override IDisposable SubscribeCore(Observer<int> observer)
{
return new _ObservableCollectionCountChanged(collection, notifyCurrentCount, observer, cancellationToken);
}
sealed class _ObservableCollectionCountChanged : ObservableCollectionObserverBase<T, int>
{
readonly IObservableCollection<T> collection;
int countPrev;
public _ObservableCollectionCountChanged(
IObservableCollection<T> collection,
bool notifyCurrentCount,
Observer<int> observer,
CancellationToken cancellationToken) : base(collection, observer, cancellationToken)
{
this.collection = collection;
this.countPrev = collection.Count;
if (notifyCurrentCount)
{
observer.OnNext(collection.Count);
}
}
protected override void Handler(in NotifyCollectionChangedEventArgs<T> eventArgs)
{
switch (eventArgs.Action)
{
case NotifyCollectionChangedAction.Add:
case NotifyCollectionChangedAction.Remove:
case NotifyCollectionChangedAction.Reset when countPrev != collection.Count:
observer.OnNext(collection.Count);
break;
}
countPrev = collection.Count;
}
}
}
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);
}

View 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>

View 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

View File

@ -0,0 +1,174 @@
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);
}
[Fact]
public void ObserveCountChanged()
{
var events = new List<int>();
var collection = new ObservableList<int>([111, 222, 333]);
using var _ = collection.ObserveCountChanged().Subscribe(count => events.Add(count));
events.Should().BeEmpty();
collection.Add(444);
events[0].Should().Be(4);
collection.Remove(111);
events[1].Should().Be(3);
collection.Move(0, 1);
events.Count.Should().Be(2);
collection[0] = 999;
events.Count.Should().Be(2);
collection.Clear();
events[2].Should().Be(0);
collection.Clear();
events.Count.Should().Be(3);
}
[Fact]
public void ObserveCountChanged_NotifyCurrent()
{
var events = new List<int>();
var collection = new ObservableList<int>([111, 222, 333]);
var subscription = collection.ObserveCountChanged(notifyCurrentCount: true).Subscribe(count => events.Add(count));
events[0].Should().Be(3);
}
}

View File

@ -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>

View File

@ -0,0 +1,2 @@
global using Xunit;
global using FluentAssertions;