diff --git a/.github/workflows/build-debug.yml b/.github/workflows/build-debug.yml index 6083d48..6b2195d 100644 --- a/.github/workflows/build-debug.yml +++ b/.github/workflows/build-debug.yml @@ -15,5 +15,5 @@ jobs: steps: - uses: actions/checkout@v3 - uses: Cysharp/Actions/.github/actions/setup-dotnet@main - - run: dotnet build ObservableCollection.WithoutSandbox.slnf -c Debug - - run: dotnet test ObservableCollection.WithoutSandbox.slnf -c Debug --no-build + - run: dotnet build -c Debug + - run: dotnet test -c Debug --no-build diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 39dc2ad..5257158 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -20,11 +20,11 @@ jobs: - uses: actions/checkout@v3 - uses: Cysharp/Actions/.github/actions/setup-dotnet@main # build and pack - - run: dotnet build ObservableCollection.WithoutSandbox.slnf -c Release -p:Version=${{ inputs.tag }} - - run: dotnet test ObservableCollection.WithoutSandbox.slnf -c Release --no-build - - run: dotnet pack ObservableCollection.WithoutSandbox.slnf -c Release --no-build -p:Version=${{ inputs.tag }} -o ./publish + - run: dotnet build -c Release -p:Version=${{ inputs.tag }} + - run: dotnet test -c Release --no-build + - run: dotnet pack -c Release --no-build -p:Version=${{ inputs.tag }} -o ./publish # Store artifacts. - - uses: actions/upload-artifact@v1 + - uses: actions/upload-artifact@v2 with: name: nuget path: ./publish/ diff --git a/src/ObservableCollections/Icon.png b/Icon.png similarity index 100% rename from src/ObservableCollections/Icon.png rename to Icon.png diff --git a/ObservableCollection.WithoutSandbox.slnf b/ObservableCollection.WithoutSandbox.slnf deleted file mode 100644 index a09d925..0000000 --- a/ObservableCollection.WithoutSandbox.slnf +++ /dev/null @@ -1,10 +0,0 @@ -{ - "solution": { - "path": "ObservableCollections.sln", - "projects": [ - "src\\ObservableCollections\\ObservableCollections.csproj", - "tests\\ObservableCollections.Tests\\ObservableCollections.Tests.csproj", - "tools\\PostBuildUtility\\PostBuildUtility.csproj" - ] - } -} \ No newline at end of file diff --git a/ObservableCollections.sln b/ObservableCollections.sln index d15f165..2ae2104 100644 --- a/ObservableCollections.sln +++ b/ObservableCollections.sln @@ -11,7 +11,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "sandbox", "sandbox", "{FD83 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WpfApp", "sandbox\WpfApp\WpfApp.csproj", "{4D937626-2CAE-4987-BFFA-BD53597F3338}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleApp", "sandbox\ConsoleApp\ConsoleApp.csproj", "{3F3A73AC-DA6E-4987-8AA9-9B1E226D3DD5}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleApp", "sandbox\ConsoleApp\ConsoleApp.csproj", "{3F3A73AC-DA6E-4987-8AA9-9B1E226D3DD5}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorApp", "sandbox\BlazorApp\BlazorApp.csproj", "{7E10EF01-24DC-4346-8A18-F791BB5252A7}" EndProject @@ -19,13 +19,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{B6D0425C EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ObservableCollections.Tests", "tests\ObservableCollections.Tests\ObservableCollections.Tests.csproj", "{B84027E4-9B39-4FB6-B888-C55CE4C79152}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tools", "tools", "{7133A3F7-B398-4DE0-8295-0F1ECFCC4CE4}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ObservableCollections.R3", "src\ObservableCollections.R3\ObservableCollections.R3.csproj", "{D5950521-C5B3-4B92-834E-3B12CDDD8DD6}" 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}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ObservableCollections.R3.Tests", "tests\ObservableCollections.R3.Tests\ObservableCollections.R3.Tests.csproj", "{1205F414-EE6D-49C6-9500-3E62E2120EAF}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -53,10 +49,6 @@ Global {B84027E4-9B39-4FB6-B888-C55CE4C79152}.Debug|Any CPU.Build.0 = Debug|Any CPU {B84027E4-9B39-4FB6-B888-C55CE4C79152}.Release|Any CPU.ActiveCfg = Release|Any CPU {B84027E4-9B39-4FB6-B888-C55CE4C79152}.Release|Any CPU.Build.0 = Release|Any CPU - {29E3967D-89E9-494F-B1E6-9706B8F1CD57}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {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 @@ -75,7 +67,6 @@ Global {3F3A73AC-DA6E-4987-8AA9-9B1E226D3DD5} = {FD836539-75F1-4707-BCFF-751B95DAE19C} {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 diff --git a/README.md b/README.md index 66ae336..73afd48 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # ObservableCollections [![GitHub Actions](https://github.com/Cysharp/ObservableCollections/workflows/Build-Debug/badge.svg)](https://github.com/Cysharp/ObservableCollections/actions) [![Releases](https://img.shields.io/github/release/Cysharp/ObservableCollections.svg)](https://github.com/Cysharp/ObservableCollections/releases) -ObservableCollections is a high performance observable collections(`ObservableList`, `ObservableDictionary`, `ObservableHashSet`, `ObservableQueue`, `ObservableStack`, `ObservableRingBuffer`, `ObservableFixedSizeRingBuffer`) with synchronized views. +ObservableCollections is a high performance observable collections(`ObservableList`, `ObservableDictionary`, `ObservableHashSet`, `ObservableQueue`, `ObservableStack`, `ObservableRingBuffer`, `ObservableFixedSizeRingBuffer`) with synchronized views and Observe Extension for [R3](https://github.com/Cysharp/R3). .NET has [`ObservableCollection`](https://docs.microsoft.com/en-us/dotnet/api/system.collections.objectmodel.observablecollection-1), however it has many lacks of features. @@ -46,6 +46,16 @@ SynchronizedView helps to separate between Model and View (ViewModel). We will u ObservableCollections has not just a simple list, there are many more data structures. `ObservableList`, `ObservableDictionary`, `ObservableHashSet`, `ObservableQueue`, `ObservableStack`, `ObservableRingBuffer`, `ObservableFixedSizeRingBuffer`. `RingBuffer`, especially `FixedSizeRingBuffer`, can be achieved with efficient performance when there is rotation (e.g., displaying up to 1000 logs, where old ones are deleted when new ones are added). Of course, the AddRange allows for efficient batch processing of large numbers of additions. +If you want to handle each change event with Rx, you can monitor it with the following method by combining it with [R3](https://github.com/Cysharp/R3): + +```csharp +Observable> IObservableCollection.ObserveAdd() +Observable> IObservableCollection.ObserveRemove() +Observable> IObservableCollection.ObserveReplace() +Observable> IObservableCollection.ObserveMove() +Observable> IObservableCollection.ObserveReset() +``` + Getting Started --- For .NET, use NuGet. For Unity, please read [Unity](#unity) section. @@ -112,6 +122,30 @@ view.Dispose(); The basic idea behind using ObservableCollections is to create a View. In order to automate this pipeline, the view can be sortable, filtered, and have side effects on the values when they are changed. +Reactive Extensions with R3 +--- +Once the R3 extension package is installed, you can subscribe to `ObserveAdd`, `ObserveRemove`, `ObserveReplace`, `ObserveMove`, and `ObserveReset` events as Rx, allowing you to compose events individually. + +PM> Install-Package [ObservableCollections.R3](https://www.nuget.org/packages/ObservableCollections.R3) + +```csharp +using R3; +using ObservableCollections; + +var list = new ObservableList(); +list.ObserveAdd() + .Subscribe(x => + { + Console.WriteLine(x); + }); + +list.Add(10); +list.Add(20); +list.AddRange(new[] { 10, 20, 30 }); +``` + +Since it is not supported by dotnet/reactive, please use the Rx library [R3](https://github.com/Cysharp/R3). + Blazor --- Since Blazor re-renders the whole thing by StateHasChanged, you may think that Observable collections are unnecessary. However, when you split it into Components, it is beneficial for Component confidence to detect the change and change its own State. @@ -174,7 +208,7 @@ public partial class DataTable : ComponentBase, IDisposable WPF --- -Because of data binding in WPF, it is important that the collection is Observable. ObservableCollections high-performance `IObservableCollection` cannot be bind to WPF. Call `WithINotifyCollectionChanged` to convert it to `INotifyCollectionChanged`. Also, although ObservableCollections and Views are thread-safe, the WPF UI does not support change notifications from different threads. `BindingOperations.EnableCollectionSynchronization` to work safely with change notifications from different threads. +Because of data binding in WPF, it is important that the collection is Observable. ObservableCollections high-performance `IObservableCollection` cannot be bind to WPF. Call `ToNotifyCollectionChanged()` to convert it to `INotifyCollectionChanged`. Also, although ObservableCollections and Views are thread-safe, the WPF UI does not support change notifications from different threads. `BindingOperations.EnableCollectionSynchronization` to work safely with change notifications from different threads. ```csharp // WPF simple sample. @@ -188,7 +222,7 @@ public MainWindow() this.DataContext = this; list = new ObservableList(); - ItemsView = list.CreateView(x => x).WithINotifyCollectionChanged(); + ItemsView = list.CreateView(x => x).ToNotifyCollectionChanged(); BindingOperations.EnableCollectionSynchronization(ItemsView, new object()); // for ui synchronization safety of viewmodel } @@ -203,9 +237,7 @@ protected override void OnClosed(EventArgs e) Unity --- - -In Unity projects, you can installing `ObservableCollections` with [NugetForUnity](https://github.com/GlitchEnzo/NuGetForUnity). - +In Unity projects, you can installing `ObservableCollections` with [NugetForUnity](https://github.com/GlitchEnzo/NuGetForUnity). If R3 integration is required, similarly install `ObservableCollections.R3` via NuGetForUnity. In Unity, ObservableCollections and Views are useful as CollectionManagers, since they need to convert T to Prefab for display. diff --git a/sandbox/ConsoleApp/ConsoleApp.csproj b/sandbox/ConsoleApp/ConsoleApp.csproj index 9a761c6..1db2200 100644 --- a/sandbox/ConsoleApp/ConsoleApp.csproj +++ b/sandbox/ConsoleApp/ConsoleApp.csproj @@ -9,7 +9,9 @@ + + diff --git a/sandbox/ConsoleApp/Program.cs b/sandbox/ConsoleApp/Program.cs index 7ac2912..f2b8a02 100644 --- a/sandbox/ConsoleApp/Program.cs +++ b/sandbox/ConsoleApp/Program.cs @@ -1,7 +1,31 @@ using System; +using R3; using System.Linq; using ObservableCollections; - + + + + +var list = new ObservableList(); +list.ObserveAdd() + .Subscribe(x => + { + Console.WriteLine(x); + }); + +list.Add(10); +list.Add(20); +list.AddRange(new[] { 10, 20, 30 }); + + +return; + + + + + + + var models = new ObservableList(Enumerable.Range(0, 10)); var viewModels = models.CreateView(x => new ViewModel diff --git a/sandbox/WpfApp/WpfApp.csproj b/sandbox/WpfApp/WpfApp.csproj index 64c80a6..5f5208f 100644 --- a/sandbox/WpfApp/WpfApp.csproj +++ b/sandbox/WpfApp/WpfApp.csproj @@ -1,15 +1,16 @@  - - WinExe - net6.0-windows - enable - true - false - + + WinExe + net6.0-windows + enable + true + false + true + - - - + + + diff --git a/src/ObservableCollections.R3/ObservableCollections.R3.csproj b/src/ObservableCollections.R3/ObservableCollections.R3.csproj index fa7e49b..8378e41 100644 --- a/src/ObservableCollections.R3/ObservableCollections.R3.csproj +++ b/src/ObservableCollections.R3/ObservableCollections.R3.csproj @@ -1,22 +1,32 @@  - - netstandard2.0;netstandard2.1;net6.0;net8.0 - disable - enable - 12 - + + netstandard2.0;netstandard2.1;net6.0;net8.0 + disable + enable + 12 - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + collection + R3 Extensions of ObservableCollections. + true + true + - - - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + diff --git a/src/ObservableCollections/ObservableCollections.csproj b/src/ObservableCollections/ObservableCollections.csproj index b6404b1..9e531f8 100644 --- a/src/ObservableCollections/ObservableCollections.csproj +++ b/src/ObservableCollections/ObservableCollections.csproj @@ -18,6 +18,7 @@ - + + diff --git a/tests/ObservableCollections.R3.Tests/ObservableCollections.R3.Tests.csproj b/tests/ObservableCollections.R3.Tests/ObservableCollections.R3.Tests.csproj index 5412267..710ec0f 100644 --- a/tests/ObservableCollections.R3.Tests/ObservableCollections.R3.Tests.csproj +++ b/tests/ObservableCollections.R3.Tests/ObservableCollections.R3.Tests.csproj @@ -10,7 +10,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/tools/PostBuildUtility/PostBuildUtility.csproj b/tools/PostBuildUtility/PostBuildUtility.csproj deleted file mode 100644 index d96d8d5..0000000 --- a/tools/PostBuildUtility/PostBuildUtility.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - - Exe - net6.0 - enable - false - - - - - - diff --git a/tools/PostBuildUtility/Program.cs b/tools/PostBuildUtility/Program.cs deleted file mode 100644 index 7a35395..0000000 --- a/tools/PostBuildUtility/Program.cs +++ /dev/null @@ -1,64 +0,0 @@ -using ConsoleAppFramework; -using Microsoft.Extensions.Hosting; -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -namespace PostBuildUtility -{ - class Program : ConsoleAppBase - { - static async Task Main(string[] args) - { - await Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); - } - - [Command("replace-to-unity")] - public void ReplaceToUnity([Option(0)] string directory) - { - var replaceSet = new Dictionary - { - // Remove nullable - {"#nullable disable", "" }, - {"where T : notnull", "" }, - {"where TKey : notnull, IComparable", "where TKey : IComparable" }, // override project specified - {"where TKey : notnull", "" }, - {">?", ">" }, // generics ? - {"T?", "T" }, - {"T[]?", "T[]" }, - {"default!", "default" }, - {"null!", "null" }, - // project specified - {"array!", "array" }, - {"Current!", "Current" }, - {"NotifyCollectionChangedEventHandler?", "NotifyCollectionChangedEventHandler" }, - {"PropertyChangedEventHandler?", "PropertyChangedEventHandler" }, - }; - - System.Console.WriteLine("Start to replace code, remove nullability."); - var noBomUtf8 = new UTF8Encoding(false); - - foreach (var path in Directory.EnumerateFiles(directory, "*.cs", SearchOption.AllDirectories)) - { - var text = File.ReadAllText(path, Encoding.UTF8); - var original = text; - - foreach (var item in replaceSet) - { - text = text.Replace(item.Key, item.Value); - } - - if (text != original) - { - Console.WriteLine("Replace Output:" + path); - File.WriteAllText(path, text, noBomUtf8); - } - } - - System.Console.WriteLine("Replace complete."); - } - } -} \ No newline at end of file