Compare commits

..

1 Commits

Author SHA1 Message Date
hadashiA
3f103a6070 Add unity install instruction to README 2024-02-01 16:12:22 +09:00
182 changed files with 11743 additions and 6445 deletions

View File

@ -1,41 +0,0 @@
# top-most EditorConfig file
root = true
[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
# Visual Studio Spell checker configs (https://learn.microsoft.com/en-us/visualstudio/ide/text-spell-checker?view=vs-2022#how-to-customize-the-spell-checker)
spelling_exclusion_path = ./exclusion.dic
[*.cs]
indent_size = 4
charset = utf-8-bom
end_of_line = unset
# Solution files
[*.{sln,slnx}]
end_of_line = unset
# MSBuild project files
[*.{csproj,props,targets}]
end_of_line = unset
# Xml config files
[*.{ruleset,config,nuspec,resx,runsettings,DotSettings}]
end_of_line = unset
[*{_AssemblyInfo.cs,.notsupported.cs}]
generated_code = true
# C# code style settings
[*.{cs}]
dotnet_diagnostic.IDE0044.severity = none # IDE0044: Make field readonly
# https://stackoverflow.com/questions/79195382/how-to-disable-fading-unused-methods-in-visual-studio-2022-17-12-0
dotnet_diagnostic.IDE0051.severity = none # IDE0051: Remove unused private member
dotnet_diagnostic.IDE0130.severity = none # IDE0130: Namespace does not match folder structure

View File

@ -1,12 +0,0 @@
# ref: https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly" # Check for updates to GitHub Actions every week
ignore:
# I just want update action when major/minor version is updated. patch updates are too noisy.
- dependency-name: '*'
update-types:
- version-update:semver-patch

View File

@ -1,21 +0,0 @@
name: Build-Debug
on:
push:
branches:
- "master"
pull_request:
branches:
- master
jobs:
build-dotnet:
permissions:
contents: read
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- uses: Cysharp/Actions/.github/actions/checkout@main
- uses: Cysharp/Actions/.github/actions/setup-dotnet@main
- run: dotnet build -c Debug
- run: dotnet test -c Debug --no-build

52
.github/workflows/build-debug.yml vendored Normal file
View File

@ -0,0 +1,52 @@
name: Build-Debug
on:
push:
branches:
- "master"
pull_request:
branches:
- master
jobs:
build-dotnet:
runs-on: ubuntu-latest
timeout-minutes: 10
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
build-unity:
if: "((github.event_name == 'push' && github.repository_owner == 'Cysharp') || startsWith(github.event.pull_request.head.label, 'Cysharp:'))"
strategy:
matrix:
unity: ["2019.4.25f1"]
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v3
# Execute scripts: Export Package
# /opt/Unity/Editor/Unity -quit -batchmode -nographics -silent-crashes -logFile -projectPath . -executeMethod PackageExporter.Export
- name: Build Unity (.unitypacakge)
uses: Cysharp/Actions/.github/actions/unity-builder@main
env:
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }}
with:
projectPath: src/ObservableCollections.Unity
unityVersion: ${{ matrix.unity }}
targetPlatform: StandaloneLinux64
buildMethod: PackageExporter.Export
- uses: Cysharp/Actions/.github/actions/check-metas@main # check meta files
with:
directory: src/ObservableCollections.Unity
# Store artifacts.
- uses: actions/upload-artifact@v2
with:
name: ObservableCollections.unitypackage-${{ matrix.unity }}.zip
path: ./src/ObservableCollections.Unity/*.unitypackage

View File

@ -1,47 +0,0 @@
name: build-release
on:
workflow_dispatch:
inputs:
tag:
description: "tag: git tag you want create. (sample 1.0.0)"
required: true
dry-run:
description: "dry-run: true will never create relase/nuget."
required: true
default: false
type: boolean
jobs:
build-dotnet:
permissions:
contents: read
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- uses: Cysharp/Actions/.github/actions/checkout@main
- uses: Cysharp/Actions/.github/actions/setup-dotnet@main
# build and pack
- 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: Cysharp/Actions/.github/actions/upload-artifact@main
with:
name: nuget
path: ./publish/
retention-days: 1
# release
create-release:
needs: [build-dotnet]
permissions:
contents: write
uses: Cysharp/Actions/.github/workflows/create-release.yaml@main
with:
commit-id: ${{ github.sha }}
dry-run: ${{ inputs.dry-run }}
tag: ${{ inputs.tag }}
nuget-push: true
release-upload: false
secrets: inherit

132
.github/workflows/build-release.yml vendored Normal file
View File

@ -0,0 +1,132 @@
name: build-release
on:
workflow_dispatch:
inputs:
tag:
description: "tag: git tag you want create. (sample 1.0.0)"
required: true
dry-run:
description: "dry-run: true will never create relase/nuget."
required: true
default: false
type: boolean
env:
GIT_TAG: ${{ github.event.inputs.tag }}
DRY_RUN: ${{ github.event.inputs.dry-run }}
jobs:
update-packagejson:
uses: Cysharp/Actions/.github/workflows/update-packagejson.yaml@main
with:
file-path: ./src/ObservableCollections.Unity/Assets/Plugins/ObservableCollections/package.json
tag: ${{ github.event.inputs.tag }}
dry-run: ${{ fromJson(github.event.inputs.dry-run) }}
build-dotnet:
needs: [update-packagejson]
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- run: echo ${{ needs.update-packagejson.outputs.sha }}
- uses: actions/checkout@v3
with:
ref: ${{ needs.update-packagejson.outputs.sha }}
- uses: Cysharp/Actions/.github/actions/setup-dotnet@main
# build and pack
- run: dotnet build ObservableCollection.WithoutSandbox.slnf -c Release -p:Version=${{ env.GIT_TAG }}
- run: dotnet test ObservableCollection.WithoutSandbox.slnf -c Release --no-build
- run: dotnet pack ObservableCollection.WithoutSandbox.slnf -c Release --no-build -p:Version=${{ env.GIT_TAG }} -o ./publish
# Store artifacts.
- uses: actions/upload-artifact@v1
with:
name: nuget
path: ./publish/
build-unity:
needs: [update-packagejson]
strategy:
matrix:
unity: ["2019.4.25f1"]
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- run: echo ${{ needs.update-packagejson.outputs.sha }}
- uses: actions/checkout@v3
with:
ref: ${{ needs.update-packagejson.outputs.sha }}
# Execute scripts: Export Package
# /opt/Unity/Editor/Unity -quit -batchmode -nographics -silent-crashes -logFile -projectPath . -executeMethod PackageExporter.Export
- name: Build Unity (.unitypacakge)
uses: Cysharp/Actions/.github/actions/unity-builder@main
env:
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }}
with:
projectPath: src/ObservableCollections.Unity
unityVersion: ${{ matrix.unity }}
targetPlatform: StandaloneLinux64
buildMethod: PackageExporter.Export
- uses: Cysharp/Actions/.github/actions/check-metas@main # check meta files
with:
directory: src/ObservableCollections.Unity
# Store artifacts.
- uses: actions/upload-artifact@v2
with:
name: ObservableCollections.${{ env.GIT_TAG }}.unitypackage
path: ./src/ObservableCollections.Unity/ObservableCollections.${{ env.GIT_TAG }}.unitypackage
create-release:
if: github.event.inputs.dry-run == 'false'
needs: [update-packagejson, build-dotnet, build-unity]
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: Cysharp/Actions/.github/actions/setup-dotnet@main
# Create Releases
- uses: actions/create-release@v1
id: create_release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ env.GIT_TAG }}
release_name: Ver.${{ env.GIT_TAG }}
commitish: ${{ needs.update-packagejson.outputs.sha }}
draft: true
prerelease: false
# Download(All) Artifacts to current directory
- uses: actions/download-artifact@v2
# Upload to NuGet
- run: dotnet nuget push "./nuget/*.nupkg" --skip-duplicate -s https://www.nuget.org/api/v2/package -k ${{ secrets.NUGET_KEY }}
# Upload to Releases(unitypackage)
- uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./ObservableCollections.${{ env.GIT_TAG }}.unitypackage/ObservableCollections.${{ env.GIT_TAG }}.unitypackage
asset_name: ObservableCollections.${{ env.GIT_TAG }}.unitypackage
asset_content_type: application/octet-stream
check-artifacts:
if: github.event.inputs.dry-run == 'true'
needs: [update-packagejson, build-dotnet, build-unity]
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: Cysharp/Actions/.github/actions/setup-dotnet@main
# Download(All) Artifacts to current directory
- uses: actions/download-artifact@v2
- name: check directory
run: ls -l
cleanup:
if: needs.update-packagejson.outputs.is-branch-created == 'true'
needs: [update-packagejson, build-unity]
uses: Cysharp/Actions/.github/workflows/clean-packagejson-branch.yaml@main
with:
branch: ${{ needs.update-packagejson.outputs.branch-name }}

View File

@ -7,6 +7,4 @@ on:
jobs: jobs:
detect: detect:
permissions:
contents: read
uses: Cysharp/Actions/.github/workflows/prevent-github-change.yaml@main uses: Cysharp/Actions/.github/workflows/prevent-github-change.yaml@main

View File

@ -7,8 +7,4 @@ on:
jobs: jobs:
stale: stale:
permissions:
contents: read
pull-requests: write
issues: write
uses: Cysharp/Actions/.github/workflows/stale-issue.yaml@main uses: Cysharp/Actions/.github/workflows/stale-issue.yaml@main

View File

@ -0,0 +1,10 @@
{
"solution": {
"path": "ObservableCollections.sln",
"projects": [
"src\\ObservableCollections\\ObservableCollections.csproj",
"tests\\ObservableCollections.Tests\\ObservableCollections.Tests.csproj",
"tools\\PostBuildUtility\\PostBuildUtility.csproj"
]
}
}

View File

@ -11,7 +11,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "sandbox", "sandbox", "{FD83
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WpfApp", "sandbox\WpfApp\WpfApp.csproj", "{4D937626-2CAE-4987-BFFA-BD53597F3338}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WpfApp", "sandbox\WpfApp\WpfApp.csproj", "{4D937626-2CAE-4987-BFFA-BD53597F3338}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleApp", "sandbox\ConsoleApp\ConsoleApp.csproj", "{3F3A73AC-DA6E-4987-8AA9-9B1E226D3DD5}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleApp", "sandbox\ConsoleApp\ConsoleApp.csproj", "{3F3A73AC-DA6E-4987-8AA9-9B1E226D3DD5}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorApp", "sandbox\BlazorApp\BlazorApp.csproj", "{7E10EF01-24DC-4346-8A18-F791BB5252A7}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorApp", "sandbox\BlazorApp\BlazorApp.csproj", "{7E10EF01-24DC-4346-8A18-F791BB5252A7}"
EndProject EndProject
@ -19,11 +19,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{B6D0425C
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ObservableCollections.Tests", "tests\ObservableCollections.Tests\ObservableCollections.Tests.csproj", "{B84027E4-9B39-4FB6-B888-C55CE4C79152}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ObservableCollections.Tests", "tests\ObservableCollections.Tests\ObservableCollections.Tests.csproj", "{B84027E4-9B39-4FB6-B888-C55CE4C79152}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ObservableCollections.R3", "src\ObservableCollections.R3\ObservableCollections.R3.csproj", "{D5950521-C5B3-4B92-834E-3B12CDDD8DD6}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tools", "tools", "{7133A3F7-B398-4DE0-8295-0F1ECFCC4CE4}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ObservableCollections.R3.Tests", "tests\ObservableCollections.R3.Tests\ObservableCollections.R3.Tests.csproj", "{1205F414-EE6D-49C6-9500-3E62E2120EAF}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PostBuildUtility", "tools\PostBuildUtility\PostBuildUtility.csproj", "{29E3967D-89E9-494F-B1E6-9706B8F1CD57}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AvaloniaApp", "sandbox\AvaloniaApp\AvaloniaApp.csproj", "{48D7CA5F-9956-4CF0-908E-1DECA7FE4257}"
EndProject EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -51,18 +49,10 @@ Global
{B84027E4-9B39-4FB6-B888-C55CE4C79152}.Debug|Any CPU.Build.0 = Debug|Any CPU {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.ActiveCfg = Release|Any CPU
{B84027E4-9B39-4FB6-B888-C55CE4C79152}.Release|Any CPU.Build.0 = Release|Any CPU {B84027E4-9B39-4FB6-B888-C55CE4C79152}.Release|Any CPU.Build.0 = Release|Any CPU
{D5950521-C5B3-4B92-834E-3B12CDDD8DD6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {29E3967D-89E9-494F-B1E6-9706B8F1CD57}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D5950521-C5B3-4B92-834E-3B12CDDD8DD6}.Debug|Any CPU.Build.0 = Debug|Any CPU {29E3967D-89E9-494F-B1E6-9706B8F1CD57}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D5950521-C5B3-4B92-834E-3B12CDDD8DD6}.Release|Any CPU.ActiveCfg = Release|Any CPU {29E3967D-89E9-494F-B1E6-9706B8F1CD57}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D5950521-C5B3-4B92-834E-3B12CDDD8DD6}.Release|Any CPU.Build.0 = Release|Any CPU {29E3967D-89E9-494F-B1E6-9706B8F1CD57}.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
{48D7CA5F-9956-4CF0-908E-1DECA7FE4257}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{48D7CA5F-9956-4CF0-908E-1DECA7FE4257}.Debug|Any CPU.Build.0 = Debug|Any CPU
{48D7CA5F-9956-4CF0-908E-1DECA7FE4257}.Release|Any CPU.ActiveCfg = Release|Any CPU
{48D7CA5F-9956-4CF0-908E-1DECA7FE4257}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@ -73,9 +63,7 @@ Global
{3F3A73AC-DA6E-4987-8AA9-9B1E226D3DD5} = {FD836539-75F1-4707-BCFF-751B95DAE19C} {3F3A73AC-DA6E-4987-8AA9-9B1E226D3DD5} = {FD836539-75F1-4707-BCFF-751B95DAE19C}
{7E10EF01-24DC-4346-8A18-F791BB5252A7} = {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} {B84027E4-9B39-4FB6-B888-C55CE4C79152} = {B6D0425C-7902-4EFB-B0EA-99F164C20835}
{D5950521-C5B3-4B92-834E-3B12CDDD8DD6} = {8F60DC54-F617-4841-8C79-6B0137500D1C} {29E3967D-89E9-494F-B1E6-9706B8F1CD57} = {7133A3F7-B398-4DE0-8295-0F1ECFCC4CE4}
{1205F414-EE6D-49C6-9500-3E62E2120EAF} = {B6D0425C-7902-4EFB-B0EA-99F164C20835}
{48D7CA5F-9956-4CF0-908E-1DECA7FE4257} = {FD836539-75F1-4707-BCFF-751B95DAE19C}
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {4156A725-69F4-469F-9BBB-0EE9921CA83E} SolutionGuid = {4156A725-69F4-469F-9BBB-0EE9921CA83E}

655
README.md
View File

@ -1,11 +1,13 @@
# ObservableCollections # 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) [![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<T>`, `ObservableDictionary<TKey, TValue>`, `ObservableHashSet<T>`, `ObservableQueue<T>`, `ObservableStack<T>`, `ObservableRingBuffer<T>`, `ObservableFixedSizeRingBuffer<T>`) with synchronized views and Observe Extension for [R3](https://github.com/Cysharp/R3). ObservableCollections is a high performance observable collections(`ObservableList<T>`, `ObservableDictionary<TKey, TValue>`, `ObservableHashSet<T>`, `ObservableQueue<T>`, `ObservableStack<T>`, `ObservableRingBuffer<T>`, `ObservableFixedSizeRingBuffer<T>`) with synchronized views.
.NET has [`ObservableCollection<T>`](https://docs.microsoft.com/en-us/dotnet/api/system.collections.objectmodel.observablecollection-1), however it has many lacks of features. It based `INotifyCollectionChanged`, `NotifyCollectionChangedEventHandler` and `NotifyCollectionChangedEventArgs`. There are no generics so everything boxed, allocate memory every time. Also `NotifyCollectionChangedEventArgs` holds all values to `IList` even if it is single value, this also causes allocations. `ObservableCollection<T>` has no Range feature so a lot of wastage occurs when adding multiple values, because it is a single value notification. Also, it is not thread-safe is hard to do linkage with the notifier. .NET has [`ObservableCollection<T>`](https://docs.microsoft.com/en-us/dotnet/api/system.collections.objectmodel.observablecollection-1), however it has many lacks of features.
ObservableCollections introduces there generics version, `NotifyCollectionChangedEventHandler<T>` and `NotifyCollectionChangedEventArgs<T>`, it using latest C# features(`in`, `readonly ref struct`, `ReadOnlySpan<T>`). Also, Sort and Reverse will now be notified. It based `INotifyCollectionChanged`, `NotifyCollectionChangedEventHandler` and `NotifyCollectionChangedEventArgs`. There are no generics so everything boxed, allocate memory every time. Also `NotifyCollectionChangedEventArgs` holds all values to `IList` even if it is single value, this also causes allocations. `ObservableCollection<T>` has no Range feature so a lot of wastage occurs when adding multiple values, because it is a single value notification. Also, it is not thread-safe is hard to do linkage with the notifier.
ObservableCollections introduces generics version of `NotifyCollectionChangedEventHandler` and `NotifyCollectionChangedEventArgs`, it using latest C# features(`in`, `readonly ref struct`, `ReadOnlySpan<T>`).
```csharp ```csharp
public delegate void NotifyCollectionChangedEventHandler<T>(in NotifyCollectionChangedEventArgs<T> e); public delegate void NotifyCollectionChangedEventHandler<T>(in NotifyCollectionChangedEventArgs<T> e);
@ -20,7 +22,6 @@ public readonly ref struct NotifyCollectionChangedEventArgs<T>
public readonly ReadOnlySpan<T> OldItems; public readonly ReadOnlySpan<T> OldItems;
public readonly int NewStartingIndex; public readonly int NewStartingIndex;
public readonly int OldStartingIndex; public readonly int OldStartingIndex;
public readonly SortOperation<T> SortOperation;
} }
``` ```
@ -29,44 +30,27 @@ Also, use the interface `IObservableCollection<T>` instead of `INotifyCollection
```csharp ```csharp
public interface IObservableCollection<T> : IReadOnlyCollection<T> public interface IObservableCollection<T> : IReadOnlyCollection<T>
{ {
event NotifyCollectionChangedEventHandler<T>? CollectionChanged; event NotifyCollectionChangedEventHandler<T> CollectionChanged;
object SyncRoot { get; } object SyncRoot { get; }
ISynchronizedView<T, TView> CreateView<TView>(Func<T, TView> transform); ISynchronizedView<T, TView> CreateView<TView>(Func<T, TView> transform, bool reverse = false);
} }
// also exists SortedView
public static ISynchronizedView<T, TView> CreateSortedView<T, TKey, TView>(this IObservableCollection<T> source, Func<T, TKey> identitySelector, Func<T, TView> transform, IComparer<T> comparer);
public static ISynchronizedView<T, TView> CreateSortedView<T, TKey, TView>(this IObservableCollection<T> source, Func<T, TKey> identitySelector, Func<T, TView> transform, IComparer<TView> viewComparer);
``` ```
SynchronizedView helps to separate between Model and View (ViewModel). We will use ObservableCollections as the Model and generate SynchronizedView as the View (ViewModel). This architecture can be applied not only to WPF, but also to Blazor, Unity, etc. SynchronizedView helps to separate between Model and View (ViewModel). We will use ObservableCollections as the Model and generate SynchronizedView as the View (ViewModel). This architecture can be applied not only to WPF, but also to Blazor, Unity, etc.
![image](https://user-images.githubusercontent.com/46207/131979264-2463403b-0fba-474b-8f49-277c2abe1b05.png) ![image](https://user-images.githubusercontent.com/46207/131979264-2463403b-0fba-474b-8f49-277c2abe1b05.png)
The View retains the transformed values. The transform function is called only once during Add, so costly objects that are linked can also be instantiated. Additionally, it has a feature to dynamically show or hide values using filters.
Observable Collections themselves do not implement `INotifyCollectionChanged`, so they cannot be bound on XAML platforms and the like. However, they can be converted to collections that implement `INotifyCollectionChanged` using `ToNotifyCollectionChanged()`, making them suitable for binding.
![image](https://github.com/user-attachments/assets/b5590bb8-16d6-4f9c-be07-1288a6801e68)
ObservableCollections has not just a simple list, there are many more data structures. `ObservableList<T>`, `ObservableDictionary<TKey, TValue>`, `ObservableHashSet<T>`, `ObservableQueue<T>`, `ObservableStack<T>`, `ObservableRingBuffer<T>`, `ObservableFixedSizeRingBuffer<T>`. `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. ObservableCollections has not just a simple list, there are many more data structures. `ObservableList<T>`, `ObservableDictionary<TKey, TValue>`, `ObservableHashSet<T>`, `ObservableQueue<T>`, `ObservableStack<T>`, `ObservableRingBuffer<T>`, `ObservableFixedSizeRingBuffer<T>`. `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<CollectionChangedEvent<T>> IObservableCollection<T>.ObserveChanged()
Observable<CollectionAddEvent<T>> IObservableCollection<T>.ObserveAdd()
Observable<CollectionRemoveEvent<T>> IObservableCollection<T>.ObserveRemove()
Observable<CollectionReplaceEvent<T>> IObservableCollection<T>.ObserveReplace()
Observable<CollectionMoveEvent<T>> IObservableCollection<T>.ObserveMove()
Observable<CollectionResetEvent<T>> IObservableCollection<T>.ObserveReset()
Observable<Unit> IObservableCollection<T>.ObserveClear<T>()
Observable<(int Index, int Count)> IObservableCollection<T>.ObserveReverse<T>()
Observable<(int Index, int Count, IComparer<T>? Comparer)> IObservableCollection<T>.ObserveSort<T>()
Observable<int> IObservableCollection<T>.ObserveCountChanged<T>()
```
Getting Started Getting Started
--- ---
For .NET, use NuGet. For Unity, please read [Unity](#unity) section. For .NET, use NuGet. For Unity, please read [Unity](#unity) section.
> dotnet add package [ObservableCollections](https://www.nuget.org/packages/ObservableCollections) PM> Install-Package [ObservableCollections](https://www.nuget.org/packages/ObservableCollections)
create new `ObservableList<T>`, `ObservableDictionary<TKey, TValue>`, `ObservableHashSet<T>`, `ObservableQueue<T>`, `ObservableStack<T>`, `ObservableRingBuffer<T>`, `ObservableFixedSizeRingBuffer<T>`. create new `ObservableList<T>`, `ObservableDictionary<TKey, TValue>`, `ObservableHashSet<T>`, `ObservableQueue<T>`, `ObservableStack<T>`, `ObservableRingBuffer<T>`, `ObservableFixedSizeRingBuffer<T>`.
@ -104,7 +88,7 @@ static void List_CollectionChanged(in NotifyCollectionChangedEventArgs<int> e)
} }
``` ```
While it is possible to manually handle the `CollectionChanged` event as shown in the example above, you can also create a `SynchronizedView` as a collection that holds a separate synchronized value. Handling all `CollectionChanged` event manually is hard. We recommend to use `SynchronizedView` that transform element and handling all collection changed event for view synchronize.
```csharp ```csharp
var list = new ObservableList<int>(); var list = new ObservableList<int>();
@ -116,7 +100,7 @@ list.AddRange(new[] { 30, 40, 50 });
list[1] = 60; list[1] = 60;
list.RemoveAt(3); list.RemoveAt(3);
foreach (var v in view) foreach (var (_, v) in view)
{ {
// 10$, 60$, 30$, 50$ // 10$, 60$, 30$, 50$
Console.WriteLine(v); Console.WriteLine(v);
@ -126,200 +110,77 @@ foreach (var v in view)
view.Dispose(); view.Dispose();
``` ```
The view can modify the objects being enumerated by attaching a Filter. 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.
```csharp
var list = new ObservableList<int>();
using var view = list.CreateView(x => x.ToString() + "$");
list.Add(1);
list.Add(20);
list.AddRange(new[] { 30, 31, 32 });
// attach filter
view.AttachFilter(x => x % 2 == 0);
foreach (var v in view)
{
// 20$, 30$, 32$
Console.WriteLine(v);
}
// attach other filter(removed previous filter)
view.AttachFilter(x => x % 2 == 1);
foreach (var v in view)
{
// 1$, 31$
Console.WriteLine(v);
}
// Count shows filtered length
Console.WriteLine(view.Count); // 2
```
The View only allows iteration and Count; it cannot be accessed via an indexer. If indexer access is required, you need to convert it using `ToViewList()`. Additionally, `ToNotifyCollectionChanged()` converts it to a synchronized view that implements `INotifyCollectionChanged`, which is necessary for XAML binding, in addition to providing indexer access.
```csharp
// Queue <-> List Synchronization
var queue = new ObservableQueue<int>();
queue.Enqueue(1);
queue.Enqueue(10);
queue.Enqueue(100);
queue.Enqueue(1000);
queue.Enqueue(10000);
using var view = queue.CreateView(x => x.ToString() + "$");
using var viewList = view.ToViewList();
Console.WriteLine(viewList[2]); // 100$
```
In the case of ObservableList, calls to `Sort` and `Reverse` can also be synchronized with the view.
```csharp
var list = new ObservableList<int> { 1, 301, 20, 50001, 4000 };
using var view = list.CreateView(x => x.ToString() + "$");
view.AttachFilter(x => x % 2 == 0);
foreach (var v in view)
{
// 20$, 4000$
Console.WriteLine(v);
}
// Reverse operations on the list will affect the view
list.Reverse();
foreach (var v in view)
{
// 4000$, 20$
Console.WriteLine(v);
}
// remove filter
view.ResetFilter();
// The reverse operation is also reflected in the values hidden by the filter
foreach (var v in view)
{
// 4000$, 50001$, 20$, 301$, 1$
Console.WriteLine(v);
}
// also affect Sort Operations
list.Sort();
foreach (var v in view)
{
// 1$, 20$, 301$, 4000$, 50001$
Console.WriteLine(v);
}
// you can use custom comparer
list.Sort(new DescendantComaprer());
foreach (var v in view)
{
// 50001$, 4000$, 301$, 20$, 1$
Console.WriteLine(v);
}
class DescendantComaprer : IComparer<int>
{
public int Compare(int x, int y)
{
return y.CompareTo(x);
}
}
```
Reactive Extensions with R3
---
Once the R3 extension package is installed, you can subscribe to `ObserveChanged`, `ObserveAdd`, `ObserveRemove`, `ObserveReplace`, `ObserveMove`, `ObserveReset`, `ObserveClear`, `ObserveReverse`, `ObserveSort`, `ObserveCounteChanged` events as Rx, allowing you to compose events individually.
> dotnet add package [ObservableCollections.R3](https://www.nuget.org/packages/ObservableCollections.R3)
```csharp
using R3;
using ObservableCollections;
var list = new ObservableList<int>();
list.ObserveAdd()
.Subscribe(x =>
{
Console.WriteLine(x);
});
list.Add(10);
list.Add(20);
list.AddRange(new[] { 10, 20, 30 });
```
Note that `ObserveReset` is used to subscribe to Clear, Reverse, and Sort operations in bulk.
In addition to `IObservableCollection<T>`, there is also a subscription event for `ISynchronizedView<T, TView>`. In the case of View, `ObserveRejected` is also added.
Since it is not supported by dotnet/reactive, please use the Rx library [R3](https://github.com/Cysharp/R3).
Blazor Blazor
--- ---
In the case of Blazor, `StateHasChanged` is called and re-enumeration occurs in response to changes in the collection. It's advisable to use the `CollectionStateChanged` event for this purpose. 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.
The View selector in ObservableCollections is also useful for converting data to a View that represents a Cell, for example, when creating something like a table.
```csharp ```csharp
public partial class Index : IDisposable public partial class DataTable<T> : ComponentBase, IDisposable
{ {
ObservableList<int> list; [Parameter, EditorRequired]
public ISynchronizedView<int, int> ItemsView { get; set; } public IReadOnlyList<T> Items { get; set; } = default!;
int count = 0;
[Parameter, EditorRequired]
public Func<T, Cell[]> DataTemplate { get; set; } = default!;
ISynchronizedView<T, Cell[]> view = default!;
protected override void OnInitialized() protected override void OnInitialized()
{ {
list = new ObservableList<int>(); if (Items is IObservableCollection<T> observableCollection)
ItemsView = list.CreateView(x => x);
ItemsView.CollectionStateChanged += action =>
{ {
InvokeAsync(StateHasChanged); // Note: If the table has the ability to sort columns, then it will be automatically sorted using SortedView.
view = observableCollection.CreateView(DataTemplate);
}
else
{
// It is often the case that Items is not Observable.
// In that case, FreezedList is provided to create a View with the same API for normal collections.
var freezedList = new FreezedList<T>(Items);
view = freezedList.CreateView(DataTemplate);
}
// View also has a change notification.
view.CollectionStateChanged += async _ =>
{
await InvokeAsync(StateHasChanged);
}; };
} }
void OnClick()
{
list.Add(count++);
}
public void Dispose() public void Dispose()
{ {
ItemsView.Dispose(); // unsubscribe.
view.Dispose();
} }
} }
// .razor, iterate view // .razor, iterate view
@page "/" @foreach (var (row, cells) in view)
{
<button @onclick=OnClick>button</button> <tr>
@foreach (var item in cells)
<table> {
@foreach (var item in ItemsView) <td>
{ <CellView Item="item" />
<tr> </td>
<td>@item</td> }
</tr> </tr>
} }
</table>
``` ```
WPF/Avalonia/WinUI (XAML based UI platforms) WPF
--- ---
Because of data binding in WPF, it is important that the collection is Observable. ObservableCollections high-performance `IObservableCollection<T>` 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. To`ToNotifyCollectionChanged(IColllectionEventDispatcher)` allows multi thread changed. Because of data binding in WPF, it is important that the collection is Observable. ObservableCollections high-performance `IObservableCollection<T>` 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.
```csharp ```csharp
// WPF simple sample. // WPF simple sample.
ObservableList<int> list; ObservableList<int> list;
public NotifyCollectionChangedSynchronizedViewList<int> ItemsView { get; set; } public ISynchronizedView<int, int> ItemsView { get; set; }
public MainWindow() public MainWindow()
{ {
@ -327,12 +188,9 @@ public MainWindow()
this.DataContext = this; this.DataContext = this;
list = new ObservableList<int>(); list = new ObservableList<int>();
ItemsView = list.CreateView(x => x).WithINotifyCollectionChanged();
// for ui synchronization safety of viewmodel BindingOperations.EnableCollectionSynchronization(ItemsView, new object()); // for ui synchronization safety of viewmodel
ItemsView = list.ToNotifyCollectionChanged(SynchronizationContextCollectionEventDispatcher.Current);
// if collection is changed only from ui-thread, can use this overload
// ItemsView = list.ToNotifyCollectionChanged();
} }
protected override void OnClosed(EventArgs e) protected override void OnClosed(EventArgs e)
@ -341,109 +199,32 @@ protected override void OnClosed(EventArgs e)
} }
``` ```
`SynchronizationContextCollectionEventDispatcher.Current` is default implementation of `IColllectionEventDispatcher`, it is used `SynchronizationContext.Current` for dispatche ui thread. You can create custom `ICollectionEventDispatcher` to use custom dispatcher object. For example use WPF Dispatcher: > WPF can not use SortedView because SortedView can not provide sort event to INotifyCollectionChanged.
```csharp
public class WpfDispatcherCollection(Dispatcher dispatcher) : ICollectionEventDispatcher
{
public void Post(CollectionEventDispatcherEventArgs ev)
{
dispatcher.InvokeAsync(() =>
{
// notify in dispatcher
ev.Invoke();
});
}
}
```
`ToNotifyCollectionChanged()` can also be called without going through a View. In this case, it's guaranteed that no filters will be applied, making it faster. If you want to apply filters, please generate a View before calling it. Additionally, `ObservableList` has a variation called `ToNotifyCollectionChangedSlim()`. This option doesn't generate a list for the View and shares the actual data, making it the fastest and most memory-efficient option. However, range operations such as `AddRange`, `InsertRange` and `RemoveRange` are not supported by WPF (or Avalonia), so they will throw runtime exceptions.
Views and ToNotifyCollectionChanged are internally connected by events, so they need to be `Dispose` to release those connections.
Standard Views are readonly. If you want to reflect the results of binding back to the original collection, use `CreateWritableView` to generate an `IWritableSynchronizedView`, and then use `ToWritableNotifyCollectionChanged` to create an `INotifyCollectionChanged` collection from it.
```csharp
public delegate T WritableViewChangedEventHandler<T, TView>(TView newView, T originalValue, ref bool setValue);
public interface IWritableSynchronizedView<T, TView> : ISynchronizedView<T, TView>
{
NotifyCollectionChangedSynchronizedViewList<TView> ToWritableNotifyCollectionChanged(WritableViewChangedEventHandler<T, TView> converter);
NotifyCollectionChangedSynchronizedViewList<TView> ToWritableNotifyCollectionChanged(WritableViewChangedEventHandler<T, TView> converter, ICollectionEventDispatcher? collectionEventDispatcher);
}
```
`ToWritableNotifyCollectionChanged` accepts a delegate called `WritableViewChangedEventHandler`. `newView` receives the newly bound value. If `setValue` is true, it sets a new value to the original collection, triggering notification propagation. The View is also regenerated. If `T originalValue` is a reference type, you can prevent such propagation by setting `setValue` to `false`.
```csharp
var list = new ObservableList<Person>()
{
new (){ Age = 10, Name = "John" },
new (){ Age = 22, Name = "Jeyne" },
new (){ Age = 30, Name = "Mike" },
};
var view = list.CreateWritableView(x => x.Name);
view.AttachFilter(x => x.Age >= 20);
IList<string?> bindable = view.ToWritableNotifyCollectionChanged((string? newView, Person original, ref bool setValue) =>
{
if (setValue)
{
// default setValue == true is Set operation
original.Name = newView;
// You can modify setValue to false, it does not set original collection to new value.
// For mutable reference types, when there is only a single,
// bound View and to avoid recreating the View, setting false is effective.
// Otherwise, keeping it true will set the value in the original collection as well,
// and change notifications will be sent to lower-level Views(the delegate for View generation will also be called anew).
setValue = false;
return original;
}
else
{
// default setValue == false is Add operation
return new Person { Age = null, Name = newView };
}
});
bindable[1] = "Bob"; // change Mike(filtered view's [1]) to Bob.
bindable.Add("Ken");
// Show Views
foreach (var item in view)
{
Console.WriteLine(item);
}
Console.WriteLine("---");
// Show Originals
foreach (var item in list)
{
Console.WriteLine((item.Age, item.Name));
}
public class Person
{
public int? Age { get; set; }
public string? Name { get; set; }
}
```
Unity Unity
--- ---
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. Since View objects are generated only once, it's possible to complement GameObjects tied to the collection. To install in a Unity project, you can use package manager.
- Open Window > Package Manager
- The "+" button > Add package from git URL
- Enter the following URL
- ```
https://github.com/Cysharp/ObservableCollections.git?path=src/ObservableCollections.Unity/Assets/Plugins/ObservableCollections
```
In Unity, ObservableCollections and Views are useful as CollectionManagers, since they need to convert T to Prefab for display.
Since we need to have side effects on GameObjects, we will prepare a filter and apply an action on changes.
```csharp ```csharp
// Unity, with filter sample.
public class SampleScript : MonoBehaviour public class SampleScript : MonoBehaviour
{ {
public Button prefab; public Button prefab;
public GameObject root; public GameObject root;
ObservableRingBuffer<int> collection; ObservableRingBuffer<int> collection;
ISynchronizedView<int, GameObject> view; ISynchronizedView<GameObject> view;
void Start() void Start()
{ {
@ -452,248 +233,182 @@ public class SampleScript : MonoBehaviour
{ {
var item = GameObject.Instantiate(prefab); var item = GameObject.Instantiate(prefab);
item.GetComponentInChildren<Text>().text = x.ToString(); item.GetComponentInChildren<Text>().text = x.ToString();
// add to root
item.transform.SetParent(root.transform);
return item.gameObject; return item.gameObject;
}); });
view.ViewChanged += View_ViewChanged; view.AttachFilter(new GameObjectFilter(root));
}
void View_ViewChanged(in SynchronizedViewChangedEventArgs<int, GameObject> eventArgs)
{
// hook remove event
if (eventArgs.Action == NotifyCollectionChangedAction.Remove)
{
GameObject.Destroy(eventArgs.OldItem.View);
}
// hook for Filter attached, clear, etc...
// if (NotifyCollectionChangedAction.Reset) { }
} }
void OnDestroy() void OnDestroy()
{ {
view.Dispose(); view.Dispose();
} }
public class GameObjectFilter : ISynchronizedViewFilter<int, GameObject>
{
readonly GameObject root;
public GameObjectFilter(GameObject root)
{
this.root = root;
}
public void OnCollectionChanged(ChangedKind changedKind, int value, GameObject view, in NotifyCollectionChangedEventArgs<int> eventArgs)
{
if (changedKind == ChangedKind.Add)
{
view.transform.SetParent(root.transform);
}
else if (changedKind == ChangedKind.Remove)
{
GameObject.Destroy(view);
}
}
public bool IsMatch(int value, GameObject view)
{
return true;
}
public void WhenTrue(int value, GameObject view)
{
view.SetActive(true);
}
public void WhenFalse(int value, GameObject view)
{
view.SetActive(false);
}
}
} }
``` ```
Reference It is also possible to manage Order by managing indexes inserted from eventArgs, but it is very difficult with many caveats. If you don't have major performance issues, you can foreach the View itself on CollectionStateChanged (like Blazor) and reorder the transforms. If you have such a architecture, you can also use SortedView.
View/SortedView
--- ---
ObservableCollections provides these collections. View can create from `IObservableCollection<T>`, it completely synchronized and thread-safe.
```csharp ```csharp
class ObservableList<T> : IList<T>, IReadOnlyList<T>, IObservableCollection<T>, IReadOnlyObservableList<T>
class ObservableDictionary<TKey, TValue> : IDictionary<TKey, TValue>, IReadOnlyDictionary<TKey, TValue>, IObservableCollection<KeyValuePair<TKey, TValue>>, IReadOnlyObservableDictionary<TKey, TValue> where TKey : notnull
class ObservableHashSet<T> : IReadOnlySet<T>, IReadOnlyCollection<T>, IObservableCollection<T> where T : notnull
class ObservableQueue<T> : IReadOnlyCollection<T>, IObservableCollection<T>
class ObservableStack<T> : IReadOnlyCollection<T>, IObservableCollection<T>
class ObservableRingBuffer<T> : IList<T>, IReadOnlyList<T>, IObservableCollection<T>
class RingBuffer<T> : IList<T>, IReadOnlyList<T>
class ObservableFixedSizeRingBuffer<T> : IList<T>, IReadOnlyList<T>, IObservableCollection<T>
class AlternateIndexList<T> : IEnumerable<T>
```
The `IObservableCollection<T>` is the base interface for all, containing the `CollectionChanged` event and the `CreateView` method.
```csharp
public delegate void NotifyCollectionChangedEventHandler<T>(in NotifyCollectionChangedEventArgs<T> e);
public interface IObservableCollection<T> : IReadOnlyCollection<T> public interface IObservableCollection<T> : IReadOnlyCollection<T>
{ {
object SyncRoot { get; } // snip...
event NotifyCollectionChangedEventHandler<T>? CollectionChanged; ISynchronizedView<T, TView> CreateView<TView>(Func<T, TView> transform, bool reverse = false);
ISynchronizedView<T, TView> CreateView<TView>(Func<T, TView> transform);
} }
``` ```
The notification event `NotifyCollectionChangedEventArgs<T>` has the following definition: When reverse = true, foreach view as reverse order(Dictionary, etc. are not supported).
`ISynchronizedView<T, TView>` is `IReadOnlyCollection` and hold both value and view(transformed value when added).
```csharp ```csharp
/// <summary> public interface ISynchronizedView<T, TView> : IReadOnlyCollection<(T Value, TView View)>, IDisposable
/// Contract:
/// IsSingleItem ? (NewItem, OldItem) : (NewItems, OldItems)
/// Action.Add
/// NewItem, NewItems, NewStartingIndex
/// Action.Remove
/// OldItem, OldItems, OldStartingIndex
/// Action.Replace
/// NewItem, NewItems, OldItem, OldItems, (NewStartingIndex, OldStartingIndex = samevalue)
/// Action.Move
/// NewStartingIndex, OldStartingIndex
/// Action.Reset
/// SortOperation(IsClear, IsReverse, IsSort)
/// </summary>
[StructLayout(LayoutKind.Auto)]
public readonly ref struct NotifyCollectionChangedEventArgs<T>
{
public readonly NotifyCollectionChangedAction Action;
public readonly bool IsSingleItem;
public readonly T NewItem;
public readonly T OldItem;
public readonly ReadOnlySpan<T> NewItems;
public readonly ReadOnlySpan<T> OldItems;
public readonly int NewStartingIndex;
public readonly int OldStartingIndex;
public readonly SortOperation<T> SortOperation;
}
```
This is the interface for View:
```csharp
public delegate void NotifyViewChangedEventHandler<T, TView>(in SynchronizedViewChangedEventArgs<T, TView> e);
public enum RejectedViewChangedAction
{
Add, Remove, Move
}
public interface ISynchronizedView<T, TView> : IReadOnlyCollection<TView>, IDisposable
{ {
object SyncRoot { get; } object SyncRoot { get; }
ISynchronizedViewFilter<T, TView> Filter { get; }
IEnumerable<(T Value, TView View)> Filtered { get; }
IEnumerable<(T Value, TView View)> Unfiltered { get; }
int UnfilteredCount { get; }
event NotifyViewChangedEventHandler<T, TView>? ViewChanged; event NotifyCollectionChangedEventHandler<T>? RoutingCollectionChanged;
event Action<RejectedViewChangedAction, int, int>? RejectedViewChanged; // int index, int oldIndex(when RejectedViewChangedAction is Move)
event Action<NotifyCollectionChangedAction>? CollectionStateChanged; event Action<NotifyCollectionChangedAction>? CollectionStateChanged;
void AttachFilter(ISynchronizedViewFilter<T, TView> filter); void AttachFilter(ISynchronizedViewFilter<T, TView> filter);
void ResetFilter(); void ResetFilter(Action<T, TView>? resetAction);
ISynchronizedViewList<TView> ToViewList(); INotifyCollectionChangedSynchronizedView<T, TView> WithINotifyCollectionChanged();
NotifyCollectionChangedSynchronizedViewList<TView> ToNotifyCollectionChanged();
NotifyCollectionChangedSynchronizedViewList<TView> ToNotifyCollectionChanged(ICollectionEventDispatcher? collectionEventDispatcher);
} }
``` ```
The `Count` of the View returns the filtered value, but if you need the unfiltered value, use `UnfilteredCount`. Also, normal enumeration returns only `TView`, but if you need `T` or want to enumerate pre-filtered values, you can get them with `Filtered` and `Unfiltered`.
The View's notification event `SynchronizedViewChangedEventArgs<T>` has the following definition:
see [filter](#filter) section.
```csharp ```csharp
public readonly ref struct SynchronizedViewChangedEventArgs<T, TView> var view = transform(value);
if (filter.IsMatch(value, view))
{ {
public readonly NotifyCollectionChangedAction Action; filter.WhenTrue(value, view);
public readonly bool IsSingleItem;
public readonly (T Value, TView View) NewItem;
public readonly (T Value, TView View) OldItem;
public readonly ReadOnlySpan<T> NewValues;
public readonly ReadOnlySpan<TView> NewViews;
public readonly ReadOnlySpan<T> OldValues;
public readonly ReadOnlySpan<TView> OldViews;
public readonly int NewStartingIndex;
public readonly int OldStartingIndex;
public readonly SortOperation<T> SortOperation;
} }
else
{
filter.WhenFalse(value, view);
}
AddToCollectionInnerStructure(value, view);
filter.OnCollectionChanged(ChangeKind.Add, value, view, eventArgs);
RoutingCollectionChanged(eventArgs);
CollectionStateChanged();
``` ```
When `NotifyCollectionChangedAction` is `Reset`, additional determination can be made with `SortOperation<T>`.
```csharp ```csharp
public readonly struct SortOperation<T> public static ISynchronizedView<T, TView> CreateSortedView<T, TKey, TView>(this IObservableCollection<T> source, Func<T, TKey> identitySelector, Func<T, TView> transform, IComparer<T> comparer)
{ where TKey : notnull
public readonly int Index;
public readonly int Count;
public readonly IComparer<T>? Comparer;
public bool IsReverse { get; } public static ISynchronizedView<T, TView> CreateSortedView<T, TKey, TView>(this IObservableCollection<T> source, Func<T, TKey> identitySelector, Func<T, TView> transform, IComparer<TView> viewComparer)
public bool IsClear { get; } where TKey : notnull
public bool IsSort { get; }
} public static ISynchronizedView<T, TView> CreateSortedView<T, TKey, TView, TCompare>(this IObservableCollection<T> source, Func<T, TKey> identitySelector, Func<T, TView> transform, Func<T, TCompare> compareSelector, bool ascending = true)
where TKey : notnull
``` ```
When `IsReverse` is true, you need to use `Index` and `Count`. When `IsSort` is true, you need to use `Index`, `Count`, and `Comparer` values. > Notice: foreach ObservableCollections and Views are thread-safe but it uses lock at iterating. In other words, the obtained Enumerator must be Dispose. foreach and LINQ are guaranteed to be Dispose, but be careful when you extract the Enumerator by yourself.
For Filter, you can either create one that implements this interface or generate one from a lambda expression using extension methods. Filter
---
```csharp ```csharp
public interface ISynchronizedViewFilter<T, TView> public interface ISynchronizedViewFilter<T, TView>
{ {
bool IsMatch(T value, TView view); bool IsMatch(T value, TView view);
void WhenTrue(T value, TView view);
void WhenFalse(T value, TView view);
void OnCollectionChanged(ChangedKind changedKind, T value, TView view, in NotifyCollectionChangedEventArgs<T> eventArgs);
} }
public static class SynchronizedViewExtensions public enum ChangedKind
{ {
public static void AttachFilter<T, TView>(this ISynchronizedView<T, TView> source, Func<T, bool> filter) Add, Remove, Move
{
}
public static void AttachFilter<T, TView>(this ISynchronizedView<T, TView> source, Func<T, TView, bool> filter)
{
}
} }
``` ```
`ObservableList<T>` has writable view.
Collections
---
```csharp ```csharp
public sealed partial class ObservableList<T> public sealed partial class ObservableDictionary<TKey, TValue> : IDictionary<TKey, TValue>, IReadOnlyDictionary<TKey, TValue>, IObservableCollection<KeyValuePair<TKey, TValue>> where TKey : notnull
{ public sealed partial class ObservableFixedSizeRingBuffer<T> : IList<T>, IReadOnlyList<T>, IObservableCollection<T>
public IWritableSynchronizedView<T, TView> CreateWritableView<TView>(Func<T, TView> transform); public sealed partial class ObservableHashSet<T> : IReadOnlySet<T>, IReadOnlyCollection<T>, IObservableCollection<T> where T : notnull
public NotifyCollectionChangedSynchronizedViewList<T> ToWritableNotifyCollectionChanged(); public sealed partial class ObservableHashSet<T> : IReadOnlySet<T>, IReadOnlyCollection<T>, IObservableCollection<T>
public NotifyCollectionChangedSynchronizedViewList<T> ToWritableNotifyCollectionChanged(ICollectionEventDispatcher? collectionEventDispatcher); where T : notnull
public NotifyCollectionChangedSynchronizedViewList<TView> ToWritableNotifyCollectionChanged<TView>(Func<T, TView> transform, WritableViewChangedEventHandler<T, TView>? converter);
public NotifyCollectionChangedSynchronizedViewList<TView> ToWritableNotifyCollectionChanged<TView>(Func<T, TView> transform, ICollectionEventDispatcher? collectionEventDispatcher, WritableViewChangedEventHandler<T, TView>? converter);
}
public delegate T WritableViewChangedEventHandler<T, TView>(TView newView, T originalValue, ref bool setValue); public sealed partial class ObservableList<T> : IList<T>, IReadOnlyList<T>, IObservableCollection<T>
public interface IWritableSynchronizedView<T, TView> : ISynchronizedView<T, TView> public sealed partial class ObservableQueue<T> : IReadOnlyCollection<T>, IObservableCollection<T>
{ public sealed partial class ObservableRingBuffer<T> : IList<T>, IReadOnlyList<T>, IObservableCollection<T>
(T Value, TView View) GetAt(int index);
void SetViewAt(int index, TView view);
void SetToSourceCollection(int index, T value);
void AddToSourceCollection(T value);
void InsertIntoSourceCollection(int index, T value);
bool RemoveFromSourceCollection(T value);
void RemoveAtSourceCollection(int index);
void ClearSourceCollection();
IWritableSynchronizedViewList<TView> ToWritableViewList(WritableViewChangedEventHandler<T, TView> converter);
INotifyCollectionChangedSynchronizedViewList<TView> ToWritableNotifyCollectionChanged(WritableViewChangedEventHandler<T, TView> converter);
INotifyCollectionChangedSynchronizedViewList<TView> ToWritableNotifyCollectionChanged(WritableViewChangedEventHandler<T, TView> converter, ICollectionEventDispatcher? collectionEventDispatcher);
}
public interface IWritableSynchronizedViewList<TView> : ISynchronizedViewList<TView> public sealed partial class ObservableStack<T> : IReadOnlyCollection<T>, IObservableCollection<T>
{
new TView this[int index] { get; set; } public sealed class RingBuffer<T> : IList<T>, IReadOnlyList<T>
}
``` ```
Here are definitions for other collections: Freezed
---
```csharp ```csharp
public interface IReadOnlyObservableList<T> : public sealed class FreezedList<T> : IReadOnlyList<T>, IFreezedCollection<T>
IReadOnlyList<T>, IObservableCollection<T> public sealed class FreezedDictionary<TKey, TValue> : IReadOnlyDictionary<TKey, TValue>, IFreezedCollection<KeyValuePair<TKey, TValue>> where TKey : notnull
public interface IFreezedCollection<T>
{ {
ISynchronizedView<T, TView> CreateView<TView>(Func<T, TView> transform, bool reverse = false);
ISortableSynchronizedView<T, TView> CreateSortableView<TView>(Func<T, TView> transform);
} }
public interface IReadOnlyObservableDictionary<TKey, TValue> : public static ISortableSynchronizedView<T, TView> CreateSortableView<T, TView>(this IFreezedCollection<T> source, Func<T, TView> transform, IComparer<T> initialSort)
IReadOnlyDictionary<TKey, TValue>, IObservableCollection<KeyValuePair<TKey, TValue>> public static ISortableSynchronizedView<T, TView> CreateSortableView<T, TView>(this IFreezedCollection<T> source, Func<T, TView> transform, IComparer<TView> initialViewSort)
{ public static ISortableSynchronizedView<T, TView> CreateSortableView<T, TView, TCompare>(this IFreezedCollection<T> source, Func<T, TView> transform, Func<T, TCompare> initialCompareSelector, bool ascending = true)
} public static void Sort<T, TView, TCompare>(this ISortableSynchronizedView<T, TView> source, Func<T, TCompare> compareSelector, bool ascending = true)
public interface ISynchronizedViewList<out TView> : IReadOnlyList<TView>, IDisposable
{
}
// Obsolete for public use
public interface INotifyCollectionChangedSynchronizedViewList<TView> : IList<TView>, IList, ISynchronizedViewList<TView>, INotifyCollectionChanged, INotifyPropertyChanged
{
}
public abstract class NotifyCollectionChangedSynchronizedViewList<TView> :
INotifyCollectionChangedSynchronizedViewList<TView>,
IWritableSynchronizedViewList<TView>,
IList<TView>,
IList
{
}
``` ```
License License

Binary file not shown.

View File

@ -1,10 +0,0 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="AvaloniaApp.App"
RequestedThemeVariant="Default">
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
<Application.Styles>
<FluentTheme />
</Application.Styles>
</Application>

View File

@ -1,24 +0,0 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
namespace AvaloniaApp
{
public partial class App : Application
{
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow = new MainWindow();
}
base.OnFrameworkInitializationCompleted();
}
}
}

View File

@ -1,29 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<ApplicationManifest>app.manifest</ApplicationManifest>
<AvaloniaUseCompiledBindingsByDefault>false</AvaloniaUseCompiledBindingsByDefault>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="R3Extensions.Avalonia" Version="1.2.5" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.1.0" />
<PackageReference Include="Avalonia.Desktop" Version="11.1.0" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.1.0" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.1.0" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.1.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\ObservableCollections.R3\ObservableCollections.R3.csproj" />
<ProjectReference Include="..\..\src\ObservableCollections\ObservableCollections.csproj" />
</ItemGroup>
</Project>

View File

@ -1,20 +0,0 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="AvaloniaApp.MainWindow"
Title="AvaloniaApp">
<StackPanel>
<ListBox ItemsSource="{Binding ItemsView}" x:CompileBindings="False" />
<Button Content="Add" Command="{Binding AddCommand}" x:CompileBindings="False"/>
<Button Content="AddRange" Command="{Binding AddRangeCommand}" x:CompileBindings="False"/>
<Button Content="Insert" Command="{Binding InsertAtRandomCommand}" x:CompileBindings="False" />
<Button Content="Remove" Command="{Binding RemoveAtRandomCommand}" x:CompileBindings="False" />
<Button Content="Clear" Command="{Binding ClearCommand}" x:CompileBindings="False"/>
<Button Content="Reverse" Command="{Binding ReverseCommand}" x:CompileBindings="False" />
<Button Content="Sort" Command="{Binding SortCommand}" x:CompileBindings="False"/>
<Button Content="AttachFilter" Command="{Binding AttachFilterCommand}" x:CompileBindings="False" />
<Button Content="ResetFilter" Command="{Binding ResetFilterCommand}" x:CompileBindings="False" />
</StackPanel>
</Window>

View File

@ -1,104 +0,0 @@
using Avalonia.Controls;
using ObservableCollections;
using R3;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Threading;
namespace AvaloniaApp
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new ViewModel();
}
}
public class ViewModel
{
private ObservableList<int> observableList { get; } = new ObservableList<int>();
public INotifyCollectionChangedSynchronizedViewList<int> ItemsView { get; }
public ReactiveCommand<Unit> AddCommand { get; } = new ReactiveCommand<Unit>();
public ReactiveCommand<Unit> AddRangeCommand { get; } = new ReactiveCommand<Unit>();
public ReactiveCommand<Unit> InsertAtRandomCommand { get; } = new ReactiveCommand<Unit>();
public ReactiveCommand<Unit> RemoveAtRandomCommand { get; } = new ReactiveCommand<Unit>();
public ReactiveCommand<Unit> ClearCommand { get; } = new ReactiveCommand<Unit>();
public ReactiveCommand<Unit> ReverseCommand { get; } = new ReactiveCommand<Unit>();
public ReactiveCommand<Unit> SortCommand { get; } = new ReactiveCommand<Unit>();
public ReactiveCommand<Unit> AttachFilterCommand { get; } = new ReactiveCommand<Unit>();
public ReactiveCommand<Unit> ResetFilterCommand { get; } = new ReactiveCommand<Unit>();
public ViewModel()
{
observableList.Add(1);
observableList.Add(2);
var view = observableList.CreateView(x => x);
ItemsView = view.ToNotifyCollectionChanged(SynchronizationContextCollectionEventDispatcher.Current);
// check for optimize list
// ItemsView = observableList.ToNotifyCollectionChanged(SynchronizationContextCollectionEventDispatcher.Current);
AddCommand.Subscribe(_ =>
{
ThreadPool.QueueUserWorkItem(_ =>
{
observableList.Add(Random.Shared.Next());
});
});
AddRangeCommand.Subscribe(_ =>
{
var xs = Enumerable.Range(1, 5).Select(_ => Random.Shared.Next()).ToArray();
observableList.AddRange(xs);
});
InsertAtRandomCommand.Subscribe(_ =>
{
var from = Random.Shared.Next(0, view.Count);
observableList.Insert(from, Random.Shared.Next());
});
RemoveAtRandomCommand.Subscribe(_ =>
{
var from = Random.Shared.Next(0, view.Count);
observableList.RemoveAt(from);
});
ClearCommand.Subscribe(_ =>
{
observableList.Clear();
});
ReverseCommand.Subscribe(_ =>
{
observableList.Reverse();
});
SortCommand.Subscribe(_ =>
{
observableList.Sort();
});
AttachFilterCommand.Subscribe(_ =>
{
view.AttachFilter(x => x % 2 == 0);
});
ResetFilterCommand.Subscribe(_ =>
{
view.ResetFilter();
});
}
}
}

View File

@ -1,23 +0,0 @@
using Avalonia;
using System;
namespace AvaloniaApp
{
internal class Program
{
// Initialization code. Don't use any Avalonia, third-party APIs or any
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
// yet and stuff might break.
[STAThread]
public static void Main(string[] args) => BuildAvaloniaApp()
.StartWithClassicDesktopLifetime(args);
// Avalonia configuration, don't remove; also used by visual designer.
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UsePlatformDetect()
.WithInterFont()
.LogToTrace()
.UseR3();
}
}

View File

@ -1,18 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<!-- This manifest is used on Windows only.
Don't remove it as it might cause problems with window transparency and embedded controls.
For more details visit https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests -->
<assemblyIdentity version="1.0.0.0" name="AvaloniaApp.Desktop"/>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- A list of the Windows versions that this application has been tested on
and is designed to work with. Uncomment the appropriate elements
and Windows will automatically select the most compatible environment. -->
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
</assembly>

View File

@ -7,7 +7,7 @@ public partial class Index : IDisposable
{ {
ObservableList<int> list; ObservableList<int> list;
public ISynchronizedView<int, int> ItemsView { get; set; } public ISynchronizedView<int, int> ItemsView { get; set; }
int count = 99; int adder = 99;
protected override void OnInitialized() protected override void OnInitialized()
{ {
@ -20,13 +20,19 @@ public partial class Index : IDisposable
}; };
} }
void OnClick()
{
list.Add(count++);
}
public void Dispose() public void Dispose()
{ {
ItemsView.Dispose(); ItemsView.Dispose();
} }
void OnClick()
{
ThreadPool.QueueUserWorkItem(_ =>
{
list.Add(adder++);
});
}
} }

View File

@ -1,17 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<LangVersion>10.0</LangVersion> <LangVersion>10.0</LangVersion>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\src\ObservableCollections.R3\ObservableCollections.R3.csproj" /> <ProjectReference Include="..\..\src\ObservableCollections\ObservableCollections.csproj" />
<ProjectReference Include="..\..\src\ObservableCollections\ObservableCollections.csproj" /> </ItemGroup>
<PackageReference Include="R3" Version="1.0.0" />
</ItemGroup>
</Project> </Project>

View File

@ -1,105 +1,24 @@
using ObservableCollections;
using System; using System;
using System.Collections.Specialized; using System.Collections.Specialized;
using R3;
using System.Linq;
using ObservableCollections;
using System.Collections;
using System.Collections.Generic;
using System.Threading.Tasks.Sources;
using System.Reflection.Emit;
var list = new ObservableList<Person>()
// Basic sample, use like ObservableCollection<T>.
// CollectionChanged observes all collection modification
var list = new ObservableList<int>();
var view = list.CreateView(x => x.ToString() + "$");
list.Add(10);
list.Add(20);
list.AddRange(new[] { 30, 40, 50 });
list[1] = 60;
list.RemoveAt(3);
foreach (var (_, v) in view)
{ {
new (){ Age = 10, Name = "John" }, // 10$, 60$, 30$, 50$
new (){ Age = 22, Name = "Jeyne" }, Console.WriteLine(v);
new (){ Age = 30, Name = "Mike" },
};
var view = list.CreateWritableView(x => x.Name);
view.AttachFilter(x => x.Age >= 20);
var bindable = view.ToWritableNotifyCollectionChanged((string? newView, Person original, ref bool setValue) =>
{
if (setValue)
{
// default setValue == true is Set operation
original.Name = newView;
// You can modify setValue to false, it does not set original collection to new value.
// For mutable reference types, when there is only a single,
// bound View and to avoid recreating the View, setting false is effective.
// Otherwise, keeping it true will set the value in the original collection as well,
// and change notifications will be sent to lower-level Views(the delegate for View generation will also be called anew).
setValue = false;
return original;
}
else
{
// default setValue == false is Add operation
return new Person { Age = null, Name = newView };
}
});
list.Clear();
list.Add(new() { Age = 99, Name = "tako" });
// bindable[0] = "takoyaki";
foreach (var item in view)
{
Console.WriteLine(item);
} }
Console.WriteLine("---"); // Dispose view is unsubscribe collection changed event.
view.Dispose();
foreach (var item in list)
{
Console.WriteLine((item.Age, item.Name));
}
public class Person
{
public int? Age { get; set; }
public string? Name { get; set; }
}
//var buffer = new ObservableFixedSizeRingBuffer<int>(5);
//var view = buffer.CreateView(value => value);
//view.AttachFilter(value => value % 2 == 1); // when filtered, mismatch...!
////{
//// INotifyCollectionChangedSynchronizedViewList created from ISynchronizedView with a filter.
//var collection = view.ToNotifyCollectionChanged();
//// Not disposed here.
////}
//buffer.AddFirst(1);
//buffer.AddFirst(1);
//buffer.AddFirst(2);
//buffer.AddFirst(3);
//buffer.AddFirst(5);
//buffer.AddFirst(8); // Argument out of range
//buffer.AddFirst(13);
//foreach (var item in collection)
//{
// Console.WriteLine(item);
//}
//Console.WriteLine("---");
//foreach (var item in view)
//{
// Console.WriteLine(item);
//}
//Console.WriteLine("---");
//foreach (var item in buffer)
//{
// Console.WriteLine(item);
//}

View File

@ -5,62 +5,16 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApp" xmlns:local="clr-namespace:WpfApp"
mc:Ignorable="d" mc:Ignorable="d"
Title="MainWindow" Height="800" Width="800"> Title="MainWindow" Height="450" Width="800">
<Grid> <Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="*" /> <ColumnDefinition />
<ColumnDefinition Width="*" /> <ColumnDefinition />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<!-- original --> <ListView ItemsSource="{Binding ItemsView}"></ListView>
<StackPanel Grid.Row="0" Grid.ColumnSpan="2" Orientation="Vertical">
<ListBox ItemsSource="{Binding ItemsView}" /> <Button Grid.Column="1" Click="Button_Click">Insert</Button>
<Button Content="Add" Command="{Binding AddCommand}" />
<Button Content="AddRange" Command="{Binding AddRangeCommand}" />
<Button Content="Insert" Command="{Binding InsertAtRandomCommand}" />
<Button Content="Remove" Command="{Binding RemoveAtRandomCommand}" />
<Button Content="RemoveRange" Command="{Binding RemoveRangeCommand}" />
<Button Content="Clear" Command="{Binding ClearCommand}" />
<Button Content="Reverse" Command="{Binding ReverseCommand}" />
<Button Content="Sort" Command="{Binding SortCommand}" />
</StackPanel>
<!-- Upper left (NotWritable, NonFilter) -->
<GroupBox Grid.Row="1" Grid.Column="0" Header="NotWritable, NonFilter">
<StackPanel Orientation="Vertical">
<DataGrid ItemsSource="{Binding NotWritableNonFilterView}" />
</StackPanel>
</GroupBox>
<!-- Upper right (Writable, NonFilter) -->
<GroupBox Grid.Row="1" Grid.Column="1" Header="Writable, NonFilter">
<StackPanel Orientation="Vertical">
<DataGrid ItemsSource="{Binding WritableNonFilterPersonView}" />
</StackPanel>
</GroupBox>
<!-- Lower left (NotWritable, Filter) -->
<GroupBox Grid.Row="2" Grid.Column="0" Header="NotWritable, Filter">
<StackPanel Orientation="Vertical">
<DataGrid ItemsSource="{Binding NotWritableFilterView}" />
<Button Content="AttachFilter" Command="{Binding AttachFilterCommand2}" />
<Button Content="ResetFilter" Command="{Binding ResetFilterCommand2}" />
</StackPanel>
</GroupBox>
<!-- Lower right (Writable, Filter) -->
<GroupBox Grid.Row="2" Grid.Column="1" Header="Writable, Filter">
<StackPanel Orientation="Vertical">
<DataGrid ItemsSource="{Binding WritableFilterPersonView}" />
<Button Content="AttachFilter" Command="{Binding AttachFilterCommand3}" />
<Button Content="ResetFilter" Command="{Binding ResetFilterCommand3}" />
</StackPanel>
</GroupBox>
</Grid> </Grid>
</Window> </Window>

View File

@ -1,10 +1,6 @@
using ObservableCollections; using ObservableCollections;
using R3;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
@ -18,7 +14,6 @@ using System.Windows.Media;
using System.Windows.Media.Imaging; using System.Windows.Media.Imaging;
using System.Windows.Navigation; using System.Windows.Navigation;
using System.Windows.Shapes; using System.Windows.Shapes;
using System.Windows.Threading;
namespace WpfApp namespace WpfApp
{ {
@ -27,247 +22,38 @@ namespace WpfApp
/// </summary> /// </summary>
public partial class MainWindow : Window public partial class MainWindow : Window
{ {
//ObservableList<int> list; ObservableList<int> list;
//public INotifyCollectionChangedSynchronizedView<int> ItemsView { get; set; } public ISynchronizedView<int, int> ItemsView { get; set; }
public MainWindow() public MainWindow()
{ {
InitializeComponent(); InitializeComponent();
this.DataContext = this;
R3.WpfProviderInitializer.SetDefaultObservableSystem(x =>
{
Trace.WriteLine(x);
});
this.DataContext = new ViewModel();
// Dispatcher.BeginInvoke(
//list = new ObservableList<int>(); list = new ObservableList<int>();
//list.AddRange(new[] { 1, 10, 188 }); list.AddRange(new[] { 1, 10, 188 });
//ItemsView = list.CreateSortedView(x => x, x => x, comparer: Comparer<int>.Default).ToNotifyCollectionChanged(); ItemsView = list.CreateSortedView(x => x, x => x, comparer: Comparer<int>.Default).WithINotifyCollectionChanged();
//BindingOperations.EnableCollectionSynchronization(ItemsView, new object()); BindingOperations.EnableCollectionSynchronization(ItemsView, new object());
} }
//int adder = 99; int adder = 99;
//private void Button_Click(object sender, RoutedEventArgs e) private void Button_Click(object sender, RoutedEventArgs e)
//{
// ThreadPool.QueueUserWorkItem(_ =>
// {
// list.Add(adder++);
// });
//}
//protected override void OnClosed(EventArgs e)
//{
// ItemsView.Dispose();
//}
}
public class ViewModel
{
private ObservableList<int> observableList { get; } = new ObservableList<int>();
public INotifyCollectionChangedSynchronizedViewList<int> ItemsView { get; }
public ReactiveCommand<Unit> AddCommand { get; } = new ReactiveCommand<Unit>();
public ReactiveCommand<Unit> AddRangeCommand { get; } = new ReactiveCommand<Unit>();
public ReactiveCommand<Unit> InsertAtRandomCommand { get; } = new ReactiveCommand<Unit>();
public ReactiveCommand<Unit> RemoveAtRandomCommand { get; } = new ReactiveCommand<Unit>();
public ReactiveCommand<Unit> RemoveRangeCommand { get; } = new ReactiveCommand<Unit>();
public ReactiveCommand<Unit> ClearCommand { get; } = new ReactiveCommand<Unit>();
public ReactiveCommand<Unit> ReverseCommand { get; } = new ReactiveCommand<Unit>();
public ReactiveCommand<Unit> SortCommand { get; } = new ReactiveCommand<Unit>();
public ReactiveCommand<Unit> AttachFilterCommand { get; } = new ReactiveCommand<Unit>();
public ReactiveCommand<Unit> ResetFilterCommand { get; } = new ReactiveCommand<Unit>();
private ObservableList<Person> SourceList { get; } = [];
private IWritableSynchronizedView<Person, Person> writableFilter;
private ISynchronizedView<Person, Person> notWritableFilter;
public NotifyCollectionChangedSynchronizedViewList<Person> NotWritableNonFilterView { get; }
public NotifyCollectionChangedSynchronizedViewList<Person> NotWritableFilterView { get; }
public NotifyCollectionChangedSynchronizedViewList<Person> WritableNonFilterPersonView { get; }
public NotifyCollectionChangedSynchronizedViewList<Person> WritableFilterPersonView { get; }
public ReactiveCommand<Unit> AttachFilterCommand2 { get; } = new ReactiveCommand<Unit>();
public ReactiveCommand<Unit> ResetFilterCommand2 { get; } = new ReactiveCommand<Unit>();
public ReactiveCommand<Unit> AttachFilterCommand3 { get; } = new ReactiveCommand<Unit>();
public ReactiveCommand<Unit> ResetFilterCommand3 { get; } = new ReactiveCommand<Unit>();
public ViewModel()
{ {
observableList.Add(1); ThreadPool.QueueUserWorkItem(_ =>
observableList.Add(2);
var view = observableList.CreateView(x => x);
//ItemsView = view.ToNotifyCollectionChanged();
ItemsView = observableList.ToNotifyCollectionChanged();
// check for optimize list
// ItemsView = observableList.ToNotifyCollectionChanged(SynchronizationContextCollectionEventDispatcher.Current);
AddCommand.Subscribe(_ =>
{ {
// ThreadPool.QueueUserWorkItem(_ => list.Add(adder++);
{
observableList.Add(Random.Shared.Next());
}
});
AddRangeCommand.Subscribe(_ =>
{
var xs = Enumerable.Range(1, 5).Select(_ => Random.Shared.Next()).ToArray();
observableList.AddRange(xs);
});
InsertAtRandomCommand.Subscribe(_ =>
{
var from = Random.Shared.Next(0, view.Count);
observableList.Insert(from, Random.Shared.Next());
});
RemoveAtRandomCommand.Subscribe(_ =>
{
var from = Random.Shared.Next(0, view.Count);
observableList.RemoveAt(from);
});
RemoveRangeCommand.Subscribe(_ =>
{
observableList.RemoveRange(2, 5);
});
ClearCommand.Subscribe(_ =>
{
observableList.Clear();
});
ReverseCommand.Subscribe(_ =>
{
observableList.Reverse();
});
SortCommand.Subscribe(_ =>
{
observableList.Sort();
});
AttachFilterCommand.Subscribe(_ =>
{
view.AttachFilter(x => x % 2 == 0);
});
ResetFilterCommand.Subscribe(_ =>
{
view.ResetFilter();
});
SourceList.Add(new() { Name = "a", Age = 1 });
SourceList.Add(new() { Name = "b", Age = 2 });
SourceList.Add(new() { Name = "c", Age = 3 });
SourceList.Add(new() { Name = "d", Age = 4 });
//NotWritable, NonFilter
NotWritableNonFilterView = SourceList.ToNotifyCollectionChanged();
//NotWritable, Filter
notWritableFilter = SourceList.CreateView(x => x);
NotWritableFilterView = notWritableFilter.ToNotifyCollectionChanged();
//Writable, NonFilter
WritableNonFilterPersonView = SourceList.ToWritableNotifyCollectionChanged();
//WritableNonFilterPersonView = SourceList.ToWritableNotifyCollectionChanged(x => x, (Person newView, Person original, ref bool setValue) =>
//{
// if (setValue)
// {
// // default setValue == true is Set operation
// original.Name = newView.Name;
// original.Age = newView.Age;
// // You can modify setValue to false, it does not set original collection to new value.
// // For mutable reference types, when there is only a single,
// // bound View and to avoid recreating the View, setting false is effective.
// // Otherwise, keeping it true will set the value in the original collection as well,
// // and change notifications will be sent to lower-level Views(the delegate for View generation will also be called anew).
// setValue = false;
// return original;
// }
// else
// {
// // default setValue == false is Add operation
// return new Person { Age = newView.Age, Name = newView.Name };
// }
//}, null);
//Writable, Filter
writableFilter = SourceList.CreateWritableView(x => x);
WritableFilterPersonView = writableFilter.ToWritableNotifyCollectionChanged();
//WritableFilterPersonView = writableFilter.ToWritableNotifyCollectionChanged((Person newView, Person original, ref bool setValue) =>
//{
// if (setValue)
// {
// // default setValue == true is Set operation
// original.Name = newView.Name;
// original.Age = newView.Age;
// // You can modify setValue to false, it does not set original collection to new value.
// // For mutable reference types, when there is only a single,
// // bound View and to avoid recreating the View, setting false is effective.
// // Otherwise, keeping it true will set the value in the original collection as well,
// // and change notifications will be sent to lower-level Views(the delegate for View generation will also be called anew).
// setValue = false;
// return original;
// }
// else
// {
// // default setValue == false is Add operation
// return new Person { Age = newView.Age, Name = newView.Name };
// }
//});
AttachFilterCommand2.Subscribe(_ =>
{
notWritableFilter.AttachFilter(x => x.Age % 2 == 0);
});
ResetFilterCommand2.Subscribe(_ =>
{
notWritableFilter.ResetFilter();
});
AttachFilterCommand3.Subscribe(_ =>
{
writableFilter.AttachFilter(x => x.Age % 2 == 0);
});
ResetFilterCommand3.Subscribe(_ =>
{
writableFilter.ResetFilter();
}); });
} }
}
public class Person
{
public int? Age { get; set; }
public string? Name { get; set; }
}
public class WpfDispatcherCollection(Dispatcher dispatcher) : ICollectionEventDispatcher protected override void OnClosed(EventArgs e)
{
public void Post(CollectionEventDispatcherEventArgs ev)
{ {
dispatcher.InvokeAsync(() => ItemsView.Dispose();
{
ev.Invoke();
});
} }
} }
} }

View File

@ -1,21 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<OutputType>WinExe</OutputType> <OutputType>WinExe</OutputType>
<TargetFramework>net6.0-windows</TargetFramework> <TargetFramework>net6.0-windows</TargetFramework>
<LangVersion>12</LangVersion> <Nullable>enable</Nullable>
<Nullable>enable</Nullable> <UseWPF>true</UseWPF>
<UseWPF>true</UseWPF> <IsPackable>false</IsPackable>
<IsPackable>false</IsPackable> </PropertyGroup>
<EnableWindowsTargeting>true</EnableWindowsTargeting>
</PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="R3Extensions.WPF" Version="1.0.4" /> <ProjectReference Include="..\..\src\ObservableCollections\ObservableCollections.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\ObservableCollections\ObservableCollections.csproj" />
</ItemGroup>
</Project> </Project>

View File

@ -1,509 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics.Tracing;
using System.Runtime.InteropServices;
using System.Threading;
using R3;
namespace ObservableCollections;
[StructLayout(LayoutKind.Auto)]
public readonly record struct ViewChangedEvent<T, TView>
{
public readonly NotifyCollectionChangedAction Action;
public readonly (T Value, TView View) NewItem;
public readonly (T Value, TView View) OldItem;
public readonly int NewStartingIndex;
public readonly int OldStartingIndex;
public readonly SortOperation<T> SortOperation;
public ViewChangedEvent(NotifyCollectionChangedAction action, (T, TView) newItem, (T, TView) oldItem, int newStartingIndex, int oldStartingIndex, SortOperation<T> sortOperation)
{
Action = action;
NewItem = newItem;
OldItem = oldItem;
NewStartingIndex = newStartingIndex;
OldStartingIndex = oldStartingIndex;
SortOperation = sortOperation;
}
}
[StructLayout(LayoutKind.Auto)]
public readonly record struct RejectedViewChangedEvent
{
public readonly RejectedViewChangedAction Action;
public readonly int NewIndex;
public readonly int OldIndex;
public RejectedViewChangedEvent(RejectedViewChangedAction action, int newIndex, int oldIndex)
{
Action = action;
NewIndex = newIndex;
OldIndex = oldIndex;
}
}
public static partial class ObservableCollectionR3Extensions
{
public static Observable<RejectedViewChangedEvent> ObserveRejected<T, TView>(this ISynchronizedView<T, TView> source, CancellationToken cancellationToken = default)
{
return new SynchronizedViewRejected<T, TView>(source, cancellationToken);
}
public static Observable<ViewChangedEvent<T, TView>> ObserveChanged<T, TView>(this ISynchronizedView<T, TView> source, CancellationToken cancellationToken = default)
{
return new SynchronizedViewChanged<T, TView>(source, cancellationToken);
}
public static Observable<CollectionAddEvent<(T Value, TView View)>> ObserveAdd<T, TView>(this ISynchronizedView<T, TView> source, CancellationToken cancellationToken = default)
{
return new SynchronizedViewAdd<T, TView>(source, cancellationToken);
}
public static Observable<CollectionRemoveEvent<(T Value, TView View)>> ObserveRemove<T, TView>(this ISynchronizedView<T, TView> source, CancellationToken cancellationToken = default)
{
return new SynchronizedViewRemove<T, TView>(source, cancellationToken);
}
public static Observable<CollectionReplaceEvent<(T Value, TView View)>> ObserveReplace<T, TView>(this ISynchronizedView<T, TView> source, CancellationToken cancellationToken = default)
{
return new SynchronizedViewReplace<T, TView>(source, cancellationToken);
}
public static Observable<CollectionMoveEvent<(T Value, TView View)>> ObserveMove<T, TView>(this ISynchronizedView<T, TView> source, CancellationToken cancellationToken = default)
{
return new SynchronizedViewMove<T, TView>(source, cancellationToken);
}
public static Observable<CollectionResetEvent<T>> ObserveReset<T, TView>(this ISynchronizedView<T, TView> source, CancellationToken cancellationToken = default)
{
return new SynchronizedViewReset<T, TView>(source, cancellationToken);
}
public static Observable<Unit> ObserveClear<T, TView>(this ISynchronizedView<T, TView> source, CancellationToken cancellationToken = default)
{
return new SynchronizedViewClear<T, TView>(source, cancellationToken);
}
public static Observable<(int Index, int Count)> ObserveReverse<T, TView>(this ISynchronizedView<T, TView> source, CancellationToken cancellationToken = default)
{
return new SynchronizedViewReverse<T, TView>(source, cancellationToken);
}
public static Observable<(int Index, int Count, IComparer<T>? Comparer)> ObserveSort<T, TView>(this ISynchronizedView<T, TView> source, CancellationToken cancellationToken = default)
{
return new SynchronizedViewSort<T, TView>(source, cancellationToken);
}
public static Observable<int> ObserveCountChanged<T, TView>(this ISynchronizedView<T, TView> source, bool notifyCurrentCount = false, CancellationToken cancellationToken = default)
{
return new SynchronizedViewCountChanged<T, TView>(source, notifyCurrentCount, cancellationToken);
}
}
sealed class SynchronizedViewChanged<T, TView>(ISynchronizedView<T, TView> source, CancellationToken cancellationToken)
: Observable<ViewChangedEvent<T, TView>>
{
protected override IDisposable SubscribeCore(Observer<ViewChangedEvent<T, TView>> observer)
{
return new _SynchronizedViewChanged(source, observer, cancellationToken);
}
sealed class _SynchronizedViewChanged(
ISynchronizedView<T, TView> source,
Observer<ViewChangedEvent<T, TView>> observer,
CancellationToken cancellationToken)
: SynchronizedViewObserverBase<T, TView, ViewChangedEvent<T, TView>>(source, observer, cancellationToken)
{
protected override void Handler(in SynchronizedViewChangedEventArgs<T, TView> eventArgs)
{
if (eventArgs.IsSingleItem)
{
var newArgs = new ViewChangedEvent<T, TView>(
eventArgs.Action,
eventArgs.NewItem,
eventArgs.OldItem,
eventArgs.NewStartingIndex,
eventArgs.OldStartingIndex,
eventArgs.SortOperation);
observer.OnNext(newArgs);
}
else
{
if (eventArgs.Action == NotifyCollectionChangedAction.Add)
{
var index = eventArgs.NewStartingIndex;
for (int i = 0; i < eventArgs.NewValues.Length; i++)
{
var newItem = (eventArgs.NewValues[i], eventArgs.NewViews[i]);
var newArgs = new ViewChangedEvent<T, TView>(
eventArgs.Action,
newItem,
default,
index++,
eventArgs.OldStartingIndex,
eventArgs.SortOperation);
observer.OnNext(newArgs);
}
}
else if (eventArgs.Action == NotifyCollectionChangedAction.Remove)
{
for (int i = 0; i < eventArgs.OldValues.Length; i++)
{
var oldItem = (eventArgs.OldValues[i], eventArgs.OldViews[i]);
var newArgs = new ViewChangedEvent<T, TView>(
eventArgs.Action,
default,
oldItem,
eventArgs.NewStartingIndex,
eventArgs.OldStartingIndex, // removed, uses same index
eventArgs.SortOperation);
observer.OnNext(newArgs);
}
}
}
}
}
}
sealed class SynchronizedViewAdd<T, TView>(ISynchronizedView<T, TView> source, CancellationToken cancellationToken)
: Observable<CollectionAddEvent<(T, TView)>>
{
protected override IDisposable SubscribeCore(Observer<CollectionAddEvent<(T, TView)>> observer)
{
return new _SynchronizedViewAdd(source, observer, cancellationToken);
}
sealed class _SynchronizedViewAdd(
ISynchronizedView<T, TView> source,
Observer<CollectionAddEvent<(T, TView)>> observer,
CancellationToken cancellationToken)
: SynchronizedViewObserverBase<T, TView, CollectionAddEvent<(T, TView)>>(source, observer, cancellationToken)
{
protected override void Handler(in SynchronizedViewChangedEventArgs<T, TView> eventArgs)
{
if (eventArgs.Action == NotifyCollectionChangedAction.Add)
{
if (eventArgs.IsSingleItem)
{
observer.OnNext(new CollectionAddEvent<(T, TView)>(eventArgs.NewStartingIndex, eventArgs.NewItem));
}
else
{
var index = eventArgs.NewStartingIndex;
for (int i = 0; i < eventArgs.NewValues.Length; i++)
{
observer.OnNext(new CollectionAddEvent<(T, TView)>(index++, (eventArgs.NewValues[i], eventArgs.NewViews[i])));
}
}
}
}
}
}
sealed class SynchronizedViewRemove<T, TView>(ISynchronizedView<T, TView> source, CancellationToken cancellationToken)
: Observable<CollectionRemoveEvent<(T, TView)>>
{
protected override IDisposable SubscribeCore(Observer<CollectionRemoveEvent<(T, TView)>> observer)
{
return new _SynchronizedViewRemove(source, observer, cancellationToken);
}
sealed class _SynchronizedViewRemove(
ISynchronizedView<T, TView> source,
Observer<CollectionRemoveEvent<(T, TView)>> observer,
CancellationToken cancellationToken)
: SynchronizedViewObserverBase<T, TView, CollectionRemoveEvent<(T, TView)>>(source, observer, cancellationToken)
{
protected override void Handler(in SynchronizedViewChangedEventArgs<T, TView> eventArgs)
{
if (eventArgs.Action == NotifyCollectionChangedAction.Remove)
{
if (eventArgs.IsSingleItem)
{
observer.OnNext(new CollectionRemoveEvent<(T, TView)>(eventArgs.OldStartingIndex, eventArgs.OldItem));
}
else
{
for (int i = 0; i < eventArgs.OldValues.Length; i++)
{
observer.OnNext(new CollectionRemoveEvent<(T, TView)>(eventArgs.OldStartingIndex, (eventArgs.OldValues[i], eventArgs.OldViews[i])));
}
}
}
}
}
}
sealed class SynchronizedViewReplace<T, TView>(ISynchronizedView<T, TView> source, CancellationToken cancellationToken)
: Observable<CollectionReplaceEvent<(T, TView)>>
{
protected override IDisposable SubscribeCore(Observer<CollectionReplaceEvent<(T, TView)>> observer)
{
return new _SynchronizedViewReplace(source, observer, cancellationToken);
}
sealed class _SynchronizedViewReplace(
ISynchronizedView<T, TView> source,
Observer<CollectionReplaceEvent<(T, TView)>> observer,
CancellationToken cancellationToken)
: SynchronizedViewObserverBase<T, TView, CollectionReplaceEvent<(T, TView)>>(source, observer, cancellationToken)
{
protected override void Handler(in SynchronizedViewChangedEventArgs<T, TView> eventArgs)
{
if (eventArgs.Action == NotifyCollectionChangedAction.Replace)
{
observer.OnNext(new CollectionReplaceEvent<(T, TView)>(eventArgs.NewStartingIndex, eventArgs.OldItem, eventArgs.NewItem));
}
}
}
}
sealed class SynchronizedViewMove<T, TView>(ISynchronizedView<T, TView> source, CancellationToken cancellationToken)
: Observable<CollectionMoveEvent<(T, TView)>>
{
protected override IDisposable SubscribeCore(Observer<CollectionMoveEvent<(T, TView)>> observer)
{
return new _SynchronizedViewMove(source, observer, cancellationToken);
}
sealed class _SynchronizedViewMove(
ISynchronizedView<T, TView> source,
Observer<CollectionMoveEvent<(T, TView)>> observer,
CancellationToken cancellationToken)
: SynchronizedViewObserverBase<T, TView, CollectionMoveEvent<(T, TView)>>(source, observer, cancellationToken)
{
protected override void Handler(in SynchronizedViewChangedEventArgs<T, TView> eventArgs)
{
if (eventArgs.Action == NotifyCollectionChangedAction.Move)
{
observer.OnNext(new CollectionMoveEvent<(T, TView)>(eventArgs.OldStartingIndex, eventArgs.NewStartingIndex, eventArgs.NewItem));
}
}
}
}
sealed class SynchronizedViewReset<T, TView>(ISynchronizedView<T, TView> source, CancellationToken cancellationToken)
: Observable<CollectionResetEvent<T>>
{
protected override IDisposable SubscribeCore(Observer<CollectionResetEvent<T>> observer)
{
return new _SynchronizedViewReset(source, observer, cancellationToken);
}
sealed class _SynchronizedViewReset(
ISynchronizedView<T, TView> source,
Observer<CollectionResetEvent<T>> observer,
CancellationToken cancellationToken)
: SynchronizedViewObserverBase<T, TView, CollectionResetEvent<T>>(source, observer, cancellationToken)
{
protected override void Handler(in SynchronizedViewChangedEventArgs<T, TView> eventArgs)
{
if (eventArgs.Action == NotifyCollectionChangedAction.Reset)
{
observer.OnNext(new CollectionResetEvent<T>(eventArgs.SortOperation));
}
}
}
}
sealed class SynchronizedViewClear<T, TView>(ISynchronizedView<T, TView> source, CancellationToken cancellationToken)
: Observable<Unit>
{
protected override IDisposable SubscribeCore(Observer<Unit> observer)
{
return new _SynchronizedViewClear(source, observer, cancellationToken);
}
sealed class _SynchronizedViewClear(
ISynchronizedView<T, TView> source,
Observer<Unit> observer,
CancellationToken cancellationToken)
: SynchronizedViewObserverBase<T, TView, Unit>(source, observer, cancellationToken)
{
protected override void Handler(in SynchronizedViewChangedEventArgs<T, TView> eventArgs)
{
if (eventArgs.Action == NotifyCollectionChangedAction.Reset && eventArgs.SortOperation.IsClear)
{
observer.OnNext(Unit.Default);
}
}
}
}
sealed class SynchronizedViewReverse<T, TView>(ISynchronizedView<T, TView> source, CancellationToken cancellationToken)
: Observable<(int Index, int Count)>
{
protected override IDisposable SubscribeCore(Observer<(int Index, int Count)> observer)
{
return new _SynchronizedViewReverse(source, observer, cancellationToken);
}
sealed class _SynchronizedViewReverse(
ISynchronizedView<T, TView> source,
Observer<(int Index, int Count)> observer,
CancellationToken cancellationToken)
: SynchronizedViewObserverBase<T, TView, (int Index, int Count)>(source, observer, cancellationToken)
{
protected override void Handler(in SynchronizedViewChangedEventArgs<T, TView> eventArgs)
{
if (eventArgs.Action == NotifyCollectionChangedAction.Reset && eventArgs.SortOperation.IsReverse)
{
observer.OnNext((eventArgs.SortOperation.Index, eventArgs.SortOperation.Count));
}
}
}
}
sealed class SynchronizedViewSort<T, TView>(ISynchronizedView<T, TView> source, CancellationToken cancellationToken)
: Observable<(int Index, int Count, IComparer<T>? Comparer)>
{
protected override IDisposable SubscribeCore(Observer<(int Index, int Count, IComparer<T>? Comparer)> observer)
{
return new _SynchronizedViewSort(source, observer, cancellationToken);
}
sealed class _SynchronizedViewSort(
ISynchronizedView<T, TView> source,
Observer<(int Index, int Count, IComparer<T>? Comparer)> observer,
CancellationToken cancellationToken)
: SynchronizedViewObserverBase<T, TView, (int Index, int Count, IComparer<T>? Comparer)>(source, observer, cancellationToken)
{
protected override void Handler(in SynchronizedViewChangedEventArgs<T, TView> eventArgs)
{
if (eventArgs.Action == NotifyCollectionChangedAction.Reset && eventArgs.SortOperation.IsSort)
{
observer.OnNext(eventArgs.SortOperation.AsTuple());
}
}
}
}
sealed class SynchronizedViewCountChanged<T, TView>(ISynchronizedView<T, TView> source, bool notifyCurrentCount, CancellationToken cancellationToken)
: Observable<int>
{
protected override IDisposable SubscribeCore(Observer<int> observer)
{
return new _SynchronizedViewCountChanged(source, notifyCurrentCount, observer, cancellationToken);
}
sealed class _SynchronizedViewCountChanged : SynchronizedViewObserverBase<T, TView, int>
{
int countPrev;
public _SynchronizedViewCountChanged(
ISynchronizedView<T, TView> source,
bool notifyCurrentCount,
Observer<int> observer,
CancellationToken cancellationToken) : base(source, observer, cancellationToken)
{
this.countPrev = source.Count;
if (notifyCurrentCount)
{
observer.OnNext(source.Count);
}
}
protected override void Handler(in SynchronizedViewChangedEventArgs<T, TView> eventArgs)
{
switch (eventArgs.Action)
{
case NotifyCollectionChangedAction.Add:
case NotifyCollectionChangedAction.Remove:
case NotifyCollectionChangedAction.Reset when countPrev != source.Count:
observer.OnNext(source.Count);
break;
}
countPrev = source.Count;
}
}
}
sealed class SynchronizedViewRejected<T, TView>(ISynchronizedView<T, TView> source, CancellationToken cancellationToken)
: Observable<RejectedViewChangedEvent>
{
protected override IDisposable SubscribeCore(Observer<RejectedViewChangedEvent> observer)
{
return new _SynchronizedViewRejected(source, observer, cancellationToken);
}
sealed class _SynchronizedViewRejected : IDisposable
{
readonly ISynchronizedView<T, TView> source;
readonly Observer<RejectedViewChangedEvent> observer;
readonly CancellationTokenRegistration cancellationTokenRegistration;
readonly Action<RejectedViewChangedAction, int, int> handlerDelegate;
public _SynchronizedViewRejected(ISynchronizedView<T, TView> source, Observer<RejectedViewChangedEvent> observer, CancellationToken cancellationToken)
{
this.source = source;
this.observer = observer;
this.handlerDelegate = Handler;
source.RejectedViewChanged += handlerDelegate;
if (cancellationToken.CanBeCanceled)
{
cancellationTokenRegistration = cancellationToken.UnsafeRegister(static state =>
{
var s = (_SynchronizedViewRejected)state!;
s.observer.OnCompleted();
s.Dispose();
}, this);
}
}
public void Dispose()
{
source.RejectedViewChanged -= handlerDelegate;
cancellationTokenRegistration.Dispose();
}
void Handler(RejectedViewChangedAction rejectedViewChangedAction, int newIndex, int oldIndex)
{
observer.OnNext(new RejectedViewChangedEvent(rejectedViewChangedAction, newIndex, oldIndex));
}
}
}
abstract class SynchronizedViewObserverBase<T, TView, TEvent> : IDisposable
{
protected readonly ISynchronizedView<T, TView> source;
protected readonly Observer<TEvent> observer;
readonly CancellationTokenRegistration cancellationTokenRegistration;
readonly NotifyViewChangedEventHandler<T, TView> handlerDelegate;
public SynchronizedViewObserverBase(ISynchronizedView<T, TView> source, Observer<TEvent> observer, CancellationToken cancellationToken)
{
this.source = source;
this.observer = observer;
this.handlerDelegate = Handler;
source.ViewChanged += handlerDelegate;
if (cancellationToken.CanBeCanceled)
{
cancellationTokenRegistration = cancellationToken.UnsafeRegister(static state =>
{
var s = (SynchronizedViewObserverBase<T, TView, TEvent>)state!;
s.observer.OnCompleted();
s.Dispose();
}, this);
}
}
public void Dispose()
{
source.ViewChanged -= handlerDelegate;
cancellationTokenRegistration.Dispose();
}
protected abstract void Handler(in SynchronizedViewChangedEventArgs<T, TView> eventArgs);
}

View File

@ -1,587 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Runtime.InteropServices;
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 readonly record struct CollectionResetEvent<T>
{
readonly SortOperation<T> sortOperation;
public bool IsClear => sortOperation.IsClear;
public bool IsSort => sortOperation.IsSort;
public bool IsReverse => sortOperation.IsReverse;
public int Index => sortOperation.Index;
public int Count => sortOperation.Count;
public IComparer<T>? Comparer => sortOperation.Comparer;
public CollectionResetEvent(SortOperation<T> sortOperation)
{
this.sortOperation = sortOperation;
}
}
[StructLayout(LayoutKind.Auto)]
public readonly record struct CollectionChangedEvent<T>
{
public readonly NotifyCollectionChangedAction Action;
public readonly T NewItem;
public readonly T OldItem;
public readonly int NewStartingIndex;
public readonly int OldStartingIndex;
public readonly SortOperation<T> SortOperation;
public CollectionChangedEvent(NotifyCollectionChangedAction action, T newItem, T oldItem, int newStartingIndex, int oldStartingIndex, SortOperation<T> sortOperation)
{
Action = action;
NewItem = newItem;
OldItem = oldItem;
NewStartingIndex = newStartingIndex;
OldStartingIndex = oldStartingIndex;
SortOperation = sortOperation;
}
}
public readonly record struct DictionaryAddEvent<TKey, TValue>(TKey Key, TValue Value);
public readonly record struct DictionaryRemoveEvent<TKey, TValue>(TKey Key, TValue Value);
public readonly record struct DictionaryReplaceEvent<TKey, TValue>(TKey Key, TValue OldValue, TValue NewValue);
public static partial class ObservableCollectionR3Extensions
{
public static Observable<CollectionChangedEvent<T>> ObserveChanged<T>(this IObservableCollection<T> source, CancellationToken cancellationToken = default)
{
return new ObservableCollectionChanged<T>(source, cancellationToken);
}
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<CollectionResetEvent<T>> ObserveReset<T>(this IObservableCollection<T> source, CancellationToken cancellationToken = default)
{
return new ObservableCollectionReset<T>(source, cancellationToken);
}
public static Observable<Unit> ObserveClear<T>(this IObservableCollection<T> source, CancellationToken cancellationToken = default)
{
return new ObservableCollectionClear<T>(source, cancellationToken);
}
public static Observable<(int Index, int Count)> ObserveReverse<T>(this IObservableCollection<T> source, CancellationToken cancellationToken = default)
{
return new ObservableCollectionReverse<T>(source, cancellationToken);
}
public static Observable<(int Index, int Count, IComparer<T>? Comparer)> ObserveSort<T>(this IObservableCollection<T> source, CancellationToken cancellationToken = default)
{
return new ObservableCollectionSort<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);
}
}
public static class ObservableDictionaryR3Extensions
{
public static Observable<DictionaryAddEvent<TKey, TValue>> ObserveDictionaryAdd<TKey, TValue>(this IReadOnlyObservableDictionary<TKey, TValue> source,
CancellationToken cancellationToken = default)
{
return new ObservableDictionaryAdd<TKey, TValue>(source, cancellationToken);
}
public static Observable<DictionaryRemoveEvent<TKey, TValue>> ObserveDictionaryRemove<TKey, TValue>(this IReadOnlyObservableDictionary<TKey, TValue> source,
CancellationToken cancellationToken = default)
{
return new ObservableDictionaryRemove<TKey, TValue>(source, cancellationToken);
}
public static Observable<DictionaryReplaceEvent<TKey, TValue>> ObserveDictionaryReplace<TKey, TValue>(this IReadOnlyObservableDictionary<TKey, TValue> source,
CancellationToken cancellationToken = default)
{
return new ObservableDictionaryReplace<TKey, TValue>(source, cancellationToken);
}
}
sealed class ObservableCollectionChanged<T>(IObservableCollection<T> collection, CancellationToken cancellationToken)
: Observable<CollectionChangedEvent<T>>
{
protected override IDisposable SubscribeCore(Observer<CollectionChangedEvent<T>> observer)
{
return new _ObservableCollectionAdd(collection, observer, cancellationToken);
}
sealed class _ObservableCollectionAdd(
IObservableCollection<T> collection,
Observer<CollectionChangedEvent<T>> observer,
CancellationToken cancellationToken)
: ObservableCollectionObserverBase<T, CollectionChangedEvent<T>>(collection, observer, cancellationToken)
{
protected override void Handler(in NotifyCollectionChangedEventArgs<T> eventArgs)
{
if (eventArgs.IsSingleItem)
{
var newArgs = new CollectionChangedEvent<T>(
eventArgs.Action,
eventArgs.NewItem,
eventArgs.OldItem,
eventArgs.NewStartingIndex,
eventArgs.OldStartingIndex,
eventArgs.SortOperation);
observer.OnNext(newArgs);
}
else
{
if (eventArgs.Action == NotifyCollectionChangedAction.Add)
{
var i = eventArgs.NewStartingIndex;
foreach (var item in eventArgs.NewItems)
{
var newArgs = new CollectionChangedEvent<T>(
eventArgs.Action,
item,
eventArgs.OldItem,
newStartingIndex: i,
eventArgs.OldStartingIndex,
eventArgs.SortOperation);
if (eventArgs.NewStartingIndex != -1) i++;
observer.OnNext(newArgs);
}
}
else if (eventArgs.Action == NotifyCollectionChangedAction.Remove)
{
foreach (var item in eventArgs.OldItems)
{
var newArgs = new CollectionChangedEvent<T>(
eventArgs.Action,
eventArgs.NewItem,
item,
eventArgs.NewStartingIndex,
eventArgs.OldStartingIndex, // removed, uses same index
eventArgs.SortOperation);
observer.OnNext(newArgs);
}
}
}
}
}
}
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
{
foreach (var item in eventArgs.OldItems)
{
observer.OnNext(new CollectionRemoveEvent<T>(eventArgs.OldStartingIndex, item)); // remove uses same index
}
}
}
}
}
}
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<CollectionResetEvent<T>>
{
protected override IDisposable SubscribeCore(Observer<CollectionResetEvent<T>> observer)
{
return new _ObservableCollectionReset(collection, observer, cancellationToken);
}
sealed class _ObservableCollectionReset(
IObservableCollection<T> collection,
Observer<CollectionResetEvent<T>> observer,
CancellationToken cancellationToken)
: ObservableCollectionObserverBase<T, CollectionResetEvent<T>>(collection, observer, cancellationToken)
{
protected override void Handler(in NotifyCollectionChangedEventArgs<T> eventArgs)
{
if (eventArgs.Action == NotifyCollectionChangedAction.Reset)
{
observer.OnNext(new CollectionResetEvent<T>(eventArgs.SortOperation));
}
}
}
}
sealed class ObservableCollectionClear<T>(IObservableCollection<T> collection, CancellationToken cancellationToken)
: Observable<Unit>
{
protected override IDisposable SubscribeCore(Observer<Unit> observer)
{
return new _ObservableCollectionClear(collection, observer, cancellationToken);
}
sealed class _ObservableCollectionClear(
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 && eventArgs.SortOperation.IsClear)
{
observer.OnNext(Unit.Default);
}
}
}
}
sealed class ObservableCollectionReverse<T>(IObservableCollection<T> collection, CancellationToken cancellationToken) : Observable<(int Index, int Count)>
{
protected override IDisposable SubscribeCore(Observer<(int Index, int Count)> observer)
{
return new _ObservableCollectionReverse(collection, observer, cancellationToken);
}
sealed class _ObservableCollectionReverse(
IObservableCollection<T> collection,
Observer<(int Index, int Count)> observer,
CancellationToken cancellationToken)
: ObservableCollectionObserverBase<T, (int Index, int Count)>(collection, observer, cancellationToken)
{
protected override void Handler(in NotifyCollectionChangedEventArgs<T> eventArgs)
{
if (eventArgs.Action == NotifyCollectionChangedAction.Reset && eventArgs.SortOperation.IsReverse)
{
observer.OnNext((eventArgs.SortOperation.Index, eventArgs.SortOperation.Count));
}
}
}
}
sealed class ObservableCollectionSort<T>(IObservableCollection<T> collection, CancellationToken cancellationToken) : Observable<(int Index, int Count, IComparer<T>? Comparer)>
{
protected override IDisposable SubscribeCore(Observer<(int Index, int Count, IComparer<T>? Comparer)> observer)
{
return new _ObservableCollectionSort(collection, observer, cancellationToken);
}
sealed class _ObservableCollectionSort(
IObservableCollection<T> collection,
Observer<(int Index, int Count, IComparer<T>? Comparer)> observer,
CancellationToken cancellationToken)
: ObservableCollectionObserverBase<T, (int Index, int Count, IComparer<T>? Comparer)>(collection, observer, cancellationToken)
{
protected override void Handler(in NotifyCollectionChangedEventArgs<T> eventArgs)
{
if (eventArgs.Action == NotifyCollectionChangedAction.Reset && eventArgs.SortOperation.IsSort)
{
observer.OnNext(eventArgs.SortOperation.AsTuple());
}
}
}
}
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>
{
int countPrev;
public _ObservableCollectionCountChanged(
IObservableCollection<T> collection,
bool notifyCurrentCount,
Observer<int> observer,
CancellationToken cancellationToken) : base(collection, observer, cancellationToken)
{
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;
}
}
}
sealed class ObservableDictionaryAdd<TKey, TValue>(
IReadOnlyObservableDictionary<TKey, TValue> dictionary,
CancellationToken cancellationToken) : Observable<DictionaryAddEvent<TKey, TValue>>
{
protected override IDisposable SubscribeCore(Observer<DictionaryAddEvent<TKey, TValue>> observer)
{
return new _DictionaryCollectionAdd(dictionary, observer, cancellationToken);
}
sealed class _DictionaryCollectionAdd(
IObservableCollection<KeyValuePair<TKey, TValue>> collection,
Observer<DictionaryAddEvent<TKey, TValue>> observer,
CancellationToken cancellationToken) :
ObservableCollectionObserverBase<KeyValuePair<TKey, TValue>, DictionaryAddEvent<TKey, TValue>>(collection,
observer, cancellationToken)
{
protected override void Handler(in NotifyCollectionChangedEventArgs<KeyValuePair<TKey, TValue>> eventArgs)
{
if (eventArgs.Action == NotifyCollectionChangedAction.Add)
{
if (eventArgs.IsSingleItem)
{
observer.OnNext(
new DictionaryAddEvent<TKey, TValue>(eventArgs.NewItem.Key, eventArgs.NewItem.Value));
}
else
{
var i = eventArgs.NewStartingIndex;
foreach (var item in eventArgs.NewItems)
{
observer.OnNext(new DictionaryAddEvent<TKey, TValue>(item.Key, item.Value));
}
}
}
}
}
}
sealed class ObservableDictionaryRemove<TKey, TValue>(
IReadOnlyObservableDictionary<TKey, TValue> dictionary,
CancellationToken cancellationToken) : Observable<DictionaryRemoveEvent<TKey, TValue>>
{
protected override IDisposable SubscribeCore(Observer<DictionaryRemoveEvent<TKey, TValue>> observer)
{
return new _DictionaryCollectionRemove(dictionary, observer, cancellationToken);
}
sealed class _DictionaryCollectionRemove(
IObservableCollection<KeyValuePair<TKey, TValue>> collection,
Observer<DictionaryRemoveEvent<TKey, TValue>> observer,
CancellationToken cancellationToken) :
ObservableCollectionObserverBase<KeyValuePair<TKey, TValue>, DictionaryRemoveEvent<TKey, TValue>>(collection,
observer, cancellationToken)
{
protected override void Handler(in NotifyCollectionChangedEventArgs<KeyValuePair<TKey, TValue>> eventArgs)
{
if (eventArgs.Action == NotifyCollectionChangedAction.Remove)
{
if (eventArgs.IsSingleItem)
{
observer.OnNext(
new DictionaryRemoveEvent<TKey, TValue>(eventArgs.OldItem.Key, eventArgs.OldItem.Value));
}
else
{
var i = eventArgs.NewStartingIndex;
foreach (var item in eventArgs.NewItems)
{
observer.OnNext(new DictionaryRemoveEvent<TKey, TValue>(item.Key, item.Value));
}
}
}
}
}
}
sealed class ObservableDictionaryReplace<TKey, TValue>(
IReadOnlyObservableDictionary<TKey, TValue> dictionary,
CancellationToken cancellationToken) : Observable<DictionaryReplaceEvent<TKey, TValue>>
{
protected override IDisposable SubscribeCore(Observer<DictionaryReplaceEvent<TKey, TValue>> observer)
{
return new _DictionaryCollectionReplace(dictionary, observer, cancellationToken);
}
sealed class _DictionaryCollectionReplace(
IObservableCollection<KeyValuePair<TKey, TValue>> collection,
Observer<DictionaryReplaceEvent<TKey, TValue>> observer,
CancellationToken cancellationToken) :
ObservableCollectionObserverBase<KeyValuePair<TKey, TValue>, DictionaryReplaceEvent<TKey, TValue>>(collection,
observer, cancellationToken)
{
protected override void Handler(in NotifyCollectionChangedEventArgs<KeyValuePair<TKey, TValue>> eventArgs)
{
if (eventArgs.Action == NotifyCollectionChangedAction.Replace)
{
observer.OnNext(new DictionaryReplaceEvent<TKey, TValue>(
eventArgs.NewItem.Key,
eventArgs.OldItem.Value,
eventArgs.NewItem.Value));
}
}
}
}
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

@ -1,32 +0,0 @@
<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>
<!-- NuGet Packaging -->
<PackageTags>collection</PackageTags>
<Description>R3 Extensions of ObservableCollections.</Description>
<SignAssembly>true</SignAssembly>
<IsPackable>true</IsPackable>
</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="1.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ObservableCollections\ObservableCollections.csproj" />
</ItemGroup>
<ItemGroup>
<None Include="../../Icon.png" Pack="true" PackagePath="/" />
<EmbeddedResource Include="..\..\LICENSE" />
</ItemGroup>
</Project>

View File

@ -1,13 +0,0 @@
#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,8 @@
fileFormatVersion: 2
guid: 23bde05b5e9d4d049a38c4edf79f9ac6
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,78 @@
#if UNITY_EDITOR
using System;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEngine;
public static class PackageExporter
{
[MenuItem("Tools/Export Unitypackage")]
public static void Export()
{
var roots = new[] { "Plugins/ObservableCollections" };
foreach (var root in roots)
{
var version = GetVersion(root);
var fn = root.Split('/').Last();
var fileName = string.IsNullOrEmpty(version) ? $"{fn}.unitypackage" : $"{fn}.{version}.unitypackage";
var exportPath = "./" + fileName;
var path = Path.Combine(Application.dataPath, root);
var assets = Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories)
.Where(x => Path.GetExtension(x) == ".cs" || Path.GetExtension(x) == ".asmdef" || Path.GetExtension(x) == ".json" || Path.GetExtension(x) == ".meta")
.Select(x => "Assets" + x.Replace(Application.dataPath, "").Replace(@"\", "/"))
.ToArray();
UnityEngine.Debug.Log("Export below files" + Environment.NewLine + string.Join(Environment.NewLine, assets));
AssetDatabase.ExportPackage(
assets,
exportPath,
ExportPackageOptions.Default);
UnityEngine.Debug.Log("Export complete: " + Path.GetFullPath(exportPath));
}
}
static string GetVersion(string root)
{
var version = Environment.GetEnvironmentVariable("UNITY_PACKAGE_VERSION");
var versionJson = Path.Combine(Application.dataPath, root, "package.json");
if (File.Exists(versionJson))
{
var v = JsonUtility.FromJson<Version>(File.ReadAllText(versionJson));
if (!string.IsNullOrEmpty(version))
{
if (v.version != version)
{
var msg = $"package.json and env version are mismatched. UNITY_PACKAGE_VERSION:{version}, package.json:{v.version}";
if (Application.isBatchMode)
{
Console.WriteLine(msg);
Application.Quit(1);
}
throw new Exception("package.json and env version are mismatched.");
}
}
version = v.version;
}
return version;
}
public class Version
{
public string version;
}
}
#endif

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 34885e00b06e4c847b8e2958ebb2d26b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 58b896faeb4c95c4a939b19f8dbf74ab
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 791bdc9996d0f5c42bea2aa68c95dced
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: a2ca402f41ce60d4c95ee34b6715ee8a
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,59 @@
using ObservableCollections.Internal;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
namespace ObservableCollections
{
public sealed class FreezedDictionary<TKey, TValue> : IReadOnlyDictionary<TKey, TValue>, IFreezedCollection<KeyValuePair<TKey, TValue>>
{
readonly IReadOnlyDictionary<TKey, TValue> dictionary;
public FreezedDictionary(IReadOnlyDictionary<TKey, TValue> dictionary)
{
this.dictionary = dictionary;
}
public TValue this[TKey key] => dictionary[key];
public IEnumerable<TKey> Keys => dictionary.Keys;
public IEnumerable<TValue> Values => dictionary.Values;
public int Count => dictionary.Count;
public bool ContainsKey(TKey key)
{
return dictionary.ContainsKey(key);
}
public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator()
{
return dictionary.GetEnumerator();
}
public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value)
{
return dictionary.TryGetValue(key, out value);
}
IEnumerator IEnumerable.GetEnumerator()
{
return ((IEnumerable)dictionary).GetEnumerator();
}
public ISynchronizedView<KeyValuePair<TKey, TValue>, TView> CreateView<TView>(Func<KeyValuePair<TKey, TValue>, TView> transform, bool reverse = false)
{
return new FreezedView<KeyValuePair<TKey, TValue>, TView>(dictionary, transform, reverse);
}
public ISortableSynchronizedView<KeyValuePair<TKey, TValue>, TView> CreateSortableView<TView>(Func<KeyValuePair<TKey, TValue>, TView> transform)
{
return new FreezedSortableView<KeyValuePair<TKey, TValue>, TView>(dictionary, transform);
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1470201cf9f7db249b7f441e515b77b0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,61 @@
using ObservableCollections.Internal;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
namespace ObservableCollections
{
public sealed class FreezedList<T> : IReadOnlyList<T>, IFreezedCollection<T>
{
readonly IReadOnlyList<T> list;
public T this[int index]
{
get
{
return list[index];
}
}
public int Count
{
get
{
return list.Count;
}
}
public bool IsReadOnly => true;
public FreezedList(IReadOnlyList<T> list)
{
this.list = list;
}
public ISynchronizedView<T, TView> CreateView<TView>(Func<T, TView> transform, bool reverse = false)
{
return new FreezedView<T, TView>(list, transform, reverse);
}
public ISortableSynchronizedView<T, TView> CreateSortableView<TView>(Func<T, TView> transform)
{
return new FreezedSortableView<T, TView>(list, transform);
}
public bool Contains(T item)
{
return list.Contains(item);
}
public IEnumerator<T> GetEnumerator()
{
return list.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3207eb016a325514b8f137b4cf17df40
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,118 @@
using ObservableCollections.Internal;
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
namespace ObservableCollections
{
public delegate void NotifyCollectionChangedEventHandler<T>(in NotifyCollectionChangedEventArgs<T> e);
public interface IObservableCollection<T> : IReadOnlyCollection<T>
{
event NotifyCollectionChangedEventHandler<T> CollectionChanged;
object SyncRoot { get; }
ISynchronizedView<T, TView> CreateView<TView>(Func<T, TView> transform, bool reverse = false);
}
public interface IFreezedCollection<T>
{
ISynchronizedView<T, TView> CreateView<TView>(Func<T, TView> transform, bool reverse = false);
ISortableSynchronizedView<T, TView> CreateSortableView<TView>(Func<T, TView> transform);
}
public interface ISynchronizedView<T, TView> : IReadOnlyCollection<(T Value, TView View)>, IDisposable
{
object SyncRoot { get; }
event NotifyCollectionChangedEventHandler<T> RoutingCollectionChanged;
event Action<NotifyCollectionChangedAction> CollectionStateChanged;
void AttachFilter(ISynchronizedViewFilter<T, TView> filter);
void ResetFilter(Action<T, TView> resetAction);
INotifyCollectionChangedSynchronizedView<T, TView> WithINotifyCollectionChanged();
}
public interface ISortableSynchronizedView<T, TView> : ISynchronizedView<T, TView>
{
void Sort(IComparer<T> comparer);
void Sort(IComparer<TView> viewComparer);
}
// will be implemented in the future?
//public interface IGroupedSynchoronizedView<T, TKey, TView> : ILookup<TKey, (T, TView)>, ISynchronizedView<T, TView>
//{
//}
public interface INotifyCollectionChangedSynchronizedView<T, TView> : ISynchronizedView<T, TView>, INotifyCollectionChanged, INotifyPropertyChanged
{
}
public static class ObservableCollectionsExtensions
{
public static ISynchronizedView<T, TView> CreateSortedView<T, TKey, TView>(this IObservableCollection<T> source, Func<T, TKey> identitySelector, Func<T, TView> transform, IComparer<T> comparer)
{
return new SortedView<T, TKey, TView>(source, identitySelector, transform, comparer);
}
public static ISynchronizedView<T, TView> CreateSortedView<T, TKey, TView>(this IObservableCollection<T> source, Func<T, TKey> identitySelector, Func<T, TView> transform, IComparer<TView> viewComparer)
{
return new SortedViewViewComparer<T, TKey, TView>(source, identitySelector, transform, viewComparer);
}
public static ISynchronizedView<T, TView> CreateSortedView<T, TKey, TView, TCompare>(this IObservableCollection<T> source, Func<T, TKey> identitySelector, Func<T, TView> transform, Func<T, TCompare> compareSelector, bool ascending = true)
{
return source.CreateSortedView(identitySelector, transform, new AnonymousComparer<T, TCompare>(compareSelector, ascending));
}
public static ISortableSynchronizedView<T, TView> CreateSortableView<T, TView>(this IFreezedCollection<T> source, Func<T, TView> transform, IComparer<T> initialSort)
{
var view = source.CreateSortableView(transform);
view.Sort(initialSort);
return view;
}
public static ISortableSynchronizedView<T, TView> CreateSortableView<T, TView>(this IFreezedCollection<T> source, Func<T, TView> transform, IComparer<TView> initialViewSort)
{
var view = source.CreateSortableView(transform);
view.Sort(initialViewSort);
return view;
}
public static ISortableSynchronizedView<T, TView> CreateSortableView<T, TView, TCompare>(this IFreezedCollection<T> source, Func<T, TView> transform, Func<T, TCompare> initialCompareSelector, bool ascending = true)
{
var view = source.CreateSortableView(transform);
view.Sort(initialCompareSelector, ascending);
return view;
}
public static void Sort<T, TView, TCompare>(this ISortableSynchronizedView<T, TView> source, Func<T, TCompare> compareSelector, bool ascending = true)
{
source.Sort(new AnonymousComparer<T, TCompare>(compareSelector, ascending));
}
class AnonymousComparer<T, TCompare> : IComparer<T>
{
readonly Func<T, TCompare> selector;
readonly int f;
public AnonymousComparer(Func<T, TCompare> selector, bool ascending)
{
this.selector = selector;
this.f = ascending ? 1 : -1;
}
public int Compare(T x, T y)
{
if (x == null && y == null) return 0;
if (x == null) return 1 * f;
if (y == null) return -1 * f;
return Comparer<TCompare>.Default.Compare(selector(x), selector(y)) * f;
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: bdb4cc0f6a8b396418c8250eeaf2d2c8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,121 @@
using System;
namespace ObservableCollections
{
public interface ISynchronizedViewFilter<T, TView>
{
bool IsMatch(T value, TView view);
void WhenTrue(T value, TView view);
void WhenFalse(T value, TView view);
void OnCollectionChanged(ChangedKind changedKind, T value, TView view, in NotifyCollectionChangedEventArgs<T> eventArgs);
}
public enum ChangedKind
{
Add, Remove, Move
}
public class SynchronizedViewFilter<T, TView> : ISynchronizedViewFilter<T, TView>
{
public static readonly ISynchronizedViewFilter<T, TView> Null = new NullViewFilter();
readonly Func<T, TView, bool> isMatch;
readonly Action<T, TView> whenTrue;
readonly Action<T, TView> whenFalse;
readonly Action<ChangedKind, T, TView> onCollectionChanged;
public SynchronizedViewFilter(Func<T, TView, bool> isMatch, Action<T, TView> whenTrue, Action<T, TView> whenFalse, Action<ChangedKind, T, TView> onCollectionChanged)
{
this.isMatch = isMatch;
this.whenTrue = whenTrue;
this.whenFalse = whenFalse;
this.onCollectionChanged = onCollectionChanged;
}
public bool IsMatch(T value, TView view) => isMatch(value, view);
public void WhenFalse(T value, TView view) => whenFalse?.Invoke(value, view);
public void WhenTrue(T value, TView view) => whenTrue?.Invoke(value, view);
public void OnCollectionChanged(ChangedKind changedKind, T value, TView view, in NotifyCollectionChangedEventArgs<T> eventArgs) => onCollectionChanged?.Invoke(changedKind, value, view);
class NullViewFilter : ISynchronizedViewFilter<T, TView>
{
public bool IsMatch(T value, TView view) => true;
public void WhenFalse(T value, TView view) { }
public void WhenTrue(T value, TView view) { }
public void OnCollectionChanged(ChangedKind changedKind, T value, TView view, in NotifyCollectionChangedEventArgs<T> eventArgs) { }
}
}
public static class SynchronizedViewFilterExtensions
{
public static void AttachFilter<T, TView>(this ISynchronizedView<T, TView> source, Func<T, TView, bool> filter)
{
source.AttachFilter(new SynchronizedViewFilter<T, TView>(filter, null, null, null));
}
public static void AttachFilter<T, TView>(this ISynchronizedView<T, TView> source, Func<T, TView, bool> isMatch, Action<T, TView> whenTrue, Action<T, TView> whenFalse)
{
source.AttachFilter(new SynchronizedViewFilter<T, TView>(isMatch, whenTrue, whenFalse, null));
}
public static void AttachFilter<T, TView>(this ISynchronizedView<T, TView> source, Func<T, TView, bool> isMatch, Action<T, TView> whenTrue, Action<T, TView> whenFalse, Action<ChangedKind, T, TView> onCollectionChanged)
{
source.AttachFilter(new SynchronizedViewFilter<T, TView>(isMatch, whenTrue, whenFalse, onCollectionChanged));
}
public static bool IsNullFilter<T, TView>(this ISynchronizedViewFilter<T, TView> filter)
{
return filter == SynchronizedViewFilter<T, TView>.Null;
}
internal static void InvokeOnAdd<T, TView>(this ISynchronizedViewFilter<T, TView> filter, (T value, TView view) value, in NotifyCollectionChangedEventArgs<T> eventArgs)
{
InvokeOnAdd(filter, value.value, value.view, eventArgs);
}
internal static void InvokeOnAdd<T, TView>(this ISynchronizedViewFilter<T, TView> filter, T value, TView view, in NotifyCollectionChangedEventArgs<T> eventArgs)
{
if (filter.IsMatch(value, view))
{
filter.WhenTrue(value, view);
}
else
{
filter.WhenFalse(value, view);
}
filter.OnCollectionChanged(ChangedKind.Add, value, view, eventArgs);
}
internal static void InvokeOnRemove<T, TView>(this ISynchronizedViewFilter<T, TView> filter, (T value, TView view) value, in NotifyCollectionChangedEventArgs<T> eventArgs)
{
InvokeOnRemove(filter, value.value, value.view, eventArgs);
}
internal static void InvokeOnRemove<T, TView>(this ISynchronizedViewFilter<T, TView> filter, T value, TView view, in NotifyCollectionChangedEventArgs<T> eventArgs)
{
filter.OnCollectionChanged(ChangedKind.Remove, value, view, eventArgs);
}
internal static void InvokeOnMove<T, TView>(this ISynchronizedViewFilter<T, TView> filter, (T value, TView view) value, in NotifyCollectionChangedEventArgs<T> eventArgs)
{
InvokeOnMove(filter, value.value, value.view, eventArgs);
}
internal static void InvokeOnMove<T, TView>(this ISynchronizedViewFilter<T, TView> filter, T value, TView view, in NotifyCollectionChangedEventArgs<T> eventArgs)
{
filter.OnCollectionChanged(ChangedKind.Move, value, view, eventArgs);
}
internal static void InvokeOnAttach<T, TView>(this ISynchronizedViewFilter<T, TView> filter, T value, TView view)
{
if (filter.IsMatch(value, view))
{
filter.WhenTrue(value, view);
}
else
{
filter.WhenFalse(value, view);
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0ffdcef914adf8848a485ac600cc41c4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 0b3e01975c6d488409ce0ac753c89870
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,132 @@
using System;
using System.Buffers;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Linq;
namespace ObservableCollections.Internal
{
/// <summary>
/// ReadOnly cloned collection.
/// </summary>
internal struct CloneCollection<T> : IDisposable
{
T[] array;
int length;
public ReadOnlySpan<T> Span => array.AsSpan(0, length);
public IEnumerable<T> AsEnumerable() => new EnumerableCollection(array, length);
public CloneCollection(T item)
{
this.array = ArrayPool<T>.Shared.Rent(1);
this.length = 1;
this.array[0] = item;
}
public CloneCollection(IEnumerable<T> source)
{
if (source.TryGetNonEnumeratedCount(out var count))
{
var array = ArrayPool<T>.Shared.Rent(count);
if (source is ICollection<T> c)
{
c.CopyTo(array, 0);
}
else
{
var i = 0;
foreach (var item in source)
{
array[i++] = item;
}
}
this.array = array;
this.length = count;
}
else
{
var array = ArrayPool<T>.Shared.Rent(count);
var i = 0;
foreach (var item in source)
{
TryEnsureCapacity(ref array, i);
array[i++] = item;
}
this.array = array;
this.length = i;
}
}
public CloneCollection(ReadOnlySpan<T> source)
{
var array = ArrayPool<T>.Shared.Rent(source.Length);
source.CopyTo(array);
this.array = array;
this.length = source.Length;
}
static void TryEnsureCapacity(ref T[] array, int index)
{
if (array.Length == index)
{
ArrayPool<T>.Shared.Return(array, RuntimeHelpersEx.IsReferenceOrContainsReferences<T>());
}
array = ArrayPool<T>.Shared.Rent(index * 2);
}
public void Dispose()
{
if (array != null)
{
ArrayPool<T>.Shared.Return(array, RuntimeHelpersEx.IsReferenceOrContainsReferences<T>());
array = null;
}
}
// Optimize to use Count and CopyTo
class EnumerableCollection : ICollection<T>
{
readonly T[] array;
readonly int count;
public EnumerableCollection(T[] array, int count)
{
if (array == null)
{
this.array = Array.Empty<T>();
this.count = 0;
}
else
{
this.array = array;
this.count = count;
}
}
public int Count => count;
public bool IsReadOnly => true;
public void Add(T item) => throw new NotSupportedException();
public void Clear() => throw new NotSupportedException();
public bool Contains(T item) => throw new NotSupportedException();
public void CopyTo(T[] dest, int destIndex) => Array.Copy(array, 0, dest, destIndex, count);
public IEnumerator<T> GetEnumerator()
{
for (int i = 0; i < count; i++)
{
yield return array[i];
}
}
public bool Remove(T item) => throw new NotSupportedException();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7b9643f0fa1bf7447961693f16cb557b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,229 @@
#pragma warning disable CS0067
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
namespace ObservableCollections.Internal
{
internal sealed class FreezedView<T, TView> : ISynchronizedView<T, TView>
{
readonly bool reverse;
readonly List<(T, TView)> list;
ISynchronizedViewFilter<T, TView> filter;
public event Action<NotifyCollectionChangedAction> CollectionStateChanged;
public event NotifyCollectionChangedEventHandler<T> RoutingCollectionChanged;
public object SyncRoot { get; } = new object();
public FreezedView(IEnumerable<T> source, Func<T, TView> selector, bool reverse)
{
this.reverse = reverse;
this.filter = SynchronizedViewFilter<T, TView>.Null;
this.list = source.Select(x => (x, selector(x))).ToList();
}
public int Count
{
get
{
lock (SyncRoot)
{
return list.Count;
}
}
}
public void AttachFilter(ISynchronizedViewFilter<T, TView> filter)
{
lock (SyncRoot)
{
this.filter = filter;
foreach (var (value, view) in list)
{
filter.InvokeOnAttach(value, view);
}
}
}
public void ResetFilter(Action<T, TView> resetAction)
{
lock (SyncRoot)
{
this.filter = SynchronizedViewFilter<T, TView>.Null;
if (resetAction != null)
{
foreach (var (item, view) in list)
{
resetAction(item, view);
}
}
}
}
public IEnumerator<(T, TView)> GetEnumerator()
{
lock (SyncRoot)
{
if (!reverse)
{
foreach (var item in list)
{
if (filter.IsMatch(item.Item1, item.Item2))
{
yield return item;
}
}
}
else
{
foreach (var item in list.AsEnumerable().Reverse())
{
if (filter.IsMatch(item.Item1, item.Item2))
{
yield return item;
}
}
}
}
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public void Dispose()
{
}
public INotifyCollectionChangedSynchronizedView<T, TView> WithINotifyCollectionChanged()
{
return new NotifyCollectionChangedSynchronizedView<T, TView>(this);
}
}
internal sealed class FreezedSortableView<T, TView> : ISortableSynchronizedView<T, TView>
{
readonly (T, TView)[] array;
ISynchronizedViewFilter<T, TView> filter;
public event Action<NotifyCollectionChangedAction> CollectionStateChanged;
public event NotifyCollectionChangedEventHandler<T> RoutingCollectionChanged;
public object SyncRoot { get; } = new object();
public FreezedSortableView(IEnumerable<T> source, Func<T, TView> selector)
{
this.filter = SynchronizedViewFilter<T, TView>.Null;
this.array = source.Select(x => (x, selector(x))).ToArray();
}
public int Count
{
get
{
lock (SyncRoot)
{
return array.Length;
}
}
}
public void AttachFilter(ISynchronizedViewFilter<T, TView> filter)
{
lock (SyncRoot)
{
this.filter = filter;
foreach (var (value, view) in array)
{
filter.InvokeOnAttach(value, view);
}
}
}
public void ResetFilter(Action<T, TView> resetAction)
{
lock (SyncRoot)
{
this.filter = SynchronizedViewFilter<T, TView>.Null;
if (resetAction != null)
{
foreach (var (item, view) in array)
{
resetAction(item, view);
}
}
}
}
public IEnumerator<(T, TView)> GetEnumerator()
{
lock (SyncRoot)
{
foreach (var item in array)
{
if (filter.IsMatch(item.Item1, item.Item2))
{
yield return item;
}
}
}
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public void Dispose()
{
}
public void Sort(IComparer<T> comparer)
{
Array.Sort(array, new TComparer(comparer));
}
public void Sort(IComparer<TView> viewComparer)
{
Array.Sort(array, new TViewComparer(viewComparer));
}
public INotifyCollectionChangedSynchronizedView<T, TView> WithINotifyCollectionChanged()
{
return new NotifyCollectionChangedSynchronizedView<T, TView>(this);
}
class TComparer : IComparer<(T, TView)>
{
readonly IComparer<T> comparer;
public TComparer(IComparer<T> comparer)
{
this.comparer = comparer;
}
public int Compare((T, TView) x, (T, TView) y)
{
return comparer.Compare(x.Item1, y.Item1);
}
}
class TViewComparer : IComparer<(T, TView)>
{
readonly IComparer<TView> comparer;
public TViewComparer(IComparer<TView> comparer)
{
this.comparer = comparer;
}
public int Compare((T, TView) x, (T, TView) y)
{
return comparer.Compare(x.Item2, y.Item2);
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a768cdaf951b40345b6d5b21ade3e282
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,70 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
namespace ObservableCollections.Internal
{
internal class NotifyCollectionChangedSynchronizedView<T, TView> : INotifyCollectionChangedSynchronizedView<T, TView>
{
readonly ISynchronizedView<T, TView> parent;
static readonly PropertyChangedEventArgs CountPropertyChangedEventArgs = new PropertyChangedEventArgs("Count");
public NotifyCollectionChangedSynchronizedView(ISynchronizedView<T, TView> parent)
{
this.parent = parent;
this.parent.RoutingCollectionChanged += Parent_RoutingCollectionChanged;
}
private void Parent_RoutingCollectionChanged(in NotifyCollectionChangedEventArgs<T> e)
{
CollectionChanged?.Invoke(this, e.ToStandardEventArgs());
switch (e.Action)
{
// add, remove, reset will change the count.
case NotifyCollectionChangedAction.Add:
case NotifyCollectionChangedAction.Remove:
case NotifyCollectionChangedAction.Reset:
PropertyChanged?.Invoke(this, CountPropertyChangedEventArgs);
break;
case NotifyCollectionChangedAction.Replace:
case NotifyCollectionChangedAction.Move:
default:
break;
}
}
public object SyncRoot => parent.SyncRoot;
public int Count => parent.Count;
public event NotifyCollectionChangedEventHandler CollectionChanged;
public event PropertyChangedEventHandler PropertyChanged;
public event Action<NotifyCollectionChangedAction> CollectionStateChanged
{
add { parent.CollectionStateChanged += value; }
remove { parent.CollectionStateChanged -= value; }
}
public event NotifyCollectionChangedEventHandler<T> RoutingCollectionChanged
{
add { parent.RoutingCollectionChanged += value; }
remove { parent.RoutingCollectionChanged -= value; }
}
public void AttachFilter(ISynchronizedViewFilter<T, TView> filter) => parent.AttachFilter(filter);
public void ResetFilter(Action<T, TView> resetAction) => parent.ResetFilter(resetAction);
public INotifyCollectionChangedSynchronizedView<T, TView> WithINotifyCollectionChanged() => this;
public void Dispose()
{
this.parent.RoutingCollectionChanged -= Parent_RoutingCollectionChanged;
parent.Dispose();
}
public IEnumerator<(T, TView)> GetEnumerator() => parent.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => parent.GetEnumerator();
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b0e2d983b8f809a45b60f610d0542761
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,56 @@
using System;
using System.Buffers;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
namespace ObservableCollections.Internal
{
// internal ref struct ResizableArray<T>
internal struct ResizableArray<T> : IDisposable
{
T[] array;
int count;
public ReadOnlySpan<T> Span => array.AsSpan(0, count);
public ResizableArray(int initialCapacity)
{
array = ArrayPool<T>.Shared.Rent(initialCapacity);
count = 0;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Add(T item)
{
if (array == null) Throw();
if (array.Length == count)
{
EnsureCapacity();
}
array[count++] = item;
}
[MethodImpl(MethodImplOptions.NoInlining)]
void EnsureCapacity()
{
var newArray = array.AsSpan().ToArray();
ArrayPool<T>.Shared.Return(array, RuntimeHelpersEx.IsReferenceOrContainsReferences<T>());
array = newArray;
}
public void Dispose()
{
if (array != null)
{
ArrayPool<T>.Shared.Return(array, RuntimeHelpersEx.IsReferenceOrContainsReferences<T>());
array = null;
}
}
[DoesNotReturn]
void Throw()
{
throw new ObjectDisposedException("ResizableArray");
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b1477839cc1e36a4187e91e4fd001dd5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,225 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
namespace ObservableCollections.Internal
{
internal class SortedView<T, TKey, TView> : ISynchronizedView<T, TView>
{
readonly IObservableCollection<T> source;
readonly Func<T, TView> transform;
readonly Func<T, TKey> identitySelector;
readonly SortedDictionary<(T Value, TKey Key), (T Value, TView View)> dict;
ISynchronizedViewFilter<T, TView> filter;
public event NotifyCollectionChangedEventHandler<T> RoutingCollectionChanged;
public event Action<NotifyCollectionChangedAction> CollectionStateChanged;
public object SyncRoot { get; } = new object();
public SortedView(IObservableCollection<T> source, Func<T, TKey> identitySelector, Func<T, TView> transform, IComparer<T> comparer)
{
this.source = source;
this.identitySelector = identitySelector;
this.transform = transform;
this.filter = SynchronizedViewFilter<T, TView>.Null;
lock (source.SyncRoot)
{
var dict = new SortedDictionary<(T, TKey), (T, TView)>(new Comparer(comparer));
foreach (var v in source)
{
dict.Add((v, identitySelector(v)), (v, transform(v)));
}
this.dict = dict;
this.source.CollectionChanged += SourceCollectionChanged;
}
}
public int Count
{
get
{
lock (SyncRoot)
{
return dict.Count;
}
}
}
public void AttachFilter(ISynchronizedViewFilter<T, TView> filter)
{
lock (SyncRoot)
{
this.filter = filter;
foreach (var (_, (value, view)) in dict)
{
filter.InvokeOnAttach(value, view);
}
}
}
public void ResetFilter(Action<T, TView> resetAction)
{
lock (SyncRoot)
{
this.filter = SynchronizedViewFilter<T, TView>.Null;
if (resetAction != null)
{
foreach (var (_, (value, view)) in dict)
{
resetAction(value, view);
}
}
}
}
public INotifyCollectionChangedSynchronizedView<T, TView> WithINotifyCollectionChanged()
{
lock (SyncRoot)
{
return new NotifyCollectionChangedSynchronizedView<T, TView>(this);
}
}
public IEnumerator<(T, TView)> GetEnumerator()
{
lock (SyncRoot)
{
foreach (var item in dict)
{
if (filter.IsMatch(item.Value.Value, item.Value.View))
{
yield return item.Value;
}
}
}
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public void Dispose()
{
this.source.CollectionChanged -= SourceCollectionChanged;
}
private void SourceCollectionChanged(in NotifyCollectionChangedEventArgs<T> e)
{
lock (SyncRoot)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
{
// Add, Insert
if (e.IsSingleItem)
{
var value = e.NewItem;
var view = transform(value);
var id = identitySelector(value);
dict.Add((value, id), (value, view));
filter.InvokeOnAdd(value, view, e);
}
else
{
foreach (var value in e.NewItems)
{
var view = transform(value);
var id = identitySelector(value);
dict.Add((value, id), (value, view));
filter.InvokeOnAdd(value, view, e);
}
}
}
break;
case NotifyCollectionChangedAction.Remove:
{
if (e.IsSingleItem)
{
var value = e.OldItem;
var id = identitySelector(value);
dict.Remove((value, id), out var v);
filter.InvokeOnRemove(v.Value, v.View, e);
}
else
{
foreach (var value in e.OldItems)
{
var id = identitySelector(value);
dict.Remove((value, id), out var v);
filter.InvokeOnRemove(v.Value, v.View, e);
}
}
}
break;
case NotifyCollectionChangedAction.Replace:
// ReplaceRange is not supported in all ObservableCollections collections
// Replace is remove old item and insert new item.
{
var oldValue = e.OldItem;
dict.Remove((oldValue, identitySelector(oldValue)), out var oldView);
var value = e.NewItem;
var view = transform(value);
var id = identitySelector(value);
dict.Add((value, id), (value, view));
filter.InvokeOnRemove(oldView, e);
filter.InvokeOnAdd(value, view, e);
}
break;
case NotifyCollectionChangedAction.Move:
{
// Move(index change) does not affect sorted list.
var oldValue = e.OldItem;
if (dict.TryGetValue((oldValue, identitySelector(oldValue)), out var view))
{
filter.InvokeOnMove(view, e);
}
}
break;
case NotifyCollectionChangedAction.Reset:
if (!filter.IsNullFilter())
{
foreach (var item in dict)
{
filter.InvokeOnRemove(item.Value, e);
}
}
dict.Clear();
break;
default:
break;
}
RoutingCollectionChanged?.Invoke(e);
CollectionStateChanged?.Invoke(e.Action);
}
}
sealed class Comparer : IComparer<(T value, TKey id)>
{
readonly IComparer<T> comparer;
public Comparer(IComparer<T> comparer)
{
this.comparer = comparer;
}
public int Compare((T value, TKey id) x, (T value, TKey id) y)
{
var compare = comparer.Compare(x.value, y.value);
if (compare == 0)
{
compare = Comparer<TKey>.Default.Compare(x.id, y.id);
}
return compare;
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 768c43ec998d9754d8cc13398ed69d39
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,244 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
namespace ObservableCollections.Internal
{
internal class SortedViewViewComparer<T, TKey, TView> : ISynchronizedView<T, TView>
{
readonly IObservableCollection<T> source;
readonly Func<T, TView> transform;
readonly Func<T, TKey> identitySelector;
readonly Dictionary<TKey, TView> viewMap; // view-map needs to use in remove.
readonly SortedDictionary<(TView View, TKey Key), (T Value, TView View)> dict;
ISynchronizedViewFilter<T, TView> filter;
public event NotifyCollectionChangedEventHandler<T> RoutingCollectionChanged;
public event Action<NotifyCollectionChangedAction> CollectionStateChanged;
public object SyncRoot { get; } = new object();
public SortedViewViewComparer(IObservableCollection<T> source, Func<T, TKey> identitySelector, Func<T, TView> transform, IComparer<TView> comparer)
{
this.source = source;
this.identitySelector = identitySelector;
this.transform = transform;
this.filter = SynchronizedViewFilter<T, TView>.Null;
lock (source.SyncRoot)
{
var dict = new SortedDictionary<(TView, TKey), (T, TView)>(new Comparer(comparer));
this.viewMap = new Dictionary<TKey, TView>();
foreach (var value in source)
{
var view = transform(value);
var id = identitySelector(value);
dict.Add((view, id), (value, view));
viewMap.Add(id, view);
}
this.dict = dict;
this.source.CollectionChanged += SourceCollectionChanged;
}
}
public int Count
{
get
{
lock (SyncRoot)
{
return dict.Count;
}
}
}
public void AttachFilter(ISynchronizedViewFilter<T, TView> filter)
{
lock (SyncRoot)
{
this.filter = filter;
foreach (var (_, (value, view)) in dict)
{
filter.InvokeOnAttach(value, view);
}
}
}
public void ResetFilter(Action<T, TView> resetAction)
{
lock (SyncRoot)
{
this.filter = SynchronizedViewFilter<T, TView>.Null;
if (resetAction != null)
{
foreach (var (_, (value, view)) in dict)
{
resetAction(value, view);
}
}
}
}
public INotifyCollectionChangedSynchronizedView<T, TView> WithINotifyCollectionChanged()
{
lock (SyncRoot)
{
return new NotifyCollectionChangedSynchronizedView<T, TView>(this);
}
}
public IEnumerator<(T, TView)> GetEnumerator()
{
lock (SyncRoot)
{
foreach (var item in dict)
{
if (filter.IsMatch(item.Value.Value, item.Value.View))
{
yield return item.Value;
}
}
}
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public void Dispose()
{
this.source.CollectionChanged -= SourceCollectionChanged;
}
private void SourceCollectionChanged(in NotifyCollectionChangedEventArgs<T> e)
{
lock (SyncRoot)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
{
// Add, Insert
if (e.IsSingleItem)
{
var value = e.NewItem;
var view = transform(value);
var id = identitySelector(value);
dict.Add((view, id), (value, view));
viewMap.Add(id, view);
filter.InvokeOnAdd(value, view, e);
}
else
{
foreach (var value in e.NewItems)
{
var view = transform(value);
var id = identitySelector(value);
dict.Add((view, id), (value, view));
viewMap.Add(id, view);
filter.InvokeOnAdd(value, view, e);
}
}
}
break;
case NotifyCollectionChangedAction.Remove:
{
if (e.IsSingleItem)
{
var value = e.OldItem;
var id = identitySelector(value);
if (viewMap.Remove(id, out var view))
{
dict.Remove((view, id), out var v);
filter.InvokeOnRemove(v, e);
}
}
else
{
foreach (var value in e.OldItems)
{
var id = identitySelector(value);
if (viewMap.Remove(id, out var view))
{
dict.Remove((view, id), out var v);
filter.InvokeOnRemove(v, e);
}
}
}
}
break;
case NotifyCollectionChangedAction.Replace:
// Replace is remove old item and insert new item.
{
var oldValue = e.OldItem;
var oldKey = identitySelector(oldValue);
if (viewMap.Remove(oldKey, out var oldView))
{
dict.Remove((oldView, oldKey));
filter.InvokeOnRemove(oldValue, oldView, e);
}
var value = e.NewItem;
var view = transform(value);
var id = identitySelector(value);
dict.Add((view, id), (value, view));
viewMap.Add(id, view);
filter.InvokeOnAdd(value, view, e);
}
break;
case NotifyCollectionChangedAction.Move:
// Move(index change) does not affect soreted dict.
{
var value = e.OldItem;
var key = identitySelector(value);
if (viewMap.TryGetValue(key, out var view))
{
filter.InvokeOnMove(value, view, e);
}
}
break;
case NotifyCollectionChangedAction.Reset:
if (!filter.IsNullFilter())
{
foreach (var item in dict)
{
filter.InvokeOnRemove(item.Value, e);
}
}
dict.Clear();
viewMap.Clear();
break;
default:
break;
}
RoutingCollectionChanged?.Invoke(e);
CollectionStateChanged?.Invoke(e.Action);
}
}
sealed class Comparer : IComparer<(TView view, TKey id)>
{
readonly IComparer<TView> comparer;
public Comparer(IComparer<TView> comparer)
{
this.comparer = comparer;
}
public int Compare((TView view, TKey id) x, (TView view, TKey id) y)
{
var compare = comparer.Compare(x.view, y.view);
if (compare == 0)
{
compare = Comparer<TKey>.Default.Compare(x.id, y.id);
}
return compare;
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 08e8703f31991094aa9216bf7166a549
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,127 @@
using System;
using System.Collections.Specialized;
using System.Runtime.InteropServices;
namespace ObservableCollections
{
/// <summary>
/// Contract:
/// IsSingleItem ? (NewItem, OldItem) : (NewItems, OldItems)
/// Action.Add
/// NewItem, NewItems, NewStartingIndex
/// Action.Remove
/// OldItem, OldItems, OldStartingIndex
/// Action.Replace
/// NewItem, NewItems, OldItem, OldItems, (NewStartingIndex, OldStartingIndex = samevalue)
/// Action.Move
/// NewStartingIndex, OldStartingIndex
/// Action.Reset
/// -
/// </summary>
[StructLayout(LayoutKind.Auto)]
public readonly ref struct NotifyCollectionChangedEventArgs<T>
{
public readonly NotifyCollectionChangedAction Action;
public readonly bool IsSingleItem;
public readonly T NewItem;
public readonly T OldItem;
public readonly ReadOnlySpan<T> NewItems;
public readonly ReadOnlySpan<T> OldItems;
public readonly int NewStartingIndex;
public readonly int OldStartingIndex;
public NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction action, bool isSingleItem, T newItem = default, T oldItem = default, ReadOnlySpan<T> newItems = default, ReadOnlySpan<T> oldItems = default, int newStartingIndex = -1, int oldStartingIndex = -1)
{
Action = action;
IsSingleItem = isSingleItem;
NewItem = newItem;
OldItem = oldItem;
NewItems = newItems;
OldItems = oldItems;
NewStartingIndex = newStartingIndex;
OldStartingIndex = oldStartingIndex;
}
public NotifyCollectionChangedEventArgs ToStandardEventArgs()
{
switch (Action)
{
case NotifyCollectionChangedAction.Add:
if (IsSingleItem)
{
return new NotifyCollectionChangedEventArgs(Action, NewItem, NewStartingIndex);
}
else
{
return new NotifyCollectionChangedEventArgs(Action, NewItems.ToArray(), NewStartingIndex);
}
case NotifyCollectionChangedAction.Remove:
if (IsSingleItem)
{
return new NotifyCollectionChangedEventArgs(Action, OldItem, OldStartingIndex);
}
else
{
return new NotifyCollectionChangedEventArgs(Action, OldItems.ToArray(), OldStartingIndex);
}
case NotifyCollectionChangedAction.Replace:
if (IsSingleItem)
{
return new NotifyCollectionChangedEventArgs(Action, NewItem, OldItem, NewStartingIndex);
}
else
{
return new NotifyCollectionChangedEventArgs(Action, NewItems.ToArray(), OldItems.ToArray(), NewStartingIndex);
}
case NotifyCollectionChangedAction.Move:
{
return new NotifyCollectionChangedEventArgs(Action, OldItem, NewStartingIndex, OldStartingIndex);
}
case NotifyCollectionChangedAction.Reset:
return new NotifyCollectionChangedEventArgs(Action);
default:
throw new ArgumentOutOfRangeException();
}
}
public static NotifyCollectionChangedEventArgs<T> Add(T newItem, int newStartingIndex)
{
return new NotifyCollectionChangedEventArgs<T>(NotifyCollectionChangedAction.Add, true, newItem: newItem, newStartingIndex: newStartingIndex);
}
public static NotifyCollectionChangedEventArgs<T> Add(ReadOnlySpan<T> newItems, int newStartingIndex)
{
return new NotifyCollectionChangedEventArgs<T>(NotifyCollectionChangedAction.Add, false, newItems: newItems, newStartingIndex: newStartingIndex);
}
public static NotifyCollectionChangedEventArgs<T> Remove(T oldItem, int oldStartingIndex)
{
return new NotifyCollectionChangedEventArgs<T>(NotifyCollectionChangedAction.Remove, true, oldItem: oldItem, oldStartingIndex: oldStartingIndex);
}
public static NotifyCollectionChangedEventArgs<T> Remove(ReadOnlySpan<T> oldItems, int oldStartingIndex)
{
return new NotifyCollectionChangedEventArgs<T>(NotifyCollectionChangedAction.Remove, false, oldItems: oldItems, oldStartingIndex: oldStartingIndex);
}
public static NotifyCollectionChangedEventArgs<T> Replace(T newItem, T oldItem, int startingIndex)
{
return new NotifyCollectionChangedEventArgs<T>(NotifyCollectionChangedAction.Replace, true, newItem: newItem, oldItem: oldItem, newStartingIndex: startingIndex, oldStartingIndex: startingIndex);
}
public static NotifyCollectionChangedEventArgs<T> Replace(ReadOnlySpan<T> newItems, ReadOnlySpan<T> oldItems, int startingIndex)
{
return new NotifyCollectionChangedEventArgs<T>(NotifyCollectionChangedAction.Replace, false, newItems: newItems, oldItems: oldItems, newStartingIndex: startingIndex, oldStartingIndex: startingIndex);
}
public static NotifyCollectionChangedEventArgs<T> Move(T changedItem, int newStartingIndex, int oldStartingIndex)
{
return new NotifyCollectionChangedEventArgs<T>(NotifyCollectionChangedAction.Move, true, oldItem: changedItem, newItem: changedItem, newStartingIndex: newStartingIndex, oldStartingIndex: oldStartingIndex);
}
public static NotifyCollectionChangedEventArgs<T> Reset()
{
return new NotifyCollectionChangedEventArgs<T>(NotifyCollectionChangedAction.Reset, true);
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 70f96ba473572df4d91f125c2663b868
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,18 @@
{
"name": "ObservableCollections",
"rootNamespace": "ObservableCollections",
"references": [],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": true,
"precompiledReferences": [
"System.Memory.dll",
"System.Buffers.dll",
"System.Runtime.CompilerServices.Unsafe.dll"
],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": true
}

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: a25f8e99c4faa4b4a8c38f41a406f5d3
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,171 @@
using ObservableCollections.Internal;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
namespace ObservableCollections
{
public sealed partial class ObservableDictionary<TKey, TValue>
{
public ISynchronizedView<KeyValuePair<TKey, TValue>, TView> CreateView<TView>(Func<KeyValuePair<TKey, TValue>, TView> transform, bool _ = false)
{
// reverse is no used.
return new View<TView>(this, transform);
}
class View<TView> : ISynchronizedView<KeyValuePair<TKey, TValue>, TView>
{
readonly ObservableDictionary<TKey, TValue> source;
readonly Func<KeyValuePair<TKey, TValue>, TView> selector;
ISynchronizedViewFilter<KeyValuePair<TKey, TValue>, TView> filter;
readonly Dictionary<TKey, (TValue, TView)> dict;
public View(ObservableDictionary<TKey, TValue> source, Func<KeyValuePair<TKey, TValue>, TView> selector)
{
this.source = source;
this.selector = selector;
this.filter = SynchronizedViewFilter<KeyValuePair<TKey, TValue>, TView>.Null;
this.SyncRoot = new object();
lock (source.SyncRoot)
{
this.dict = source.dictionary.ToDictionary(x => x.Key, x => (x.Value, selector(x)));
this.source.CollectionChanged += SourceCollectionChanged;
}
}
public object SyncRoot { get; }
public event NotifyCollectionChangedEventHandler<KeyValuePair<TKey, TValue>> RoutingCollectionChanged;
public event Action<NotifyCollectionChangedAction> CollectionStateChanged;
public int Count
{
get
{
lock (SyncRoot)
{
return dict.Count;
}
}
}
public void Dispose()
{
this.source.CollectionChanged -= SourceCollectionChanged;
}
public void AttachFilter(ISynchronizedViewFilter<KeyValuePair<TKey, TValue>, TView> filter)
{
lock (SyncRoot)
{
this.filter = filter;
foreach (var v in dict)
{
filter.InvokeOnAttach(new KeyValuePair<TKey, TValue>(v.Key, v.Value.Item1), v.Value.Item2);
}
}
}
public void ResetFilter(Action<KeyValuePair<TKey, TValue>, TView> resetAction)
{
lock (SyncRoot)
{
this.filter = SynchronizedViewFilter<KeyValuePair<TKey, TValue>, TView>.Null;
if (resetAction != null)
{
foreach (var v in dict)
{
resetAction(new KeyValuePair<TKey, TValue>(v.Key, v.Value.Item1), v.Value.Item2);
}
}
}
}
public INotifyCollectionChangedSynchronizedView<KeyValuePair<TKey, TValue>, TView> WithINotifyCollectionChanged()
{
lock (SyncRoot)
{
return new NotifyCollectionChangedSynchronizedView<KeyValuePair<TKey, TValue>, TView>(this);
}
}
public IEnumerator<(KeyValuePair<TKey, TValue>, TView)> GetEnumerator()
{
lock (SyncRoot)
{
foreach (var item in dict)
{
var v = (new KeyValuePair<TKey, TValue>(item.Key, item.Value.Item1), item.Value.Item2);
if (filter.IsMatch(v.Item1, v.Item2))
{
yield return v;
}
}
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
private void SourceCollectionChanged(in NotifyCollectionChangedEventArgs<KeyValuePair<TKey, TValue>> e)
{
// ObservableDictionary only provides single item operation and does not use int index.
lock (SyncRoot)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
{
var v = selector(e.NewItem);
dict.Add(e.NewItem.Key, (e.NewItem.Value, v));
filter.InvokeOnAdd(new KeyValuePair<TKey, TValue>(e.NewItem.Key, e.NewItem.Value), v, e);
}
break;
case NotifyCollectionChangedAction.Remove:
{
if (dict.Remove(e.OldItem.Key, out var v))
{
filter.InvokeOnRemove((new KeyValuePair<TKey, TValue>(e.OldItem.Key, v.Item1), v.Item2), e);
}
}
break;
case NotifyCollectionChangedAction.Replace:
{
if (dict.Remove(e.OldItem.Key, out var oldView))
{
filter.InvokeOnRemove((new KeyValuePair<TKey, TValue>(e.OldItem.Key, oldView.Item1), oldView.Item2), e);
}
var v = selector(e.NewItem);
dict[e.NewItem.Key] = (e.NewItem.Value, v);
filter.InvokeOnAdd(new KeyValuePair<TKey, TValue>(e.NewItem.Key, e.NewItem.Value), v, e);
}
break;
case NotifyCollectionChangedAction.Reset:
{
if (!filter.IsNullFilter())
{
foreach (var item in dict)
{
filter.InvokeOnRemove((new KeyValuePair<TKey, TValue>(item.Key, item.Value.Item1), item.Value.Item2), e);
}
}
dict.Clear();
}
break;
case NotifyCollectionChangedAction.Move: // ObservableDictionary have no Move operation.
default:
break;
}
RoutingCollectionChanged?.Invoke(e);
CollectionStateChanged?.Invoke(e.Action);
}
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f512d4ba320de58428976eb614bfa06d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,228 @@
using ObservableCollections.Internal;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
namespace ObservableCollections
{
public sealed partial class ObservableDictionary<TKey, TValue>
: IDictionary<TKey, TValue>, IReadOnlyDictionary<TKey, TValue>, IObservableCollection<KeyValuePair<TKey, TValue>>
{
readonly Dictionary<TKey, TValue> dictionary;
public object SyncRoot { get; } = new object();
public ObservableDictionary()
{
this.dictionary = new Dictionary<TKey, TValue>();
}
public ObservableDictionary(IEnumerable<KeyValuePair<TKey, TValue>> collection)
{
#if NET6_0_OR_GREATER
this.dictionary = new Dictionary<TKey, TValue>(collection);
#else
this.dictionary = new Dictionary<TKey, TValue>();
foreach (var item in collection)
{
dictionary.Add(item.Key, item.Value);
}
#endif
}
public event NotifyCollectionChangedEventHandler<KeyValuePair<TKey, TValue>> CollectionChanged;
public TValue this[TKey key]
{
get
{
lock (SyncRoot)
{
return dictionary[key];
}
}
set
{
lock (SyncRoot)
{
if (dictionary.TryGetValue(key, out var oldValue))
{
dictionary[key] = value;
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<KeyValuePair<TKey, TValue>>.Replace(
new KeyValuePair<TKey, TValue>(key, value),
new KeyValuePair<TKey, TValue>(key, oldValue),
-1));
}
else
{
Add(key, value);
}
}
}
}
// for lock synchronization, hide keys and values.
ICollection<TKey> IDictionary<TKey, TValue>.Keys
{
get
{
lock (SyncRoot)
{
return dictionary.Keys;
}
}
}
ICollection<TValue> IDictionary<TKey, TValue>.Values
{
get
{
lock (SyncRoot)
{
return dictionary.Values;
}
}
}
public int Count
{
get
{
lock (SyncRoot)
{
return dictionary.Count;
}
}
}
public bool IsReadOnly => false;
IEnumerable<TKey> IReadOnlyDictionary<TKey, TValue>.Keys
{
get
{
lock (SyncRoot)
{
return dictionary.Keys;
}
}
}
IEnumerable<TValue> IReadOnlyDictionary<TKey, TValue>.Values
{
get
{
lock (SyncRoot)
{
return dictionary.Values;
}
}
}
public void Add(TKey key, TValue value)
{
lock (SyncRoot)
{
dictionary.Add(key, value);
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<KeyValuePair<TKey, TValue>>.Add(new KeyValuePair<TKey, TValue>(key, value), -1));
}
}
public void Add(KeyValuePair<TKey, TValue> item)
{
Add(item.Key, item.Value);
}
public void Clear()
{
lock (SyncRoot)
{
dictionary.Clear();
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<KeyValuePair<TKey, TValue>>.Reset());
}
}
public bool Contains(KeyValuePair<TKey, TValue> item)
{
lock (SyncRoot)
{
return ((ICollection<KeyValuePair<TKey, TValue>>)dictionary).Contains(item);
}
}
public bool ContainsKey(TKey key)
{
lock (SyncRoot)
{
return ((IDictionary<TKey, TValue>)dictionary).ContainsKey(key);
}
}
public void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex)
{
lock (SyncRoot)
{
((ICollection<KeyValuePair<TKey, TValue>>)dictionary).CopyTo(array, arrayIndex);
}
}
public bool Remove(TKey key)
{
lock (SyncRoot)
{
if (dictionary.Remove(key, out var value))
{
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<KeyValuePair<TKey, TValue>>.Remove(new KeyValuePair<TKey, TValue>(key, value), -1));
return true;
}
return false;
}
}
public bool Remove(KeyValuePair<TKey, TValue> item)
{
lock (SyncRoot)
{
if (dictionary.TryGetValue(item.Key, out var value))
{
if (EqualityComparer<TValue>.Default.Equals(value, item.Value))
{
if (dictionary.Remove(item.Key, out var value2))
{
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<KeyValuePair<TKey, TValue>>.Remove(new KeyValuePair<TKey, TValue>(item.Key, value2), -1));
return true;
}
}
}
return false;
}
}
#pragma warning disable CS8767
public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value)
#pragma warning restore CS8767
{
lock (SyncRoot)
{
return dictionary.TryGetValue(key, out value);
}
}
public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator()
{
lock (SyncRoot)
{
foreach (var item in dictionary)
{
yield return item;
}
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b4dc0886473cf8c49947c9081454c614
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,326 @@
using ObservableCollections.Internal;
using System;
using System.Collections;
using System.Collections.Generic;
namespace ObservableCollections
{
public sealed partial class ObservableFixedSizeRingBuffer<T> : IList<T>, IReadOnlyList<T>, IObservableCollection<T>
{
readonly RingBuffer<T> buffer;
readonly int capacity;
public event NotifyCollectionChangedEventHandler<T> CollectionChanged;
public ObservableFixedSizeRingBuffer(int capacity)
{
this.capacity = capacity;
this.buffer = new RingBuffer<T>(capacity);
}
public ObservableFixedSizeRingBuffer(int capacity, IEnumerable<T> collection)
{
this.capacity = capacity;
this.buffer = new RingBuffer<T>(capacity);
foreach (var item in collection)
{
if (capacity == buffer.Count)
{
buffer.RemoveFirst();
}
buffer.AddLast(item);
}
}
public bool IsReadOnly => false;
public object SyncRoot { get; } = new object();
public T this[int index]
{
get
{
lock (SyncRoot)
{
return this.buffer[index];
}
}
set
{
lock (SyncRoot)
{
var oldValue = buffer[index];
buffer[index] = value;
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Replace(value, oldValue, index));
}
}
}
public int Count
{
get
{
lock (SyncRoot)
{
return buffer.Count;
}
}
}
public void AddFirst(T item)
{
lock (SyncRoot)
{
if (capacity == buffer.Count)
{
var remItem = buffer.RemoveLast();
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Remove(remItem, capacity - 1));
}
buffer.AddFirst(item);
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Add(item, 0));
}
}
public void AddLast(T item)
{
lock (SyncRoot)
{
if (capacity == buffer.Count)
{
var remItem = buffer.RemoveFirst();
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Remove(remItem, 0));
}
buffer.AddLast(item);
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Add(item, buffer.Count - 1));
}
}
public T RemoveFirst()
{
lock (SyncRoot)
{
var item = buffer.RemoveFirst();
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Remove(item, 0));
return item;
}
}
public T RemoveLast()
{
lock (SyncRoot)
{
var index = buffer.Count - 1;
var item = buffer.RemoveLast();
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Remove(item, index));
return item;
}
}
// AddFirstRange is not exists.
public void AddLastRange(IEnumerable<T> items)
{
lock (SyncRoot)
{
using (var xs = new CloneCollection<T>(items))
{
if (capacity <= buffer.Count + xs.Span.Length)
{
// calc remove count
var remCount = Math.Min(buffer.Count, buffer.Count + xs.Span.Length - capacity);
using (var ys = new ResizableArray<T>(remCount))
{
for (int i = 0; i < remCount; i++)
{
ys.Add(buffer.RemoveFirst());
}
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Remove(ys.Span, 0));
}
}
var index = buffer.Count;
var span = xs.Span;
if (span.Length > capacity)
{
span = span.Slice(span.Length - capacity);
}
foreach (var item in span)
{
buffer.AddLast(item);
}
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Add(span, index));
}
}
}
public void AddLastRange(T[] items)
{
lock (SyncRoot)
{
if (capacity <= buffer.Count + items.Length)
{
// calc remove count
var remCount = Math.Min(buffer.Count, buffer.Count + items.Length - capacity);
using (var ys = new ResizableArray<T>(remCount))
{
for (int i = 0; i < remCount; i++)
{
ys.Add(buffer.RemoveFirst());
}
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Remove(ys.Span, 0));
}
}
var index = buffer.Count;
var span = items.AsSpan();
if (span.Length > capacity)
{
span = span.Slice(span.Length - capacity);
}
foreach (var item in span)
{
buffer.AddLast(item);
}
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Add(span, index));
}
}
public void AddLastRange(ReadOnlySpan<T> items)
{
lock (SyncRoot)
{
if (capacity <= buffer.Count + items.Length)
{
// calc remove count
var remCount = Math.Min(buffer.Count, buffer.Count + items.Length - capacity);
using (var ys = new ResizableArray<T>(remCount))
{
for (int i = 0; i < remCount; i++)
{
ys.Add(buffer.RemoveFirst());
}
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Remove(ys.Span, 0));
}
}
var index = buffer.Count;
var span = items;
if (span.Length > capacity)
{
span = span.Slice(span.Length - capacity);
}
foreach (var item in span)
{
buffer.AddLast(item);
}
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Add(span, index));
}
}
public int IndexOf(T item)
{
lock (SyncRoot)
{
return buffer.IndexOf(item);
}
}
void IList<T>.Insert(int index, T item)
{
throw new NotSupportedException();
}
bool ICollection<T>.Remove(T item)
{
throw new NotSupportedException();
}
void IList<T>.RemoveAt(int index)
{
throw new NotSupportedException();
}
void ICollection<T>.Add(T item)
{
AddLast(item);
}
public void Clear()
{
lock (SyncRoot)
{
buffer.Clear();
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Reset());
}
}
public bool Contains(T item)
{
lock (SyncRoot)
{
return buffer.Contains(item);
}
}
public void CopyTo(T[] array, int arrayIndex)
{
lock (SyncRoot)
{
buffer.CopyTo(array, arrayIndex);
}
}
public T[] ToArray()
{
lock (SyncRoot)
{
return buffer.ToArray();
}
}
public int BinarySearch(T item)
{
lock (SyncRoot)
{
return buffer.BinarySearch(item);
}
}
public int BinarySearch(T item, IComparer<T> comparer)
{
lock (SyncRoot)
{
return buffer.BinarySearch(item, comparer);
}
}
public IEnumerator<T> GetEnumerator()
{
lock (SyncRoot)
{
foreach (var item in buffer)
{
yield return item;
}
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
public ISynchronizedView<T, TView> CreateView<TView>(Func<T, TView> transform, bool reverse = false)
{
return new ObservableRingBuffer<T>.View<TView>(this, transform, reverse);
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 81d799cc7eb02954d89c7ee52b7c9148
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,174 @@
using ObservableCollections.Internal;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System;
using System.Linq;
namespace ObservableCollections
{
public sealed partial class ObservableHashSet<T> : IReadOnlyCollection<T>, IObservableCollection<T>
{
public ISynchronizedView<T, TView> CreateView<TView>(Func<T, TView> transform, bool _ = false)
{
return new View<TView>(this, transform);
}
sealed class View<TView> : ISynchronizedView<T, TView>
{
readonly ObservableHashSet<T> source;
readonly Func<T, TView> selector;
readonly Dictionary<T, (T, TView)> dict;
ISynchronizedViewFilter<T, TView> filter;
public event NotifyCollectionChangedEventHandler<T> RoutingCollectionChanged;
public event Action<NotifyCollectionChangedAction> CollectionStateChanged;
public object SyncRoot { get; }
public View(ObservableHashSet<T> source, Func<T, TView> selector)
{
this.source = source;
this.selector = selector;
this.filter = SynchronizedViewFilter<T, TView>.Null;
this.SyncRoot = new object();
lock (source.SyncRoot)
{
this.dict = source.set.ToDictionary(x => x, x => (x, selector(x)));
this.source.CollectionChanged += SourceCollectionChanged;
}
}
public int Count
{
get
{
lock (SyncRoot)
{
return dict.Count;
}
}
}
public void AttachFilter(ISynchronizedViewFilter<T, TView> filter)
{
lock (SyncRoot)
{
this.filter = filter;
foreach (var (_, (value, view)) in dict)
{
filter.InvokeOnAttach(value, view);
}
}
}
public void ResetFilter(Action<T, TView> resetAction)
{
lock (SyncRoot)
{
this.filter = SynchronizedViewFilter<T, TView>.Null;
if (resetAction != null)
{
foreach (var (_, (value, view)) in dict)
{
resetAction(value, view);
}
}
}
}
public INotifyCollectionChangedSynchronizedView<T, TView> WithINotifyCollectionChanged()
{
lock (SyncRoot)
{
return new NotifyCollectionChangedSynchronizedView<T, TView>(this);
}
}
public IEnumerator<(T, TView)> GetEnumerator()
{
lock (SyncRoot)
{
foreach (var item in dict)
{
if (filter.IsMatch(item.Value.Item1, item.Value.Item2))
{
yield return item.Value;
}
}
}
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public void Dispose()
{
this.source.CollectionChanged -= SourceCollectionChanged;
}
private void SourceCollectionChanged(in NotifyCollectionChangedEventArgs<T> e)
{
lock (SyncRoot)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
if (e.IsSingleItem)
{
var v = (e.NewItem, selector(e.NewItem));
dict.Add(e.NewItem, v);
filter.InvokeOnAdd(v, e);
}
else
{
foreach (var item in e.NewItems)
{
var v = (item, selector(item));
dict.Add(item, v);
filter.InvokeOnAdd(v, e);
}
}
break;
case NotifyCollectionChangedAction.Remove:
if (e.IsSingleItem)
{
if (dict.Remove(e.OldItem, out var value))
{
filter.InvokeOnRemove(value.Item1, value.Item2, e);
}
}
else
{
foreach (var item in e.OldItems)
{
if (dict.Remove(item, out var value))
{
filter.InvokeOnRemove(value.Item1, value.Item2, e);
}
}
}
break;
case NotifyCollectionChangedAction.Reset:
if (!filter.IsNullFilter())
{
foreach (var item in dict)
{
filter.InvokeOnRemove(item.Value, e);
}
}
dict.Clear();
break;
case NotifyCollectionChangedAction.Replace:
case NotifyCollectionChangedAction.Move:
default:
break;
}
RoutingCollectionChanged?.Invoke(e);
CollectionStateChanged?.Invoke(e.Action);
}
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a7b9e5ab06e68b242a4a722866654fd7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,265 @@
using ObservableCollections.Internal;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
namespace ObservableCollections
{
// can not implements ISet<T> because set operation can not get added/removed values.
public sealed partial class ObservableHashSet<T> : IReadOnlySet<T>, IReadOnlyCollection<T>, IObservableCollection<T>
{
readonly HashSet<T> set;
public object SyncRoot { get; } = new object();
public ObservableHashSet()
{
this.set = new HashSet<T>();
}
#if NETSTANDARD2_1_OR_GREATER || NET5_0_OR_GREATER
public ObservableHashSet(int capacity)
{
this.set = new HashSet<T>(capacity);
}
#endif
public ObservableHashSet(IEnumerable<T> collection)
{
this.set = new HashSet<T>(collection);
}
public event NotifyCollectionChangedEventHandler<T> CollectionChanged;
public int Count
{
get
{
lock (SyncRoot)
{
return set.Count;
}
}
}
public bool IsReadOnly => false;
public bool Add(T item)
{
lock (SyncRoot)
{
if (set.Add(item))
{
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Add(item, -1));
return true;
}
return false;
}
}
public void AddRange(IEnumerable<T> items)
{
lock (SyncRoot)
{
if (!items.TryGetNonEnumeratedCount(out var capacity))
{
capacity = 4;
}
using (var list = new ResizableArray<T>(capacity))
{
foreach (var item in items)
{
if (set.Add(item))
{
list.Add(item);
}
}
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Add(list.Span, -1));
}
}
}
public void AddRange(T[] items)
{
AddRange(items.AsSpan());
}
public void AddRange(ReadOnlySpan<T> items)
{
lock (SyncRoot)
{
using (var list = new ResizableArray<T>(items.Length))
{
foreach (var item in items)
{
if (set.Add(item))
{
list.Add(item);
}
}
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Add(list.Span, -1));
}
}
}
public bool Remove(T item)
{
lock (SyncRoot)
{
if (set.Remove(item))
{
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Remove(item, -1));
return true;
}
return false;
}
}
public void RemoveRange(IEnumerable<T> items)
{
lock (SyncRoot)
{
if (!items.TryGetNonEnumeratedCount(out var capacity))
{
capacity = 4;
}
using (var list = new ResizableArray<T>(capacity))
{
foreach (var item in items)
{
if (set.Remove(item))
{
list.Add(item);
}
}
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Remove(list.Span, -1));
}
}
}
public void RemoveRange(T[] items)
{
RemoveRange(items.AsSpan());
}
public void RemoveRange(ReadOnlySpan<T> items)
{
lock (SyncRoot)
{
using (var list = new ResizableArray<T>(items.Length))
{
foreach (var item in items)
{
if (set.Remove(item))
{
list.Add(item);
}
}
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Remove(list.Span, -1));
}
}
}
public void Clear()
{
lock (SyncRoot)
{
set.Clear();
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Reset());
}
}
#if !NETSTANDARD2_0 && !NET_STANDARD_2_0 && !NET_4_6
public bool TryGetValue(T equalValue, [MaybeNullWhen(false)] out T actualValue)
{
return set.TryGetValue(equalValue, out actualValue);
}
#endif
public bool Contains(T item)
{
lock (SyncRoot)
{
return set.Contains(item);
}
}
public bool IsProperSubsetOf(IEnumerable<T> other)
{
lock (SyncRoot)
{
return set.IsProperSubsetOf(other);
}
}
public bool IsProperSupersetOf(IEnumerable<T> other)
{
lock (SyncRoot)
{
return set.IsProperSupersetOf(other);
}
}
public bool IsSubsetOf(IEnumerable<T> other)
{
lock (SyncRoot)
{
return set.IsSubsetOf(other);
}
}
public bool IsSupersetOf(IEnumerable<T> other)
{
lock (SyncRoot)
{
return set.IsSupersetOf(other);
}
}
public bool Overlaps(IEnumerable<T> other)
{
lock (SyncRoot)
{
return set.Overlaps(other);
}
}
public bool SetEquals(IEnumerable<T> other)
{
lock (SyncRoot)
{
return set.SetEquals(other);
}
}
public IEnumerator<T> GetEnumerator()
{
lock (SyncRoot)
{
foreach (var item in set)
{
yield return item;
}
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 38d982a9907bb0047b158794a1cf35e7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,235 @@
using ObservableCollections.Internal;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
namespace ObservableCollections
{
public sealed partial class ObservableList<T> : IList<T>, IReadOnlyList<T>, IObservableCollection<T>
{
public ISynchronizedView<T, TView> CreateView<TView>(Func<T, TView> transform, bool reverse = false)
{
return new View<TView>(this, transform, reverse);
}
sealed class View<TView> : ISynchronizedView<T, TView>
{
readonly ObservableList<T> source;
readonly Func<T, TView> selector;
readonly bool reverse;
readonly List<(T, TView)> list;
ISynchronizedViewFilter<T, TView> filter;
public event NotifyCollectionChangedEventHandler<T> RoutingCollectionChanged;
public event Action<NotifyCollectionChangedAction> CollectionStateChanged;
public object SyncRoot { get; }
public View(ObservableList<T> source, Func<T, TView> selector, bool reverse)
{
this.source = source;
this.selector = selector;
this.reverse = reverse;
this.filter = SynchronizedViewFilter<T, TView>.Null;
this.SyncRoot = new object();
lock (source.SyncRoot)
{
this.list = source.list.Select(x => (x, selector(x))).ToList();
this.source.CollectionChanged += SourceCollectionChanged;
}
}
public int Count
{
get
{
lock (SyncRoot)
{
return list.Count;
}
}
}
public void AttachFilter(ISynchronizedViewFilter<T, TView> filter)
{
lock (SyncRoot)
{
this.filter = filter;
foreach (var (value, view) in list)
{
filter.InvokeOnAttach(value, view);
}
}
}
public void ResetFilter(Action<T, TView> resetAction)
{
lock (SyncRoot)
{
this.filter = SynchronizedViewFilter<T, TView>.Null;
if (resetAction != null)
{
foreach (var (item, view) in list)
{
resetAction(item, view);
}
}
}
}
public INotifyCollectionChangedSynchronizedView<T, TView> WithINotifyCollectionChanged()
{
lock (SyncRoot)
{
return new NotifyCollectionChangedSynchronizedView<T, TView>(this);
}
}
public IEnumerator<(T, TView)> GetEnumerator()
{
lock (SyncRoot)
{
if (!reverse)
{
foreach (var item in list)
{
if (filter.IsMatch(item.Item1, item.Item2))
{
yield return item;
}
}
}
else
{
foreach (var item in list.AsEnumerable().Reverse())
{
if (filter.IsMatch(item.Item1, item.Item2))
{
yield return item;
}
}
}
}
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public void Dispose()
{
this.source.CollectionChanged -= SourceCollectionChanged;
}
private void SourceCollectionChanged(in NotifyCollectionChangedEventArgs<T> e)
{
lock (SyncRoot)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
// Add
if (e.NewStartingIndex == list.Count)
{
if (e.IsSingleItem)
{
var v = (e.NewItem, selector(e.NewItem));
list.Add(v);
filter.InvokeOnAdd(v, e);
}
else
{
foreach (var item in e.NewItems)
{
var v = (item, selector(item));
list.Add(v);
filter.InvokeOnAdd(v, e);
}
}
}
// Insert
else
{
if (e.IsSingleItem)
{
var v = (e.NewItem, selector(e.NewItem));
list.Insert(e.NewStartingIndex, v);
filter.InvokeOnAdd(v, e);
}
else
{
// inefficient copy, need refactoring
var newArray = new (T, TView)[e.NewItems.Length];
var span = e.NewItems;
for (int i = 0; i < span.Length; i++)
{
var v = (span[i], selector(span[i]));
newArray[i] = v;
filter.InvokeOnAdd(v, e);
}
list.InsertRange(e.NewStartingIndex, newArray);
}
}
break;
case NotifyCollectionChangedAction.Remove:
if (e.IsSingleItem)
{
var v = list[e.OldStartingIndex];
list.RemoveAt(e.OldStartingIndex);
filter.InvokeOnRemove(v.Item1, v.Item2, e);
}
else
{
var len = e.OldStartingIndex + e.OldItems.Length;
for (int i = e.OldStartingIndex; i < len; i++)
{
var v = list[i];
filter.InvokeOnRemove(v.Item1, v.Item2, e);
}
list.RemoveRange(e.OldStartingIndex, e.OldItems.Length);
}
break;
case NotifyCollectionChangedAction.Replace:
// ObservableList does not support replace range
{
var v = (e.NewItem, selector(e.NewItem));
var oldItem = list[e.NewStartingIndex];
list[e.NewStartingIndex] = v;
filter.InvokeOnRemove(oldItem, e);
filter.InvokeOnAdd(v, e);
break;
}
case NotifyCollectionChangedAction.Move:
{
var removeItem = list[e.OldStartingIndex];
list.RemoveAt(e.OldStartingIndex);
list.Insert(e.NewStartingIndex, removeItem);
filter.InvokeOnMove(removeItem, e);
}
break;
case NotifyCollectionChangedAction.Reset:
if (!filter.IsNullFilter())
{
foreach (var item in list)
{
filter.InvokeOnRemove(item, e);
}
}
list.Clear();
break;
default:
break;
}
RoutingCollectionChanged?.Invoke(e);
CollectionStateChanged?.Invoke(e.Action);
}
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b98cf9d4d5426ad478ede70c5ac637d2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,276 @@
using ObservableCollections.Internal;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
namespace ObservableCollections
{
public sealed partial class ObservableList<T> : IList<T>, IReadOnlyList<T>, IObservableCollection<T>
{
readonly List<T> list;
public object SyncRoot { get; } = new object();
public ObservableList()
{
list = new List<T>();
}
public ObservableList(int capacity)
{
list = new List<T>(capacity);
}
public ObservableList(IEnumerable<T> collection)
{
list = collection.ToList();
}
public T this[int index]
{
get
{
lock (SyncRoot)
{
return list[index];
}
}
set
{
lock (SyncRoot)
{
var oldValue = list[index];
list[index] = value;
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Replace(value, oldValue, index));
}
}
}
public int Count
{
get
{
lock (SyncRoot)
{
return list.Count;
}
}
}
public bool IsReadOnly => false;
public event NotifyCollectionChangedEventHandler<T> CollectionChanged;
public void Add(T item)
{
lock (SyncRoot)
{
var index = list.Count;
list.Add(item);
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Add(item, index));
}
}
public void AddRange(IEnumerable<T> items)
{
lock (SyncRoot)
{
var index = list.Count;
using (var xs = new CloneCollection<T>(items))
{
// to avoid iterate twice, require copy before insert.
list.AddRange(xs.AsEnumerable());
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Add(xs.Span, index));
}
}
}
public void AddRange(T[] items)
{
lock (SyncRoot)
{
var index = list.Count;
list.AddRange(items);
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Add(items, index));
}
}
public void AddRange(ReadOnlySpan<T> items)
{
lock (SyncRoot)
{
var index = list.Count;
foreach (var item in items)
{
list.Add(item);
}
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Add(items, index));
}
}
public void Clear()
{
var l = new List<int>();
lock (SyncRoot)
{
list.Clear();
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Reset());
}
}
public bool Contains(T item)
{
lock (SyncRoot)
{
return list.Contains(item);
}
}
public void CopyTo(T[] array, int arrayIndex)
{
lock (SyncRoot)
{
list.CopyTo(array, arrayIndex);
}
}
public IEnumerator<T> GetEnumerator()
{
lock (SyncRoot)
{
foreach (var item in list)
{
yield return item;
}
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
public void ForEach(Action<T> action)
{
lock (SyncRoot)
{
foreach (var item in list)
{
action(item);
}
}
}
public int IndexOf(T item)
{
lock (SyncRoot)
{
return list.IndexOf(item);
}
}
public void Insert(int index, T item)
{
lock (SyncRoot)
{
list.Insert(index, item);
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Add(item, index));
}
}
public void InsertRange(int index, T[] items)
{
lock (SyncRoot)
{
list.InsertRange(index, items);
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Add(items, index));
}
}
public void InsertRange(int index, IEnumerable<T> items)
{
lock (SyncRoot)
{
using (var xs = new CloneCollection<T>(items))
{
list.InsertRange(index, xs.AsEnumerable());
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Add(xs.Span, index));
}
}
}
public void InsertRange(int index, ReadOnlySpan<T> items)
{
lock (SyncRoot)
{
using (var xs = new CloneCollection<T>(items))
{
list.InsertRange(index, xs.AsEnumerable());
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Add(xs.Span, index));
}
}
}
public bool Remove(T item)
{
lock (SyncRoot)
{
var index = list.IndexOf(item);
if (index >= 0)
{
list.RemoveAt(index);
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Remove(item, index));
return true;
}
else
{
return false;
}
}
}
public void RemoveAt(int index)
{
lock (SyncRoot)
{
var item = list[index];
list.RemoveAt(index);
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Remove(item, index));
}
}
public void RemoveRange(int index, int count)
{
lock (SyncRoot)
{
#if NET5_0_OR_GREATER
var range = CollectionsMarshal.AsSpan(list).Slice(index, count);
#else
var range = list.GetRange(index, count);
#endif
// require copy before remove
using (var xs = new CloneCollection<T>(range))
{
list.RemoveRange(index, count);
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Remove(xs.Span, index));
}
}
}
public void Move(int oldIndex, int newIndex)
{
lock (SyncRoot)
{
var removedItem = list[oldIndex];
list.RemoveAt(oldIndex);
list.Insert(newIndex, removedItem);
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Move(removedItem, newIndex, oldIndex));
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 963681d5debafe6448e09cd9a4029cec
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,188 @@
using ObservableCollections.Internal;
using System.Collections;
using System.Collections.Specialized;
using System;
using System.Collections.Generic;
using System.Linq;
namespace ObservableCollections
{
public sealed partial class ObservableQueue<T> : IReadOnlyCollection<T>, IObservableCollection<T>
{
public ISynchronizedView<T, TView> CreateView<TView>(Func<T, TView> transform, bool reverse = false)
{
return new View<TView>(this, transform, reverse);
}
class View<TView> : ISynchronizedView<T, TView>
{
readonly ObservableQueue<T> source;
readonly Func<T, TView> selector;
readonly bool reverse;
protected readonly Queue<(T, TView)> queue;
ISynchronizedViewFilter<T, TView> filter;
public event NotifyCollectionChangedEventHandler<T> RoutingCollectionChanged;
public event Action<NotifyCollectionChangedAction> CollectionStateChanged;
public object SyncRoot { get; }
public View(ObservableQueue<T> source, Func<T, TView> selector, bool reverse)
{
this.source = source;
this.selector = selector;
this.reverse = reverse;
this.filter = SynchronizedViewFilter<T, TView>.Null;
this.SyncRoot = new object();
lock (source.SyncRoot)
{
this.queue = new Queue<(T, TView)>(source.queue.Select(x => (x, selector(x))));
this.source.CollectionChanged += SourceCollectionChanged;
}
}
public int Count
{
get
{
lock (SyncRoot)
{
return queue.Count;
}
}
}
public void AttachFilter(ISynchronizedViewFilter<T, TView> filter)
{
lock (SyncRoot)
{
this.filter = filter;
foreach (var (value, view) in queue)
{
filter.InvokeOnAttach(value, view);
}
}
}
public void ResetFilter(Action<T, TView> resetAction)
{
lock (SyncRoot)
{
this.filter = SynchronizedViewFilter<T, TView>.Null;
if (resetAction != null)
{
foreach (var (item, view) in queue)
{
resetAction(item, view);
}
}
}
}
public INotifyCollectionChangedSynchronizedView<T, TView> WithINotifyCollectionChanged()
{
lock (SyncRoot)
{
return new NotifyCollectionChangedSynchronizedView<T, TView>(this);
}
}
public IEnumerator<(T, TView)> GetEnumerator()
{
lock (SyncRoot)
{
if (!reverse)
{
foreach (var item in queue)
{
if (filter.IsMatch(item.Item1, item.Item2))
{
yield return item;
}
}
}
else
{
foreach (var item in queue.AsEnumerable().Reverse())
{
if (filter.IsMatch(item.Item1, item.Item2))
{
yield return item;
}
}
}
}
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public void Dispose()
{
this.source.CollectionChanged -= SourceCollectionChanged;
}
private void SourceCollectionChanged(in NotifyCollectionChangedEventArgs<T> e)
{
lock (SyncRoot)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
// Add(Enqueue, EnqueueRange)
if (e.IsSingleItem)
{
var v = (e.NewItem, selector(e.NewItem));
queue.Enqueue(v);
filter.InvokeOnAdd(v, e);
}
else
{
foreach (var item in e.NewItems)
{
var v = (item, selector(item));
queue.Enqueue(v);
filter.InvokeOnAdd(v, e);
}
}
break;
case NotifyCollectionChangedAction.Remove:
// Dequeue, DequeuRange
if (e.IsSingleItem)
{
var v = queue.Dequeue();
filter.InvokeOnRemove(v.Item1, v.Item2, e);
}
else
{
var len = e.OldItems.Length;
for (int i = 0; i < len; i++)
{
var v = queue.Dequeue();
filter.InvokeOnRemove(v.Item1, v.Item2, e);
}
}
break;
case NotifyCollectionChangedAction.Reset:
if (!filter.IsNullFilter())
{
foreach (var item in queue)
{
filter.InvokeOnRemove(item, e);
}
}
queue.Clear();
break;
case NotifyCollectionChangedAction.Replace:
case NotifyCollectionChangedAction.Move:
default:
break;
}
RoutingCollectionChanged?.Invoke(e);
CollectionStateChanged?.Invoke(e.Action);
}
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3fea76167e2a69749931bf01b5b5ed92
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,220 @@
using ObservableCollections.Internal;
using System.Buffers;
using System.Collections;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
namespace ObservableCollections
{
public sealed partial class ObservableQueue<T> : IReadOnlyCollection<T>, IObservableCollection<T>
{
readonly Queue<T> queue;
public object SyncRoot { get; } = new object();
public ObservableQueue()
{
this.queue = new Queue<T>();
}
public ObservableQueue(int capacity)
{
this.queue = new Queue<T>(capacity);
}
public ObservableQueue(IEnumerable<T> collection)
{
this.queue = new Queue<T>(collection);
}
public event NotifyCollectionChangedEventHandler<T> CollectionChanged;
public int Count
{
get
{
lock (SyncRoot)
{
return queue.Count;
}
}
}
public void Enqueue(T item)
{
lock (SyncRoot)
{
var index = queue.Count;
queue.Enqueue(item);
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Add(item, index));
}
}
public void EnqueueRange(IEnumerable<T> items)
{
lock (SyncRoot)
{
var index = queue.Count;
using (var xs = new CloneCollection<T>(items))
{
foreach (var item in xs.Span)
{
queue.Enqueue(item);
}
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Add(xs.Span, index));
}
}
}
public void EnqueueRange(T[] items)
{
lock (SyncRoot)
{
var index = queue.Count;
foreach (var item in items)
{
queue.Enqueue(item);
}
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Add(items, index));
}
}
public void EnqueueRange(ReadOnlySpan<T> items)
{
lock (SyncRoot)
{
var index = queue.Count;
foreach (var item in items)
{
queue.Enqueue(item);
}
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Add(items, index));
}
}
public T Dequeue()
{
lock (SyncRoot)
{
var v = queue.Dequeue();
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Remove(v, 0));
return v;
}
}
public bool TryDequeue([MaybeNullWhen(false)] out T result)
{
lock (SyncRoot)
{
if (queue.Count != 0)
{
result = queue.Dequeue();
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Remove(result, 0));
return true;
}
result = default;
return false;
}
}
public void DequeueRange(int count)
{
lock (SyncRoot)
{
var dest = ArrayPool<T>.Shared.Rent(count);
try
{
for (int i = 0; i < count; i++)
{
dest[i] = queue.Dequeue();
}
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Remove(dest.AsSpan(0, count), 0));
}
finally
{
ArrayPool<T>.Shared.Return(dest, RuntimeHelpersEx.IsReferenceOrContainsReferences<T>());
}
}
}
public void DequeueRange(Span<T> dest)
{
lock (SyncRoot)
{
for (int i = 0; i < dest.Length; i++)
{
dest[i] = queue.Dequeue();
}
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Remove(dest, 0));
}
}
public void Clear()
{
lock (SyncRoot)
{
queue.Clear();
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Reset());
}
}
public T Peek()
{
lock (SyncRoot)
{
return queue.Peek();
}
}
public bool TryPeek([MaybeNullWhen(false)] T result)
{
lock (SyncRoot)
{
if (queue.Count != 0)
{
result = queue.Peek();
return true;
}
result = default;
return false;
}
}
public T[] ToArray()
{
lock (SyncRoot)
{
return queue.ToArray();
}
}
public void TrimExcess()
{
lock (SyncRoot)
{
queue.TrimExcess();
}
}
public IEnumerator<T> GetEnumerator()
{
lock (SyncRoot)
{
foreach (var item in queue)
{
yield return item;
}
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: eb4558e36aaf5c84597551bb9a0d42e9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,246 @@
using ObservableCollections.Internal;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
namespace ObservableCollections
{
public sealed partial class ObservableRingBuffer<T>
{
public ISynchronizedView<T, TView> CreateView<TView>(Func<T, TView> transform, bool reverse = false)
{
return new View<TView>(this, transform, reverse);
}
// used with ObservableFixedSizeRingBuffer
internal sealed class View<TView> : ISynchronizedView<T, TView>
{
readonly IObservableCollection<T> source;
readonly Func<T, TView> selector;
readonly bool reverse;
readonly RingBuffer<(T, TView)> ringBuffer;
ISynchronizedViewFilter<T, TView> filter;
public event NotifyCollectionChangedEventHandler<T> RoutingCollectionChanged;
public event Action<NotifyCollectionChangedAction> CollectionStateChanged;
public object SyncRoot { get; }
public View(IObservableCollection<T> source, Func<T, TView> selector, bool reverse)
{
this.source = source;
this.selector = selector;
this.reverse = reverse;
this.filter = SynchronizedViewFilter<T, TView>.Null;
this.SyncRoot = new object();
lock (source.SyncRoot)
{
this.ringBuffer = new RingBuffer<(T, TView)>(source.Select(x => (x, selector(x))));
this.source.CollectionChanged += SourceCollectionChanged;
}
}
public int Count
{
get
{
lock (SyncRoot)
{
return ringBuffer.Count;
}
}
}
public void AttachFilter(ISynchronizedViewFilter<T, TView> filter)
{
lock (SyncRoot)
{
this.filter = filter;
foreach (var (value, view) in ringBuffer)
{
filter.InvokeOnAttach(value, view);
}
}
}
public void ResetFilter(Action<T, TView> resetAction)
{
lock (SyncRoot)
{
this.filter = SynchronizedViewFilter<T, TView>.Null;
if (resetAction != null)
{
foreach (var (item, view) in ringBuffer)
{
resetAction(item, view);
}
}
}
}
public INotifyCollectionChangedSynchronizedView<T, TView> WithINotifyCollectionChanged()
{
lock (SyncRoot)
{
return new NotifyCollectionChangedSynchronizedView<T, TView>(this);
}
}
public IEnumerator<(T, TView)> GetEnumerator()
{
lock (SyncRoot)
{
if (!reverse)
{
foreach (var item in ringBuffer)
{
if (filter.IsMatch(item.Item1, item.Item2))
{
yield return item;
}
}
}
else
{
foreach (var item in ringBuffer.AsEnumerable().Reverse())
{
if (filter.IsMatch(item.Item1, item.Item2))
{
yield return item;
}
}
}
}
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public void Dispose()
{
this.source.CollectionChanged -= SourceCollectionChanged;
}
private void SourceCollectionChanged(in NotifyCollectionChangedEventArgs<T> e)
{
lock (SyncRoot)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
// can not distinguish AddFirst and AddLast when collection count is 0.
// So, in that case, use AddLast.
// The internal structure may be different from the parent, but the result is same.
// RangeOperation is only exists AddLastRange because we can not distinguish FirstRange or LastRange.
if (e.NewStartingIndex == 0 && ringBuffer.Count != 0)
{
// AddFirst
if (e.IsSingleItem)
{
var v = (e.NewItem, selector(e.NewItem));
ringBuffer.AddFirst(v);
filter.InvokeOnAdd(v, e);
}
else
{
foreach (var item in e.NewItems)
{
var v = (item, selector(item));
ringBuffer.AddFirst(v);
filter.InvokeOnAdd(v, e);
}
}
}
else
{
// AddLast
if (e.IsSingleItem)
{
var v = (e.NewItem, selector(e.NewItem));
ringBuffer.AddLast(v);
filter.InvokeOnAdd(v, e);
}
else
{
foreach (var item in e.NewItems)
{
var v = (item, selector(item));
ringBuffer.AddLast(v);
filter.InvokeOnAdd(v, e);
}
}
}
break;
case NotifyCollectionChangedAction.Remove:
// starting from 0 is RemoveFirst
if (e.OldStartingIndex == 0)
{
// RemoveFirst
if (e.IsSingleItem)
{
var v = ringBuffer.RemoveFirst();
filter.InvokeOnRemove(v, e);
}
else
{
for (int i = 0; i < e.OldItems.Length; i++)
{
var v = ringBuffer.RemoveFirst();
filter.InvokeOnRemove(v, e);
}
}
}
else
{
// RemoveLast
if (e.IsSingleItem)
{
var v = ringBuffer.RemoveLast();
filter.InvokeOnRemove(v, e);
}
else
{
for (int i = 0; i < e.OldItems.Length; i++)
{
var v = ringBuffer.RemoveLast();
filter.InvokeOnRemove(v, e);
}
}
}
break;
case NotifyCollectionChangedAction.Reset:
if (!filter.IsNullFilter())
{
foreach (var item in ringBuffer)
{
filter.InvokeOnRemove(item, e);
}
}
ringBuffer.Clear();
break;
case NotifyCollectionChangedAction.Replace:
// range is not supported
{
var v = (e.NewItem, selector(e.NewItem));
var oldItem = ringBuffer[e.NewStartingIndex];
ringBuffer[e.NewStartingIndex] = v;
filter.InvokeOnRemove(oldItem, e);
filter.InvokeOnAdd(v, e);
break;
}
case NotifyCollectionChangedAction.Move:
default:
break;
}
RoutingCollectionChanged?.Invoke(e);
CollectionStateChanged?.Invoke(e.Action);
}
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9a4b3308e0d0c4c4bafd65b41e7d66cd
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,236 @@
using ObservableCollections.Internal;
using System;
using System.Linq;
using System.Collections.Generic;
using System.Collections;
namespace ObservableCollections
{
public sealed partial class ObservableRingBuffer<T> : IList<T>, IReadOnlyList<T>, IObservableCollection<T>
{
readonly RingBuffer<T> buffer;
public event NotifyCollectionChangedEventHandler<T> CollectionChanged;
public ObservableRingBuffer()
{
this.buffer = new RingBuffer<T>();
}
public ObservableRingBuffer(IEnumerable<T> collection)
{
this.buffer = new RingBuffer<T>(collection);
}
public bool IsReadOnly => false;
public object SyncRoot { get; } = new object();
public T this[int index]
{
get
{
lock (SyncRoot)
{
return this.buffer[index];
}
}
set
{
lock (SyncRoot)
{
var oldValue = buffer[index];
buffer[index] = value;
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Replace(value, oldValue, index));
}
}
}
public int Count
{
get
{
lock (SyncRoot)
{
return buffer.Count;
}
}
}
public void AddFirst(T item)
{
lock (SyncRoot)
{
buffer.AddFirst(item);
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Add(item, 0));
}
}
public void AddLast(T item)
{
lock (SyncRoot)
{
buffer.AddLast(item);
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Add(item, buffer.Count - 1));
}
}
public T RemoveFirst()
{
lock (SyncRoot)
{
var item = buffer.RemoveFirst();
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Remove(item, 0));
return item;
}
}
public T RemoveLast()
{
lock (SyncRoot)
{
var index = buffer.Count - 1;
var item = buffer.RemoveLast();
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Remove(item, index));
return item;
}
}
// AddFirstRange is not exists.
public void AddLastRange(IEnumerable<T> items)
{
lock (SyncRoot)
{
var index = buffer.Count;
using (var xs = new CloneCollection<T>(items))
{
foreach (var item in xs.Span)
{
buffer.AddLast(item);
}
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Add(xs.Span, index));
}
}
}
public void AddLastRange(T[] items)
{
lock (SyncRoot)
{
var index = buffer.Count;
foreach (var item in items)
{
buffer.AddLast(item);
}
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Add(items, index));
}
}
public void AddLastRange(ReadOnlySpan<T> items)
{
lock (SyncRoot)
{
var index = buffer.Count;
foreach (var item in items)
{
buffer.AddLast(item);
}
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Add(items, index));
}
}
public int IndexOf(T item)
{
lock (SyncRoot)
{
return buffer.IndexOf(item);
}
}
void IList<T>.Insert(int index, T item)
{
throw new NotSupportedException();
}
bool ICollection<T>.Remove(T item)
{
throw new NotSupportedException();
}
void IList<T>.RemoveAt(int index)
{
throw new NotSupportedException();
}
void ICollection<T>.Add(T item)
{
AddLast(item);
}
public void Clear()
{
lock (SyncRoot)
{
buffer.Clear();
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Reset());
}
}
public bool Contains(T item)
{
lock (SyncRoot)
{
return buffer.Contains(item);
}
}
public void CopyTo(T[] array, int arrayIndex)
{
lock (SyncRoot)
{
buffer.CopyTo(array, arrayIndex);
}
}
public T[] ToArray()
{
lock (SyncRoot)
{
return buffer.ToArray();
}
}
public int BinarySearch(T item)
{
lock (SyncRoot)
{
return buffer.BinarySearch(item);
}
}
public int BinarySearch(T item, IComparer<T> comparer)
{
lock (SyncRoot)
{
return buffer.BinarySearch(item, comparer);
}
}
public IEnumerator<T> GetEnumerator()
{
lock (SyncRoot)
{
foreach (var item in buffer)
{
yield return item;
}
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c858015a9e82dc84487b3e627916a7ff
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,188 @@
using ObservableCollections.Internal;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
namespace ObservableCollections
{
public sealed partial class ObservableStack<T> : IReadOnlyCollection<T>, IObservableCollection<T>
{
public ISynchronizedView<T, TView> CreateView<TView>(Func<T, TView> transform, bool reverse = false)
{
return new View<TView>(this, transform, reverse);
}
class View<TView> : ISynchronizedView<T, TView>
{
readonly ObservableStack<T> source;
readonly Func<T, TView> selector;
readonly bool reverse;
protected readonly Stack<(T, TView)> stack;
ISynchronizedViewFilter<T, TView> filter;
public event NotifyCollectionChangedEventHandler<T> RoutingCollectionChanged;
public event Action<NotifyCollectionChangedAction> CollectionStateChanged;
public object SyncRoot { get; }
public View(ObservableStack<T> source, Func<T, TView> selector, bool reverse)
{
this.source = source;
this.selector = selector;
this.reverse = reverse;
this.filter = SynchronizedViewFilter<T, TView>.Null;
this.SyncRoot = new object();
lock (source.SyncRoot)
{
this.stack = new Stack<(T, TView)>(source.stack.Select(x => (x, selector(x))));
this.source.CollectionChanged += SourceCollectionChanged;
}
}
public int Count
{
get
{
lock (SyncRoot)
{
return stack.Count;
}
}
}
public void AttachFilter(ISynchronizedViewFilter<T, TView> filter)
{
lock (SyncRoot)
{
this.filter = filter;
foreach (var (value, view) in stack)
{
filter.InvokeOnAttach(value, view);
}
}
}
public void ResetFilter(Action<T, TView> resetAction)
{
lock (SyncRoot)
{
this.filter = SynchronizedViewFilter<T, TView>.Null;
if (resetAction != null)
{
foreach (var (item, view) in stack)
{
resetAction(item, view);
}
}
}
}
public INotifyCollectionChangedSynchronizedView<T, TView> WithINotifyCollectionChanged()
{
lock (SyncRoot)
{
return new NotifyCollectionChangedSynchronizedView<T, TView>(this);
}
}
public IEnumerator<(T, TView)> GetEnumerator()
{
lock (SyncRoot)
{
if (!reverse)
{
foreach (var item in stack)
{
if (filter.IsMatch(item.Item1, item.Item2))
{
yield return item;
}
}
}
else
{
foreach (var item in stack.AsEnumerable().Reverse())
{
if (filter.IsMatch(item.Item1, item.Item2))
{
yield return item;
}
}
}
}
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public void Dispose()
{
this.source.CollectionChanged -= SourceCollectionChanged;
}
private void SourceCollectionChanged(in NotifyCollectionChangedEventArgs<T> e)
{
lock (SyncRoot)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
// Add(Push, PushRange)
if (e.IsSingleItem)
{
var v = (e.NewItem, selector(e.NewItem));
stack.Push(v);
filter.InvokeOnAdd(v, e);
}
else
{
foreach (var item in e.NewItems)
{
var v = (item, selector(item));
stack.Push(v);
filter.InvokeOnAdd(v, e);
}
}
break;
case NotifyCollectionChangedAction.Remove:
// Pop, PopRange
if (e.IsSingleItem)
{
var v = stack.Pop();
filter.InvokeOnRemove(v.Item1, v.Item2, e);
}
else
{
var len = e.OldItems.Length;
for (int i = 0; i < len; i++)
{
var v = stack.Pop();
filter.InvokeOnRemove(v.Item1, v.Item2, e);
}
}
break;
case NotifyCollectionChangedAction.Reset:
if (!filter.IsNullFilter())
{
foreach (var item in stack)
{
filter.InvokeOnRemove(item, e);
}
}
stack.Clear();
break;
case NotifyCollectionChangedAction.Replace:
case NotifyCollectionChangedAction.Move:
default:
break;
}
RoutingCollectionChanged?.Invoke(e);
CollectionStateChanged?.Invoke(e.Action);
}
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: bfc46ddd0f9e1e246931d87579f595b0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,216 @@
using ObservableCollections.Internal;
using System;
using System.Buffers;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
namespace ObservableCollections
{
public sealed partial class ObservableStack<T> : IReadOnlyCollection<T>, IObservableCollection<T>
{
readonly Stack<T> stack;
public object SyncRoot { get; } = new object();
public ObservableStack()
{
this.stack = new Stack<T>();
}
public ObservableStack(int capacity)
{
this.stack = new Stack<T>(capacity);
}
public ObservableStack(IEnumerable<T> collection)
{
this.stack = new Stack<T>(collection);
}
public event NotifyCollectionChangedEventHandler<T> CollectionChanged;
public int Count
{
get
{
lock (SyncRoot)
{
return stack.Count;
}
}
}
public void Push(T item)
{
lock (SyncRoot)
{
var index = stack.Count;
stack.Push(item);
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Add(item, index));
}
}
public void PushRange(IEnumerable<T> items)
{
lock (SyncRoot)
{
using (var xs = new CloneCollection<T>(items))
{
foreach (var item in xs.Span)
{
stack.Push(item);
}
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Add(xs.Span, 0));
}
}
}
public void PushRange(T[] items)
{
lock (SyncRoot)
{
foreach (var item in items)
{
stack.Push(item);
}
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Add(items, 0));
}
}
public void PushRange(ReadOnlySpan<T> items)
{
lock (SyncRoot)
{
foreach (var item in items)
{
stack.Push(item);
}
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Add(items, 0));
}
}
public T Pop()
{
lock (SyncRoot)
{
var v = stack.Pop();
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Remove(v, 0));
return v;
}
}
public bool TryPop([MaybeNullWhen(false)] out T result)
{
lock (SyncRoot)
{
if (stack.Count != 0)
{
result = stack.Pop();
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Remove(result, 0));
return true;
}
result = default;
return false;
}
}
public void PopRange(int count)
{
lock (SyncRoot)
{
var dest = ArrayPool<T>.Shared.Rent(count);
try
{
for (int i = 0; i < count; i++)
{
dest[i] = stack.Pop();
}
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Remove(dest.AsSpan(0, count), 0));
}
finally
{
ArrayPool<T>.Shared.Return(dest, RuntimeHelpersEx.IsReferenceOrContainsReferences<T>());
}
}
}
public void PopRange(Span<T> dest)
{
lock (SyncRoot)
{
for (int i = 0; i < dest.Length; i++)
{
dest[i] = stack.Pop();
}
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Remove(dest, 0));
}
}
public void Clear()
{
lock (SyncRoot)
{
stack.Clear();
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Reset());
}
}
public T Peek()
{
lock (SyncRoot)
{
return stack.Peek();
}
}
public bool TryPeek([MaybeNullWhen(false)] T result)
{
lock (SyncRoot)
{
if (stack.Count != 0)
{
result = stack.Peek();
return true;
}
result = default;
return false;
}
}
public T[] ToArray()
{
lock (SyncRoot)
{
return stack.ToArray();
}
}
public void TrimExcess()
{
lock (SyncRoot)
{
stack.TrimExcess();
}
}
public IEnumerator<T> GetEnumerator()
{
lock (SyncRoot)
{
foreach (var item in stack)
{
yield return item;
}
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e89128583a1ffee4babe861213e14be2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,405 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
namespace ObservableCollections
{
public sealed class RingBuffer<T> : IList<T>, IReadOnlyList<T>
{
T[] buffer;
int head;
int count;
int mask;
public RingBuffer()
{
this.buffer = new T[8];
this.head = 0;
this.count = 0;
this.mask = buffer.Length - 1;
}
public RingBuffer(int capacity)
{
this.buffer = new T[CalculateCapacity(capacity)];
this.head = 0;
this.count = 0;
this.mask = buffer.Length - 1;
}
public RingBuffer(IEnumerable<T> collection)
{
var array = collection.TryGetNonEnumeratedCount(out var count)
? new T[CalculateCapacity(count)]
: new T[8];
var i = 0;
foreach (var item in collection)
{
if (i == array.Length)
{
Array.Resize(ref array, i * 2);
}
array[i++] = item;
}
this.buffer = array;
this.head = 0;
this.count = i;
this.mask = buffer.Length - 1;
}
static int CalculateCapacity(int size)
{
size--;
size |= size >> 1;
size |= size >> 2;
size |= size >> 4;
size |= size >> 8;
size |= size >> 16;
size += 1;
if (size < 8)
{
size = 8;
}
return size;
}
public T this[int index]
{
get
{
var i = (head + index) & mask;
return buffer[i];
}
set
{
var i = (head + index) & mask;
buffer[i] = value;
}
}
public int Count => count;
public bool IsReadOnly => false;
public void AddLast(T item)
{
if (count == buffer.Length) EnsureCapacity();
var index = (head + count) & mask;
buffer[index] = item;
count++;
}
public void AddFirst(T item)
{
if (count == buffer.Length) EnsureCapacity();
head = (head - 1) & mask;
buffer[head] = item;
count++;
}
public T RemoveLast()
{
if (count == 0) ThrowForEmpty();
var index = (head + count - 1) & mask;
var v = buffer[index];
buffer[index] = default;
count--;
return v;
}
public T RemoveFirst()
{
if (count == 0) ThrowForEmpty();
var index = head & mask;
var v = buffer[index];
buffer[index] = default;
head = head + 1;
count--;
return v;
}
void EnsureCapacity()
{
var newBuffer = new T[buffer.Length * 2];
var i = head & mask;
buffer.AsSpan(i).CopyTo(newBuffer);
if (i != 0)
{
buffer.AsSpan(0, i).CopyTo(newBuffer.AsSpan(buffer.Length - i));
}
head = 0;
buffer = newBuffer;
mask = newBuffer.Length - 1;
}
void ICollection<T>.Add(T item)
{
AddLast(item);
}
public void Clear()
{
Array.Clear(buffer, 0, buffer.Length);
head = 0;
count = 0;
}
public RingBufferSpan<T> GetSpan()
{
if (count == 0)
{
return new RingBufferSpan<T>(Array.Empty<T>(), Array.Empty<T>(), 0);
}
var start = head & mask;
var end = (head + count) & mask;
if (end > start)
{
var first = buffer.AsSpan(start, count);
var second = Array.Empty<T>().AsSpan();
return new RingBufferSpan<T>(first, second, count);
}
else
{
var first = buffer.AsSpan(start, buffer.Length - start);
var second = buffer.AsSpan(0, end);
return new RingBufferSpan<T>(first, second, count);
}
}
public IEnumerator<T> GetEnumerator()
{
if (count == 0) yield break;
var start = head & mask;
var end = (head + count) & mask;
if (end > start)
{
// start...end
for (int i = start; i < end; i++)
{
yield return buffer[i];
}
}
else
{
// start...
for (int i = start; i < buffer.Length; i++)
{
yield return buffer[i];
}
// 0...end
for (int i = 0; i < end; i++)
{
yield return buffer[i];
}
}
}
public IEnumerable<T> Reverse()
{
if (count == 0) yield break;
var start = head & mask;
var end = (head + count) & mask;
if (end > start)
{
// end...start
for (int i = end - 1; i >= start; i--)
{
yield return buffer[i];
}
}
else
{
// end...0
for (int i = end - 1; i >= 0; i--)
{
yield return buffer[i];
}
// ...start
for (int i = buffer.Length - 1; i >= start; i--)
{
yield return buffer[i];
}
}
}
public bool Contains(T item)
{
return IndexOf(item) != -1;
}
public void CopyTo(T[] array, int arrayIndex)
{
var span = GetSpan();
var dest = array.AsSpan(arrayIndex);
span.First.CopyTo(dest);
span.Second.CopyTo(dest.Slice(span.First.Length));
}
public int IndexOf(T item)
{
var i = 0;
foreach (var v in GetSpan())
{
if (EqualityComparer<T>.Default.Equals(item, v))
{
return i;
}
i++;
}
return -1;
}
public T[] ToArray()
{
var result = new T[count];
var i = 0;
foreach (var item in GetSpan())
{
result[i++] = item;
}
return result;
}
public int BinarySearch(T item)
{
return BinarySearch(item, Comparer<T>.Default);
}
public int BinarySearch(T item, IComparer<T> comparer)
{
var lo = 0;
var hi = count - 1;
while (lo <= hi)
{
var mid = (int)(((uint)hi + (uint)lo) >> 1);
var found = comparer.Compare(this[mid], item);
if (found == 0) return mid;
if (found < 0)
{
lo = mid + 1;
}
else
{
hi = mid - 1;
}
}
return ~lo;
}
void IList<T>.Insert(int index, T item)
{
throw new NotSupportedException();
}
bool ICollection<T>.Remove(T item)
{
throw new NotSupportedException();
}
void IList<T>.RemoveAt(int index)
{
throw new NotSupportedException();
}
IEnumerator IEnumerable.GetEnumerator()
{
return ((IEnumerable<T>)this).GetEnumerator();
}
[DoesNotReturn]
static void ThrowForEmpty()
{
throw new InvalidOperationException("RingBuffer is empty.");
}
}
public ref struct RingBufferSpan<T>
{
public readonly ReadOnlySpan<T> First;
public readonly ReadOnlySpan<T> Second;
public readonly int Count;
internal RingBufferSpan(ReadOnlySpan<T> first, ReadOnlySpan<T> second, int count)
{
First = first;
Second = second;
Count = count;
}
public Enumerator GetEnumerator()
{
return new Enumerator(this);
}
public ref struct Enumerator
{
ReadOnlySpan<T>.Enumerator firstEnumerator;
ReadOnlySpan<T>.Enumerator secondEnumerator;
bool useFirst;
public Enumerator(RingBufferSpan<T> span)
{
this.firstEnumerator = span.First.GetEnumerator();
this.secondEnumerator = span.Second.GetEnumerator();
this.useFirst = true;
}
public bool MoveNext()
{
if (useFirst)
{
if (firstEnumerator.MoveNext())
{
return true;
}
else
{
useFirst = false;
}
}
if (secondEnumerator.MoveNext())
{
return true;
}
return false;
}
public T Current
{
get
{
if (useFirst)
{
return firstEnumerator.Current;
}
else
{
return secondEnumerator.Current;
}
}
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 090e1b8528f51164695f1ed11c685465
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 8c8f516891f9df44a847a0bb51a064f0
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,64 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace System.Collections.Generic
{
internal static class CollectionExtensions
{
public static void Deconstruct<TKey, TValue>(this KeyValuePair<TKey, TValue> kvp, out TKey key, out TValue value)
{
key = kvp.Key;
value = kvp.Value;
}
public static bool Remove<TKey, TValue>(this SortedDictionary<TKey, TValue> dict, TKey key, out TValue value)
{
if (dict.TryGetValue(key, out value))
{
return dict.Remove(key);
}
return false;
}
public static bool Remove<TKey, TValue>(this Dictionary<TKey, TValue> dict, TKey key, out TValue value)
{
if (dict.TryGetValue(key, out value))
{
return dict.Remove(key);
}
return false;
}
#if !NET6_0_OR_GREATER
public static bool TryGetNonEnumeratedCount<T>(this IEnumerable<T> source, out int count)
{
if (source is ICollection<T> collection)
{
count = collection.Count;
return true;
}
if (source is IReadOnlyCollection<T> rCollection)
{
count = rCollection.Count;
return true;
}
count = 0;
return false;
}
#endif
}
#if !NET5_0_OR_GREATER
internal interface IReadOnlySet<T> : System.Collections.Generic.IEnumerable<T>, System.Collections.Generic.IReadOnlyCollection<T>
{
}
#endif
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 02ab78c5678c3bd4ebb45ca99fa0eaea
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace System.Runtime.CompilerServices
{
internal static class RuntimeHelpersEx
{
internal static bool IsReferenceOrContainsReferences<T>()
{
#if NETSTANDARD2_0 || NET_STANDARD_2_0 || NET_4_6
return true;
#else
return RuntimeHelpers.IsReferenceOrContainsReferences<T>();
#endif
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 90c6c97d9daed56458c4b0f69217c442
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.Text;
#if (NETSTANDARD2_0 || NET_STANDARD_2_0 || NET_4_6) && !UNITY_2021_1_OR_NEWER
namespace System.Diagnostics.CodeAnalysis
{
internal sealed class MaybeNullWhenAttribute : Attribute
{
public MaybeNullWhenAttribute(bool returnValue)
{
}
}
internal sealed class DoesNotReturnAttribute : Attribute
{
public DoesNotReturnAttribute()
{
}
}
}
#endif

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2b96ddbca917d2749a613d7dff847f37
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,12 @@
{
"name": "com.cysharp.observablecollections",
"displayName": "ObservableCollections",
"author": { "name": "Cysharp, Inc.", "url": "https://cysharp.co.jp/en/" },
"version": "1.1.3",
"unity": "2018.4",
"description": "High performance observable collections and synchronized views.",
"keywords": [ "Scripting", "DI" ],
"license": "MIT",
"category": "Scripting",
"dependencies": {}
}

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 142803aeb06678d4faef8b76468c110b
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,33 @@
fileFormatVersion: 2
guid: 80babde185fa5f5f58e1c7c451054bf5
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 0
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
Any:
second:
enabled: 1
settings: {}
- first:
Editor: Editor
second:
enabled: 0
settings:
DefaultValueInitialized: true
- first:
Windows Store Apps: WindowsStoreApps
second:
enabled: 0
settings:
CPU: AnyCPU
userData:
assetBundleName:
assetBundleVariant:

Some files were not shown because too many files have changed in this diff Show More