Compare commits
147 Commits
hadashiA/r
...
master
Author | SHA1 | Date | |
---|---|---|---|
![]() |
e9cf488b0d | ||
![]() |
57fdc250ce | ||
![]() |
8beb9e1996 | ||
![]() |
5bc5ea17c7 | ||
![]() |
bd6e249304 | ||
![]() |
6f5de33bff | ||
![]() |
7c4974c36a | ||
![]() |
a16edc16ed | ||
![]() |
6e263fa123 | ||
![]() |
b92e0de242 | ||
![]() |
890388ea5e | ||
![]() |
a4d80bda85 | ||
![]() |
43357a5198 | ||
![]() |
c1c9d86ff7 | ||
![]() |
73c5b8e83e | ||
![]() |
bd7cc350f2 | ||
![]() |
b000cc8521 | ||
![]() |
2e678c8559 | ||
![]() |
e7e9145011 | ||
![]() |
62b959c2c2 | ||
![]() |
367be8717c | ||
![]() |
290b455c47 | ||
![]() |
8913192459 | ||
![]() |
8b75c41da7 | ||
![]() |
33fcb7365b | ||
![]() |
7987d75b8d | ||
![]() |
81baa40aab | ||
![]() |
3dc3cb26f9 | ||
![]() |
8afb3fb100 | ||
![]() |
dcfb3edd5a | ||
![]() |
1b3a81ad8d | ||
![]() |
4cdbe8ce34 | ||
![]() |
14893136e5 | ||
![]() |
ee3281ab4a | ||
![]() |
b46163e045 | ||
![]() |
0ce9b3a10e | ||
![]() |
3d26e1d9de | ||
![]() |
9fe7a38b96 | ||
![]() |
9c4f7b2d9e | ||
![]() |
b0bc7c2151 | ||
![]() |
efec73f052 | ||
![]() |
7ad977ffca | ||
![]() |
7a573289ea | ||
![]() |
dce0bb6189 | ||
![]() |
b73a30c366 | ||
![]() |
074f52a15d | ||
![]() |
e19ebb4942 | ||
![]() |
bef0269e02 | ||
![]() |
67c41178dd | ||
![]() |
a38a751f48 | ||
![]() |
d84965e20e | ||
![]() |
a981b3121f | ||
![]() |
591163e844 | ||
![]() |
90e6a54218 | ||
![]() |
c0c9cd48d7 | ||
![]() |
08b328c16f | ||
![]() |
b69f32c450 | ||
![]() |
6ee7fb7301 | ||
![]() |
3488094882 | ||
![]() |
9109711524 | ||
![]() |
7e37cfc878 | ||
![]() |
7eef45cadb | ||
![]() |
5b3eb80158 | ||
![]() |
cb0cf8b386 | ||
![]() |
295cef5ae5 | ||
![]() |
ce624265f3 | ||
![]() |
d8b0d8f117 | ||
![]() |
9ffd0417ba | ||
![]() |
68618fda10 | ||
![]() |
abed307158 | ||
![]() |
4aacf11bee | ||
![]() |
ffa8a97e35 | ||
![]() |
998f74c18d | ||
![]() |
d7fac361bd | ||
![]() |
d5f52ee6b7 | ||
![]() |
b0f2a500d6 | ||
![]() |
f718492d9f | ||
![]() |
9b2d05798b | ||
![]() |
32f83949f6 | ||
![]() |
82ae67ab29 | ||
![]() |
0fc595f8a5 | ||
![]() |
a7d6e51831 | ||
![]() |
de8cbeb795 | ||
![]() |
5b20164dad | ||
![]() |
d3493356fd | ||
![]() |
7162edd503 | ||
![]() |
fbdbfe0d5e | ||
![]() |
07ab2bd8dd | ||
![]() |
fa6a843a49 | ||
![]() |
f2e26e95a3 | ||
![]() |
d0013a61e6 | ||
![]() |
80e29f0c25 | ||
![]() |
b5cf2a0415 | ||
![]() |
a99b10ca49 | ||
![]() |
923f8869a5 | ||
![]() |
c321a18094 | ||
![]() |
7e139dc57a | ||
![]() |
229ad89a02 | ||
![]() |
232955ecac | ||
![]() |
ecce6be786 | ||
![]() |
89a9424ec3 | ||
![]() |
53b58f9827 | ||
![]() |
e34d0231c6 | ||
![]() |
787c927b4d | ||
![]() |
21d6ef6067 | ||
![]() |
c26ba030fe | ||
![]() |
f326a0523b | ||
![]() |
0b571b9a45 | ||
![]() |
1dd9a0e021 | ||
![]() |
5b502679ec | ||
![]() |
20a1c35ecb | ||
![]() |
557adc7bce | ||
![]() |
170eecf922 | ||
![]() |
f5bf911c82 | ||
![]() |
f02eba26e1 | ||
![]() |
12a18a71f6 | ||
![]() |
39e2ae5f9d | ||
![]() |
8da9587cba | ||
![]() |
bdbb5c0c76 | ||
![]() |
c46721cffa | ||
![]() |
edbaf0bbaa | ||
![]() |
600a300d90 | ||
![]() |
97dafad64a | ||
![]() |
62094a088c | ||
![]() |
6585d749b9 | ||
![]() |
504494ad36 | ||
![]() |
f8379ac6d9 | ||
![]() |
17ae2c400a | ||
![]() |
4023a28e4f | ||
![]() |
ba5b572d2e | ||
![]() |
76de096069 | ||
![]() |
e2dcbd804b | ||
![]() |
43ee6ecb9d | ||
![]() |
25b84b08fd | ||
![]() |
2d35d946e2 | ||
![]() |
13294c4b40 | ||
![]() |
ae98b69690 | ||
![]() |
eff61e9f76 | ||
![]() |
9c1ee7b991 | ||
![]() |
90a4a83e78 | ||
![]() |
23e52d1990 | ||
![]() |
b912fc6324 | ||
![]() |
b3ac75e101 | ||
![]() |
b848a80a61 | ||
![]() |
320012d840 | ||
![]() |
7b75bf2940 | ||
![]() |
ab077ebd3c |
41
.editorconfig
Normal file
41
.editorconfig
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
# 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
|
12
.github/dependabot.yaml
vendored
Normal file
12
.github/dependabot.yaml
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# 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
|
21
.github/workflows/build-debug.yaml
vendored
Normal file
21
.github/workflows/build-debug.yaml
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
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
52
.github/workflows/build-debug.yml
vendored
@ -1,52 +0,0 @@
|
|||||||
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
|
|
47
.github/workflows/build-release.yaml
vendored
Normal file
47
.github/workflows/build-release.yaml
vendored
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
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
132
.github/workflows/build-release.yml
vendored
@ -1,132 +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
|
|
||||||
|
|
||||||
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 }}
|
|
@ -7,4 +7,6 @@ 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
|
@ -7,4 +7,8 @@ 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
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"solution": {
|
|
||||||
"path": "ObservableCollections.sln",
|
|
||||||
"projects": [
|
|
||||||
"src\\ObservableCollections\\ObservableCollections.csproj",
|
|
||||||
"tests\\ObservableCollections.Tests\\ObservableCollections.Tests.csproj",
|
|
||||||
"tools\\PostBuildUtility\\PostBuildUtility.csproj"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
@ -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("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleApp", "sandbox\ConsoleApp\ConsoleApp.csproj", "{3F3A73AC-DA6E-4987-8AA9-9B1E226D3DD5}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleApp", "sandbox\ConsoleApp\ConsoleApp.csproj", "{3F3A73AC-DA6E-4987-8AA9-9B1E226D3DD5}"
|
||||||
EndProject
|
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,9 +19,11 @@ 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("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tools", "tools", "{7133A3F7-B398-4DE0-8295-0F1ECFCC4CE4}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ObservableCollections.R3", "src\ObservableCollections.R3\ObservableCollections.R3.csproj", "{D5950521-C5B3-4B92-834E-3B12CDDD8DD6}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PostBuildUtility", "tools\PostBuildUtility\PostBuildUtility.csproj", "{29E3967D-89E9-494F-B1E6-9706B8F1CD57}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ObservableCollections.R3.Tests", "tests\ObservableCollections.R3.Tests\ObservableCollections.R3.Tests.csproj", "{1205F414-EE6D-49C6-9500-3E62E2120EAF}"
|
||||||
|
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
|
||||||
@ -49,10 +51,18 @@ 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
|
||||||
{29E3967D-89E9-494F-B1E6-9706B8F1CD57}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{D5950521-C5B3-4B92-834E-3B12CDDD8DD6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{29E3967D-89E9-494F-B1E6-9706B8F1CD57}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{D5950521-C5B3-4B92-834E-3B12CDDD8DD6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{29E3967D-89E9-494F-B1E6-9706B8F1CD57}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{D5950521-C5B3-4B92-834E-3B12CDDD8DD6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{29E3967D-89E9-494F-B1E6-9706B8F1CD57}.Release|Any CPU.Build.0 = Release|Any CPU
|
{D5950521-C5B3-4B92-834E-3B12CDDD8DD6}.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
|
||||||
@ -63,7 +73,9 @@ 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}
|
||||||
{29E3967D-89E9-494F-B1E6-9706B8F1CD57} = {7133A3F7-B398-4DE0-8295-0F1ECFCC4CE4}
|
{D5950521-C5B3-4B92-834E-3B12CDDD8DD6} = {8F60DC54-F617-4841-8C79-6B0137500D1C}
|
||||||
|
{1205F414-EE6D-49C6-9500-3E62E2120EAF} = {B6D0425C-7902-4EFB-B0EA-99F164C20835}
|
||||||
|
{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}
|
||||||
|
659
README.md
659
README.md
@ -1,13 +1,11 @@
|
|||||||
# ObservableCollections
|
# ObservableCollections
|
||||||
[](https://github.com/Cysharp/ObservableCollections/actions) [](https://github.com/Cysharp/ObservableCollections/releases)
|
[](https://github.com/Cysharp/ObservableCollections/actions) [](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.
|
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).
|
||||||
|
|
||||||
.NET has [`ObservableCollection<T>`](https://docs.microsoft.com/en-us/dotnet/api/system.collections.objectmodel.observablecollection-1), however it has many lacks of features.
|
.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.
|
||||||
|
|
||||||
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 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.
|
||||||
|
|
||||||
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);
|
||||||
@ -22,6 +20,7 @@ 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;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -30,27 +29,44 @@ 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, bool reverse = false);
|
ISynchronizedView<T, TView> CreateView<TView>(Func<T, TView> transform);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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.
|
||||||
|
|
||||||

|

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

|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
PM> Install-Package [ObservableCollections](https://www.nuget.org/packages/ObservableCollections)
|
> dotnet add 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>`.
|
||||||
|
|
||||||
@ -88,7 +104,7 @@ static void List_CollectionChanged(in NotifyCollectionChangedEventArgs<int> e)
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Handling all `CollectionChanged` event manually is hard. We recommend to use `SynchronizedView` that transform element and handling all collection changed event for view synchronize.
|
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.
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
var list = new ObservableList<int>();
|
var list = new ObservableList<int>();
|
||||||
@ -100,7 +116,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);
|
||||||
@ -110,77 +126,200 @@ foreach (var (_, v) in view)
|
|||||||
view.Dispose();
|
view.Dispose();
|
||||||
```
|
```
|
||||||
|
|
||||||
The basic idea behind using ObservableCollections is to create a View. In order to automate this pipeline, the view can be sortable, filtered, and have side effects on the values when they are changed.
|
The view can modify the objects being enumerated by attaching a Filter.
|
||||||
|
|
||||||
|
```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
|
||||||
---
|
---
|
||||||
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.
|
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.
|
||||||
|
|
||||||
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 DataTable<T> : ComponentBase, IDisposable
|
public partial class Index : IDisposable
|
||||||
{
|
{
|
||||||
[Parameter, EditorRequired]
|
ObservableList<int> list;
|
||||||
public IReadOnlyList<T> Items { get; set; } = default!;
|
public ISynchronizedView<int, int> ItemsView { get; set; }
|
||||||
|
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()
|
||||||
{
|
{
|
||||||
if (Items is IObservableCollection<T> observableCollection)
|
list = new ObservableList<int>();
|
||||||
{
|
ItemsView = list.CreateView(x => x);
|
||||||
// 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.
|
ItemsView.CollectionStateChanged += action =>
|
||||||
view.CollectionStateChanged += async _ =>
|
|
||||||
{
|
{
|
||||||
await InvokeAsync(StateHasChanged);
|
InvokeAsync(StateHasChanged);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void OnClick()
|
||||||
|
{
|
||||||
|
list.Add(count++);
|
||||||
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
// unsubscribe.
|
ItemsView.Dispose();
|
||||||
view.Dispose();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// .razor, iterate view
|
// .razor, iterate view
|
||||||
@foreach (var (row, cells) in view)
|
@page "/"
|
||||||
{
|
|
||||||
<tr>
|
<button @onclick=OnClick>button</button>
|
||||||
@foreach (var item in cells)
|
|
||||||
{
|
<table>
|
||||||
<td>
|
@foreach (var item in ItemsView)
|
||||||
<CellView Item="item" />
|
{
|
||||||
</td>
|
<tr>
|
||||||
}
|
<td>@item</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
|
</table>
|
||||||
```
|
```
|
||||||
|
|
||||||
WPF
|
WPF/Avalonia/WinUI (XAML based UI platforms)
|
||||||
---
|
---
|
||||||
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.
|
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.
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
// WPF simple sample.
|
// WPF simple sample.
|
||||||
|
|
||||||
ObservableList<int> list;
|
ObservableList<int> list;
|
||||||
public ISynchronizedView<int, int> ItemsView { get; set; }
|
public NotifyCollectionChangedSynchronizedViewList<int> ItemsView { get; set; }
|
||||||
|
|
||||||
public MainWindow()
|
public MainWindow()
|
||||||
{
|
{
|
||||||
@ -188,9 +327,12 @@ public MainWindow()
|
|||||||
this.DataContext = this;
|
this.DataContext = this;
|
||||||
|
|
||||||
list = new ObservableList<int>();
|
list = new ObservableList<int>();
|
||||||
ItemsView = list.CreateView(x => x).WithINotifyCollectionChanged();
|
|
||||||
|
|
||||||
BindingOperations.EnableCollectionSynchronization(ItemsView, new object()); // for ui synchronization safety of viewmodel
|
// 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)
|
||||||
@ -199,22 +341,109 @@ protected override void OnClosed(EventArgs e)
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
> WPF can not use SortedView because SortedView can not provide sort event to INotifyCollectionChanged.
|
`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:
|
||||||
|
|
||||||
|
```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, ObservableCollections and Views are useful as CollectionManagers, since they need to convert T to Prefab for display.
|
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.
|
||||||
|
|
||||||
Since we need to have side effects on GameObjects, we will prepare a filter and apply an action on changes.
|
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.
|
||||||
|
|
||||||
```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<GameObject> view;
|
ISynchronizedView<int, GameObject> view;
|
||||||
|
|
||||||
void Start()
|
void Start()
|
||||||
{
|
{
|
||||||
@ -223,182 +452,248 @@ 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.AttachFilter(new GameObjectFilter(root));
|
view.ViewChanged += View_ViewChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
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.
|
Reference
|
||||||
|
|
||||||
View/SortedView
|
|
||||||
---
|
---
|
||||||
View can create from `IObservableCollection<T>`, it completely synchronized and thread-safe.
|
ObservableCollections provides these collections.
|
||||||
|
|
||||||
```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>
|
||||||
{
|
{
|
||||||
// snip...
|
object SyncRoot { get; }
|
||||||
ISynchronizedView<T, TView> CreateView<TView>(Func<T, TView> transform, bool reverse = false);
|
event NotifyCollectionChangedEventHandler<T>? CollectionChanged;
|
||||||
|
ISynchronizedView<T, TView> CreateView<TView>(Func<T, TView> transform);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
When reverse = true, foreach view as reverse order(Dictionary, etc. are not supported).
|
The notification event `NotifyCollectionChangedEventArgs<T>` has the following definition:
|
||||||
|
|
||||||
`ISynchronizedView<T, TView>` is `IReadOnlyCollection` and hold both value and view(transformed value when added).
|
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
public interface ISynchronizedView<T, TView> : IReadOnlyCollection<(T Value, TView View)>, IDisposable
|
/// <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
|
||||||
|
/// 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 NotifyCollectionChangedEventHandler<T>? RoutingCollectionChanged;
|
event NotifyViewChangedEventHandler<T, TView>? ViewChanged;
|
||||||
|
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(Action<T, TView>? resetAction);
|
void ResetFilter();
|
||||||
INotifyCollectionChangedSynchronizedView<T, TView> WithINotifyCollectionChanged();
|
ISynchronizedViewList<TView> ToViewList();
|
||||||
|
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
|
||||||
var view = transform(value);
|
public readonly ref struct SynchronizedViewChangedEventArgs<T, TView>
|
||||||
if (filter.IsMatch(value, view))
|
|
||||||
{
|
{
|
||||||
filter.WhenTrue(value, view);
|
public readonly NotifyCollectionChangedAction Action;
|
||||||
|
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 static ISynchronizedView<T, TView> CreateSortedView<T, TKey, TView>(this IObservableCollection<T> source, Func<T, TKey> identitySelector, Func<T, TView> transform, IComparer<T> comparer)
|
public readonly struct SortOperation<T>
|
||||||
where TKey : notnull
|
{
|
||||||
|
public readonly int Index;
|
||||||
|
public readonly int Count;
|
||||||
|
public readonly 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)
|
public bool IsReverse { get; }
|
||||||
where TKey : notnull
|
public bool IsClear { get; }
|
||||||
|
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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
> 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.
|
When `IsReverse` is true, you need to use `Index` and `Count`. When `IsSort` is true, you need to use `Index`, `Count`, and `Comparer` values.
|
||||||
|
|
||||||
Filter
|
For Filter, you can either create one that implements this interface or generate one from a lambda expression using extension methods.
|
||||||
---
|
|
||||||
|
|
||||||
```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 enum ChangedKind
|
public static class SynchronizedViewExtensions
|
||||||
{
|
{
|
||||||
Add, Remove, Move
|
public static void AttachFilter<T, TView>(this ISynchronizedView<T, TView> source, Func<T, bool> filter)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
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 ObservableDictionary<TKey, TValue> : IDictionary<TKey, TValue>, IReadOnlyDictionary<TKey, TValue>, IObservableCollection<KeyValuePair<TKey, TValue>> where TKey : notnull
|
public sealed partial class ObservableList<T>
|
||||||
public sealed partial class ObservableFixedSizeRingBuffer<T> : IList<T>, IReadOnlyList<T>, IObservableCollection<T>
|
|
||||||
public sealed partial class ObservableHashSet<T> : IReadOnlySet<T>, IReadOnlyCollection<T>, IObservableCollection<T> where T : notnull
|
|
||||||
|
|
||||||
public sealed partial class ObservableHashSet<T> : IReadOnlySet<T>, IReadOnlyCollection<T>, IObservableCollection<T>
|
|
||||||
where T : notnull
|
|
||||||
|
|
||||||
public sealed partial class ObservableList<T> : IList<T>, IReadOnlyList<T>, IObservableCollection<T>
|
|
||||||
|
|
||||||
public sealed partial class ObservableQueue<T> : IReadOnlyCollection<T>, IObservableCollection<T>
|
|
||||||
public sealed partial class ObservableRingBuffer<T> : IList<T>, IReadOnlyList<T>, IObservableCollection<T>
|
|
||||||
|
|
||||||
public sealed partial class ObservableStack<T> : IReadOnlyCollection<T>, IObservableCollection<T>
|
|
||||||
|
|
||||||
public sealed class RingBuffer<T> : IList<T>, IReadOnlyList<T>
|
|
||||||
```
|
|
||||||
|
|
||||||
Freezed
|
|
||||||
---
|
|
||||||
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
public sealed class FreezedList<T> : IReadOnlyList<T>, IFreezedCollection<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);
|
public IWritableSynchronizedView<T, TView> CreateWritableView<TView>(Func<T, TView> transform);
|
||||||
ISortableSynchronizedView<T, TView> CreateSortableView<TView>(Func<T, TView> transform);
|
|
||||||
|
public NotifyCollectionChangedSynchronizedViewList<T> ToWritableNotifyCollectionChanged();
|
||||||
|
public NotifyCollectionChangedSynchronizedViewList<T> ToWritableNotifyCollectionChanged(ICollectionEventDispatcher? collectionEventDispatcher);
|
||||||
|
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 static ISortableSynchronizedView<T, TView> CreateSortableView<T, TView>(this IFreezedCollection<T> source, Func<T, TView> transform, IComparer<T> initialSort)
|
public delegate T WritableViewChangedEventHandler<T, TView>(TView newView, T originalValue, ref bool setValue);
|
||||||
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 interface IWritableSynchronizedView<T, TView> : ISynchronizedView<T, TView>
|
||||||
public static void Sort<T, TView, TCompare>(this ISortableSynchronizedView<T, TView> source, Func<T, TCompare> compareSelector, bool ascending = true)
|
{
|
||||||
|
(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>
|
||||||
|
{
|
||||||
|
new TView this[int index] { get; set; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Here are definitions for other collections:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public interface IReadOnlyObservableList<T> :
|
||||||
|
IReadOnlyList<T>, IObservableCollection<T>
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IReadOnlyObservableDictionary<TKey, TValue> :
|
||||||
|
IReadOnlyDictionary<TKey, TValue>, IObservableCollection<KeyValuePair<TKey, TValue>>
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
BIN
docs/assets.pptx
BIN
docs/assets.pptx
Binary file not shown.
10
sandbox/AvaloniaApp/App.axaml
Normal file
10
sandbox/AvaloniaApp/App.axaml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<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>
|
24
sandbox/AvaloniaApp/App.axaml.cs
Normal file
24
sandbox/AvaloniaApp/App.axaml.cs
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
29
sandbox/AvaloniaApp/AvaloniaApp.csproj
Normal file
29
sandbox/AvaloniaApp/AvaloniaApp.csproj
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<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>
|
20
sandbox/AvaloniaApp/MainWindow.axaml
Normal file
20
sandbox/AvaloniaApp/MainWindow.axaml
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<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>
|
104
sandbox/AvaloniaApp/MainWindow.axaml.cs
Normal file
104
sandbox/AvaloniaApp/MainWindow.axaml.cs
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
23
sandbox/AvaloniaApp/Program.cs
Normal file
23
sandbox/AvaloniaApp/Program.cs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
18
sandbox/AvaloniaApp/app.manifest
Normal file
18
sandbox/AvaloniaApp/app.manifest
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<?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>
|
@ -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 adder = 99;
|
int count = 99;
|
||||||
|
|
||||||
protected override void OnInitialized()
|
protected override void OnInitialized()
|
||||||
{
|
{
|
||||||
@ -20,19 +20,13 @@ 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++);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,17 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<TargetFramework>net6.0</TargetFramework>
|
<TargetFramework>net8.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\ObservableCollections.csproj" />
|
<ProjectReference Include="..\..\src\ObservableCollections.R3\ObservableCollections.R3.csproj" />
|
||||||
</ItemGroup>
|
<ProjectReference Include="..\..\src\ObservableCollections\ObservableCollections.csproj" />
|
||||||
|
<PackageReference Include="R3" Version="1.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -1,24 +1,105 @@
|
|||||||
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)
|
|
||||||
{
|
{
|
||||||
// 10$, 60$, 30$, 50$
|
new (){ Age = 10, Name = "John" },
|
||||||
Console.WriteLine(v);
|
new (){ Age = 22, Name = "Jeyne" },
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dispose view is unsubscribe collection changed event.
|
Console.WriteLine("---");
|
||||||
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);
|
||||||
|
//}
|
@ -5,16 +5,62 @@
|
|||||||
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="450" Width="800">
|
Title="MainWindow" Height="800" Width="800">
|
||||||
<Grid>
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition />
|
<ColumnDefinition Width="*" />
|
||||||
<ColumnDefinition />
|
<ColumnDefinition Width="*" />
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
<ListView ItemsSource="{Binding ItemsView}"></ListView>
|
<!-- original -->
|
||||||
|
<StackPanel Grid.Row="0" Grid.ColumnSpan="2" Orientation="Vertical">
|
||||||
<Button Grid.Column="1" Click="Button_Click">Insert</Button>
|
<ListBox ItemsSource="{Binding ItemsView}" />
|
||||||
|
<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>
|
@ -1,6 +1,10 @@
|
|||||||
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;
|
||||||
@ -14,6 +18,7 @@ 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
|
||||||
{
|
{
|
||||||
@ -22,38 +27,247 @@ namespace WpfApp
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class MainWindow : Window
|
public partial class MainWindow : Window
|
||||||
{
|
{
|
||||||
ObservableList<int> list;
|
//ObservableList<int> list;
|
||||||
public ISynchronizedView<int, int> ItemsView { get; set; }
|
//public INotifyCollectionChangedSynchronizedView<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).WithINotifyCollectionChanged();
|
//ItemsView = list.CreateSortedView(x => x, x => x, comparer: Comparer<int>.Default).ToNotifyCollectionChanged();
|
||||||
|
|
||||||
|
|
||||||
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()
|
||||||
{
|
{
|
||||||
ThreadPool.QueueUserWorkItem(_ =>
|
observableList.Add(1);
|
||||||
|
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(_ =>
|
||||||
{
|
{
|
||||||
list.Add(adder++);
|
// 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);
|
||||||
|
});
|
||||||
|
|
||||||
|
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; }
|
||||||
|
}
|
||||||
|
|
||||||
protected override void OnClosed(EventArgs e)
|
public class WpfDispatcherCollection(Dispatcher dispatcher) : ICollectionEventDispatcher
|
||||||
|
{
|
||||||
|
public void Post(CollectionEventDispatcherEventArgs ev)
|
||||||
{
|
{
|
||||||
ItemsView.Dispose();
|
dispatcher.InvokeAsync(() =>
|
||||||
|
{
|
||||||
|
ev.Invoke();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,15 +1,21 @@
|
|||||||
<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>
|
||||||
<Nullable>enable</Nullable>
|
<LangVersion>12</LangVersion>
|
||||||
<UseWPF>true</UseWPF>
|
<Nullable>enable</Nullable>
|
||||||
<IsPackable>false</IsPackable>
|
<UseWPF>true</UseWPF>
|
||||||
</PropertyGroup>
|
<IsPackable>false</IsPackable>
|
||||||
|
<EnableWindowsTargeting>true</EnableWindowsTargeting>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\src\ObservableCollections\ObservableCollections.csproj" />
|
<PackageReference Include="R3Extensions.WPF" Version="1.0.4" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\src\ObservableCollections\ObservableCollections.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -0,0 +1,509 @@
|
|||||||
|
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);
|
||||||
|
}
|
587
src/ObservableCollections.R3/ObservableCollectionR3Extensions.cs
Normal file
587
src/ObservableCollections.R3/ObservableCollectionR3Extensions.cs
Normal file
@ -0,0 +1,587 @@
|
|||||||
|
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);
|
||||||
|
}
|
32
src/ObservableCollections.R3/ObservableCollections.R3.csproj
Normal file
32
src/ObservableCollections.R3/ObservableCollections.R3.csproj
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<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>
|
13
src/ObservableCollections.R3/Shims.cs
Normal file
13
src/ObservableCollections.R3/Shims.cs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
#if NETSTANDARD2_0 || NETSTANDARD2_1
|
||||||
|
|
||||||
|
namespace System.Threading;
|
||||||
|
|
||||||
|
internal static class CancellationTokenExtensions
|
||||||
|
{
|
||||||
|
public static CancellationTokenRegistration UnsafeRegister(this CancellationToken cancellationToken, Action<object?> callback, object? state)
|
||||||
|
{
|
||||||
|
return cancellationToken.Register(callback, state, useSynchronizationContext: false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
@ -1,8 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 23bde05b5e9d4d049a38c4edf79f9ac6
|
|
||||||
folderAsset: yes
|
|
||||||
DefaultImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
@ -1,78 +0,0 @@
|
|||||||
#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
|
|
@ -1,11 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 34885e00b06e4c847b8e2958ebb2d26b
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
@ -1,8 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 58b896faeb4c95c4a939b19f8dbf74ab
|
|
||||||
folderAsset: yes
|
|
||||||
DefaultImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
@ -1,8 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 791bdc9996d0f5c42bea2aa68c95dced
|
|
||||||
folderAsset: yes
|
|
||||||
DefaultImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
@ -1,8 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: a2ca402f41ce60d4c95ee34b6715ee8a
|
|
||||||
folderAsset: yes
|
|
||||||
DefaultImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
@ -1,59 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 1470201cf9f7db249b7f441e515b77b0
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
@ -1,61 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 3207eb016a325514b8f137b4cf17df40
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
@ -1,118 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: bdb4cc0f6a8b396418c8250eeaf2d2c8
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
@ -1,121 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 0ffdcef914adf8848a485ac600cc41c4
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
@ -1,8 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 0b3e01975c6d488409ce0ac753c89870
|
|
||||||
folderAsset: yes
|
|
||||||
DefaultImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
@ -1,132 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 7b9643f0fa1bf7447961693f16cb557b
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
@ -1,229 +0,0 @@
|
|||||||
#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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: a768cdaf951b40345b6d5b21ade3e282
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
@ -1,70 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: b0e2d983b8f809a45b60f610d0542761
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
@ -1,56 +0,0 @@
|
|||||||
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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: b1477839cc1e36a4187e91e4fd001dd5
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
@ -1,225 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 768c43ec998d9754d8cc13398ed69d39
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
@ -1,244 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 08e8703f31991094aa9216bf7166a549
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
@ -1,127 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 70f96ba473572df4d91f125c2663b868
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"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
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: a25f8e99c4faa4b4a8c38f41a406f5d3
|
|
||||||
AssemblyDefinitionImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
@ -1,171 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: f512d4ba320de58428976eb614bfa06d
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
@ -1,228 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: b4dc0886473cf8c49947c9081454c614
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
@ -1,326 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 81d799cc7eb02954d89c7ee52b7c9148
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
@ -1,174 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: a7b9e5ab06e68b242a4a722866654fd7
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
@ -1,265 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 38d982a9907bb0047b158794a1cf35e7
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
@ -1,235 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: b98cf9d4d5426ad478ede70c5ac637d2
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
@ -1,276 +0,0 @@
|
|||||||
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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 963681d5debafe6448e09cd9a4029cec
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
@ -1,188 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 3fea76167e2a69749931bf01b5b5ed92
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
@ -1,220 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: eb4558e36aaf5c84597551bb9a0d42e9
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
@ -1,246 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 9a4b3308e0d0c4c4bafd65b41e7d66cd
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
@ -1,236 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: c858015a9e82dc84487b3e627916a7ff
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
@ -1,188 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: bfc46ddd0f9e1e246931d87579f595b0
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
@ -1,216 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: e89128583a1ffee4babe861213e14be2
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
@ -1,405 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 090e1b8528f51164695f1ed11c685465
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
@ -1,8 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 8c8f516891f9df44a847a0bb51a064f0
|
|
||||||
folderAsset: yes
|
|
||||||
DefaultImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
@ -1,64 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 02ab78c5678c3bd4ebb45ca99fa0eaea
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
@ -1,18 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 90c6c97d9daed56458c4b0f69217c442
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
@ -1,24 +0,0 @@
|
|||||||
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
|
|
@ -1,11 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 2b96ddbca917d2749a613d7dff847f37
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"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": {}
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 142803aeb06678d4faef8b76468c110b
|
|
||||||
TextScriptImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
Binary file not shown.
@ -1,33 +0,0 @@
|
|||||||
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
Loading…
x
Reference in New Issue
Block a user