Compare commits

..

1 Commits

Author SHA1 Message Date
neuecc
3a4ef7e1f0 Add ChangeKind.Clear but may needs more 2024-02-21 19:50:09 +09:00
70 changed files with 2713 additions and 5706 deletions

View File

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

View File

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

View File

@ -10,12 +10,10 @@ on:
jobs:
build-dotnet:
permissions:
contents: read
runs-on: ubuntu-24.04
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: Cysharp/Actions/.github/actions/checkout@main
- uses: actions/checkout@v3
- uses: Cysharp/Actions/.github/actions/setup-dotnet@main
- run: dotnet build -c Debug
- run: dotnet test -c Debug --no-build

View File

@ -14,29 +14,24 @@ on:
jobs:
build-dotnet:
permissions:
contents: read
runs-on: ubuntu-24.04
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: Cysharp/Actions/.github/actions/checkout@main
- uses: actions/checkout@v3
- 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
- uses: actions/upload-artifact@v2
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 }}

View File

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

View File

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

View File

@ -23,8 +23,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ObservableCollections.R3",
EndProject
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
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -59,10 +57,6 @@ Global
{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
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -75,7 +69,6 @@ Global
{B84027E4-9B39-4FB6-B888-C55CE4C79152} = {B6D0425C-7902-4EFB-B0EA-99F164C20835}
{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
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {4156A725-69F4-469F-9BBB-0EE9921CA83E}

613
README.md
View File

@ -3,9 +3,11 @@
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. It based `INotifyCollectionChanged`, `NotifyCollectionChangedEventHandler` and `NotifyCollectionChangedEventArgs`. There are no generics so everything boxed, allocate memory every time. Also `NotifyCollectionChangedEventArgs` holds all values to `IList` even if it is single value, this also causes allocations. `ObservableCollection<T>` has no Range feature so a lot of wastage occurs when adding multiple values, because it is a single value notification. Also, it is not thread-safe is hard to do linkage with the notifier.
.NET has [`ObservableCollection<T>`](https://docs.microsoft.com/en-us/dotnet/api/system.collections.objectmodel.observablecollection-1), however it has many lacks of features.
ObservableCollections introduces there generics version, `NotifyCollectionChangedEventHandler<T>` and `NotifyCollectionChangedEventArgs<T>`, it using latest C# features(`in`, `readonly ref struct`, `ReadOnlySpan<T>`). Also, Sort and Reverse will now be notified.
It based `INotifyCollectionChanged`, `NotifyCollectionChangedEventHandler` and `NotifyCollectionChangedEventArgs`. There are no generics so everything boxed, allocate memory every time. Also `NotifyCollectionChangedEventArgs` holds all values to `IList` even if it is single value, this also causes allocations. `ObservableCollection<T>` has no Range feature so a lot of wastage occurs when adding multiple values, because it is a single value notification. Also, it is not thread-safe is hard to do linkage with the notifier.
ObservableCollections introduces generics version of `NotifyCollectionChangedEventHandler` and `NotifyCollectionChangedEventArgs`, it using latest C# features(`in`, `readonly ref struct`, `ReadOnlySpan<T>`).
```csharp
public delegate void NotifyCollectionChangedEventHandler<T>(in NotifyCollectionChangedEventArgs<T> e);
@ -20,7 +22,6 @@ public readonly ref struct NotifyCollectionChangedEventArgs<T>
public readonly ReadOnlySpan<T> OldItems;
public readonly int NewStartingIndex;
public readonly int OldStartingIndex;
public readonly SortOperation<T> SortOperation;
}
```
@ -29,44 +30,37 @@ Also, use the interface `IObservableCollection<T>` instead of `INotifyCollection
```csharp
public interface IObservableCollection<T> : IReadOnlyCollection<T>
{
event NotifyCollectionChangedEventHandler<T>? CollectionChanged;
event NotifyCollectionChangedEventHandler<T> CollectionChanged;
object SyncRoot { get; }
ISynchronizedView<T, TView> CreateView<TView>(Func<T, TView> transform);
ISynchronizedView<T, TView> CreateView<TView>(Func<T, TView> transform, bool reverse = false);
}
// also exists SortedView
public static ISynchronizedView<T, TView> CreateSortedView<T, TKey, TView>(this IObservableCollection<T> source, Func<T, TKey> identitySelector, Func<T, TView> transform, IComparer<T> comparer);
public static ISynchronizedView<T, TView> CreateSortedView<T, TKey, TView>(this IObservableCollection<T> source, Func<T, TKey> identitySelector, Func<T, TView> transform, IComparer<TView> viewComparer);
```
SynchronizedView helps to separate between Model and View (ViewModel). We will use ObservableCollections as the Model and generate SynchronizedView as the View (ViewModel). This architecture can be applied not only to WPF, but also to Blazor, Unity, etc.
![image](https://user-images.githubusercontent.com/46207/131979264-2463403b-0fba-474b-8f49-277c2abe1b05.png)
The View retains the transformed values. The transform function is called only once during Add, so costly objects that are linked can also be instantiated. Additionally, it has a feature to dynamically show or hide values using filters.
Observable Collections themselves do not implement `INotifyCollectionChanged`, so they cannot be bound on XAML platforms and the like. However, they can be converted to collections that implement `INotifyCollectionChanged` using `ToNotifyCollectionChanged()`, making them suitable for binding.
![image](https://github.com/user-attachments/assets/b5590bb8-16d6-4f9c-be07-1288a6801e68)
ObservableCollections has not just a simple list, there are many more data structures. `ObservableList<T>`, `ObservableDictionary<TKey, TValue>`, `ObservableHashSet<T>`, `ObservableQueue<T>`, `ObservableStack<T>`, `ObservableRingBuffer<T>`, `ObservableFixedSizeRingBuffer<T>`. `RingBuffer`, especially `FixedSizeRingBuffer`, can be achieved with efficient performance when there is rotation (e.g., displaying up to 1000 logs, where old ones are deleted when new ones are added). Of course, the AddRange allows for efficient batch processing of large numbers of additions.
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
---
For .NET, use NuGet. For Unity, please read [Unity](#unity) section.
> dotnet add package [ObservableCollections](https://www.nuget.org/packages/ObservableCollections)
PM> Install-Package [ObservableCollections](https://www.nuget.org/packages/ObservableCollections)
create new `ObservableList<T>`, `ObservableDictionary<TKey, TValue>`, `ObservableHashSet<T>`, `ObservableQueue<T>`, `ObservableStack<T>`, `ObservableRingBuffer<T>`, `ObservableFixedSizeRingBuffer<T>`.
@ -104,7 +98,7 @@ static void List_CollectionChanged(in NotifyCollectionChangedEventArgs<int> e)
}
```
While it is possible to manually handle the `CollectionChanged` event as shown in the example above, you can also create a `SynchronizedView` as a collection that holds a separate synchronized value.
Handling all `CollectionChanged` event manually is hard. We recommend to use `SynchronizedView` that transform element and handling all collection changed event for view synchronize.
```csharp
var list = new ObservableList<int>();
@ -116,7 +110,7 @@ list.AddRange(new[] { 30, 40, 50 });
list[1] = 60;
list.RemoveAt(3);
foreach (var v in view)
foreach (var (_, v) in view)
{
// 10$, 60$, 30$, 50$
Console.WriteLine(v);
@ -126,120 +120,13 @@ foreach (var v in view)
view.Dispose();
```
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);
}
}
```
The basic idea behind using ObservableCollections is to create a View. In order to automate this pipeline, the view can be sortable, filtered, and have side effects on the values when they are changed.
Reactive Extensions with R3
---
Once the R3 extension package is installed, you can subscribe to `ObserveChanged`, `ObserveAdd`, `ObserveRemove`, `ObserveReplace`, `ObserveMove`, `ObserveReset`, `ObserveClear`, `ObserveReverse`, `ObserveSort`, `ObserveCounteChanged` events as Rx, allowing you to compose events individually.
Once the R3 extension package is installed, you can subscribe to `ObserveAdd`, `ObserveRemove`, `ObserveReplace`, `ObserveMove`, and `ObserveReset` events as Rx, allowing you to compose events individually.
> dotnet add package [ObservableCollections.R3](https://www.nuget.org/packages/ObservableCollections.R3)
PM> Install-Package [ObservableCollections.R3](https://www.nuget.org/packages/ObservableCollections.R3)
```csharp
using R3;
@ -257,69 +144,77 @@ 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
---
In the case of Blazor, `StateHasChanged` is called and re-enumeration occurs in response to changes in the collection. It's advisable to use the `CollectionStateChanged` event for this purpose.
Since Blazor re-renders the whole thing by StateHasChanged, you may think that Observable collections are unnecessary. However, when you split it into Components, it is beneficial for Component confidence to detect the change and change its own State.
The View selector in ObservableCollections is also useful for converting data to a View that represents a Cell, for example, when creating something like a table.
```csharp
public partial class Index : IDisposable
public partial class DataTable<T> : ComponentBase, IDisposable
{
ObservableList<int> list;
public ISynchronizedView<int, int> ItemsView { get; set; }
int count = 0;
[Parameter, EditorRequired]
public IReadOnlyList<T> Items { get; set; } = default!;
[Parameter, EditorRequired]
public Func<T, Cell[]> DataTemplate { get; set; } = default!;
ISynchronizedView<T, Cell[]> view = default!;
protected override void OnInitialized()
{
list = new ObservableList<int>();
ItemsView = list.CreateView(x => x);
ItemsView.CollectionStateChanged += action =>
if (Items is IObservableCollection<T> observableCollection)
{
InvokeAsync(StateHasChanged);
// Note: If the table has the ability to sort columns, then it will be automatically sorted using SortedView.
view = observableCollection.CreateView(DataTemplate);
}
else
{
// It is often the case that Items is not Observable.
// In that case, FreezedList is provided to create a View with the same API for normal collections.
var freezedList = new FreezedList<T>(Items);
view = freezedList.CreateView(DataTemplate);
}
// View also has a change notification.
view.CollectionStateChanged += async _ =>
{
await InvokeAsync(StateHasChanged);
};
}
void OnClick()
{
list.Add(count++);
}
public void Dispose()
{
ItemsView.Dispose();
// unsubscribe.
view.Dispose();
}
}
// .razor, iterate view
@page "/"
<button @onclick=OnClick>button</button>
<table>
@foreach (var item in ItemsView)
{
<tr>
<td>@item</td>
</tr>
}
</table>
@foreach (var (row, cells) in view)
{
<tr>
@foreach (var item in cells)
{
<td>
<CellView Item="item" />
</td>
}
</tr>
}
```
WPF/Avalonia/WinUI (XAML based UI platforms)
WPF
---
Because of data binding in WPF, it is important that the collection is Observable. ObservableCollections high-performance `IObservableCollection<T>` cannot be bind to WPF. Call `ToNotifyCollectionChanged()` to convert it to `INotifyCollectionChanged`. Also, although ObservableCollections and Views are thread-safe, the WPF UI does not support change notifications from different threads. To`ToNotifyCollectionChanged(IColllectionEventDispatcher)` allows multi thread changed.
Because of data binding in WPF, it is important that the collection is Observable. ObservableCollections high-performance `IObservableCollection<T>` cannot be bind to WPF. Call `ToNotifyCollectionChanged()` to convert it to `INotifyCollectionChanged`. Also, although ObservableCollections and Views are thread-safe, the WPF UI does not support change notifications from different threads. `BindingOperations.EnableCollectionSynchronization` to work safely with change notifications from different threads.
```csharp
// WPF simple sample.
ObservableList<int> list;
public NotifyCollectionChangedSynchronizedViewList<int> ItemsView { get; set; }
public ISynchronizedView<int, int> ItemsView { get; set; }
public MainWindow()
{
@ -327,12 +222,9 @@ public MainWindow()
this.DataContext = this;
list = new ObservableList<int>();
ItemsView = list.CreateView(x => x).ToNotifyCollectionChanged();
// 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();
BindingOperations.EnableCollectionSynchronization(ItemsView, new object()); // for ui synchronization safety of viewmodel
}
protected override void OnClosed(EventArgs e)
@ -341,109 +233,24 @@ protected override void OnClosed(EventArgs e)
}
```
`SynchronizationContextCollectionEventDispatcher.Current` is default implementation of `IColllectionEventDispatcher`, it is used `SynchronizationContext.Current` for dispatche ui thread. You can create custom `ICollectionEventDispatcher` to use custom dispatcher object. For example use WPF Dispatcher:
```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; }
}
```
> WPF can not use SortedView because SortedView can not provide sort event to INotifyCollectionChanged.
Unity
---
In Unity projects, you can installing `ObservableCollections` with [NugetForUnity](https://github.com/GlitchEnzo/NuGetForUnity). If R3 integration is required, similarly install `ObservableCollections.R3` via NuGetForUnity.
In Unity, ObservableCollections and Views are useful as CollectionManagers, since they need to convert T to Prefab for display. Since View objects are generated only once, it's possible to complement GameObjects tied to the collection.
In Unity, ObservableCollections and Views are useful as CollectionManagers, since they need to convert T to Prefab for display.
Since we need to have side effects on GameObjects, we will prepare a filter and apply an action on changes.
```csharp
// Unity, with filter sample.
public class SampleScript : MonoBehaviour
{
public Button prefab;
public GameObject root;
ObservableRingBuffer<int> collection;
ISynchronizedView<int, GameObject> view;
ISynchronizedView<GameObject> view;
void Start()
{
@ -452,248 +259,182 @@ public class SampleScript : MonoBehaviour
{
var item = GameObject.Instantiate(prefab);
item.GetComponentInChildren<Text>().text = x.ToString();
// add to root
item.transform.SetParent(root.transform);
return item.gameObject;
});
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) { }
view.AttachFilter(new GameObjectFilter(root));
}
void OnDestroy()
{
view.Dispose();
}
public class GameObjectFilter : ISynchronizedViewFilter<int, GameObject>
{
readonly GameObject root;
public GameObjectFilter(GameObject root)
{
this.root = root;
}
public void OnCollectionChanged(ChangedKind changedKind, int value, GameObject view, in NotifyCollectionChangedEventArgs<int> eventArgs)
{
if (changedKind == ChangedKind.Add)
{
view.transform.SetParent(root.transform);
}
else if (changedKind == ChangedKind.Remove)
{
GameObject.Destroy(view);
}
}
public bool IsMatch(int value, GameObject view)
{
return true;
}
public void WhenTrue(int value, GameObject view)
{
view.SetActive(true);
}
public void WhenFalse(int value, GameObject view)
{
view.SetActive(false);
}
}
}
```
Reference
It is also possible to manage Order by managing indexes inserted from eventArgs, but it is very difficult with many caveats. If you don't have major performance issues, you can foreach the View itself on CollectionStateChanged (like Blazor) and reorder the transforms. If you have such a architecture, you can also use SortedView.
View/SortedView
---
ObservableCollections provides these collections.
View can create from `IObservableCollection<T>`, it completely synchronized and thread-safe.
```csharp
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>
{
object SyncRoot { get; }
event NotifyCollectionChangedEventHandler<T>? CollectionChanged;
ISynchronizedView<T, TView> CreateView<TView>(Func<T, TView> transform);
// snip...
ISynchronizedView<T, TView> CreateView<TView>(Func<T, TView> transform, bool reverse = false);
}
```
The notification event `NotifyCollectionChangedEventArgs<T>` has the following definition:
When reverse = true, foreach view as reverse order(Dictionary, etc. are not supported).
`ISynchronizedView<T, TView>` is `IReadOnlyCollection` and hold both value and view(transformed value when added).
```csharp
/// <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
public interface ISynchronizedView<T, TView> : IReadOnlyCollection<(T Value, TView View)>, IDisposable
{
object SyncRoot { get; }
ISynchronizedViewFilter<T, TView> Filter { get; }
IEnumerable<(T Value, TView View)> Filtered { get; }
IEnumerable<(T Value, TView View)> Unfiltered { get; }
int UnfilteredCount { get; }
event NotifyViewChangedEventHandler<T, TView>? ViewChanged;
event Action<RejectedViewChangedAction, int, int>? RejectedViewChanged; // int index, int oldIndex(when RejectedViewChangedAction is Move)
event NotifyCollectionChangedEventHandler<T>? RoutingCollectionChanged;
event Action<NotifyCollectionChangedAction>? CollectionStateChanged;
void AttachFilter(ISynchronizedViewFilter<T, TView> filter);
void ResetFilter();
ISynchronizedViewList<TView> ToViewList();
NotifyCollectionChangedSynchronizedViewList<TView> ToNotifyCollectionChanged();
NotifyCollectionChangedSynchronizedViewList<TView> ToNotifyCollectionChanged(ICollectionEventDispatcher? collectionEventDispatcher);
void ResetFilter(Action<T, TView>? resetAction);
INotifyCollectionChangedSynchronizedView<T, TView> WithINotifyCollectionChanged();
}
```
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
public readonly ref struct SynchronizedViewChangedEventArgs<T, TView>
var view = transform(value);
if (filter.IsMatch(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;
filter.WhenTrue(value, view);
}
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
public readonly struct SortOperation<T>
{
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<T> comparer)
where TKey : notnull
public bool IsReverse { get; }
public bool IsClear { get; }
public bool IsSort { get; }
}
public static ISynchronizedView<T, TView> CreateSortedView<T, TKey, TView>(this IObservableCollection<T> source, Func<T, TKey> identitySelector, Func<T, TView> transform, IComparer<TView> viewComparer)
where TKey : notnull
public static ISynchronizedView<T, TView> CreateSortedView<T, TKey, TView, TCompare>(this IObservableCollection<T> source, Func<T, TKey> identitySelector, Func<T, TView> transform, Func<T, TCompare> compareSelector, bool ascending = true)
where TKey : notnull
```
When `IsReverse` is true, you need to use `Index` and `Count`. When `IsSort` is true, you need to use `Index`, `Count`, and `Comparer` values.
> Notice: foreach ObservableCollections and Views are thread-safe but it uses lock at iterating. In other words, the obtained Enumerator must be Dispose. foreach and LINQ are guaranteed to be Dispose, but be careful when you extract the Enumerator by yourself.
For Filter, you can either create one that implements this interface or generate one from a lambda expression using extension methods.
Filter
---
```csharp
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 static class SynchronizedViewExtensions
public enum ChangedKind
{
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)
{
}
Add, Remove, Move
}
```
`ObservableList<T>` has writable view.
Collections
---
```csharp
public sealed partial class ObservableList<T>
{
public IWritableSynchronizedView<T, TView> CreateWritableView<TView>(Func<T, TView> transform);
public sealed partial class ObservableDictionary<TKey, TValue> : IDictionary<TKey, TValue>, IReadOnlyDictionary<TKey, TValue>, IObservableCollection<KeyValuePair<TKey, TValue>> where TKey : notnull
public sealed partial class ObservableFixedSizeRingBuffer<T> : IList<T>, IReadOnlyList<T>, IObservableCollection<T>
public sealed partial class ObservableHashSet<T> : IReadOnlySet<T>, IReadOnlyCollection<T>, IObservableCollection<T> where T : notnull
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 sealed partial class ObservableHashSet<T> : IReadOnlySet<T>, IReadOnlyCollection<T>, IObservableCollection<T>
where T : notnull
public delegate T WritableViewChangedEventHandler<T, TView>(TView newView, T originalValue, ref bool setValue);
public sealed partial class ObservableList<T> : IList<T>, IReadOnlyList<T>, IObservableCollection<T>
public interface IWritableSynchronizedView<T, TView> : ISynchronizedView<T, TView>
{
(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 sealed partial class ObservableQueue<T> : IReadOnlyCollection<T>, IObservableCollection<T>
public sealed partial class ObservableRingBuffer<T> : IList<T>, IReadOnlyList<T>, IObservableCollection<T>
public interface IWritableSynchronizedViewList<TView> : ISynchronizedViewList<TView>
{
new TView this[int index] { get; set; }
}
public sealed partial class ObservableStack<T> : IReadOnlyCollection<T>, IObservableCollection<T>
public sealed class RingBuffer<T> : IList<T>, IReadOnlyList<T>
```
Here are definitions for other collections:
Freezed
---
```csharp
public interface IReadOnlyObservableList<T> :
IReadOnlyList<T>, IObservableCollection<T>
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);
ISortableSynchronizedView<T, TView> CreateSortableView<TView>(Func<T, TView> transform);
}
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
{
}
public static ISortableSynchronizedView<T, TView> CreateSortableView<T, TView>(this IFreezedCollection<T> source, Func<T, TView> transform, IComparer<T> initialSort)
public static ISortableSynchronizedView<T, TView> CreateSortableView<T, TView>(this IFreezedCollection<T> source, Func<T, TView> transform, IComparer<TView> initialViewSort)
public static ISortableSynchronizedView<T, TView> CreateSortableView<T, TView, TCompare>(this IFreezedCollection<T> source, Func<T, TView> transform, Func<T, TCompare> initialCompareSelector, bool ascending = true)
public static void Sort<T, TView, TCompare>(this ISortableSynchronizedView<T, TView> source, Func<T, TCompare> compareSelector, bool ascending = true)
```
License

Binary file not shown.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,105 +1,91 @@
using System;
using System.Collections.Specialized;
using System;
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>()
{
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);
var bindable = view.ToWritableNotifyCollectionChanged((string? newView, Person original, ref bool setValue) =>
{
if (setValue)
var list = new ObservableList<int>();
list.ObserveAdd()
.Subscribe(x =>
{
// default setValue == true is Set operation
original.Name = newView;
Console.WriteLine(x);
});
// 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.Add(10);
list.Add(20);
list.AddRange(new[] { 10, 20, 30 });
var models = new ObservableList<int>(Enumerable.Range(0, 10));
var viewModels = models.CreateView(x => new ViewModel
{
Id = x,
Value = "@" + x
});
viewModels.AttachFilter(new HogeFilter(), true);
list.Clear();
models.Add(100);
list.Add(new() { Age = 99, Name = "tako" });
// bindable[0] = "takoyaki";
foreach (var item in view)
foreach (var (x, xs) in viewModels)
{
Console.WriteLine(item);
System.Console.WriteLine(xs.Value);
}
Console.WriteLine("---");
foreach (var item in list)
class ViewModel
{
Console.WriteLine((item.Age, item.Name));
public int Id { get; set; }
public string Value { get; set; } = default!;
}
public class Person
class HogeFilter : ISynchronizedViewFilter<int, ViewModel>
{
public int? Age { get; set; }
public string? Name { get; set; }
}
public bool IsMatch(int value, ViewModel view)
{
return value % 2 == 0;
}
public void WhenTrue(int value, ViewModel view)
{
view.Value = $"@{value} (even)";
}
//var buffer = new ObservableFixedSizeRingBuffer<int>(5);
public void WhenFalse(int value, ViewModel view)
{
view.Value = $"@{value} (odd)";
}
//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);
//}
public void OnCollectionChanged(
ChangedKind changedKind,
int value,
ViewModel view,
in NotifyCollectionChangedEventArgs<int> eventArgs)
{
switch (changedKind)
{
case ChangedKind.Add:
view.Value += " Add";
break;
case ChangedKind.Remove:
view.Value += " Remove";
break;
case ChangedKind.Move:
view.Value += $" Move {eventArgs.OldStartingIndex} {eventArgs.NewStartingIndex}";
break;
case ChangedKind.Clear:
view.Value += $" Clear";
break;
default:
throw new ArgumentOutOfRangeException(nameof(changedKind), changedKind, null);
}
}
}

View File

@ -5,62 +5,20 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApp"
mc:Ignorable="d"
Title="MainWindow" Height="800" Width="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
Title="MainWindow" Height="450" Width="800">
<!--<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<!-- original -->
<StackPanel Grid.Row="0" Grid.ColumnSpan="2" Orientation="Vertical">
<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>
<ListView ItemsSource="{Binding ItemsView}"></ListView>
<Button Grid.Column="1" Click="Button_Click">Insert</Button>
<!-- Upper left (NotWritable, NonFilter) -->
<GroupBox Grid.Row="1" Grid.Column="0" Header="NotWritable, NonFilter">
<StackPanel Orientation="Vertical">
<DataGrid ItemsSource="{Binding NotWritableNonFilterView}" />
</StackPanel>
</GroupBox>
</Grid>-->
<!-- 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>
</Window>
<StackPanel>
<ListBox ItemsSource="{Binding ItemsView}" />
<Button Content="Clear" Command="{Binding ClearCommand}" />
</StackPanel>
</Window>

View File

@ -2,8 +2,6 @@
using R3;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Linq;
using System.Text;
@ -18,7 +16,6 @@ using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Threading;
namespace WpfApp
{
@ -45,7 +42,7 @@ namespace WpfApp
this.DataContext = new ViewModel();
// Dispatcher.BeginInvoke(
//list = new ObservableList<int>();
@ -75,199 +72,24 @@ namespace WpfApp
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 INotifyCollectionChangedSynchronizedView<int> ItemsView { get; }
public ReactiveCommand<Unit> ClearCommand { get; } = new ReactiveCommand<Unit>();
public ReactiveCommand<Unit> ReverseCommand { get; } = new ReactiveCommand<Unit>();
public ReactiveCommand<Unit> SortCommand { get; } = new ReactiveCommand<Unit>();
public ReactiveCommand<Unit> AttachFilterCommand { get; } = new ReactiveCommand<Unit>();
public ReactiveCommand<Unit> ResetFilterCommand { get; } = new ReactiveCommand<Unit>();
private ObservableList<Person> SourceList { get; } = [];
private IWritableSynchronizedView<Person, Person> writableFilter;
private ISynchronizedView<Person, Person> notWritableFilter;
public NotifyCollectionChangedSynchronizedViewList<Person> NotWritableNonFilterView { get; }
public NotifyCollectionChangedSynchronizedViewList<Person> NotWritableFilterView { get; }
public NotifyCollectionChangedSynchronizedViewList<Person> WritableNonFilterPersonView { get; }
public NotifyCollectionChangedSynchronizedViewList<Person> WritableFilterPersonView { get; }
public ReactiveCommand<Unit> AttachFilterCommand2 { get; } = new ReactiveCommand<Unit>();
public ReactiveCommand<Unit> ResetFilterCommand2 { get; } = new ReactiveCommand<Unit>();
public ReactiveCommand<Unit> AttachFilterCommand3 { get; } = new ReactiveCommand<Unit>();
public ReactiveCommand<Unit> ResetFilterCommand3 { get; } = new ReactiveCommand<Unit>();
public ViewModel()
{
observableList.Add(1);
observableList.Add(2);
var view = observableList.CreateView(x => x);
//ItemsView = view.ToNotifyCollectionChanged();
ItemsView = observableList.ToNotifyCollectionChanged();
ItemsView = observableList.CreateView(x => x).ToNotifyCollectionChanged();
// 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);
});
RemoveRangeCommand.Subscribe(_ =>
{
observableList.RemoveRange(2, 5);
});
BindingOperations.EnableCollectionSynchronization(ItemsView, new object());
// var iii = 10;
ClearCommand.Subscribe(_ =>
{
// observableList.Add(iii++);
observableList.Clear();
});
ReverseCommand.Subscribe(_ =>
{
observableList.Reverse();
});
SortCommand.Subscribe(_ =>
{
observableList.Sort();
});
AttachFilterCommand.Subscribe(_ =>
{
view.AttachFilter(x => x % 2 == 0);
});
ResetFilterCommand.Subscribe(_ =>
{
view.ResetFilter();
});
SourceList.Add(new() { Name = "a", Age = 1 });
SourceList.Add(new() { Name = "b", Age = 2 });
SourceList.Add(new() { Name = "c", Age = 3 });
SourceList.Add(new() { Name = "d", Age = 4 });
//NotWritable, NonFilter
NotWritableNonFilterView = SourceList.ToNotifyCollectionChanged();
//NotWritable, Filter
notWritableFilter = SourceList.CreateView(x => x);
NotWritableFilterView = notWritableFilter.ToNotifyCollectionChanged();
//Writable, NonFilter
WritableNonFilterPersonView = SourceList.ToWritableNotifyCollectionChanged();
//WritableNonFilterPersonView = SourceList.ToWritableNotifyCollectionChanged(x => x, (Person newView, Person original, ref bool setValue) =>
//{
// if (setValue)
// {
// // default setValue == true is Set operation
// original.Name = newView.Name;
// original.Age = newView.Age;
// // You can modify setValue to false, it does not set original collection to new value.
// // For mutable reference types, when there is only a single,
// // bound View and to avoid recreating the View, setting false is effective.
// // Otherwise, keeping it true will set the value in the original collection as well,
// // and change notifications will be sent to lower-level Views(the delegate for View generation will also be called anew).
// setValue = false;
// return original;
// }
// else
// {
// // default setValue == false is Add operation
// return new Person { Age = newView.Age, Name = newView.Name };
// }
//}, null);
//Writable, Filter
writableFilter = SourceList.CreateWritableView(x => x);
WritableFilterPersonView = writableFilter.ToWritableNotifyCollectionChanged();
//WritableFilterPersonView = writableFilter.ToWritableNotifyCollectionChanged((Person newView, Person original, ref bool setValue) =>
//{
// if (setValue)
// {
// // default setValue == true is Set operation
// original.Name = newView.Name;
// original.Age = newView.Age;
// // You can modify setValue to false, it does not set original collection to new value.
// // For mutable reference types, when there is only a single,
// // bound View and to avoid recreating the View, setting false is effective.
// // Otherwise, keeping it true will set the value in the original collection as well,
// // and change notifications will be sent to lower-level Views(the delegate for View generation will also be called anew).
// setValue = false;
// return original;
// }
// else
// {
// // default setValue == false is Add operation
// return new Person { Age = newView.Age, Name = newView.Name };
// }
//});
AttachFilterCommand2.Subscribe(_ =>
{
notWritableFilter.AttachFilter(x => x.Age % 2 == 0);
});
ResetFilterCommand2.Subscribe(_ =>
{
notWritableFilter.ResetFilter();
});
AttachFilterCommand3.Subscribe(_ =>
{
writableFilter.AttachFilter(x => x.Age % 2 == 0);
});
ResetFilterCommand3.Subscribe(_ =>
{
writableFilter.ResetFilter();
});
}
}
public class Person
{
public int? Age { get; set; }
public string? Name { get; set; }
}
public class WpfDispatcherCollection(Dispatcher dispatcher) : ICollectionEventDispatcher
{
public void Post(CollectionEventDispatcherEventArgs ev)
{
dispatcher.InvokeAsync(() =>
{
ev.Invoke();
});
}
}
}
}

View File

@ -3,7 +3,6 @@
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net6.0-windows</TargetFramework>
<LangVersion>12</LangVersion>
<Nullable>enable</Nullable>
<UseWPF>true</UseWPF>
<IsPackable>false</IsPackable>
@ -11,10 +10,12 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="R3Extensions.WPF" Version="1.0.4" />
<PackageReference Include="R3" Version="1.0.4" />
<PackageReference Include="R3Extensions.WPF" Version="1.0.4" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\ObservableCollections.R3\ObservableCollections.R3.csproj" />
<ProjectReference Include="..\..\src\ObservableCollections\ObservableCollections.csproj" />
</ItemGroup>

View File

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

View File

@ -1,7 +1,5 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Runtime.InteropServices;
using System.Threading;
using R3;
@ -11,57 +9,9 @@ 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>
public static class ObservableCollectionR3Extensions
{
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);
@ -76,126 +26,23 @@ public static partial class ObservableCollectionR3Extensions
{
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)
public static Observable<Unit> 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>>
{
@ -255,9 +102,10 @@ sealed class ObservableCollectionRemove<T>(IObservableCollection<T> collection,
}
else
{
var i = eventArgs.OldStartingIndex;
foreach (var item in eventArgs.OldItems)
{
observer.OnNext(new CollectionRemoveEvent<T>(eventArgs.OldStartingIndex, item)); // remove uses same index
observer.OnNext(new CollectionRemoveEvent<T>(i++, item));
}
}
}
@ -272,7 +120,7 @@ sealed class ObservableCollectionReplace<T>(IObservableCollection<T> collection,
{
return new _ObservableCollectionReplace(collection, observer, cancellationToken);
}
sealed class _ObservableCollectionReplace(
IObservableCollection<T> collection,
Observer<CollectionReplaceEvent<T>> observer,
@ -296,7 +144,7 @@ sealed class ObservableCollectionMove<T>(IObservableCollection<T> collection, Ca
{
return new _ObservableCollectionMove(collection, observer, cancellationToken);
}
sealed class _ObservableCollectionMove(
IObservableCollection<T> collection,
Observer<CollectionMoveEvent<T>> observer,
@ -312,39 +160,16 @@ sealed class ObservableCollectionMove<T>(IObservableCollection<T> collection, Ca
}
}
}
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);
return new _ObservableCollectionReset(collection, observer, cancellationToken);
}
sealed class _ObservableCollectionClear(
sealed class _ObservableCollectionReset(
IObservableCollection<T> collection,
Observer<Unit> observer,
CancellationToken cancellationToken)
@ -352,7 +177,7 @@ sealed class ObservableCollectionClear<T>(IObservableCollection<T> collection, C
{
protected override void Handler(in NotifyCollectionChangedEventArgs<T> eventArgs)
{
if (eventArgs.Action == NotifyCollectionChangedAction.Reset && eventArgs.SortOperation.IsClear)
if (eventArgs.Action == NotifyCollectionChangedAction.Reset)
{
observer.OnNext(Unit.Default);
}
@ -360,52 +185,6 @@ sealed class ObservableCollectionClear<T>(IObservableCollection<T> collection, C
}
}
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>
{
@ -413,7 +192,7 @@ sealed class ObservableCollectionCountChanged<T>(IObservableCollection<T> collec
{
return new _ObservableCollectionCountChanged(collection, notifyCurrentCount, observer, cancellationToken);
}
sealed class _ObservableCollectionCountChanged : ObservableCollectionObserverBase<T, int>
{
int countPrev;
@ -446,110 +225,6 @@ sealed class ObservableCollectionCountChanged<T>(IObservableCollection<T> collec
}
}
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
{
@ -565,7 +240,7 @@ abstract class ObservableCollectionObserverBase<T, TEvent> : IDisposable
this.handlerDelegate = Handler;
collection.CollectionChanged += handlerDelegate;
if (cancellationToken.CanBeCanceled)
{
cancellationTokenRegistration = cancellationToken.UnsafeRegister(static state =>

View File

@ -1,248 +0,0 @@
#pragma warning disable CS0436
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.InteropServices;
namespace ObservableCollections;
public class AlternateIndexList<T> : IEnumerable<T>
{
List<IndexedValue> list; // alternate index is ordered
public AlternateIndexList()
{
this.list = new();
}
public AlternateIndexList(IEnumerable<(int OrderedAlternateIndex, T Value)> values)
{
this.list = values.Select(x => new IndexedValue(x.OrderedAlternateIndex, x.Value)).ToList();
}
public void UpdateAlternateIndex(int startIndex, int incr)
{
var span = CollectionsMarshal.AsSpan(list);
for (int i = startIndex; i < span.Length; i++)
{
span[i].AlternateIndex += incr;
}
}
public T this[int index]
{
get => list[index].Value;
set => CollectionsMarshal.AsSpan(list)[index].Value = value;
}
public int GetAlternateIndex(int index) => list[index].AlternateIndex;
public int Count => list.Count;
public int Insert(int alternateIndex, T value)
{
var index = list.BinarySearch(alternateIndex);
if (index < 0)
{
index = ~index;
}
list.Insert(index, new(alternateIndex, value));
UpdateAlternateIndex(index + 1, 1);
return index;
}
public int InsertRange(int startingAlternateIndex, IEnumerable<T> values)
{
var index = list.BinarySearch(startingAlternateIndex);
if (index < 0)
{
index = ~index;
}
using var iter = new InsertIterator(startingAlternateIndex, values);
list.InsertRange(index, iter);
UpdateAlternateIndex(index + iter.ConsumedCount, iter.ConsumedCount);
return index;
}
public int Remove(T value)
{
var index = list.FindIndex(x => EqualityComparer<T>.Default.Equals(x.Value, value));
if (index != -1)
{
list.RemoveAt(index);
UpdateAlternateIndex(index, -1);
}
return index;
}
public int RemoveAt(int alternateIndex)
{
var index = list.BinarySearch(alternateIndex);
if (index >= 0)
{
list.RemoveAt(index);
UpdateAlternateIndex(index, -1);
}
else
{
throw new InvalidOperationException("Index was not found. AlternateIndex:" + alternateIndex);
}
return index;
}
public int RemoveRange(int alternateIndex, int count)
{
var index = list.BinarySearch(alternateIndex);
if (index < 0)
{
index = ~index;
}
list.RemoveRange(index, count);
UpdateAlternateIndex(index, -count);
return index;
}
public bool TryGetAtAlternateIndex(int alternateIndex, [MaybeNullWhen(true)] out T value)
{
var index = list.BinarySearch(alternateIndex);
if (index < 0)
{
value = default!;
return false;
}
value = list[index].Value!;
return true;
}
public bool TrySetAtAlternateIndex(int alternateIndex, T value, out int setIndex)
{
setIndex = list.BinarySearch(alternateIndex);
if (setIndex < 0)
{
return false;
}
CollectionsMarshal.AsSpan(list)[setIndex].Value = value;
return true;
}
/// <summary>NOTE: when replace successfully, list has been sorted.</summary>
public bool TryReplaceAlternateIndex(int getAlternateIndex, int setAlternateIndex)
{
var index = list.BinarySearch(getAlternateIndex);
if (index < 0)
{
return false;
}
var span = CollectionsMarshal.AsSpan(list);
span[index].AlternateIndex = setAlternateIndex;
list.Sort(); // needs sort to keep order
return true;
}
public bool TryReplaceByValue(T searchValue, T replaceValue, out int replacedIndex)
{
replacedIndex = list.FindIndex(x => EqualityComparer<T>.Default.Equals(x.Value, searchValue));
if (replacedIndex != -1)
{
CollectionsMarshal.AsSpan(list)[replacedIndex].Value = replaceValue;
return true;
}
return false;
}
public void Clear()
{
list.Clear();
}
public void Clear(IEnumerable<(int OrderedAlternateIndex, T Value)> values)
{
list.Clear();
list.AddRange(values.Select(x => new IndexedValue(x.OrderedAlternateIndex, x.Value)));
}
public IEnumerator<T> GetEnumerator()
{
foreach (var item in list)
{
yield return item.Value;
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
public IEnumerable<(int AlternateIndex, T Value)> GetIndexedValues()
{
foreach (var item in list)
{
yield return (item.AlternateIndex, item.Value);
}
}
class InsertIterator(int startingIndex, IEnumerable<T> values) : IEnumerable<IndexedValue>, IEnumerator<IndexedValue>
{
IEnumerator<T> iter = values.GetEnumerator();
IndexedValue current;
public int ConsumedCount { get; private set; }
public IndexedValue Current => current;
object IEnumerator.Current => Current;
public void Dispose() => iter.Dispose();
public bool MoveNext()
{
if (iter.MoveNext())
{
ConsumedCount++;
current = new(startingIndex++, iter.Current);
return true;
}
return false;
}
public void Reset() { }
public IEnumerator<IndexedValue> GetEnumerator() => this;
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
struct IndexedValue : IComparable<IndexedValue>
{
public int AlternateIndex; // mutable
public T Value; // mutable
public IndexedValue(int alternateIndex, T value)
{
this.AlternateIndex = alternateIndex;
this.Value = value;
}
public static implicit operator IndexedValue(int alternateIndex) // for query
{
return new IndexedValue(alternateIndex, default!);
}
public int CompareTo(IndexedValue other)
{
return AlternateIndex.CompareTo(other.AlternateIndex);
}
public override string ToString()
{
return (AlternateIndex, Value).ToString();
}
}
}

View File

@ -0,0 +1,59 @@
#nullable disable
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>>
where TKey : notnull
{
readonly IReadOnlyDictionary<TKey, TValue> dictionary;
public FreezedDictionary(IReadOnlyDictionary<TKey, TValue> dictionary)
{
this.dictionary = dictionary;
}
public TValue this[TKey key] => dictionary[key];
public IEnumerable<TKey> Keys => dictionary.Keys;
public IEnumerable<TValue> Values => dictionary.Values;
public int Count => dictionary.Count;
public bool ContainsKey(TKey key)
{
return dictionary.ContainsKey(key);
}
public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator()
{
return dictionary.GetEnumerator();
}
public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value)
{
return dictionary.TryGetValue(key, out value);
}
IEnumerator IEnumerable.GetEnumerator()
{
return ((IEnumerable)dictionary).GetEnumerator();
}
public ISynchronizedView<KeyValuePair<TKey, TValue>, TView> CreateView<TView>(Func<KeyValuePair<TKey, TValue>, TView> transform, bool reverse = false)
{
return new FreezedView<KeyValuePair<TKey, TValue>, TView>(dictionary, transform, reverse);
}
public ISortableSynchronizedView<KeyValuePair<TKey, TValue>, TView> CreateSortableView<TView>(Func<KeyValuePair<TKey, TValue>, TView> transform)
{
return new FreezedSortableView<KeyValuePair<TKey, TValue>, TView>(dictionary, transform);
}
}
}

View File

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

View File

@ -1,128 +0,0 @@
using System;
using System.Collections;
using System.Collections.Specialized;
using System.Threading;
namespace ObservableCollections
{
public interface ICollectionEventDispatcher
{
void Post(CollectionEventDispatcherEventArgs ev);
}
public class SynchronizationContextCollectionEventDispatcher : ICollectionEventDispatcher
{
static readonly Lazy<ICollectionEventDispatcher> current = new Lazy<ICollectionEventDispatcher>(() =>
{
var current = SynchronizationContext.Current;
if (current == null)
{
throw new InvalidOperationException("SynchronizationContext.Current is null");
}
return new SynchronizationContextCollectionEventDispatcher(current);
});
public static readonly ICollectionEventDispatcher Current = current.Value;
readonly SynchronizationContext synchronizationContext;
static readonly SendOrPostCallback callback = SendOrPostCallback;
public SynchronizationContextCollectionEventDispatcher(SynchronizationContext synchronizationContext)
{
this.synchronizationContext = synchronizationContext;
}
public void Post(CollectionEventDispatcherEventArgs ev)
{
if (SynchronizationContext.Current == null)
{
// non-UI thread, post the event asynchronously
synchronizationContext.Post(callback, ev);
}
else
{
// UI thread, send the event synchronously
synchronizationContext.Send(callback, ev);
}
}
static void SendOrPostCallback(object? state)
{
var ev = (CollectionEventDispatcherEventArgs)state!;
ev.Invoke();
}
}
internal class InlineCollectionEventDispatcher : ICollectionEventDispatcher
{
public static readonly ICollectionEventDispatcher Instance = new InlineCollectionEventDispatcher();
InlineCollectionEventDispatcher()
{
}
public void Post(CollectionEventDispatcherEventArgs ev)
{
ev.Invoke();
}
}
public class CollectionEventDispatcherEventArgs : NotifyCollectionChangedEventArgs
{
// +state, init;
public object Collection { get; set; } = default!;
public bool IsInvokeCollectionChanged { get; set; }
public bool IsInvokePropertyChanged { get; set; }
internal Action<CollectionEventDispatcherEventArgs> Invoker { get; set; } = default!;
public void Invoke()
{
Invoker.Invoke(this);
}
public CollectionEventDispatcherEventArgs(NotifyCollectionChangedAction action) : base(action)
{
}
public CollectionEventDispatcherEventArgs(NotifyCollectionChangedAction action, IList? changedItems) : base(action, changedItems)
{
}
public CollectionEventDispatcherEventArgs(NotifyCollectionChangedAction action, object? changedItem) : base(action, changedItem)
{
}
public CollectionEventDispatcherEventArgs(NotifyCollectionChangedAction action, IList newItems, IList oldItems) : base(action, newItems, oldItems)
{
}
public CollectionEventDispatcherEventArgs(NotifyCollectionChangedAction action, IList? changedItems, int startingIndex) : base(action, changedItems, startingIndex)
{
}
public CollectionEventDispatcherEventArgs(NotifyCollectionChangedAction action, object? changedItem, int index) : base(action, changedItem, index)
{
}
public CollectionEventDispatcherEventArgs(NotifyCollectionChangedAction action, object? newItem, object? oldItem) : base(action, newItem, oldItem)
{
}
public CollectionEventDispatcherEventArgs(NotifyCollectionChangedAction action, IList newItems, IList oldItems, int startingIndex) : base(action, newItems, oldItems, startingIndex)
{
}
public CollectionEventDispatcherEventArgs(NotifyCollectionChangedAction action, IList? changedItems, int index, int oldIndex) : base(action, changedItems, index, oldIndex)
{
}
public CollectionEventDispatcherEventArgs(NotifyCollectionChangedAction action, object? changedItem, int index, int oldIndex) : base(action, changedItem, index, oldIndex)
{
}
public CollectionEventDispatcherEventArgs(NotifyCollectionChangedAction action, object? newItem, object? oldItem, int index) : base(action, newItem, oldItem, index)
{
}
}
}

View File

@ -1,5 +1,5 @@
using ObservableCollections.Internal;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
@ -7,230 +7,113 @@ using System.ComponentModel;
namespace ObservableCollections
{
public delegate void NotifyCollectionChangedEventHandler<T>(in NotifyCollectionChangedEventArgs<T> e);
public delegate void NotifyViewChangedEventHandler<T, TView>(in SynchronizedViewChangedEventArgs<T, TView> e);
public delegate T WritableViewChangedEventHandler<T, TView>(TView newView, T originalValue, ref bool setValue);
public interface IObservableCollection<T> : IReadOnlyCollection<T>
{
event NotifyCollectionChangedEventHandler<T>? CollectionChanged;
object SyncRoot { get; }
ISynchronizedView<T, TView> CreateView<TView>(Func<T, TView> transform);
ISynchronizedView<T, TView> CreateView<TView>(Func<T, TView> transform, bool reverse = false);
}
public interface IReadOnlyObservableList<T> :
IReadOnlyList<T>, IObservableCollection<T>
public interface IFreezedCollection<T>
{
ISynchronizedView<T, TView> CreateView<TView>(Func<T, TView> transform, bool reverse = false);
ISortableSynchronizedView<T, TView> CreateSortableView<TView>(Func<T, TView> transform);
}
public interface IReadOnlyObservableDictionary<TKey, TValue> :
IReadOnlyDictionary<TKey, TValue>, IObservableCollection<KeyValuePair<TKey, TValue>>
{
}
public enum RejectedViewChangedAction
{
Add, Remove, Move
}
public interface ISynchronizedView<T, TView> : IReadOnlyCollection<TView>, IDisposable
public interface ISynchronizedView<T, TView> : IReadOnlyCollection<(T Value, TView View)>, IDisposable
{
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; }
ISynchronizedViewFilter<T, TView> CurrentFilter { get; }
event NotifyViewChangedEventHandler<T, TView>? ViewChanged;
event Action<RejectedViewChangedAction, int, int>? RejectedViewChanged;
event NotifyCollectionChangedEventHandler<T>? RoutingCollectionChanged;
event Action<NotifyCollectionChangedAction>? CollectionStateChanged;
void AttachFilter(ISynchronizedViewFilter<T, TView> filter);
void ResetFilter();
ISynchronizedViewList<TView> ToViewList();
NotifyCollectionChangedSynchronizedViewList<TView> ToNotifyCollectionChanged();
NotifyCollectionChangedSynchronizedViewList<TView> ToNotifyCollectionChanged(ICollectionEventDispatcher? collectionEventDispatcher);
void AttachFilter(ISynchronizedViewFilter<T, TView> filter, bool invokeAddEventForInitialElements = false);
void ResetFilter(Action<T, TView>? resetAction);
INotifyCollectionChangedSynchronizedView<TView> ToNotifyCollectionChanged();
}
public interface IWritableSynchronizedView<T, TView> : ISynchronizedView<T, TView>
public interface ISortableSynchronizedView<T, TView> : ISynchronizedView<T, TView>
{
(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);
NotifyCollectionChangedSynchronizedViewList<TView> ToWritableNotifyCollectionChanged();
NotifyCollectionChangedSynchronizedViewList<TView> ToWritableNotifyCollectionChanged(WritableViewChangedEventHandler<T, TView> converter);
NotifyCollectionChangedSynchronizedViewList<TView> ToWritableNotifyCollectionChanged(ICollectionEventDispatcher? collectionEventDispatcher);
NotifyCollectionChangedSynchronizedViewList<TView> ToWritableNotifyCollectionChanged(WritableViewChangedEventHandler<T, TView> converter, ICollectionEventDispatcher? collectionEventDispatcher);
void Sort(IComparer<T> comparer);
void Sort(IComparer<TView> viewComparer);
}
public interface ISynchronizedViewList<out TView> : IReadOnlyList<TView>, IDisposable
// will be implemented in the future?
//public interface IGroupedSynchoronizedView<T, TKey, TView> : ILookup<TKey, (T, TView)>, ISynchronizedView<T, TView>
//{
//}
public interface INotifyCollectionChangedSynchronizedView<out TView> : IReadOnlyCollection<TView>, INotifyCollectionChanged, INotifyPropertyChanged, IDisposable
{
}
public interface IWritableSynchronizedViewList<TView> : ISynchronizedViewList<TView>
public static class ObservableCollectionsExtensions
{
new TView this[int index] { get; set; }
}
// only for compatibility, use NotifyCollectionChangedSynchronizedViewList insetad.
// [Obsolete] in future
public interface INotifyCollectionChangedSynchronizedViewList<TView> : IList<TView>, IList, ISynchronizedViewList<TView>, INotifyCollectionChanged, INotifyPropertyChanged
{
}
// IColleciton<T>.Count and ICollection.Count will be ambigious so use abstract class instead of interface
public abstract class NotifyCollectionChangedSynchronizedViewList<TView> :
INotifyCollectionChangedSynchronizedViewList<TView>,
IWritableSynchronizedViewList<TView>,
IList<TView>,
IList
{
protected readonly object gate = new object();
public abstract TView this[int index] { get; set; }
object? IList.this[int index]
public static ISynchronizedView<T, TView> CreateSortedView<T, TKey, TView>(this IObservableCollection<T> source, Func<T, TKey> identitySelector, Func<T, TView> transform, IComparer<T> comparer)
where TKey : notnull
{
get
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)
where TKey : notnull
{
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)
where TKey : notnull
{
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)
{
return this[index];
this.selector = selector;
this.f = ascending ? 1 : -1;
}
set => ((IList<TView>)this)[index] = (TView)value!;
}
public abstract int Count { get; }
public virtual bool IsReadOnly { get; } = true;
public bool IsFixedSize => IsReadOnly;
public bool IsSynchronized => true;
public object SyncRoot => gate;
public abstract event NotifyCollectionChangedEventHandler? CollectionChanged;
public abstract event PropertyChangedEventHandler? PropertyChanged;
public abstract void Add(TView item);
int IList.Add(object? value)
{
Add((TView)value!);
return Count - 1;
}
public abstract void Insert(int index, TView item);
public abstract bool Remove(TView item);
public abstract void RemoveAt(int index);
public abstract void Clear();
public abstract bool Contains(TView item);
bool IList.Contains(object? value)
{
if (IsCompatibleObject(value))
public int Compare(T? x, T? y)
{
return Contains((TView)value!);
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;
}
return false;
}
public abstract void Dispose();
public abstract IEnumerator<TView> GetEnumerator();
public abstract int IndexOf(TView item);
int IList.IndexOf(object? item)
{
if (IsCompatibleObject(item))
{
return IndexOf((TView)item!);
}
return -1;
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
static bool IsCompatibleObject(object? value)
{
return value is TView || value == null && default(TView) == null;
}
void ICollection<TView>.Clear()
{
Clear();
}
void IList.Clear()
{
Clear();
}
void ICollection<TView>.CopyTo(TView[] array, int arrayIndex) => throw new NotSupportedException();
void ICollection.CopyTo(Array array, int index) => throw new NotSupportedException();
void IList<TView>.Insert(int index, TView item)
{
Insert(index, item);
}
void IList.Insert(int index, object? value)
{
Insert(index, (TView)value!);
}
bool ICollection<TView>.Remove(TView item)
{
return Remove(item!);
}
void IList.Remove(object? value)
{
Remove((TView)value!);
}
void IList.RemoveAt(int index)
{
RemoveAt(index);
}
void IList<TView>.RemoveAt(int index) => throw new NotSupportedException();
}
public static class ObservableCollectionExtensions
{
public static ISynchronizedViewList<T> ToViewList<T>(this IObservableCollection<T> collection)
{
return ToViewList(collection, static x => x);
}
public static ISynchronizedViewList<TView> ToViewList<T, TView>(this IObservableCollection<T> collection, Func<T, TView> transform)
{
// Optimized for non filtered
return new NonFilteredSynchronizedViewList<T, TView>(collection.CreateView(transform), isSupportRangeFeature: true, null, null);
}
public static NotifyCollectionChangedSynchronizedViewList<T> ToNotifyCollectionChanged<T>(this IObservableCollection<T> collection)
{
return ToNotifyCollectionChanged(collection, null);
}
public static NotifyCollectionChangedSynchronizedViewList<T> ToNotifyCollectionChanged<T>(this IObservableCollection<T> collection, ICollectionEventDispatcher? collectionEventDispatcher)
{
return ToNotifyCollectionChanged(collection, static x => x, collectionEventDispatcher);
}
public static NotifyCollectionChangedSynchronizedViewList<TView> ToNotifyCollectionChanged<T, TView>(this IObservableCollection<T> collection, Func<T, TView> transform)
{
return ToNotifyCollectionChanged(collection, transform, null!);
}
public static NotifyCollectionChangedSynchronizedViewList<TView> ToNotifyCollectionChanged<T, TView>(this IObservableCollection<T> collection, Func<T, TView> transform, ICollectionEventDispatcher? collectionEventDispatcher)
{
// Optimized for non filtered
return new NonFilteredSynchronizedViewList<T, TView>(collection.CreateView(transform), isSupportRangeFeature: false, collectionEventDispatcher, null);
}
}
}
}

View File

@ -2,37 +2,125 @@
namespace ObservableCollections
{
// Obsolete...
[Obsolete("this interface is obsoleted. Use ISynchronizedViewFilter<T, TView> instead.")]
public interface ISynchronizedViewFilter<T>
{
bool IsMatch(T value);
}
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);
}
internal class SynchronizedViewValueOnlyFilter<T, TView>(Func<T, bool> isMatch) : ISynchronizedViewFilter<T, TView>
public enum ChangedKind
{
public bool IsMatch(T value, TView view) => isMatch(value);
class NullViewFilter : ISynchronizedViewFilter<T, TView>
{
public bool IsMatch(T value, TView view) => true;
}
Add, Remove, Move, Clear
}
public class SynchronizedViewFilter<T, TView>(Func<T, TView, bool> isMatch) : ISynchronizedViewFilter<T, TView>
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 InvokeOnClear<T, TView>(this ISynchronizedViewFilter<T, TView> filter, in NotifyCollectionChangedEventArgs<T> eventArgs)
{
filter.OnCollectionChanged(ChangedKind.Clear, default!, default!, eventArgs);
}
internal static void InvokeOnAttach<T, TView>(this ISynchronizedViewFilter<T, TView> filter, T value, TView view)
{
if (filter.IsMatch(value, view))
{
filter.WhenTrue(value, view);
}
else
{
filter.WhenFalse(value, view);
}
}
}
}

View File

@ -74,10 +74,8 @@ namespace ObservableCollections.Internal
{
if (array.Length == index)
{
var newArray = ArrayPool<T>.Shared.Rent(index * 2);
Array.Copy(array, newArray, index);
ArrayPool<T>.Shared.Return(array, RuntimeHelpersEx.IsReferenceOrContainsReferences<T>());
array = newArray;
array = ArrayPool<T>.Shared.Rent(index * 2);
}
}

View File

@ -1,55 +0,0 @@
using System;
using System.Buffers;
using System.Runtime.CompilerServices;
namespace ObservableCollections.Internal
{
internal ref struct FixedArray<T>
{
public readonly Span<T> Span;
T[]? array;
public FixedArray(int size)
{
array = ArrayPool<T>.Shared.Rent(size);
Span = array.AsSpan(0, size);
}
public void Dispose()
{
if (array != null)
{
ArrayPool<T>.Shared.Return(array, RuntimeHelpersEx.IsReferenceOrContainsReferences<T>());
}
}
}
internal ref struct FixedBoolArray
{
public const int StackallocSize = 128;
public readonly Span<bool> Span;
bool[]? array;
public FixedBoolArray(Span<bool> scratchBuffer, int capacity)
{
if (scratchBuffer.Length == 0)
{
array = ArrayPool<bool>.Shared.Rent(capacity);
Span = array.AsSpan(0, capacity);
}
else
{
Span = scratchBuffer;
}
}
public void Dispose()
{
if (array != null)
{
ArrayPool<bool>.Shared.Return(array);
}
}
}
}

View File

@ -0,0 +1,254 @@
#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 ISynchronizedViewFilter<T, TView> CurrentFilter
{
get { lock (SyncRoot) return 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, bool invokeAddEventForCurrentElements = false)
{
lock (SyncRoot)
{
this.filter = filter;
for (var i = 0; i < list.Count; i++)
{
var (value, view) = list[i];
if (invokeAddEventForCurrentElements)
{
filter.InvokeOnAdd(value, view, NotifyCollectionChangedEventArgs<T>.Add(value, i));
}
else
{
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<TView> ToNotifyCollectionChanged()
{
return new NotifyCollectionChangedSynchronizedView<T, TView>(this);
}
}
internal sealed class FreezedSortableView<T, TView> : ISortableSynchronizedView<T, TView>
{
readonly (T, TView)[] array;
ISynchronizedViewFilter<T, TView> filter;
public ISynchronizedViewFilter<T, TView> CurrentFilter
{
get { lock (SyncRoot) return 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, bool invokeAddEventForCurrentElements = false)
{
lock (SyncRoot)
{
this.filter = filter;
for (var i = 0; i < array.Length; i++)
{
var (value, view) = array[i];
if (invokeAddEventForCurrentElements)
{
filter.InvokeOnAdd(value, view, NotifyCollectionChangedEventArgs<T>.Add(value, i));
}
else
{
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<TView> ToNotifyCollectionChanged()
{
return new NotifyCollectionChangedSynchronizedView<T, TView>(this);
}
class TComparer : IComparer<(T, TView)>
{
readonly IComparer<T> comparer;
public TComparer(IComparer<T> comparer)
{
this.comparer = comparer;
}
public int Compare((T, TView) x, (T, TView) y)
{
return comparer.Compare(x.Item1, y.Item1);
}
}
class TViewComparer : IComparer<(T, TView)>
{
readonly IComparer<TView> comparer;
public TViewComparer(IComparer<TView> comparer)
{
this.comparer = comparer;
}
public int Compare((T, TView) x, (T, TView) y)
{
return comparer.Compare(x.Item2, y.Item2);
}
}
}
}

View File

@ -0,0 +1,87 @@
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<TView>,
ISynchronizedViewFilter<T, TView>
{
static readonly PropertyChangedEventArgs CountPropertyChangedEventArgs = new("Count");
readonly ISynchronizedView<T, TView> parent;
readonly ISynchronizedViewFilter<T, TView> currentFilter;
public NotifyCollectionChangedSynchronizedView(ISynchronizedView<T, TView> parent)
{
this.parent = parent;
currentFilter = parent.CurrentFilter;
parent.AttachFilter(this);
}
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 Dispose()
{
parent.Dispose();
}
public IEnumerator<TView> GetEnumerator()
{
foreach (var (value, view) in parent)
{
yield return view;
}
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public bool IsMatch(T value, TView view) => currentFilter.IsMatch(value, view);
public void WhenTrue(T value, TView view) => currentFilter.WhenTrue(value, view);
public void WhenFalse(T value, TView view) => currentFilter.WhenFalse(value, view);
public void OnCollectionChanged(ChangedKind changedKind, T value, TView view, in NotifyCollectionChangedEventArgs<T> eventArgs)
{
currentFilter.OnCollectionChanged(changedKind, value, view, in eventArgs);
switch (changedKind)
{
case ChangedKind.Add:
CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, view, eventArgs.NewStartingIndex));
PropertyChanged?.Invoke(this, CountPropertyChangedEventArgs);
return;
case ChangedKind.Remove:
CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, view, eventArgs.OldStartingIndex));
PropertyChanged?.Invoke(this, CountPropertyChangedEventArgs);
break;
case ChangedKind.Move:
CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Move, view, eventArgs.NewStartingIndex, eventArgs.OldStartingIndex));
break;
case ChangedKind.Clear:
PropertyChanged?.Invoke(this, CountPropertyChangedEventArgs);
CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
break;
default:
throw new ArgumentOutOfRangeException(nameof(changedKind), changedKind, null);
}
}
}
}

View File

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

View File

@ -0,0 +1,254 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
namespace ObservableCollections.Internal
{
internal class SortedView<T, TKey, TView> : ISynchronizedView<T, TView>
where TKey : notnull
{
public ISynchronizedViewFilter<T, TView> CurrentFilter
{
get { lock (SyncRoot) return filter; }
}
readonly IObservableCollection<T> source;
readonly Func<T, TView> transform;
readonly Func<T, TKey> identitySelector;
readonly SortedList<(T Value, TKey Key), (T Value, TView View)> list;
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 Dictionary<(T, TKey), (T, TView)>(source.Count);
foreach (var v in source)
{
dict.Add((v, identitySelector(v)), (v, transform(v)));
}
this.list = new SortedList<(T Value, TKey Key), (T Value, TView View)>(dict, new Comparer(comparer));
this.source.CollectionChanged += SourceCollectionChanged;
}
}
public int Count
{
get
{
lock (SyncRoot)
{
return list.Count;
}
}
}
public void AttachFilter(ISynchronizedViewFilter<T, TView> filter, bool invokeAddEventForCurrentElements = false)
{
lock (SyncRoot)
{
this.filter = filter;
foreach (var (_, (value, view)) in list)
{
if (invokeAddEventForCurrentElements)
{
filter.InvokeOnAdd(value, view, NotifyCollectionChangedEventArgs<T>.Add(value, -1));
}
else
{
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 list)
{
resetAction(value, view);
}
}
}
}
public INotifyCollectionChangedSynchronizedView<TView> ToNotifyCollectionChanged()
{
lock (SyncRoot)
{
return new NotifyCollectionChangedSynchronizedView<T, TView>(this);
}
}
public IEnumerator<(T, TView)> GetEnumerator()
{
lock (SyncRoot)
{
foreach (var item in list)
{
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);
list.Add((value, id), (value, view));
var index = list.IndexOfKey((value, id));
filter.InvokeOnAdd(value, view, NotifyCollectionChangedEventArgs<T>.Add(value, index));
}
else
{
foreach (var value in e.NewItems)
{
var view = transform(value);
var id = identitySelector(value);
list.Add((value, id), (value, view));
var index = list.IndexOfKey((value, id));
filter.InvokeOnAdd(value, view, NotifyCollectionChangedEventArgs<T>.Add(value, index));
}
}
}
break;
case NotifyCollectionChangedAction.Remove:
{
if (e.IsSingleItem)
{
var value = e.OldItem;
var id = identitySelector(value);
var key = (value, id);
if (list.TryGetValue(key, out var v))
{
var index = list.IndexOfKey(key);
list.RemoveAt(index);
filter.InvokeOnRemove(v.Value, v.View, NotifyCollectionChangedEventArgs<T>.Remove(v.Value, index));
}
}
else
{
foreach (var value in e.OldItems)
{
var id = identitySelector(value);
var key = (value, id);
if (list.TryGetValue(key, out var v))
{
var index = list.IndexOfKey((value, id));
list.RemoveAt(index);
filter.InvokeOnRemove(v.Value, v.View, NotifyCollectionChangedEventArgs<T>.Remove(v.Value, index));
}
}
}
}
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;
var oldKey = (oldValue, identitySelector(oldValue));
if (list.TryGetValue(oldKey, out var o))
{
var oldIndex = list.IndexOfKey(oldKey);
list.RemoveAt(oldIndex);
filter.InvokeOnRemove(o, NotifyCollectionChangedEventArgs<T>.Remove(oldValue, oldIndex));
}
var value = e.NewItem;
var view = transform(value);
var id = identitySelector(value);
list.Add((value, id), (value, view));
var newIndex = list.IndexOfKey((value, id));
filter.InvokeOnAdd(value, view, NotifyCollectionChangedEventArgs<T>.Add(value, newIndex));
}
break;
case NotifyCollectionChangedAction.Move:
{
// Move(index change) does not affect sorted list.
var oldValue = e.OldItem;
if (list.TryGetValue((oldValue, identitySelector(oldValue)), out var view))
{
filter.InvokeOnMove(view, e);
}
}
break;
case NotifyCollectionChangedAction.Reset:
if (!filter.IsNullFilter())
{
foreach (var item in list)
{
filter.InvokeOnRemove(item.Value, e);
}
}
list.Clear();
break;
default:
break;
}
RoutingCollectionChanged?.Invoke(e);
CollectionStateChanged?.Invoke(e.Action);
}
}
sealed class Comparer : IComparer<(T value, TKey id)>
{
readonly IComparer<T> comparer;
public Comparer(IComparer<T> comparer)
{
this.comparer = comparer;
}
public int Compare((T value, TKey id) x, (T value, TKey id) y)
{
var compare = comparer.Compare(x.value, y.value);
if (compare == 0)
{
compare = Comparer<TKey>.Default.Compare(x.id, y.id);
}
return compare;
}
}
}
}

View File

@ -0,0 +1,274 @@
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>
where TKey : notnull
{
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 SortedList<(TView View, TKey Key), (T Value, TView View)> list;
ISynchronizedViewFilter<T, TView> filter;
public event NotifyCollectionChangedEventHandler<T>? RoutingCollectionChanged;
public event Action<NotifyCollectionChangedAction>? CollectionStateChanged;
public object SyncRoot { get; } = new object();
public ISynchronizedViewFilter<T, TView> CurrentFilter
{
get { lock (SyncRoot) return filter; }
}
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 Dictionary<(TView, TKey), (T, TView)>(source.Count);
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.list = new SortedList<(TView View, TKey Key), (T Value, TView View)>(dict, new Comparer(comparer));
this.source.CollectionChanged += SourceCollectionChanged;
}
}
public int Count
{
get
{
lock (SyncRoot)
{
return list.Count;
}
}
}
public void AttachFilter(ISynchronizedViewFilter<T, TView> filter, bool invokeAddEventForCurrentElements = false)
{
lock (SyncRoot)
{
this.filter = filter;
foreach (var (_, (value, view)) in list)
{
if (invokeAddEventForCurrentElements)
{
filter.InvokeOnAdd(value, view, NotifyCollectionChangedEventArgs<T>.Add(value, -1));
}
else
{
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 list)
{
resetAction(value, view);
}
}
}
}
public INotifyCollectionChangedSynchronizedView<TView> ToNotifyCollectionChanged()
{
lock (SyncRoot)
{
return new NotifyCollectionChangedSynchronizedView<T, TView>(this);
}
}
public IEnumerator<(T, TView)> GetEnumerator()
{
lock (SyncRoot)
{
foreach (var item in list)
{
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);
list.Add((view, id), (value, view));
viewMap.Add(id, view);
var index = list.IndexOfKey((view, id));
filter.InvokeOnAdd(value, view, NotifyCollectionChangedEventArgs<T>.Add(value, index));
}
else
{
foreach (var value in e.NewItems)
{
var view = transform(value);
var id = identitySelector(value);
list.Add((view, id), (value, view));
viewMap.Add(id, view);
var index = list.IndexOfKey((view, id));
filter.InvokeOnAdd(value, view, NotifyCollectionChangedEventArgs<T>.Add(value, index));
}
}
break;
}
case NotifyCollectionChangedAction.Remove:
{
if (e.IsSingleItem)
{
var value = e.OldItem;
var id = identitySelector(value);
if (viewMap.Remove(id, out var view))
{
var key = (view, id);
if (list.TryGetValue(key, out var v))
{
var index = list.IndexOfKey(key);
list.RemoveAt(index);
filter.InvokeOnRemove(v, NotifyCollectionChangedEventArgs<T>.Remove(v.Value, index));
}
}
}
else
{
foreach (var value in e.OldItems)
{
var id = identitySelector(value);
if (viewMap.Remove(id, out var view))
{
var key = (view, id);
if (list.TryGetValue(key, out var v))
{
var index = list.IndexOfKey((view, id));
list.RemoveAt(index);
filter.InvokeOnRemove(v, NotifyCollectionChangedEventArgs<T>.Remove(v.Value, index));
}
}
}
}
break;
}
case NotifyCollectionChangedAction.Replace:
// Replace is remove old item and insert new item.
{
var oldValue = e.OldItem;
var oldId = identitySelector(oldValue);
if (viewMap.Remove(oldId, out var oldView))
{
var oldKey = (oldView, oldId);
if (list.TryGetValue(oldKey, out var v))
{
var oldIndex = list.IndexOfKey(oldKey);
list.RemoveAt(oldIndex);
filter.InvokeOnRemove(oldValue, oldView, NotifyCollectionChangedEventArgs<T>.Remove(v.Value, oldIndex));
}
}
var value = e.NewItem;
var view = transform(value);
var id = identitySelector(value);
list.Add((view, id), (value, view));
viewMap.Add(id, view);
var index = list.IndexOfKey((view, id));
filter.InvokeOnAdd(value, view, NotifyCollectionChangedEventArgs<T>.Add(value, index));
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 list)
{
filter.InvokeOnRemove(item.Value, e);
}
}
list.Clear();
viewMap.Clear();
break;
default:
break;
}
RoutingCollectionChanged?.Invoke(e);
CollectionStateChanged?.Invoke(e.Action);
}
}
sealed class Comparer : IComparer<(TView view, TKey id)>
{
readonly IComparer<TView> comparer;
public Comparer(IComparer<TView> comparer)
{
this.comparer = comparer;
}
public int Compare((TView view, TKey id) x, (TView view, TKey id) y)
{
var compare = comparer.Compare(x.view, y.view);
if (compare == 0)
{
compare = Comparer<TKey>.Default.Compare(x.id, y.id);
}
return compare;
}
}
}
}

View File

@ -1,61 +1,9 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
namespace ObservableCollections
{
// default(SortOperation<T>) == IsNull
public readonly struct SortOperation<T>
{
public readonly int Index;
public readonly int Count;
public readonly IComparer<T>? Comparer;
public bool IsReverse => Comparer == ReverseSentinel.Instance;
public bool IsClear => Comparer == null;
public bool IsSort => !IsClear && !IsReverse;
public SortOperation(int index, int count, IComparer<T>? comparer)
{
Index = index;
Count = count;
Comparer = comparer ?? NullComparerSentinel.Instance;
}
public (int Index, int Count, IComparer<T>? Comparer) AsTuple()
{
return (Index, Count, Comparer);
}
public static SortOperation<T> CreateReverse(int index, int count)
{
return new SortOperation<T>(index, count, ReverseSentinel.Instance);
}
sealed class ReverseSentinel : IComparer<T>
{
public static IComparer<T> Instance = new ReverseSentinel();
public int Compare(T? x, T? y)
{
throw new NotImplementedException();
}
}
sealed class NullComparerSentinel : IComparer<T>
{
public static IComparer<T> Instance = new NullComparerSentinel();
public int Compare(T? x, T? y)
{
return Comparer<T>.Default.Compare(x!, y!);
}
}
}
/// <summary>
/// Contract:
/// IsSingleItem ? (NewItem, OldItem) : (NewItems, OldItems)
@ -68,7 +16,7 @@ namespace ObservableCollections
/// Action.Move
/// NewStartingIndex, OldStartingIndex
/// Action.Reset
/// SortOperation(IsClear, IsReverse, Comparer)
/// -
/// </summary>
[StructLayout(LayoutKind.Auto)]
public readonly ref struct NotifyCollectionChangedEventArgs<T>
@ -81,9 +29,8 @@ namespace ObservableCollections
public readonly ReadOnlySpan<T> OldItems;
public readonly int NewStartingIndex;
public readonly int OldStartingIndex;
public readonly SortOperation<T> SortOperation;
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, SortOperation<T> sortOperation = default)
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;
@ -93,7 +40,48 @@ namespace ObservableCollections
OldItems = oldItems;
NewStartingIndex = newStartingIndex;
OldStartingIndex = oldStartingIndex;
SortOperation = sortOperation;
}
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)
@ -116,14 +104,14 @@ namespace ObservableCollections
return new NotifyCollectionChangedEventArgs<T>(NotifyCollectionChangedAction.Remove, false, oldItems: oldItems, oldStartingIndex: oldStartingIndex);
}
public static NotifyCollectionChangedEventArgs<T> Replace(T newItem, T oldItem, int newStartingIndex, int oldStartingIndex)
public static NotifyCollectionChangedEventArgs<T> Replace(T newItem, T oldItem, int startingIndex)
{
return new NotifyCollectionChangedEventArgs<T>(NotifyCollectionChangedAction.Replace, true, newItem: newItem, oldItem: oldItem, newStartingIndex: newStartingIndex, oldStartingIndex: oldStartingIndex);
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 newStartingIndex, int oldStartingIndex)
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: newStartingIndex, oldStartingIndex: oldStartingIndex);
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)
@ -135,15 +123,5 @@ namespace ObservableCollections
{
return new NotifyCollectionChangedEventArgs<T>(NotifyCollectionChangedAction.Reset, true);
}
public static NotifyCollectionChangedEventArgs<T> Reverse(int index, int count)
{
return new NotifyCollectionChangedEventArgs<T>(NotifyCollectionChangedAction.Reset, true, sortOperation: SortOperation<T>.CreateReverse(index, count));
}
public static NotifyCollectionChangedEventArgs<T> Sort(int index, int count, IComparer<T>? comparer)
{
return new NotifyCollectionChangedEventArgs<T>(NotifyCollectionChangedAction.Reset, true, sortOperation: new SortOperation<T>(index, count, comparer));
}
}
}

View File

@ -1,35 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1;net6.0;net8.0</TargetFrameworks>
<Nullable>enable</Nullable>
<LangVersion>12.0</LangVersion>
<ImplicitUsings>disable</ImplicitUsings>
<PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1;net6.0;net8.0</TargetFrameworks>
<Nullable>enable</Nullable>
<LangVersion>12.0</LangVersion>
<ImplicitUsings>disable</ImplicitUsings>
<!-- NuGet Packaging -->
<PackageTags>collection</PackageTags>
<Description>High performance observable collections and synchronized views, for WPF, Blazor, Unity.</Description>
<SignAssembly>true</SignAssembly>
<IsPackable>true</IsPackable>
</PropertyGroup>
<!-- NuGet Packaging -->
<PackageTags>collection</PackageTags>
<Description>High performance observable collections and synchronized views, for WPF, Blazor, Unity.</Description>
<SignAssembly>true</SignAssembly>
<IsPackable>true</IsPackable>
</PropertyGroup>
<ItemGroup Condition="$(TargetFramework) == 'netstandard2.0'">
<PackageReference Include="System.Memory" Version="4.5.4" />
<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="6.0.0" />
</ItemGroup>
<ItemGroup Condition="$(TargetFramework) == 'netstandard2.1'">
<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="6.0.0" />
</ItemGroup>
<ItemGroup Condition="$(TargetFramework) == 'netstandard2.0'">
<PackageReference Include="System.Memory" Version="4.5.4" />
</ItemGroup>
<ItemGroup>
<ItemGroup>
<None Include="../../Icon.png" Pack="true" PackagePath="/" />
<EmbeddedResource Include="..\..\LICENSE" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="PolySharp" Version="1.14.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</ItemGroup>
</Project>

View File

@ -7,9 +7,9 @@ using System.Linq;
namespace ObservableCollections
{
public partial class ObservableDictionary<TKey, TValue>
public sealed partial class ObservableDictionary<TKey, TValue>
{
public ISynchronizedView<KeyValuePair<TKey, TValue>, TView> CreateView<TView>(Func<KeyValuePair<TKey, TValue>, TView> transform)
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);
@ -21,7 +21,6 @@ namespace ObservableCollections
readonly Func<KeyValuePair<TKey, TValue>, TView> selector;
ISynchronizedViewFilter<KeyValuePair<TKey, TValue>, TView> filter;
readonly Dictionary<TKey, (TValue, TView)> dict;
int filteredCount;
public View(ObservableDictionary<TKey, TValue> source, Func<KeyValuePair<TKey, TValue>, TView> selector)
{
@ -32,33 +31,20 @@ namespace ObservableCollections
lock (source.SyncRoot)
{
this.dict = source.dictionary.ToDictionary(x => x.Key, x => (x.Value, selector(x)));
this.filteredCount = dict.Count;
this.source.CollectionChanged += SourceCollectionChanged;
}
}
public object SyncRoot { get; }
public event NotifyViewChangedEventHandler<KeyValuePair<TKey, TValue>, TView>? ViewChanged;
public event Action<RejectedViewChangedAction, int, int>? RejectedViewChanged;
public event NotifyCollectionChangedEventHandler<KeyValuePair<TKey, TValue>>? RoutingCollectionChanged;
public event Action<NotifyCollectionChangedAction>? CollectionStateChanged;
public ISynchronizedViewFilter<KeyValuePair<TKey, TValue>, TView> Filter
public ISynchronizedViewFilter<KeyValuePair<TKey, TValue>, TView> CurrentFilter
{
get { lock (SyncRoot) return filter; }
}
public int Count
{
get
{
lock (SyncRoot)
{
return filteredCount;
}
}
}
public int UnfilteredCount
{
get
{
@ -74,106 +60,70 @@ namespace ObservableCollections
this.source.CollectionChanged -= SourceCollectionChanged;
}
public void AttachFilter(ISynchronizedViewFilter<KeyValuePair<TKey, TValue>, TView> filter)
public void AttachFilter(ISynchronizedViewFilter<KeyValuePair<TKey, TValue>, TView> filter, bool invokeAddEventForCurrentElements = false)
{
if (filter.IsNullFilter())
{
ResetFilter();
return;
}
lock (SyncRoot)
{
this.filter = filter;
this.filteredCount = 0;
foreach (var v in dict)
{
var value = new KeyValuePair<TKey, TValue>(v.Key, v.Value.Item1);
if (filter.IsMatch(value, v.Value.Item2))
var view = v.Value.Item2;
if (invokeAddEventForCurrentElements)
{
filteredCount++;
filter.InvokeOnAdd(value, view, NotifyCollectionChangedEventArgs<KeyValuePair<TKey, TValue>>.Add(value, -1));
}
else
{
filter.InvokeOnAttach(value, view);
}
}
ViewChanged?.Invoke(new SynchronizedViewChangedEventArgs<KeyValuePair<TKey, TValue>, TView>(NotifyCollectionChangedAction.Reset, true));
}
}
public void ResetFilter()
public void ResetFilter(Action<KeyValuePair<TKey, TValue>, TView>? resetAction)
{
lock (SyncRoot)
{
this.filter = SynchronizedViewFilter<KeyValuePair<TKey, TValue>, TView>.Null;
this.filteredCount = dict.Count;
ViewChanged?.Invoke(new SynchronizedViewChangedEventArgs<KeyValuePair<TKey, TValue>, TView>(NotifyCollectionChangedAction.Reset, true));
if (resetAction != null)
{
foreach (var v in dict)
{
resetAction(new KeyValuePair<TKey, TValue>(v.Key, v.Value.Item1), v.Value.Item2);
}
}
}
}
public ISynchronizedViewList<TView> ToViewList()
public INotifyCollectionChangedSynchronizedView<TView> ToNotifyCollectionChanged()
{
return new FiltableSynchronizedViewList<KeyValuePair<TKey, TValue>, TView>(this, isSupportRangeFeature: true);
lock (SyncRoot)
{
return new NotifyCollectionChangedSynchronizedView<KeyValuePair<TKey, TValue>, TView>(this);
}
}
public NotifyCollectionChangedSynchronizedViewList<TView> ToNotifyCollectionChanged()
{
return new FiltableSynchronizedViewList<KeyValuePair<TKey, TValue>, TView>(this, isSupportRangeFeature: false);
}
public NotifyCollectionChangedSynchronizedViewList<TView> ToNotifyCollectionChanged(ICollectionEventDispatcher? collectionEventDispatcher)
{
return new FiltableSynchronizedViewList<KeyValuePair<TKey, TValue>, TView>(this, isSupportRangeFeature: false, collectionEventDispatcher);
}
public IEnumerator<TView> GetEnumerator()
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))
if (filter.IsMatch(v.Item1, v.Item2))
{
yield return v.Item2;
}
}
}
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public IEnumerable<(KeyValuePair<TKey, TValue> Value, TView View)> Filtered
{
get
{
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))
{
yield return v;
}
}
}
}
}
public IEnumerable<(KeyValuePair<TKey, TValue> Value, TView View)> Unfiltered
{
get
{
lock (SyncRoot)
{
foreach (var item in dict)
{
var v = (new KeyValuePair<TKey, TValue>(item.Key, item.Value.Item1), item.Value.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.
@ -182,40 +132,51 @@ namespace ObservableCollections
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
{
var v = selector(e.NewItem);
dict.Add(e.NewItem.Key, (e.NewItem.Value, v));
this.InvokeOnAdd(ref filteredCount, ViewChanged, RejectedViewChanged, e.NewItem, v, -1);
}
{
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))
{
if (dict.Remove(e.OldItem.Key, out var v))
{
this.InvokeOnRemove(ref filteredCount, ViewChanged, RejectedViewChanged, e.OldItem, v.Item2, -1);
}
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))
{
var v = selector(e.NewItem);
dict.Remove(e.OldItem.Key, out var ov);
dict[e.NewItem.Key] = (e.NewItem.Value, v);
this.InvokeOnReplace(ref filteredCount, ViewChanged, e.NewItem, v, e.OldItem, ov.Item2, -1);
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())
{
dict.Clear();
this.InvokeOnReset(ref filteredCount, ViewChanged);
foreach (var item in dict)
{
filter.InvokeOnRemove((new KeyValuePair<TKey, TValue>(item.Key, item.Value.Item1), item.Value.Item2), e);
}
}
dict.Clear();
}
break;
case NotifyCollectionChangedAction.Move: // ObservableDictionary have no Move operation.
default:
break;
}
RoutingCollectionChanged?.Invoke(e);
CollectionStateChanged?.Invoke(e.Action);
}
}

View File

@ -3,11 +3,11 @@ using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
namespace ObservableCollections
{
public partial class ObservableDictionary<TKey, TValue> : IDictionary<TKey, TValue>, IReadOnlyObservableDictionary<TKey, TValue>
public sealed partial class ObservableDictionary<TKey, TValue>
: IDictionary<TKey, TValue>, IReadOnlyDictionary<TKey, TValue>, IObservableCollection<KeyValuePair<TKey, TValue>>
where TKey : notnull
{
readonly Dictionary<TKey, TValue> dictionary;
@ -18,27 +18,12 @@ namespace ObservableCollections
this.dictionary = new Dictionary<TKey, TValue>();
}
public ObservableDictionary(IEqualityComparer<TKey>? comparer)
{
this.dictionary = new Dictionary<TKey, TValue>(comparer: comparer);
}
public ObservableDictionary(int capacity, IEqualityComparer<TKey>? comparer)
{
this.dictionary = new Dictionary<TKey, TValue>(capacity, comparer: comparer);
}
public ObservableDictionary(IEnumerable<KeyValuePair<TKey, TValue>> collection)
: this(collection, null)
{
}
public ObservableDictionary(IEnumerable<KeyValuePair<TKey, TValue>> collection, IEqualityComparer<TKey>? comparer)
{
#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER
this.dictionary = new Dictionary<TKey, TValue>(collection: collection, comparer: comparer);
#if NET6_0_OR_GREATER
this.dictionary = new Dictionary<TKey, TValue>(collection);
#else
this.dictionary = new Dictionary<TKey, TValue>(comparer: comparer);
this.dictionary = new Dictionary<TKey, TValue>();
foreach (var item in collection)
{
dictionary.Add(item.Key, item.Value);
@ -66,8 +51,8 @@ namespace ObservableCollections
dictionary[key] = value;
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<KeyValuePair<TKey, TValue>>.Replace(
new KeyValuePair<TKey, TValue>(key, value),
new KeyValuePair<TKey, TValue>(key, oldValue!),
-1, -1));
new KeyValuePair<TKey, TValue>(key, oldValue),
-1));
}
else
{
@ -239,16 +224,5 @@ namespace ObservableCollections
{
return GetEnumerator();
}
public IEqualityComparer<TKey> Comparer
{
get
{
lock (SyncRoot)
{
return dictionary.Comparer;
}
}
}
}
}

View File

@ -5,7 +5,7 @@ using System.Collections.Generic;
namespace ObservableCollections
{
public partial class ObservableFixedSizeRingBuffer<T> : IList<T>, IReadOnlyList<T>, IObservableCollection<T>
public sealed partial class ObservableFixedSizeRingBuffer<T> : IList<T>, IReadOnlyList<T>, IObservableCollection<T>
{
readonly RingBuffer<T> buffer;
readonly int capacity;
@ -51,7 +51,7 @@ namespace ObservableCollections
{
var oldValue = buffer[index];
buffer[index] = value;
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Replace(value, oldValue, index, index));
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Replace(value, oldValue, index));
}
}
}
@ -320,9 +320,9 @@ namespace ObservableCollections
return GetEnumerator();
}
public ISynchronizedView<T, TView> CreateView<TView>(Func<T, TView> transform)
public ISynchronizedView<T, TView> CreateView<TView>(Func<T, TView> transform, bool reverse = false)
{
return new ObservableRingBuffer<T>.View<TView>(this, transform);
return new ObservableRingBuffer<T>.View<TView>(this, transform, reverse);
}
}
}

View File

@ -4,20 +4,19 @@ using System.Collections.Generic;
using System.Collections.Specialized;
using System;
using System.Linq;
using System.Threading.Tasks;
namespace ObservableCollections
{
public partial class ObservableHashSet<T> : IReadOnlyCollection<T>, IObservableCollection<T>
public sealed partial class ObservableHashSet<T> : IReadOnlyCollection<T>, IObservableCollection<T>
{
public ISynchronizedView<T, TView> CreateView<TView>(Func<T, TView> transform)
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>
{
public ISynchronizedViewFilter<T, TView> Filter
public ISynchronizedViewFilter<T, TView> CurrentFilter
{
get { lock (SyncRoot) return filter; }
}
@ -25,12 +24,10 @@ namespace ObservableCollections
readonly ObservableHashSet<T> source;
readonly Func<T, TView> selector;
readonly Dictionary<T, (T, TView)> dict;
int filteredCount;
ISynchronizedViewFilter<T, TView> filter;
public event NotifyViewChangedEventHandler<T, TView>? ViewChanged;
public event Action<RejectedViewChangedAction, int, int>? RejectedViewChanged;
public event NotifyCollectionChangedEventHandler<T>? RoutingCollectionChanged;
public event Action<NotifyCollectionChangedAction>? CollectionStateChanged;
public object SyncRoot { get; }
@ -44,23 +41,11 @@ namespace ObservableCollections
lock (source.SyncRoot)
{
this.dict = source.set.ToDictionary(x => x, x => (x, selector(x)));
this.filteredCount = dict.Count;
this.source.CollectionChanged += SourceCollectionChanged;
}
}
public int Count
{
get
{
lock (SyncRoot)
{
return filteredCount;
}
}
}
public int UnfilteredCount
{
get
{
@ -71,94 +56,55 @@ namespace ObservableCollections
}
}
public void AttachFilter(ISynchronizedViewFilter<T, TView> filter)
public void AttachFilter(ISynchronizedViewFilter<T, TView> filter, bool invokeAddEventForCurrentElements = false)
{
if (filter.IsNullFilter())
{
ResetFilter();
return;
}
lock (SyncRoot)
{
this.filter = filter;
this.filteredCount = 0;
foreach (var (_, (value, view)) in dict)
{
if (filter.IsMatch(value, view))
if (invokeAddEventForCurrentElements)
{
filteredCount++;
filter.InvokeOnAdd((value, view), NotifyCollectionChangedEventArgs<T>.Add(value, -1));
}
else
{
filter.InvokeOnAttach(value, view);
}
}
ViewChanged?.Invoke(new SynchronizedViewChangedEventArgs<T, TView>(NotifyCollectionChangedAction.Reset, true));
}
}
public void ResetFilter()
public void ResetFilter(Action<T, TView>? resetAction)
{
lock (SyncRoot)
{
this.filter = SynchronizedViewFilter<T, TView>.Null;
this.filteredCount = dict.Count;
ViewChanged?.Invoke(new SynchronizedViewChangedEventArgs<T, TView>(NotifyCollectionChangedAction.Reset, true));
if (resetAction != null)
{
foreach (var (_, (value, view)) in dict)
{
resetAction(value, view);
}
}
}
}
public ISynchronizedViewList<TView> ToViewList()
public INotifyCollectionChangedSynchronizedView<TView> ToNotifyCollectionChanged()
{
return new FiltableSynchronizedViewList<T, TView>(this, isSupportRangeFeature: true);
lock (SyncRoot)
{
return new NotifyCollectionChangedSynchronizedView<T, TView>(this);
}
}
public NotifyCollectionChangedSynchronizedViewList<TView> ToNotifyCollectionChanged()
{
return new FiltableSynchronizedViewList<T, TView>(this, isSupportRangeFeature: false);
}
public NotifyCollectionChangedSynchronizedViewList<TView> ToNotifyCollectionChanged(ICollectionEventDispatcher? collectionEventDispatcher)
{
return new FiltableSynchronizedViewList<T, TView>(this, isSupportRangeFeature: false, collectionEventDispatcher);
}
public IEnumerator<TView> GetEnumerator()
public IEnumerator<(T, TView)> GetEnumerator()
{
lock (SyncRoot)
{
foreach (var item in dict)
{
if (filter.IsMatch(item.Value))
{
yield return item.Value.Item2;
}
}
}
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public IEnumerable<(T Value, TView View)> Filtered
{
get
{
lock (SyncRoot)
{
foreach (var item in dict)
{
if (filter.IsMatch(item.Value))
{
yield return item.Value;
}
}
}
}
}
public IEnumerable<(T Value, TView View)> Unfiltered
{
get
{
lock (SyncRoot)
{
foreach (var item in dict)
if (filter.IsMatch(item.Value.Item1, item.Value.Item2))
{
yield return item.Value;
}
@ -166,6 +112,8 @@ namespace ObservableCollections
}
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public void Dispose()
{
this.source.CollectionChanged -= SourceCollectionChanged;
@ -182,16 +130,15 @@ namespace ObservableCollections
{
var v = (e.NewItem, selector(e.NewItem));
dict.Add(e.NewItem, v);
this.InvokeOnAdd(ref filteredCount, ViewChanged, RejectedViewChanged, v, -1);
filter.InvokeOnAdd(v, e);
}
else
{
var i = e.NewStartingIndex;
foreach (var item in e.NewItems)
{
var v = (item, selector(item));
dict.Add(item, v);
this.InvokeOnAdd(ref filteredCount, ViewChanged, RejectedViewChanged, v, i++);
filter.InvokeOnAdd(v, e);
}
}
break;
@ -200,7 +147,7 @@ namespace ObservableCollections
{
if (dict.Remove(e.OldItem, out var value))
{
this.InvokeOnRemove(ref filteredCount, ViewChanged, RejectedViewChanged, value, -1);
filter.InvokeOnRemove(value.Item1, value.Item2, e);
}
}
else
@ -209,14 +156,20 @@ namespace ObservableCollections
{
if (dict.Remove(item, out var value))
{
this.InvokeOnRemove(ref filteredCount, ViewChanged, RejectedViewChanged, value, -1);
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();
this.InvokeOnReset(ref filteredCount, ViewChanged);
break;
case NotifyCollectionChangedAction.Replace:
case NotifyCollectionChangedAction.Move:
@ -224,6 +177,7 @@ namespace ObservableCollections
break;
}
RoutingCollectionChanged?.Invoke(e);
CollectionStateChanged?.Invoke(e.Action);
}
}

View File

@ -8,7 +8,7 @@ using System.Linq;
namespace ObservableCollections
{
// can not implements ISet<T> because set operation can not get added/removed values.
public partial class ObservableHashSet<T> : IReadOnlySet<T>, IReadOnlyCollection<T>, IObservableCollection<T>
public sealed partial class ObservableHashSet<T> : IReadOnlySet<T>, IReadOnlyCollection<T>, IObservableCollection<T>
where T : notnull
{
readonly HashSet<T> set;
@ -19,33 +19,18 @@ namespace ObservableCollections
this.set = new HashSet<T>();
}
public ObservableHashSet(IEqualityComparer<T>? comparer)
{
this.set = new HashSet<T>(comparer: comparer);
}
#if NETSTANDARD2_1_OR_GREATER || NET5_0_OR_GREATER
public ObservableHashSet(int capacity)
{
this.set = new HashSet<T>(capacity: capacity);
}
public ObservableHashSet(int capacity, IEqualityComparer<T>? comparer)
{
this.set = new HashSet<T>(capacity: capacity, comparer: comparer);
this.set = new HashSet<T>(capacity);
}
#endif
public ObservableHashSet(IEnumerable<T> collection)
{
this.set = new HashSet<T>(collection: collection);
}
public ObservableHashSet(IEnumerable<T> collection, IEqualityComparer<T>? comparer)
{
this.set = new HashSet<T>(collection: collection, comparer: comparer);
this.set = new HashSet<T>(collection);
}
public event NotifyCollectionChangedEventHandler<T>? CollectionChanged;
@ -200,10 +185,7 @@ namespace ObservableCollections
public bool TryGetValue(T equalValue, [MaybeNullWhen(false)] out T actualValue)
{
lock (SyncRoot)
{
return set.TryGetValue(equalValue, out actualValue);
}
return set.TryGetValue(equalValue, out actualValue);
}
#endif
@ -279,16 +261,5 @@ namespace ObservableCollections
{
return GetEnumerator();
}
public IEqualityComparer<T> Comparer
{
get
{
lock (SyncRoot)
{
return set.Comparer;
}
}
}
}
}

View File

@ -1,221 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Text;
namespace ObservableCollections;
public partial class ObservableList<T> : IList<T>, IReadOnlyObservableList<T>
{
// override extension methods(IObservableCollection.cs ObservableCollectionExtensions)
//public ISynchronizedViewList<T> ToViewList<T>(this IObservableCollection<T> collection)
//{
// return ToViewList(collection, static x => x);
//}
//public static ISynchronizedViewList<TView> ToViewList<T, TView>(this IObservableCollection<T> collection, Func<T, TView> transform)
//{
// return new NonFilteredSynchronizedViewList<T, TView>(collection.CreateView(transform));
//}
/// <summary>
/// Create faster, compact INotifyCollectionChanged view, however it does not support ***Range.
/// </summary>
public NotifyCollectionChangedSynchronizedViewList<T> ToNotifyCollectionChangedSlim()
{
return new ObservableListSynchronizedViewList<T>(this, null);
}
/// <summary>
/// Create faster, compact INotifyCollectionChanged view, however it does not support ***Range.
/// </summary>
public NotifyCollectionChangedSynchronizedViewList<T> ToNotifyCollectionChangedSlim(ICollectionEventDispatcher? collectionEventDispatcher)
{
return new ObservableListSynchronizedViewList<T>(this, collectionEventDispatcher);
}
//public static INotifyCollectionChangedSynchronizedViewList<TView> ToNotifyCollectionChanged<T, TView>(this IObservableCollection<T> collection, Func<T, TView> transform)
//{
// return ToNotifyCollectionChanged(collection, transform, null!);
//}
//public static INotifyCollectionChangedSynchronizedViewList<TView> ToNotifyCollectionChanged<T, TView>(this IObservableCollection<T> collection, Func<T, TView> transform, ICollectionEventDispatcher? collectionEventDispatcher)
//{
// return new NonFilteredNotifyCollectionChangedSynchronizedViewList<T, TView>(collection.CreateView(transform), collectionEventDispatcher);
//}
}
internal sealed class ObservableListSynchronizedViewList<T> : NotifyCollectionChangedSynchronizedViewList<T>
{
static readonly PropertyChangedEventArgs CountPropertyChangedEventArgs = new("Count");
static readonly Action<NotifyCollectionChangedEventArgs> raiseChangedEventInvoke = RaiseChangedEvent;
readonly ObservableList<T> parent;
readonly ICollectionEventDispatcher eventDispatcher;
public override event NotifyCollectionChangedEventHandler? CollectionChanged;
public override event PropertyChangedEventHandler? PropertyChanged;
public ObservableListSynchronizedViewList(ObservableList<T> parent, ICollectionEventDispatcher? eventDispatcher)
{
this.parent = parent;
this.eventDispatcher = eventDispatcher ?? InlineCollectionEventDispatcher.Instance;
parent.CollectionChanged += Parent_CollectionChanged;
}
private void Parent_CollectionChanged(in NotifyCollectionChangedEventArgs<T> args)
{
if (CollectionChanged == null && PropertyChanged == null) return;
switch (args.Action)
{
case NotifyCollectionChangedAction.Add:
if (args.IsSingleItem)
{
eventDispatcher.Post(new CollectionEventDispatcherEventArgs(NotifyCollectionChangedAction.Add, args.NewItem, args.NewStartingIndex)
{
Collection = this,
Invoker = raiseChangedEventInvoke,
IsInvokeCollectionChanged = true,
IsInvokePropertyChanged = true
});
}
else
{
eventDispatcher.Post(new CollectionEventDispatcherEventArgs(NotifyCollectionChangedAction.Add, args.NewItems.ToArray(), args.NewStartingIndex)
{
Collection = this,
Invoker = raiseChangedEventInvoke,
IsInvokeCollectionChanged = true,
IsInvokePropertyChanged = true
});
}
break;
case NotifyCollectionChangedAction.Remove:
if (args.IsSingleItem)
{
eventDispatcher.Post(new CollectionEventDispatcherEventArgs(NotifyCollectionChangedAction.Remove, args.OldItem, args.OldStartingIndex)
{
Collection = this,
Invoker = raiseChangedEventInvoke,
IsInvokeCollectionChanged = true,
IsInvokePropertyChanged = true
});
}
else
{
eventDispatcher.Post(new CollectionEventDispatcherEventArgs(NotifyCollectionChangedAction.Remove, args.OldItems.ToArray(), args.OldStartingIndex)
{
Collection = this,
Invoker = raiseChangedEventInvoke,
IsInvokeCollectionChanged = true,
IsInvokePropertyChanged = true
});
}
break;
case NotifyCollectionChangedAction.Reset:
eventDispatcher.Post(new CollectionEventDispatcherEventArgs(NotifyCollectionChangedAction.Reset)
{
Collection = this,
Invoker = raiseChangedEventInvoke,
IsInvokeCollectionChanged = true,
IsInvokePropertyChanged = true
});
break;
case NotifyCollectionChangedAction.Replace:
eventDispatcher.Post(new CollectionEventDispatcherEventArgs(NotifyCollectionChangedAction.Replace, args.NewItem, args.OldItem, args.NewStartingIndex)
{
Collection = this,
Invoker = raiseChangedEventInvoke,
IsInvokeCollectionChanged = true,
IsInvokePropertyChanged = false
});
break;
case NotifyCollectionChangedAction.Move:
eventDispatcher.Post(new CollectionEventDispatcherEventArgs(NotifyCollectionChangedAction.Move, args.NewItem, args.NewStartingIndex, args.OldStartingIndex)
{
Collection = this,
Invoker = raiseChangedEventInvoke,
IsInvokeCollectionChanged = true,
IsInvokePropertyChanged = false
});
break;
}
}
static void RaiseChangedEvent(NotifyCollectionChangedEventArgs e)
{
var e2 = (CollectionEventDispatcherEventArgs)e;
var self = (ObservableListSynchronizedViewList<T>)e2.Collection;
if (e2.IsInvokeCollectionChanged)
{
self.CollectionChanged?.Invoke(self, e);
}
if (e2.IsInvokePropertyChanged)
{
self.PropertyChanged?.Invoke(self, CountPropertyChangedEventArgs);
}
}
public override T this[int index]
{
get
{
return parent[index];
}
set
{
parent[index] = value;
}
}
public override int Count => parent.Count;
public override IEnumerator<T> GetEnumerator()
{
return parent.GetEnumerator();
}
public override void Dispose()
{
parent.CollectionChanged -= Parent_CollectionChanged;
}
public override void Add(T item)
{
parent.Add(item);
}
public override void Insert(int index, T item)
{
parent.Insert(index, item);
}
public override bool Remove(T item)
{
return parent.Remove(item);
}
public override void RemoveAt(int index)
{
parent.RemoveAt(index);
}
public override void Clear()
{
parent.Clear();
}
public override bool Contains(T item)
{
return parent.Contains(item);
}
public override int IndexOf(T item)
{
return parent.IndexOf(item);
}
}

View File

@ -7,48 +7,16 @@ using System.Linq;
namespace ObservableCollections
{
public partial class ObservableList<T> : IList<T>, IReadOnlyObservableList<T>
public sealed partial class ObservableList<T> : IList<T>, IReadOnlyList<T>, IObservableCollection<T>
{
public ISynchronizedView<T, TView> CreateView<TView>(Func<T, TView> transform)
public ISynchronizedView<T, TView> CreateView<TView>(Func<T, TView> transform, bool reverse = false)
{
return new View<TView>(this, transform);
return new View<TView>(this, transform, reverse);
}
public IWritableSynchronizedView<T, TView> CreateWritableView<TView>(Func<T, TView> transform)
sealed class View<TView> : ISynchronizedView<T, TView>
{
return new View<TView>(this, transform);
}
public NotifyCollectionChangedSynchronizedViewList<T> ToWritableNotifyCollectionChanged()
{
return ToWritableNotifyCollectionChanged(null);
}
public NotifyCollectionChangedSynchronizedViewList<T> ToWritableNotifyCollectionChanged(ICollectionEventDispatcher? collectionEventDispatcher)
{
return ToWritableNotifyCollectionChanged(
static x => x,
static (T newView, T originalValue, ref bool setValue) =>
{
setValue = true;
return newView;
},
collectionEventDispatcher);
}
public NotifyCollectionChangedSynchronizedViewList<TView> ToWritableNotifyCollectionChanged<TView>(Func<T, TView> transform, WritableViewChangedEventHandler<T, TView>? converter)
{
return ToWritableNotifyCollectionChanged(transform, converter, null!);
}
public NotifyCollectionChangedSynchronizedViewList<TView> ToWritableNotifyCollectionChanged<TView>(Func<T, TView> transform, WritableViewChangedEventHandler<T, TView>? converter, ICollectionEventDispatcher? collectionEventDispatcher)
{
return new NonFilteredSynchronizedViewList<T, TView>(CreateView(transform), isSupportRangeFeature: false, collectionEventDispatcher, converter);
}
internal sealed class View<TView> : ISynchronizedView<T, TView>, IWritableSynchronizedView<T, TView>
{
public ISynchronizedViewFilter<T, TView> Filter
public ISynchronizedViewFilter<T, TView> CurrentFilter
{
get
{
@ -58,43 +26,31 @@ namespace ObservableCollections
readonly ObservableList<T> source;
readonly Func<T, TView> selector;
internal readonly List<(T, TView)> list; // unsafe, be careful to use
int filteredCount;
readonly bool reverse;
readonly List<(T, TView)> list;
ISynchronizedViewFilter<T, TView> filter;
public event NotifyViewChangedEventHandler<T, TView>? ViewChanged;
public event Action<RejectedViewChangedAction, int, int>? RejectedViewChanged;
public event NotifyCollectionChangedEventHandler<T>? RoutingCollectionChanged;
public event Action<NotifyCollectionChangedAction>? CollectionStateChanged;
public object SyncRoot { get; }
public View(ObservableList<T> source, Func<T, TView> selector)
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.filteredCount = list.Count;
this.source.CollectionChanged += SourceCollectionChanged;
}
}
public int Count
{
get
{
lock (SyncRoot)
{
return filteredCount;
}
}
}
public int UnfilteredCount
{
get
{
@ -105,80 +61,69 @@ namespace ObservableCollections
}
}
public void AttachFilter(ISynchronizedViewFilter<T, TView> filter)
public void AttachFilter(ISynchronizedViewFilter<T, TView> filter, bool invokeAddEventForCurrentElements = false)
{
if (filter.IsNullFilter())
{
ResetFilter();
return;
}
lock (SyncRoot)
{
this.filter = filter;
this.filteredCount = 0;
for (var i = 0; i < list.Count; i++)
{
if (filter.IsMatch(list[i]))
var (value, view) = list[i];
if (invokeAddEventForCurrentElements)
{
filteredCount++;
var eventArgs = NotifyCollectionChangedEventArgs<T>.Add(value, i);
filter.InvokeOnAdd(value, view, eventArgs);
}
else
{
filter.InvokeOnAttach(value, view);
}
}
ViewChanged?.Invoke(new SynchronizedViewChangedEventArgs<T, TView>(NotifyCollectionChangedAction.Reset, true));
}
}
public void ResetFilter()
public void ResetFilter(Action<T, TView>? resetAction)
{
lock (SyncRoot)
{
this.filter = SynchronizedViewFilter<T, TView>.Null;
this.filteredCount = list.Count;
ViewChanged?.Invoke(new SynchronizedViewChangedEventArgs<T, TView>(NotifyCollectionChangedAction.Reset, true));
}
}
public ISynchronizedViewList<TView> ToViewList()
{
return new FiltableSynchronizedViewList<T, TView>(this, isSupportRangeFeature: true);
}
public NotifyCollectionChangedSynchronizedViewList<TView> ToNotifyCollectionChanged()
{
return new FiltableSynchronizedViewList<T, TView>(this, isSupportRangeFeature: false);
}
public NotifyCollectionChangedSynchronizedViewList<TView> ToNotifyCollectionChanged(ICollectionEventDispatcher? collectionEventDispatcher)
{
return new FiltableSynchronizedViewList<T, TView>(this, isSupportRangeFeature: false, collectionEventDispatcher);
}
public IEnumerator<TView> GetEnumerator()
{
lock (SyncRoot)
{
foreach (var item in list)
if (resetAction != null)
{
if (filter.IsMatch(item))
foreach (var (item, view) in list)
{
yield return item.Item2;
resetAction(item, view);
}
}
}
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public IEnumerable<(T Value, TView View)> Filtered
public INotifyCollectionChangedSynchronizedView<TView> ToNotifyCollectionChanged()
{
get
lock (SyncRoot)
{
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))
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;
}
@ -187,19 +132,7 @@ namespace ObservableCollections
}
}
public IEnumerable<(T Value, TView View)> Unfiltered
{
get
{
lock (SyncRoot)
{
foreach (var item in list)
{
yield return item;
}
}
}
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public void Dispose()
{
@ -213,41 +146,47 @@ namespace ObservableCollections
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
// Add or Insert
if (e.IsSingleItem)
// Add
if (e.NewStartingIndex == list.Count)
{
var v = (e.NewItem, selector(e.NewItem));
list.Insert(e.NewStartingIndex, v);
this.InvokeOnAdd(ref filteredCount, ViewChanged, RejectedViewChanged, v, e.NewStartingIndex);
}
else
{
var items = e.NewItems;
var length = items.Length;
using var valueViews = new FixedArray<(T, TView)>(length);
using var views = new FixedArray<TView>(length);
using var matches = new FixedBoolArray(length < FixedBoolArray.StackallocSize ? stackalloc bool[length] : default, length);
var isMatchAll = true;
for (int i = 0; i < items.Length; i++)
if (e.IsSingleItem)
{
var item = items[i];
var view = selector(item);
views.Span[i] = view;
valueViews.Span[i] = (item, view);
var isMatch = matches.Span[i] = Filter.IsMatch(item, view);
if (isMatch)
var v = (e.NewItem, selector(e.NewItem));
list.Add(v);
filter.InvokeOnAdd(v, e);
}
else
{
foreach (var item in e.NewItems)
{
filteredCount++; // increment in this process
}
else
{
isMatchAll = false;
var v = (item, selector(item));
list.Add(v);
filter.InvokeOnAdd(v, e);
}
}
list.InsertRange(e.NewStartingIndex, valueViews.Span);
this.InvokeOnAddRange(ViewChanged, RejectedViewChanged, e.NewItems, views.Span, isMatchAll, matches.Span, e.NewStartingIndex);
}
// 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:
@ -255,45 +194,30 @@ namespace ObservableCollections
{
var v = list[e.OldStartingIndex];
list.RemoveAt(e.OldStartingIndex);
this.InvokeOnRemove(ref filteredCount, ViewChanged, RejectedViewChanged, v, e.OldStartingIndex);
filter.InvokeOnRemove(v.Item1, v.Item2, e);
}
else
{
var length = e.OldItems.Length;
using var values = new FixedArray<T>(length);
using var views = new FixedArray<TView>(length);
using var matches = new FixedBoolArray(length < FixedBoolArray.StackallocSize ? stackalloc bool[length] : default, length);
var isMatchAll = true;
var to = e.OldStartingIndex + length;
var j = 0;
for (int i = e.OldStartingIndex; i < to; i++)
var len = e.OldStartingIndex + e.OldItems.Length;
for (int i = e.OldStartingIndex; i < len; i++)
{
var item = list[i];
values.Span[j] = item.Item1;
views.Span[j] = item.Item2;
var isMatch = matches.Span[j] = Filter.IsMatch(item);
if (isMatch)
{
filteredCount--; // decrement in this process
}
else
{
isMatchAll = false;
}
j++;
var v = list[i];
filter.InvokeOnRemove(v.Item1, v.Item2, e);
}
list.RemoveRange(e.OldStartingIndex, e.OldItems.Length);
this.InvokeOnRemoveRange(ViewChanged, RejectedViewChanged, values.Span, views.Span, isMatchAll, matches.Span, e.OldStartingIndex);
}
break;
case NotifyCollectionChangedAction.Replace:
// ObservableList does not support replace range
{
var v = (e.NewItem, selector(e.NewItem));
var ov = (e.OldItem, list[e.OldStartingIndex].Item2);
var oldItem = list[e.NewStartingIndex];
list[e.NewStartingIndex] = v;
this.InvokeOnReplace(ref filteredCount, ViewChanged, v, ov, e.NewStartingIndex);
filter.InvokeOnRemove(oldItem, e);
filter.InvokeOnAdd(v, e);
break;
}
case NotifyCollectionChangedAction.Move:
@ -302,157 +226,21 @@ namespace ObservableCollections
list.RemoveAt(e.OldStartingIndex);
list.Insert(e.NewStartingIndex, removeItem);
this.InvokeOnMove(ref filteredCount, ViewChanged, RejectedViewChanged, removeItem, e.NewStartingIndex, e.OldStartingIndex);
filter.InvokeOnMove(removeItem, e);
}
break;
case NotifyCollectionChangedAction.Reset:
if (e.SortOperation.IsClear)
{
// None(Clear)
list.Clear();
this.InvokeOnReset(ref filteredCount, ViewChanged);
}
else if (e.SortOperation.IsReverse)
{
// Reverse
list.Reverse(e.SortOperation.Index, e.SortOperation.Count);
this.InvokeOnReverseOrSort(ViewChanged, e.SortOperation);
}
else
{
// Sort
list.Sort(e.SortOperation.Index, e.SortOperation.Count, new IgnoreViewComparer(e.SortOperation.Comparer ?? Comparer<T>.Default));
this.InvokeOnReverseOrSort(ViewChanged, e.SortOperation);
}
list.Clear();
filter.InvokeOnClear(e);
break;
default:
break;
}
RoutingCollectionChanged?.Invoke(e);
CollectionStateChanged?.Invoke(e.Action);
}
}
#region Writable
public (T Value, TView View) GetAt(int index)
{
lock (SyncRoot)
{
return list[index];
}
}
public void SetViewAt(int index, TView view)
{
lock (SyncRoot)
{
var v = list[index];
list[index] = (v.Item1, view);
}
}
public void SetToSourceCollection(int index, T value)
{
lock (SyncRoot)
{
source[index] = value;
}
}
public void AddToSourceCollection(T value)
{
lock (SyncRoot)
{
source.Add(value);
}
}
public void InsertIntoSourceCollection(int index, T value)
{
lock (SyncRoot)
{
source.Insert(index, value);
}
}
public bool RemoveFromSourceCollection(T value)
{
lock (SyncRoot)
{
return source.Remove(value);
}
}
public void RemoveAtSourceCollection(int index)
{
lock (SyncRoot)
{
source.RemoveAt(index);
}
}
public void ClearSourceCollection()
{
lock (SyncRoot)
{
source.Clear();
}
}
public IWritableSynchronizedViewList<TView> ToWritableViewList(WritableViewChangedEventHandler<T, TView> converter)
{
return new FiltableSynchronizedViewList<T, TView>(this, isSupportRangeFeature: true, converter: converter);
}
public NotifyCollectionChangedSynchronizedViewList<TView> ToWritableNotifyCollectionChanged()
{
return new FiltableSynchronizedViewList<T, TView>(this,
isSupportRangeFeature: false,
converter: static (TView newView, T originalValue, ref bool setValue) =>
{
setValue = true;
return originalValue;
});
}
public NotifyCollectionChangedSynchronizedViewList<TView> ToWritableNotifyCollectionChanged(WritableViewChangedEventHandler<T, TView> converter)
{
return new FiltableSynchronizedViewList<T, TView>(this, isSupportRangeFeature: false, converter: converter);
}
public NotifyCollectionChangedSynchronizedViewList<TView> ToWritableNotifyCollectionChanged(ICollectionEventDispatcher? collectionEventDispatcher)
{
return new FiltableSynchronizedViewList<T, TView>(this,
isSupportRangeFeature: false,
eventDispatcher: collectionEventDispatcher,
converter: static (TView newView, T originalValue, ref bool setValue) =>
{
setValue = true;
return originalValue;
});
}
public NotifyCollectionChangedSynchronizedViewList<TView> ToWritableNotifyCollectionChanged(WritableViewChangedEventHandler<T, TView> converter, ICollectionEventDispatcher? collectionEventDispatcher)
{
return new FiltableSynchronizedViewList<T, TView>(this, isSupportRangeFeature: false, collectionEventDispatcher, converter);
}
#endregion
sealed class IgnoreViewComparer : IComparer<(T, TView)>
{
readonly IComparer<T> comparer;
public IgnoreViewComparer(IComparer<T> comparer)
{
this.comparer = comparer;
}
public int Compare((T, TView) x, (T, TView) y)
{
return comparer.Compare(x.Item1, y.Item1);
}
}
}
}
}
}

View File

@ -7,10 +7,10 @@ using System.Runtime.InteropServices;
namespace ObservableCollections
{
public partial class ObservableList<T> : IList<T>, IReadOnlyObservableList<T>
public sealed partial class ObservableList<T> : IList<T>, IReadOnlyList<T>, IObservableCollection<T>
{
readonly List<T> list;
public object SyncRoot { get; } = new();
public object SyncRoot { get; } = new object();
public ObservableList()
{
@ -42,7 +42,7 @@ namespace ObservableCollections
{
var oldValue = list[index];
list[index] = value;
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Replace(value, oldValue, index, index));
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Replace(value, oldValue, index));
}
}
}
@ -100,16 +100,11 @@ namespace ObservableCollections
{
lock (SyncRoot)
{
var index = list.Count; // starting index
#if NET8_0_OR_GREATER
list.AddRange(items);
#else
var index = list.Count;
foreach (var item in items)
{
list.Add(item);
}
#endif
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Add(items, index));
}
@ -209,16 +204,11 @@ namespace ObservableCollections
{
lock (SyncRoot)
{
#if NET8_0_OR_GREATER
list.InsertRange(index, items);
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Add(items, index));
#else
using (var xs = new CloneCollection<T>(items))
{
list.InsertRange(index, xs.AsEnumerable());
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Add(xs.Span, index));
}
#endif
}
}
@ -255,9 +245,12 @@ namespace ObservableCollections
{
lock (SyncRoot)
{
#pragma warning disable CS0436
#if NET5_0_OR_GREATER
var range = CollectionsMarshal.AsSpan(list).Slice(index, count);
#pragma warning restore CS0436
#else
var range = list.GetRange(index, count);
#endif
// require copy before remove
using (var xs = new CloneCollection<T>(range))
{
@ -277,50 +270,5 @@ namespace ObservableCollections
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Move(removedItem, newIndex, oldIndex));
}
}
public void Sort()
{
lock (SyncRoot)
{
list.Sort();
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Sort(0, list.Count, null));
}
}
public void Sort(IComparer<T> comparer)
{
lock (SyncRoot)
{
list.Sort(comparer);
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Sort(0, list.Count, comparer));
}
}
public void Sort(int index, int count, IComparer<T> comparer)
{
lock (SyncRoot)
{
list.Sort(index, count, comparer);
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Sort(index, count, comparer));
}
}
public void Reverse()
{
lock (SyncRoot)
{
list.Reverse();
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Reverse(0, list.Count));
}
}
public void Reverse(int index, int count)
{
lock (SyncRoot)
{
list.Reverse(index, count);
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Reverse(index, count));
}
}
}
}
}

View File

@ -4,63 +4,50 @@ using System.Collections.Specialized;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace ObservableCollections
{
public partial class ObservableQueue<T> : IReadOnlyCollection<T>, IObservableCollection<T>
public sealed partial class ObservableQueue<T> : IReadOnlyCollection<T>, IObservableCollection<T>
{
public ISynchronizedView<T, TView> CreateView<TView>(Func<T, TView> transform)
public ISynchronizedView<T, TView> CreateView<TView>(Func<T, TView> transform, bool reverse = false)
{
return new View<TView>(this, transform);
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;
int filteredCount;
ISynchronizedViewFilter<T, TView> filter;
public event NotifyViewChangedEventHandler<T, TView>? ViewChanged;
public event Action<RejectedViewChangedAction, int, int>? RejectedViewChanged;
public event NotifyCollectionChangedEventHandler<T>? RoutingCollectionChanged;
public event Action<NotifyCollectionChangedAction>? CollectionStateChanged;
public object SyncRoot { get; }
public ISynchronizedViewFilter<T, TView> Filter
public ISynchronizedViewFilter<T, TView> CurrentFilter
{
get { lock (SyncRoot) return filter; }
}
public View(ObservableQueue<T> source, Func<T, TView> selector)
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.filteredCount = queue.Count;
this.source.CollectionChanged += SourceCollectionChanged;
}
}
public int Count
{
get
{
lock (SyncRoot)
{
return filteredCount;
}
}
}
public int UnfilteredCount
{
get
{
@ -71,79 +58,69 @@ namespace ObservableCollections
}
}
public void AttachFilter(ISynchronizedViewFilter<T, TView> filter)
public void AttachFilter(ISynchronizedViewFilter<T, TView> filter, bool invokeAddEventForCurrentElements = false)
{
if (filter.IsNullFilter())
{
ResetFilter();
return;
}
lock (SyncRoot)
{
this.filter = filter;
this.filteredCount = 0;
var i = 0;
foreach (var (value, view) in queue)
{
if (filter.IsMatch(value, view))
if (invokeAddEventForCurrentElements)
{
filteredCount++;
filter.InvokeOnAdd(value, view, NotifyCollectionChangedEventArgs<T>.Add(value, i));
}
else
{
filter.InvokeOnAttach(value, view);
}
i++;
}
ViewChanged?.Invoke(new SynchronizedViewChangedEventArgs<T, TView>(NotifyCollectionChangedAction.Reset, true));
}
}
public void ResetFilter()
public void ResetFilter(Action<T, TView>? resetAction)
{
lock (SyncRoot)
{
this.filter = SynchronizedViewFilter<T, TView>.Null;
this.filteredCount = queue.Count;
ViewChanged?.Invoke(new SynchronizedViewChangedEventArgs<T, TView>(NotifyCollectionChangedAction.Reset, true));
}
}
public ISynchronizedViewList<TView> ToViewList()
{
return new FiltableSynchronizedViewList<T, TView>(this, isSupportRangeFeature: true);
}
public NotifyCollectionChangedSynchronizedViewList<TView> ToNotifyCollectionChanged()
{
return new FiltableSynchronizedViewList<T, TView>(this, isSupportRangeFeature: false);
}
public NotifyCollectionChangedSynchronizedViewList<TView> ToNotifyCollectionChanged(ICollectionEventDispatcher? collectionEventDispatcher)
{
return new FiltableSynchronizedViewList<T, TView>(this, isSupportRangeFeature: false, collectionEventDispatcher);
}
public IEnumerator<TView> GetEnumerator()
{
lock (SyncRoot)
{
foreach (var item in queue)
if (resetAction != null)
{
if (filter.IsMatch(item))
foreach (var (item, view) in queue)
{
yield return item.Item2;
resetAction(item, view);
}
}
}
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public IEnumerable<(T Value, TView View)> Filtered
public INotifyCollectionChangedSynchronizedView<TView> ToNotifyCollectionChanged()
{
get
lock (SyncRoot)
{
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))
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;
}
@ -152,19 +129,7 @@ namespace ObservableCollections
}
}
public IEnumerable<(T Value, TView View)> Unfiltered
{
get
{
lock (SyncRoot)
{
foreach (var item in queue)
{
yield return item;
}
}
}
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public void Dispose()
{
@ -183,16 +148,15 @@ namespace ObservableCollections
{
var v = (e.NewItem, selector(e.NewItem));
queue.Enqueue(v);
this.InvokeOnAdd(ref filteredCount, ViewChanged, RejectedViewChanged, v, e.NewStartingIndex);
filter.InvokeOnAdd(v, e);
}
else
{
var i = e.NewStartingIndex;
foreach (var item in e.NewItems)
{
var v = (item, selector(item));
queue.Enqueue(v);
this.InvokeOnAdd(ref filteredCount, ViewChanged, RejectedViewChanged, v, i++);
filter.InvokeOnAdd(v, e);
}
}
break;
@ -201,7 +165,7 @@ namespace ObservableCollections
if (e.IsSingleItem)
{
var v = queue.Dequeue();
this.InvokeOnRemove(ref filteredCount, ViewChanged, RejectedViewChanged, v.Item1, v.Item2, 0);
filter.InvokeOnRemove(v.Item1, v.Item2, e);
}
else
{
@ -209,13 +173,19 @@ namespace ObservableCollections
for (int i = 0; i < len; i++)
{
var v = queue.Dequeue();
this.InvokeOnRemove(ref filteredCount, ViewChanged, RejectedViewChanged, v.Item1, v.Item2, 0);
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();
this.InvokeOnReset(ref filteredCount, ViewChanged);
break;
case NotifyCollectionChangedAction.Replace:
case NotifyCollectionChangedAction.Move:
@ -223,6 +193,7 @@ namespace ObservableCollections
break;
}
RoutingCollectionChanged?.Invoke(e);
CollectionStateChanged?.Invoke(e.Action);
}
}

View File

@ -10,7 +10,7 @@ using System.Linq;
namespace ObservableCollections
{
public partial class ObservableQueue<T> : IReadOnlyCollection<T>, IObservableCollection<T>
public sealed partial class ObservableQueue<T> : IReadOnlyCollection<T>, IObservableCollection<T>
{
readonly Queue<T> queue;
public object SyncRoot { get; } = new object();
@ -171,7 +171,7 @@ namespace ObservableCollections
}
}
public bool TryPeek([MaybeNullWhen(false)] out T result)
public bool TryPeek([MaybeNullWhen(false)] T result)
{
lock (SyncRoot)
{

View File

@ -4,64 +4,51 @@ using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Threading.Tasks;
namespace ObservableCollections
{
public partial class ObservableRingBuffer<T>
public sealed partial class ObservableRingBuffer<T>
{
public ISynchronizedView<T, TView> CreateView<TView>(Func<T, TView> transform)
public ISynchronizedView<T, TView> CreateView<TView>(Func<T, TView> transform, bool reverse = false)
{
return new View<TView>(this, transform);
return new View<TView>(this, transform, reverse);
}
// used with ObservableFixedSizeRingBuffer
internal sealed class View<TView> : ISynchronizedView<T, TView>
{
public ISynchronizedViewFilter<T, TView> Filter
public ISynchronizedViewFilter<T, TView> CurrentFilter
{
get { lock (SyncRoot) return filter; }
}
readonly IObservableCollection<T> source;
readonly Func<T, TView> selector;
readonly bool reverse;
readonly RingBuffer<(T, TView)> ringBuffer;
int filteredCount;
ISynchronizedViewFilter<T, TView> filter;
public event NotifyViewChangedEventHandler<T, TView>? ViewChanged;
public event Action<RejectedViewChangedAction, int, int>? RejectedViewChanged;
public event NotifyCollectionChangedEventHandler<T>? RoutingCollectionChanged;
public event Action<NotifyCollectionChangedAction>? CollectionStateChanged;
public object SyncRoot { get; }
public View(IObservableCollection<T> source, Func<T, TView> selector)
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.filteredCount = ringBuffer.Count;
this.source.CollectionChanged += SourceCollectionChanged;
}
}
public int Count
{
get
{
lock (SyncRoot)
{
return filteredCount;
}
}
}
public int UnfilteredCount
{
get
{
@ -72,86 +59,68 @@ namespace ObservableCollections
}
}
public void AttachFilter(ISynchronizedViewFilter<T, TView> filter)
public void AttachFilter(ISynchronizedViewFilter<T, TView> filter, bool invokeAddEventForCurrentElements = false)
{
if (filter.IsNullFilter())
{
ResetFilter();
return;
}
lock (SyncRoot)
{
this.filter = filter;
this.filteredCount = 0;
for (var i = 0; i < ringBuffer.Count; i++)
{
var (value, view) = ringBuffer[i];
if (filter.IsMatch(value, view))
if (invokeAddEventForCurrentElements)
{
filteredCount++;
filter.InvokeOnAdd(value, view, NotifyCollectionChangedEventArgs<T>.Add(value, i));
}
else
{
filter.InvokeOnAttach(value, view);
}
}
ViewChanged?.Invoke(new SynchronizedViewChangedEventArgs<T, TView>(NotifyCollectionChangedAction.Reset, true));
}
}
public void ResetFilter()
public void ResetFilter(Action<T, TView>? resetAction)
{
lock (SyncRoot)
{
this.filter = SynchronizedViewFilter<T, TView>.Null;
this.filteredCount = ringBuffer.Count;
ViewChanged?.Invoke(new SynchronizedViewChangedEventArgs<T, TView>(NotifyCollectionChangedAction.Reset, true));
}
}
public ISynchronizedViewList<TView> ToViewList()
{
return new FiltableSynchronizedViewList<T, TView>(this, isSupportRangeFeature: true);
}
public NotifyCollectionChangedSynchronizedViewList<TView> ToNotifyCollectionChanged()
{
lock (SyncRoot)
{
return new FiltableSynchronizedViewList<T, TView>(this, isSupportRangeFeature: false);
}
}
public NotifyCollectionChangedSynchronizedViewList<TView> ToNotifyCollectionChanged(ICollectionEventDispatcher? collectionEventDispatcher)
{
lock (SyncRoot)
{
return new FiltableSynchronizedViewList<T, TView>(this, isSupportRangeFeature: false, collectionEventDispatcher);
}
}
public IEnumerator<TView> GetEnumerator()
{
lock (SyncRoot)
{
foreach (var item in ringBuffer)
if (resetAction != null)
{
if (filter.IsMatch(item))
foreach (var (item, view) in ringBuffer)
{
yield return item.Item2;
resetAction(item, view);
}
}
}
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public IEnumerable<(T Value, TView View)> Filtered
public INotifyCollectionChangedSynchronizedView<TView> ToNotifyCollectionChanged()
{
get
lock (SyncRoot)
{
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))
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;
}
@ -160,19 +129,7 @@ namespace ObservableCollections
}
}
public IEnumerable<(T Value, TView View)> Unfiltered
{
get
{
lock (SyncRoot)
{
foreach (var item in ringBuffer)
{
yield return item;
}
}
}
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public void Dispose()
{
@ -197,7 +154,7 @@ namespace ObservableCollections
{
var v = (e.NewItem, selector(e.NewItem));
ringBuffer.AddFirst(v);
this.InvokeOnAdd(ref filteredCount, ViewChanged, RejectedViewChanged, v, 0);
filter.InvokeOnAdd(v, e);
}
else
{
@ -205,7 +162,7 @@ namespace ObservableCollections
{
var v = (item, selector(item));
ringBuffer.AddFirst(v);
this.InvokeOnAdd(ref filteredCount, ViewChanged, RejectedViewChanged, v, 0);
filter.InvokeOnAdd(v, e);
}
}
}
@ -216,7 +173,7 @@ namespace ObservableCollections
{
var v = (e.NewItem, selector(e.NewItem));
ringBuffer.AddLast(v);
this.InvokeOnAdd(ref filteredCount, ViewChanged, RejectedViewChanged, v, ringBuffer.Count - 1);
filter.InvokeOnAdd(v, e);
}
else
{
@ -224,7 +181,7 @@ namespace ObservableCollections
{
var v = (item, selector(item));
ringBuffer.AddLast(v);
this.InvokeOnAdd(ref filteredCount, ViewChanged, RejectedViewChanged, v, ringBuffer.Count - 1);
filter.InvokeOnAdd(v, e);
}
}
}
@ -237,14 +194,14 @@ namespace ObservableCollections
if (e.IsSingleItem)
{
var v = ringBuffer.RemoveFirst();
this.InvokeOnRemove(ref filteredCount, ViewChanged, RejectedViewChanged, v, 0);
filter.InvokeOnRemove(v, e);
}
else
{
for (int i = 0; i < e.OldItems.Length; i++)
{
var v = ringBuffer.RemoveFirst();
this.InvokeOnRemove(ref filteredCount, ViewChanged, RejectedViewChanged, v, 0);
filter.InvokeOnRemove(v, e);
}
}
}
@ -253,32 +210,39 @@ namespace ObservableCollections
// RemoveLast
if (e.IsSingleItem)
{
var index = ringBuffer.Count - 1;
var v = ringBuffer.RemoveLast();
this.InvokeOnRemove(ref filteredCount, ViewChanged, RejectedViewChanged, v, index);
filter.InvokeOnRemove(v, e);
}
else
{
for (int i = 0; i < e.OldItems.Length; i++)
{
var index = ringBuffer.Count - 1;
var v = ringBuffer.RemoveLast();
this.InvokeOnRemove(ref filteredCount, ViewChanged, RejectedViewChanged, v, index);
filter.InvokeOnRemove(v, e);
}
}
}
break;
case NotifyCollectionChangedAction.Reset:
if (!filter.IsNullFilter())
{
foreach (var item in ringBuffer)
{
filter.InvokeOnRemove(item, e);
}
}
ringBuffer.Clear();
this.InvokeOnReset(ref filteredCount, ViewChanged);
break;
case NotifyCollectionChangedAction.Replace:
// range is not supported
{
var ov = ringBuffer[e.OldStartingIndex];
var v = (e.NewItem, selector(e.NewItem));
var oldItem = ringBuffer[e.NewStartingIndex];
ringBuffer[e.NewStartingIndex] = v;
this.InvokeOnReplace(ref filteredCount, ViewChanged, v, ov, e.NewStartingIndex);
filter.InvokeOnRemove(oldItem, e);
filter.InvokeOnAdd(v, e);
break;
}
case NotifyCollectionChangedAction.Move:
@ -286,6 +250,7 @@ namespace ObservableCollections
break;
}
RoutingCollectionChanged?.Invoke(e);
CollectionStateChanged?.Invoke(e.Action);
}
}

View File

@ -6,7 +6,7 @@ using System.Collections;
namespace ObservableCollections
{
public partial class ObservableRingBuffer<T> : IList<T>, IReadOnlyList<T>, IObservableCollection<T>
public sealed partial class ObservableRingBuffer<T> : IList<T>, IReadOnlyList<T>, IObservableCollection<T>
{
readonly RingBuffer<T> buffer;
@ -41,7 +41,7 @@ namespace ObservableCollections
{
var oldValue = buffer[index];
buffer[index] = value;
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Replace(value, oldValue, index, index));
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Replace(value, oldValue, index));
}
}
}

View File

@ -7,59 +7,47 @@ using System.Linq;
namespace ObservableCollections
{
public partial class ObservableStack<T> : IReadOnlyCollection<T>, IObservableCollection<T>
public sealed partial class ObservableStack<T> : IReadOnlyCollection<T>, IObservableCollection<T>
{
public ISynchronizedView<T, TView> CreateView<TView>(Func<T, TView> transform)
public ISynchronizedView<T, TView> CreateView<TView>(Func<T, TView> transform, bool reverse = false)
{
return new View<TView>(this, transform);
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;
int filteredCount;
ISynchronizedViewFilter<T, TView> filter;
public event NotifyViewChangedEventHandler<T, TView>? ViewChanged;
public event Action<RejectedViewChangedAction, int, int>? RejectedViewChanged;
public event NotifyCollectionChangedEventHandler<T>? RoutingCollectionChanged;
public event Action<NotifyCollectionChangedAction>? CollectionStateChanged;
public object SyncRoot { get; }
public ISynchronizedViewFilter<T, TView> Filter
public ISynchronizedViewFilter<T, TView> CurrentFilter
{
get { lock (SyncRoot) return filter; }
}
public View(ObservableStack<T> source, Func<T, TView> selector)
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.filteredCount = stack.Count;
this.source.CollectionChanged += SourceCollectionChanged;
}
}
public int Count
{
get
{
lock (SyncRoot)
{
return filteredCount;
}
}
}
public int UnfilteredCount
{
get
{
@ -70,85 +58,69 @@ namespace ObservableCollections
}
}
public void AttachFilter(ISynchronizedViewFilter<T, TView> filter)
public void AttachFilter(ISynchronizedViewFilter<T, TView> filter, bool invokeAddEventForCurrentElements = false)
{
if (filter.IsNullFilter())
{
ResetFilter();
return;
}
lock (SyncRoot)
{
this.filter = filter;
this.filteredCount = 0;
var i = 0;
foreach (var (value, view) in stack)
{
if (filter.IsMatch(value, view))
if (invokeAddEventForCurrentElements)
{
filteredCount++;
filter.InvokeOnAdd(value, view, NotifyCollectionChangedEventArgs<T>.Add(value, i));
}
else
{
filter.InvokeOnAttach(value, view);
}
i++;
}
ViewChanged?.Invoke(new SynchronizedViewChangedEventArgs<T, TView>(NotifyCollectionChangedAction.Reset, true));
}
}
public void ResetFilter()
public void ResetFilter(Action<T, TView>? resetAction)
{
lock (SyncRoot)
{
this.filter = SynchronizedViewFilter<T, TView>.Null;
this.filteredCount = stack.Count;
ViewChanged?.Invoke(new SynchronizedViewChangedEventArgs<T, TView>(NotifyCollectionChangedAction.Reset, true));
}
}
public ISynchronizedViewList<TView> ToViewList()
{
return new FiltableSynchronizedViewList<T, TView>(this, isSupportRangeFeature: true);
}
public NotifyCollectionChangedSynchronizedViewList<TView> ToNotifyCollectionChanged()
{
lock (SyncRoot)
{
return new FiltableSynchronizedViewList<T, TView>(this, isSupportRangeFeature: false);
}
}
public NotifyCollectionChangedSynchronizedViewList<TView> ToNotifyCollectionChanged(ICollectionEventDispatcher? collectionEventDispatcher)
{
lock (SyncRoot)
{
return new FiltableSynchronizedViewList<T, TView>(this, isSupportRangeFeature: false, collectionEventDispatcher);
}
}
public IEnumerator<TView> GetEnumerator()
{
lock (SyncRoot)
{
foreach (var item in stack)
if (resetAction != null)
{
if (filter.IsMatch(item))
foreach (var (item, view) in stack)
{
yield return item.Item2;
resetAction(item, view);
}
}
}
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public IEnumerable<(T Value, TView View)> Filtered
public INotifyCollectionChangedSynchronizedView<TView> ToNotifyCollectionChanged()
{
get
lock (SyncRoot)
{
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))
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;
}
@ -157,19 +129,7 @@ namespace ObservableCollections
}
}
public IEnumerable<(T Value, TView View)> Unfiltered
{
get
{
lock (SyncRoot)
{
foreach (var item in stack)
{
yield return item;
}
}
}
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public void Dispose()
{
@ -188,7 +148,7 @@ namespace ObservableCollections
{
var v = (e.NewItem, selector(e.NewItem));
stack.Push(v);
this.InvokeOnAdd(ref filteredCount, ViewChanged, RejectedViewChanged, v, 0);
filter.InvokeOnAdd(v, e);
}
else
{
@ -196,7 +156,7 @@ namespace ObservableCollections
{
var v = (item, selector(item));
stack.Push(v);
this.InvokeOnAdd(ref filteredCount, ViewChanged, RejectedViewChanged, v, 0);
filter.InvokeOnAdd(v, e);
}
}
break;
@ -205,7 +165,7 @@ namespace ObservableCollections
if (e.IsSingleItem)
{
var v = stack.Pop();
this.InvokeOnRemove(ref filteredCount, ViewChanged, RejectedViewChanged, v.Item1, v.Item2, 0);
filter.InvokeOnRemove(v.Item1, v.Item2, e);
}
else
{
@ -213,13 +173,19 @@ namespace ObservableCollections
for (int i = 0; i < len; i++)
{
var v = stack.Pop();
this.InvokeOnRemove(ref filteredCount, ViewChanged, RejectedViewChanged, v.Item1, v.Item2, 0);
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();
this.InvokeOnReset(ref filteredCount, ViewChanged);
break;
case NotifyCollectionChangedAction.Replace:
case NotifyCollectionChangedAction.Move:
@ -227,6 +193,7 @@ namespace ObservableCollections
break;
}
RoutingCollectionChanged?.Invoke(e);
CollectionStateChanged?.Invoke(e.Action);
}
}

View File

@ -8,7 +8,7 @@ using System.Runtime.CompilerServices;
namespace ObservableCollections
{
public partial class ObservableStack<T> : IReadOnlyCollection<T>, IObservableCollection<T>
public sealed partial class ObservableStack<T> : IReadOnlyCollection<T>, IObservableCollection<T>
{
readonly Stack<T> stack;
public object SyncRoot { get; } = new object();
@ -45,8 +45,9 @@ namespace ObservableCollections
{
lock (SyncRoot)
{
var index = stack.Count;
stack.Push(item);
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Add(item, 0));
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Add(item, index));
}
}
@ -166,7 +167,7 @@ namespace ObservableCollections
}
}
public bool TryPeek([MaybeNullWhen(false)] out T result)
public bool TryPeek([MaybeNullWhen(false)] T result)
{
lock (SyncRoot)
{
@ -212,4 +213,4 @@ namespace ObservableCollections
return GetEnumerator();
}
}
}
}

View File

@ -6,7 +6,7 @@ using System.Linq;
namespace ObservableCollections
{
public class RingBuffer<T> : IList<T>, IReadOnlyList<T>
public sealed class RingBuffer<T> : IList<T>, IReadOnlyList<T>
{
T[] buffer;
int head;

View File

@ -2,17 +2,12 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Text;
using System.Runtime.InteropServices;
namespace System.Collections.Generic
{
internal static class CollectionExtensions
{
const int ArrayMaxLength = 0X7FFFFFC7;
public static void Deconstruct<TKey, TValue>(this KeyValuePair<TKey, TValue> kvp, out TKey key, out TValue value)
{
key = kvp.Key;
@ -37,89 +32,6 @@ namespace System.Collections.Generic
return false;
}
#if !NET8_0_OR_GREATER
#pragma warning disable CS0436
// CollectionExtensions.AddRange
public static void AddRange<T>(this List<T> list, ReadOnlySpan<T> source)
{
if (!source.IsEmpty)
{
ref var view = ref Unsafe.As<List<T>, CollectionsMarshal.ListView<T>>(ref list!);
if (view._items.Length - view._size < source.Length)
{
Grow(ref view, checked(view._size + source.Length));
}
source.CopyTo(view._items.AsSpan(view._size));
view._size += source.Length;
view._version++;
}
}
// CollectionExtensions.InsertRange
public static void InsertRange<T>(this List<T> list, int index, ReadOnlySpan<T> source)
{
if (!source.IsEmpty)
{
ref var view = ref Unsafe.As<List<T>, CollectionsMarshal.ListView<T>>(ref list!);
if (view._items.Length - view._size < source.Length)
{
Grow(ref view, checked(view._size + source.Length));
}
if (index < view._size)
{
Array.Copy(view._items, index, view._items, index + source.Length, view._size - index);
}
source.CopyTo(view._items.AsSpan(index));
view._size += source.Length;
view._version++;
}
}
static void Grow<T>(ref CollectionsMarshal.ListView<T> list, int capacity)
{
SetCapacity(ref list, GetNewCapacity(ref list, capacity));
}
static void SetCapacity<T>(ref CollectionsMarshal.ListView<T> list, int value)
{
if (value != list._items.Length)
{
if (value > 0)
{
T[] newItems = new T[value];
if (list._size > 0)
{
Array.Copy(list._items, newItems, list._size);
}
list._items = newItems;
}
else
{
list._items = Array.Empty<T>();
}
}
}
static int GetNewCapacity<T>(ref CollectionsMarshal.ListView<T> list, int capacity)
{
int newCapacity = list._items.Length == 0 ? 4 : 2 * list._items.Length;
if ((uint)newCapacity > ArrayMaxLength) newCapacity = ArrayMaxLength;
if (newCapacity < capacity) newCapacity = capacity;
return newCapacity;
}
#pragma warning restore CS0436
#endif
#if !NET6_0_OR_GREATER
public static bool TryGetNonEnumeratedCount<T>(this IEnumerable<T> source, out int count)

View File

@ -1,34 +0,0 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
#if !NET7_0_OR_GREATER
#pragma warning disable CS0649
#pragma warning disable CS8618
#pragma warning disable CS8619
namespace System.Runtime.InteropServices;
internal static class CollectionsMarshal
{
/// <summary>
/// similar as AsSpan but modify size to create fixed-size span.
/// </summary>
public static Span<T> AsSpan<T>(List<T>? list)
{
if (list is null) return default;
ref var view = ref Unsafe.As<List<T>, ListView<T>>(ref list!);
return view._items.AsSpan(0, view._size);
}
internal sealed class ListView<T>
{
public T[] _items;
public int _size;
public int _version;
}
}
#endif

View File

@ -1,258 +0,0 @@
#pragma warning disable CS9124
using System;
using System.Collections.Specialized;
using System.Data;
using System.Runtime.CompilerServices;
namespace ObservableCollections
{
public readonly ref struct SynchronizedViewChangedEventArgs<T, TView>(
NotifyCollectionChangedAction action,
bool isSingleItem,
(T Value, TView View) newItem = default!,
(T Value, TView View) oldItem = default!,
ReadOnlySpan<T> newValues = default!,
ReadOnlySpan<TView> newViews = default!,
ReadOnlySpan<T> oldValues = default!,
ReadOnlySpan<TView> oldViews = default!,
int newStartingIndex = -1,
int oldStartingIndex = -1,
SortOperation<T> sortOperation = default)
{
public readonly NotifyCollectionChangedAction Action = action;
public readonly bool IsSingleItem = isSingleItem;
public readonly (T Value, TView View) NewItem = newItem;
public readonly (T Value, TView View) OldItem = oldItem;
public readonly ReadOnlySpan<T> NewValues = newValues;
public readonly ReadOnlySpan<TView> NewViews = newViews;
public readonly ReadOnlySpan<T> OldValues = oldValues;
public readonly ReadOnlySpan<TView> OldViews = oldViews;
public readonly int NewStartingIndex = newStartingIndex;
public readonly int OldStartingIndex = oldStartingIndex;
public readonly SortOperation<T> SortOperation = sortOperation;
public SynchronizedViewChangedEventArgs<T, TView> WithNewStartingIndex(int newStartingIndex)
{
// MEMO: struct copy and replace only newStartingIndex memory maybe fast.
return new SynchronizedViewChangedEventArgs<T, TView>(
action,
IsSingleItem,
newItem: NewItem,
oldItem: OldItem,
newValues: NewValues,
newViews: NewViews,
oldValues: OldValues,
oldViews: OldViews,
newStartingIndex: newStartingIndex, // replace
oldStartingIndex: OldStartingIndex,
sortOperation: SortOperation);
}
public SynchronizedViewChangedEventArgs<T, TView> WithOldStartingIndex(int oldStartingIndex)
{
return new SynchronizedViewChangedEventArgs<T, TView>(
action,
IsSingleItem,
newItem: NewItem,
oldItem: OldItem,
newValues: NewValues,
newViews: NewViews,
oldValues: OldValues,
oldViews: OldViews,
newStartingIndex: NewStartingIndex,
oldStartingIndex: oldStartingIndex, // replace
sortOperation: SortOperation);
}
public SynchronizedViewChangedEventArgs<T, TView> WithNewAndOldStartingIndex(int newStartingIndex, int oldStartingIndex)
{
return new SynchronizedViewChangedEventArgs<T, TView>(
action,
IsSingleItem,
newItem: NewItem,
oldItem: OldItem,
newValues: NewValues,
newViews: NewViews,
oldValues: OldValues,
oldViews: OldViews,
newStartingIndex: newStartingIndex, // replace
oldStartingIndex: oldStartingIndex, // replace
sortOperation: SortOperation);
}
}
public static class SynchronizedViewExtensions
{
public static void AttachFilter<T, TView>(this ISynchronizedView<T, TView> source, Func<T, bool> filter)
{
source.AttachFilter(new SynchronizedViewValueOnlyFilter<T, TView>(filter));
}
public static void AttachFilter<T, TView>(this ISynchronizedView<T, TView> source, Func<T, TView, bool> filter)
{
source.AttachFilter(new SynchronizedViewFilter<T, TView>(filter));
}
public static bool IsNullFilter<T, TView>(this ISynchronizedViewFilter<T, TView> filter)
{
return filter == SynchronizedViewFilter<T, TView>.Null;
}
internal static bool IsMatch<T, TView>(this ISynchronizedViewFilter<T, TView> filter, (T, TView) item)
{
return filter.IsMatch(item.Item1, item.Item2);
}
internal static void InvokeOnAdd<T, TView>(this ISynchronizedView<T, TView> collection, ref int filteredCount, NotifyViewChangedEventHandler<T, TView>? ev, Action<RejectedViewChangedAction, int, int>? ev2, (T value, TView view) value, int index)
{
InvokeOnAdd(collection, ref filteredCount, ev, ev2, value.value, value.view, index);
}
internal static void InvokeOnAdd<T, TView>(this ISynchronizedView<T, TView> collection, ref int filteredCount, NotifyViewChangedEventHandler<T, TView>? ev, Action<RejectedViewChangedAction, int, int>? ev2, T value, TView view, int index)
{
var isMatch = collection.Filter.IsMatch(value, view);
if (isMatch)
{
filteredCount++;
ev?.Invoke(new SynchronizedViewChangedEventArgs<T, TView>(NotifyCollectionChangedAction.Add, true, newItem: (value, view), newStartingIndex: index));
}
else
{
ev2?.Invoke(RejectedViewChangedAction.Add, index, -1);
}
}
internal static void InvokeOnAddRange<T, TView>(this ISynchronizedView<T, TView> collection, NotifyViewChangedEventHandler<T, TView>? ev, Action<RejectedViewChangedAction, int, int>? ev2, ReadOnlySpan<T> values, ReadOnlySpan<TView> views, bool isMatchAll, ReadOnlySpan<bool> matches, int index)
{
if (isMatchAll)
{
ev?.Invoke(new SynchronizedViewChangedEventArgs<T, TView>(NotifyCollectionChangedAction.Add, isSingleItem: false, newValues: values, newViews: views, newStartingIndex: index));
}
else
{
for (var i = 0; i < matches.Length; i++)
{
if (matches[i])
{
var item = (values[i], views[i]);
ev?.Invoke(new SynchronizedViewChangedEventArgs<T, TView>(NotifyCollectionChangedAction.Add, isSingleItem: true, newItem: item, newStartingIndex: index));
}
else
{
ev2?.Invoke(RejectedViewChangedAction.Add, index, -1);
}
index++;
}
}
}
internal static void InvokeOnRemove<T, TView>(this ISynchronizedView<T, TView> collection, ref int filteredCount, NotifyViewChangedEventHandler<T, TView>? ev, Action<RejectedViewChangedAction, int, int>? ev2, (T value, TView view) value, int oldIndex)
{
InvokeOnRemove(collection, ref filteredCount, ev, ev2, value.value, value.view, oldIndex);
}
internal static void InvokeOnRemove<T, TView>(this ISynchronizedView<T, TView> collection, ref int filteredCount, NotifyViewChangedEventHandler<T, TView>? ev, Action<RejectedViewChangedAction, int, int>? ev2, T value, TView view, int oldIndex)
{
var isMatch = collection.Filter.IsMatch(value, view);
if (isMatch)
{
filteredCount--;
ev?.Invoke(new SynchronizedViewChangedEventArgs<T, TView>(NotifyCollectionChangedAction.Remove, true, oldItem: (value, view), oldStartingIndex: oldIndex));
}
else
{
ev2?.Invoke(RejectedViewChangedAction.Remove, oldIndex, -1);
}
}
// only use for ObservableList
internal static void InvokeOnRemoveRange<T, TView>(this ISynchronizedView<T, TView> collection, NotifyViewChangedEventHandler<T, TView>? ev, Action<RejectedViewChangedAction, int, int>? ev2, ReadOnlySpan<T> values, ReadOnlySpan<TView> views, bool isMatchAll, ReadOnlySpan<bool> matches, int index)
{
if (isMatchAll)
{
ev?.Invoke(new SynchronizedViewChangedEventArgs<T, TView>(NotifyCollectionChangedAction.Remove, isSingleItem: false, oldValues: values, oldViews: views, oldStartingIndex: index));
}
else
{
for (var i = 0; i < matches.Length; i++)
{
if (matches[i])
{
var item = (values[i], views[i]);
ev?.Invoke(new SynchronizedViewChangedEventArgs<T, TView>(NotifyCollectionChangedAction.Remove, isSingleItem: true, oldItem: item, oldStartingIndex: index)); //remove for list, always same index
}
else
{
ev2?.Invoke(RejectedViewChangedAction.Remove, index, -1);
}
}
}
}
internal static void InvokeOnMove<T, TView>(this ISynchronizedView<T, TView> collection, ref int filteredCount, NotifyViewChangedEventHandler<T, TView>? ev, Action<RejectedViewChangedAction, int, int>? ev2, (T value, TView view) value, int index, int oldIndex)
{
InvokeOnMove(collection, ref filteredCount, ev, ev2, value.value, value.view, index, oldIndex);
}
internal static void InvokeOnMove<T, TView>(this ISynchronizedView<T, TView> collection, ref int filteredCount, NotifyViewChangedEventHandler<T, TView>? ev, Action<RejectedViewChangedAction, int, int>? ev2, T value, TView view, int index, int oldIndex)
{
// move does not changes filtered-count
var isMatch = collection.Filter.IsMatch(value, view);
if (isMatch)
{
ev?.Invoke(new SynchronizedViewChangedEventArgs<T, TView>(NotifyCollectionChangedAction.Move, true, newItem: (value, view), newStartingIndex: index, oldStartingIndex: oldIndex));
}
else
{
ev2?.Invoke(RejectedViewChangedAction.Move, index, oldIndex);
}
}
internal static void InvokeOnReplace<T, TView>(this ISynchronizedView<T, TView> collection, ref int filteredCount, NotifyViewChangedEventHandler<T, TView>? ev, (T value, TView view) value, (T value, TView view) oldValue, int index, int oldIndex = -1)
{
InvokeOnReplace(collection, ref filteredCount, ev, value.value, value.view, oldValue.value, oldValue.view, index, oldIndex);
}
internal static void InvokeOnReplace<T, TView>(this ISynchronizedView<T, TView> collection, ref int filteredCount, NotifyViewChangedEventHandler<T, TView>? ev, T value, TView view, T oldValue, TView oldView, int index, int oldIndex = -1)
{
var oldMatched = collection.Filter.IsMatch(oldValue, oldView);
var newMatched = collection.Filter.IsMatch(value, view);
var bothMatched = oldMatched && newMatched;
if (bothMatched)
{
ev?.Invoke(new SynchronizedViewChangedEventArgs<T, TView>(NotifyCollectionChangedAction.Replace, true, newItem: (value, view), oldItem: (oldValue, oldView), newStartingIndex: index, oldStartingIndex: oldIndex >= 0 ? oldIndex : index));
}
else if (oldMatched)
{
// only-old is remove
filteredCount--;
ev?.Invoke(new SynchronizedViewChangedEventArgs<T, TView>(NotifyCollectionChangedAction.Remove, true, oldItem: (value, view), oldStartingIndex: oldIndex));
}
else if (newMatched)
{
// only-new is add
filteredCount++;
ev?.Invoke(new SynchronizedViewChangedEventArgs<T, TView>(NotifyCollectionChangedAction.Add, true, newItem: (value, view), newStartingIndex: index));
}
}
internal static void InvokeOnReset<T, TView>(this ISynchronizedView<T, TView> collection, ref int filteredCount, NotifyViewChangedEventHandler<T, TView>? ev)
{
filteredCount = 0;
if (ev != null)
{
ev.Invoke(new SynchronizedViewChangedEventArgs<T, TView>(NotifyCollectionChangedAction.Reset, true));
}
}
internal static void InvokeOnReverseOrSort<T, TView>(this ISynchronizedView<T, TView> collection, NotifyViewChangedEventHandler<T, TView>? ev, SortOperation<T> sortOperation)
{
if (ev != null)
{
ev.Invoke(new SynchronizedViewChangedEventArgs<T, TView>(NotifyCollectionChangedAction.Reset, true, sortOperation: sortOperation));
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -10,7 +10,7 @@ public class ObservableCollectionExtensionsTest
public void ObserveAdd()
{
var events = new List<CollectionAddEvent<int>>();
var collection = new ObservableList<int>();
var subscription = collection.ObserveAdd().Subscribe(ev => events.Add(ev));
@ -25,20 +25,20 @@ public class ObservableCollectionExtensionsTest
events[1].Value.Should().Be(50);
events[2].Index.Should().Be(2);
events[2].Value.Should().Be(30);
subscription.Dispose();
collection.Add(100);
events.Count.Should().Be(3);
}
[Fact]
public void ObserveAdd_CancellationToken()
{
var cts = new CancellationTokenSource();
var events = new List<CollectionAddEvent<int>>();
var result = default(Result?);
var collection = new ObservableList<int>();
var subscription = collection.ObserveAdd(cts.Token).Subscribe(ev => events.Add(ev), x => result = x);
@ -47,43 +47,17 @@ public class ObservableCollectionExtensionsTest
collection.Add(30);
events.Count.Should().Be(3);
cts.Cancel();
result.HasValue.Should().BeTrue();
subscription.Dispose();
collection.Add(100);
events.Count.Should().Be(3);
}
[Fact]
public void ObserveDictionaryAdd()
{
var events = new List<DictionaryAddEvent<int, string>>();
var dictionary = new ObservableDictionary<int, string>();
var subscription = dictionary.ObserveDictionaryAdd().Subscribe(ev => events.Add(ev));
dictionary.Add(0, "zero");
dictionary.Add(1, "one");
dictionary.Add(2, "two");
events.Count.Should().Be(3);
events[0].Key.Should().Be(0);
events[0].Value.Should().Be("zero");
events[1].Key.Should().Be(1);
events[1].Value.Should().Be("one");
events[2].Key.Should().Be(2);
events[2].Value.Should().Be("two");
subscription.Dispose();
dictionary.Add(4, "four");
events.Count.Should().Be(3);
}
[Fact]
public void ObserveRemove()
{
@ -98,45 +72,16 @@ public class ObservableCollectionExtensionsTest
events.Count.Should().Be(1);
events[0].Index.Should().Be(1);
events[0].Value.Should().Be(222);
cts.Cancel();
result.HasValue.Should().BeTrue();
subscription.Dispose();
collection.RemoveAt(0);
events.Count.Should().Be(1);
}
[Fact]
public void ObserveDictionaryRemove()
{
var events = new List<DictionaryRemoveEvent<int, string>>();
var dictionary = new ObservableDictionary<int,string>
{
{ 0, "zero" },
{ 1, "one" },
{ 2, "two" }
};
var cts = new CancellationTokenSource();
var result = default(Result?);
var subscription = dictionary.ObserveDictionaryRemove((cts.Token)).Subscribe(ev => events.Add(ev), x => result = x);
dictionary.Remove(0);
events.Count.Should().Be(1);
events[0].Key.Should().Be(0);
events[0].Value.Should().Be("zero");
cts.Cancel();
result.HasValue.Should().BeTrue();
subscription.Dispose();
dictionary.Remove(1);
events.Count.Should().Be(1);
}
[Fact]
public void ObserveReplace()
{
@ -152,46 +97,16 @@ public class ObservableCollectionExtensionsTest
events[0].Index.Should().Be(1);
events[0].OldValue.Should().Be(222);
events[0].NewValue.Should().Be(999);
cts.Cancel();
result.HasValue.Should().BeTrue();
subscription.Dispose();
collection[1] = 444;
events.Count.Should().Be(1);
}
[Fact]
public void ObserveDictionaryReplace()
{
var events = new List<DictionaryReplaceEvent<int,string>>();
var dictionary = new ObservableDictionary<int, string>()
{
{ 0, "zero" },
{ 1, "one" },
{ 2, "two" }
};
var cts = new CancellationTokenSource();
var result = default(Result?);
var subscription = dictionary.ObserveDictionaryReplace(cts.Token).Subscribe(ev => events.Add(ev), x => result = x);
dictionary[1] = "ten";
events.Count.Should().Be(1);
events[0].Key.Should().Be(1);
events[0].OldValue.Should().Be("one");
events[0].NewValue.Should().Be("ten");
cts.Cancel();
result.HasValue.Should().BeTrue();
subscription.Dispose();
dictionary[1] = "one hundred";
events.Count.Should().Be(1);
}
[Fact]
public void ObserveMove()
{
@ -201,17 +116,17 @@ public class ObservableCollectionExtensionsTest
var result = default(Result?);
var subscription = collection.ObserveMove(cts.Token).Subscribe(ev => events.Add(ev), x => result = x);
collection.Move(1, 2);
events.Count.Should().Be(1);
events[0].OldIndex.Should().Be(1);
events[0].NewIndex.Should().Be(2);
events[0].Value.Should().Be(222);
cts.Cancel();
result.HasValue.Should().BeTrue();
subscription.Dispose();
collection.Move(1, 2);
@ -227,22 +142,22 @@ public class ObservableCollectionExtensionsTest
using var _ = collection.ObserveCountChanged().Subscribe(count => events.Add(count));
events.Should().BeEmpty();
collection.Add(444);
events[0].Should().Be(4);
collection.Remove(111);
events[1].Should().Be(3);
collection.Move(0, 1);
events.Count.Should().Be(2);
collection[0] = 999;
events.Count.Should().Be(2);
collection.Clear();
events[2].Should().Be(0);
collection.Clear();
events.Count.Should().Be(3);
}
@ -253,8 +168,7 @@ public class ObservableCollectionExtensionsTest
var events = new List<int>();
var collection = new ObservableList<int>([111, 222, 333]);
var subscription = collection.ObserveCountChanged(notifyCurrentCount: true)
.Subscribe(count => events.Add(count));
var subscription = collection.ObserveCountChanged(notifyCurrentCount: true).Subscribe(count => events.Add(count));
events[0].Should().Be(3);
}
}

View File

@ -1,113 +0,0 @@
using ObservableCollections.Internal;
namespace ObservableCollections.Tests;
public class AlternateIndexListTest
{
[Fact]
public void Insert()
{
var list = new AlternateIndexList<string>();
list.Insert(0, "foo");
list.Insert(1, "bar");
list.Insert(2, "baz");
list.GetIndexedValues().Should().Equal((0, "foo"), (1, "bar"), (2, "baz"));
list.Insert(1, "new-bar");
list.GetIndexedValues().Should().Equal((0, "foo"), (1, "new-bar"), (2, "bar"), (3, "baz"));
list.Insert(6, "zoo");
list.GetIndexedValues().Should().Equal((0, "foo"), (1, "new-bar"), (2, "bar"), (3, "baz"), (6, "zoo"));
}
[Fact]
public void InsertRange()
{
var list = new AlternateIndexList<string>();
list.Insert(0, "foo");
list.Insert(1, "bar");
list.Insert(2, "baz");
list.InsertRange(1, new[] { "new-foo", "new-bar", "new-baz" });
list.GetIndexedValues().Should().Equal((0, "foo"), (1, "new-foo"), (2, "new-bar"), (3, "new-baz"), (4, "bar"), (5, "baz"));
}
[Fact]
public void InsertSparsed()
{
var list = new AlternateIndexList<string>();
list.Insert(2, "foo");
list.Insert(8, "baz"); // baz
list.Insert(4, "bar");
list.GetIndexedValues().Should().Equal((2, "foo"), (4, "bar"), (9, "baz"));
list.InsertRange(3, new[] { "new-foo", "new-bar", "new-baz" });
list.GetIndexedValues().Should().Equal((2, "foo"), (3, "new-foo"), (4, "new-bar"), (5, "new-baz"), (7, "bar"), (12, "baz"));
list.InsertRange(1, new[] { "zoo" });
list.GetIndexedValues().Should().Equal((1, "zoo"), (3, "foo"), (4, "new-foo"), (5, "new-bar"), (6, "new-baz"), (8, "bar"), (13, "baz"));
}
[Fact]
public void Remove()
{
var list = new AlternateIndexList<string>();
list.Insert(0, "foo");
list.Insert(1, "bar");
list.Insert(2, "baz");
list.Remove("bar");
list.GetIndexedValues().Should().Equal((0, "foo"), (1, "baz"));
list.RemoveAt(0);
list.GetIndexedValues().Should().Equal((0, "baz"));
}
[Fact]
public void RemoveRange()
{
var list = new AlternateIndexList<string>();
list.Insert(0, "foo");
list.Insert(1, "bar");
list.Insert(2, "baz");
list.RemoveRange(1, 2);
list.GetIndexedValues().Should().Equal((0, "foo"));
}
[Fact]
public void TryGetSet()
{
var list = new AlternateIndexList<string>();
list.Insert(0, "foo");
list.Insert(2, "bar");
list.Insert(4, "baz");
list.TryGetAtAlternateIndex(2, out var bar).Should().BeTrue();
bar.Should().Be("bar");
list.TrySetAtAlternateIndex(4, "new-baz", out var i).Should().BeTrue();
list.TryGetAtAlternateIndex(4, out var baz).Should().BeTrue();
baz.Should().Be("new-baz");
}
[Fact]
public void TryReplaceByValue()
{
var list = new AlternateIndexList<string>();
list.Insert(0, "foo");
list.Insert(2, "bar");
list.Insert(4, "baz");
list.TryReplaceByValue("bar", "new-bar", out var i);
list.GetIndexedValues().Should().Equal((0, "foo"), (2, "new-bar"), (4, "baz"));
}
}

View File

@ -1,7 +1,6 @@
using FluentAssertions;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Linq;
using Xunit;
@ -24,6 +23,7 @@ namespace ObservableCollections.Tests
void Equal(params int[] expected)
{
dict.Select(x => x.Value).OrderByDescending(x => x).Should().Equal(expected);
view.Select(x => x.Value.Value).OrderByDescending(x => x).Should().Equal(expected);
}
Equal(-10, -20, -30, -40, -50);
@ -41,6 +41,149 @@ namespace ObservableCollections.Tests
Equal(new int[0]);
}
[Fact]
public void ViewSorted()
{
var dict = new ObservableDictionary<int, int>();
var view1 = dict.CreateSortedView(x => x.Key, x => new ViewContainer<int>(x.Value), x => x.Value, true);
var view2 = dict.CreateSortedView(x => x.Key, x => new ViewContainer<int>(x.Value), x => x.Value, false);
dict.Add(10, 10); // 0
dict.Add(50, 50); // 1
dict.Add(30, 30); // 2
dict.Add(20, 20); // 3
dict.Add(40, 40); // 4
void Equal(params int[] expected)
{
dict.Select(x => x.Value).OrderBy(x => x).Should().Equal(expected);
view1.Select(x => x.Value.Value).Should().Equal(expected);
view2.Select(x => x.Value.Value).Should().Equal(expected.OrderByDescending(x => x));
}
Equal(10, 20, 30, 40, 50);
dict[99] = 100;
Equal(10, 20, 30, 40, 50, 100);
dict[10] = -5;
Equal(-5, 20, 30, 40, 50, 100);
dict.Remove(20);
Equal(-5, 30, 40, 50, 100);
dict.Clear();
Equal(new int[0]);
}
[Fact]
public void Freezed()
{
var dict = new FreezedDictionary<int, int>(new Dictionary<int, int>
{
[10] = 10,
[50] = 50,
[30] = 30,
[20] = 20,
[40] = 40,
[60] = 60
});
var view = dict.CreateSortableView(x => new ViewContainer<int>(x.Value));
view.Sort(x => x.Key, true);
view.Select(x => x.Value.Value).Should().Equal(10, 20, 30, 40, 50, 60);
view.Select(x => x.View).Should().Equal(10, 20, 30, 40, 50, 60);
view.Sort(x => x.Key, false);
view.Select(x => x.Value.Value).Should().Equal(60, 50, 40, 30, 20, 10);
view.Select(x => x.View).Should().Equal(60, 50, 40, 30, 20, 10);
}
[Fact]
public void FilterTest()
{
var dict = new ObservableDictionary<int, int>();
var view1 = dict.CreateView(x => new ViewContainer<int>(x.Value));
var view2 = dict.CreateSortedView(x => x.Key, x => new ViewContainer<int>(x.Value), x => x.Value, true);
var view3 = dict.CreateSortedView(x => new ViewContainer<int>(x.Value), x => x.Value, viewComparer: Comparer<ViewContainer<int>>.Default);
var filter1 = new TestFilter2<int>((x, v) => x.Value % 2 == 0);
var filter2 = new TestFilter2<int>((x, v) => x.Value % 2 == 0);
var filter3 = new TestFilter2<int>((x, v) => x.Value % 2 == 0);
dict.Add(10, -12); // 0
dict.Add(50, -53); // 1
dict.Add(30, -34); // 2
dict.Add(20, -25); // 3
dict.Add(40, -40); // 4
view1.AttachFilter(filter1);
view2.AttachFilter(filter2);
view3.AttachFilter(filter3);
filter1.CalledWhenTrue.Select(x => x.Item1.Value).Should().Equal(-12, -34, -40);
filter2.CalledWhenTrue.Select(x => x.Item1.Value).Should().Equal(-40, -34, -12);
filter3.CalledWhenTrue.Select(x => x.Item1.Value).Should().Equal(-40, -34, -12);
dict.Add(99, -100);
filter1.CalledOnCollectionChanged.Select(x => (x.changedKind, x.value.Value)).Should().Equal((ChangedKind.Add, -100));
filter2.CalledOnCollectionChanged.Select(x => (x.changedKind, x.value.Value)).Should().Equal((ChangedKind.Add, -100));
filter3.CalledOnCollectionChanged.Select(x => (x.changedKind, x.value.Value)).Should().Equal((ChangedKind.Add, -100));
foreach (var item in new[] { filter1, filter2, filter3 }) item.CalledOnCollectionChanged.Clear();
dict[10] = -1090;
filter1.CalledOnCollectionChanged.Select(x => (x.changedKind, x.value.Value)).Should().Equal((ChangedKind.Remove, -12), (ChangedKind.Add, -1090));
filter2.CalledOnCollectionChanged.Select(x => (x.changedKind, x.value.Value)).Should().Equal((ChangedKind.Remove, -12), (ChangedKind.Add, -1090));
filter3.CalledOnCollectionChanged.Select(x => (x.changedKind, x.value.Value)).Should().Equal((ChangedKind.Remove, -12), (ChangedKind.Add, -1090));
foreach (var item in new[] { filter1, filter2, filter3 }) item.CalledOnCollectionChanged.Clear();
dict.Remove(20);
filter1.CalledOnCollectionChanged.Select(x => (x.changedKind, x.value.Value)).Should().Equal((ChangedKind.Remove, -25));
filter2.CalledOnCollectionChanged.Select(x => (x.changedKind, x.value.Value)).Should().Equal((ChangedKind.Remove, -25));
filter3.CalledOnCollectionChanged.Select(x => (x.changedKind, x.value.Value)).Should().Equal((ChangedKind.Remove, -25));
foreach (var item in new[] { filter1, filter2, filter3 }) item.CalledOnCollectionChanged.Clear();
dict.Clear();
filter1.CalledOnCollectionChanged.Select(x => (x.changedKind, x.value.Value))
.OrderBy(x => x.Value)
.Should().Equal((ChangedKind.Remove, -1090), (ChangedKind.Remove, -100), (ChangedKind.Remove, -53), (ChangedKind.Remove, -40), (ChangedKind.Remove, -34));
filter2.CalledOnCollectionChanged.Select(x => (x.changedKind, x.value.Value))
.OrderBy(x => x.Value)
.Should().Equal((ChangedKind.Remove, -1090), (ChangedKind.Remove, -100), (ChangedKind.Remove, -53), (ChangedKind.Remove, -40), (ChangedKind.Remove, -34));
filter3.CalledOnCollectionChanged.Select(x => (x.changedKind, x.value.Value))
.OrderBy(x => x.Value)
.Should().Equal((ChangedKind.Remove, -1090), (ChangedKind.Remove, -100), (ChangedKind.Remove, -53), (ChangedKind.Remove, -40), (ChangedKind.Remove, -34));
}
[Fact]
public void FilterAndInvokeAddEvent()
{
var dict = new ObservableDictionary<int, int>();
var view1 = dict.CreateView(x => new ViewContainer<int>(x.Value));
var filter1 = new TestFilter2<int>((x, v) => x.Value % 2 == 0);
dict.Add(10, -12); // 0
dict.Add(50, -53); // 1
dict.Add(30, -34); // 2
dict.Add(20, -25); // 3
dict.Add(40, -40); // 4
view1.AttachFilter(filter1, true);
filter1.CalledOnCollectionChanged.Count.Should().Be(5);
filter1.CalledOnCollectionChanged[0].changedKind.Should().Be(ChangedKind.Add);
filter1.CalledOnCollectionChanged[0].value.Key.Should().Be(10);
filter1.CalledOnCollectionChanged[1].changedKind.Should().Be(ChangedKind.Add);
filter1.CalledOnCollectionChanged[1].value.Key.Should().Be(50);
filter1.CalledOnCollectionChanged[2].changedKind.Should().Be(ChangedKind.Add);
filter1.CalledOnCollectionChanged[2].value.Key.Should().Be(30);
filter1.CalledOnCollectionChanged[3].changedKind.Should().Be(ChangedKind.Add);
filter1.CalledOnCollectionChanged[3].value.Key.Should().Be(20);
filter1.CalledOnCollectionChanged[4].changedKind.Should().Be(ChangedKind.Add);
filter1.CalledOnCollectionChanged[4].value.Key.Should().Be(40);
filter1.CalledWhenTrue.Count.Should().Be(3);
filter1.CalledWhenFalse.Count.Should().Be(2);
}
}
}

View File

@ -1,7 +1,6 @@
using FluentAssertions;
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
@ -27,6 +26,7 @@ namespace ObservableCollections.Tests
{
set.Should().BeEquivalentTo(expected);
view.Select(x => x.Value).Should().BeEquivalentTo(expected);
view.Select(x => x.View.Value).Should().BeEquivalentTo(expected);
}
Equal(10, 50, 30, 20, 40);
@ -45,21 +45,66 @@ namespace ObservableCollections.Tests
Equal();
}
[Fact]
public void Filter()
{
var set = new ObservableHashSet<int>();
var view = set.CreateView(x => new ViewContainer<int>(x));
var filter = new TestFilter<int>((x, v) => x % 3 == 0);
set.Add(10);
set.Add(50);
set.Add(30);
set.Add(20);
set.Add(40);
view.AttachFilter(filter);
filter.CalledWhenTrue.Select(x => x.Item1).Should().Equal(30);
filter.CalledWhenFalse.Select(x => x.Item1).Should().Equal(10, 50, 20, 40);
view.Select(x => x.Value).Should().Equal(30);
filter.Clear();
set.Add(33);
set.AddRange(new[] { 98 });
filter.CalledOnCollectionChanged.Select(x => (x.changedKind, x.value)).Should().Equal((ChangedKind.Add, 33), (ChangedKind.Add, 98));
filter.Clear();
set.Remove(10);
set.RemoveRange(new[] { 50, 30 });
filter.CalledOnCollectionChanged.Select(x => (x.changedKind, x.value)).Should().Equal((ChangedKind.Remove, 10), (ChangedKind.Remove, 50), (ChangedKind.Remove, 30));
}
[Fact]
public void IndexOutOfRange()
public void FilterAndInvokeAddEvent()
{
// https://github.com/Cysharp/ObservableCollections/pull/51
static IEnumerable<int> Range(int count)
{
foreach (var i in Enumerable.Range(0, count))
{
yield return i;
}
}
var set = new ObservableHashSet<int>();
set.AddRange(Range(20));
}
var view = set.CreateView(x => new ViewContainer<int>(x));
var filter = new TestFilter<int>((x, v) => x % 3 == 0);
set.Add(10);
set.Add(50);
set.Add(30);
set.Add(20);
set.Add(40);
view.AttachFilter(filter, true);
filter.CalledOnCollectionChanged.Count.Should().Be(5);
filter.CalledOnCollectionChanged[0].changedKind.Should().Be(ChangedKind.Add);
filter.CalledOnCollectionChanged[0].value.Should().Be(10);
filter.CalledOnCollectionChanged[1].changedKind.Should().Be(ChangedKind.Add);
filter.CalledOnCollectionChanged[1].value.Should().Be(50);
filter.CalledOnCollectionChanged[2].changedKind.Should().Be(ChangedKind.Add);
filter.CalledOnCollectionChanged[2].value.Should().Be(30);
filter.CalledOnCollectionChanged[3].changedKind.Should().Be(ChangedKind.Add);
filter.CalledOnCollectionChanged[3].value.Should().Be(20);
filter.CalledOnCollectionChanged[4].changedKind.Should().Be(ChangedKind.Add);
filter.CalledOnCollectionChanged[4].value.Should().Be(40);
filter.CalledWhenTrue.Count.Should().Be(1);
filter.CalledWhenFalse.Count.Should().Be(4);
}
}
}

View File

@ -1,7 +1,6 @@
using FluentAssertions;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Linq;
using Xunit;
@ -27,14 +26,14 @@ namespace ObservableCollections.Tests
reference.Should().Equal(expected);
list.Should().Equal(expected);
view.Select(x => x.Value).Should().Equal(expected);
view.Filtered.Select(x => x.View).Should().Equal(expected.Select(x => new ViewContainer<int>(x)));
view.Select(x => x.View).Should().Equal(expected.Select(x => new ViewContainer<int>(x)));
}
void Equal2(params int[] expected)
{
list.Should().Equal(expected);
view.Select(x => x.Value).Should().Equal(expected);
view.Filtered.Select(x => x.View).Should().Equal(expected.Select(x => new ViewContainer<int>(x)));
view.Select(x => x.View).Should().Equal(expected.Select(x => new ViewContainer<int>(x)));
}
Equal(10, 50, 30, 20, 40);
@ -69,107 +68,178 @@ namespace ObservableCollections.Tests
Equal2(100, 400, 200, 300);
}
//[Fact]
//public void FilterTest()
//{
// var list = new ObservableList<int>();
// var view1 = list.CreateView(x => new ViewContainer<int>(x));
// list.AddRange(new[] { 10, 21, 30, 44, 45, 66, 90 });
[Fact]
public void ViewSorted()
{
var list = new ObservableList<int>();
var view1 = list.CreateSortedView(x => x, x => new ViewContainer<int>(x), comparer: Comparer<int>.Default);
var view2 = list.CreateSortedView(x => x, x => new ViewContainer<int>(x), viewComparer: Comparer<ViewContainer<int>>.Default);
var view3 = list.CreateSortedView(x => x, x => new ViewContainer<int>(x), x => x, ascending: true);
var view4 = list.CreateSortedView(x => x, x => new ViewContainer<int>(x), x => x, ascending: false);
// var filter1 = new TestFilter<int>((x, v) => x % 2 == 0);
// var filter2 = new TestFilter<int>((x, v) => x % 2 == 0);
// var filter3 = new TestFilter<int>((x, v) => x % 2 == 0);
// view1.AttachFilter(filter1);
// view2.AttachFilter(filter2);
// view3.AttachFilter(filter3);
list.Add(10); // 0
list.Add(50); // 1
list.Add(30); // 2
list.Add(20); // 3
list.Add(40); // 4
// filter1.CalledWhenTrue.Select(x => x.Item1).Should().Equal(10, 30, 44, 66, 90);
// filter2.CalledWhenTrue.Select(x => x.Item1).Should().Equal(10, 30, 44, 66, 90);
// filter3.CalledWhenTrue.Select(x => x.Item1).Should().Equal(10, 30, 44, 66, 90);
void Equal(params int[] expected)
{
list.Should().Equal(expected);
// filter1.CalledWhenFalse.Select(x => x.Item1).Should().Equal(21, 45);
// filter2.CalledWhenFalse.Select(x => x.Item1).Should().Equal(21, 45);
// filter3.CalledWhenFalse.Select(x => x.Item1).Should().Equal(21, 45);
var sorted = expected.OrderBy(x => x).ToArray();
view1.Select(x => x.Value).Should().Equal(sorted);
view2.Select(x => x.View).Should().Equal(sorted.Select(x => new ViewContainer<int>(x)));
view3.Select(x => x.Value).Should().Equal(sorted);
view4.Select(x => x.Value).Should().Equal(expected.OrderByDescending(x => x).ToArray());
}
// view1.Select(x => x.Value).Should().Equal(10, 30, 44, 66, 90);
// view2.Select(x => x.Value).Should().Equal(10, 30, 44, 66, 90);
// view3.Select(x => x.Value).Should().Equal(10, 30, 44, 66, 90);
Equal(10, 50, 30, 20, 40);
// filter1.Clear();
// filter2.Clear();
// filter3.Clear();
list.Move(3, 1);
Equal(10, 20, 50, 30, 40);
// list.Add(100);
// list.AddRange(new[] { 101 });
// filter1.CalledWhenTrue.Select(x => x.Item1).Should().Equal(100);
// filter2.CalledWhenTrue.Select(x => x.Item1).Should().Equal(100);
// filter3.CalledWhenTrue.Select(x => x.Item1).Should().Equal(100);
// filter1.CalledWhenFalse.Select(x => x.Item1).Should().Equal(101);
// filter2.CalledWhenFalse.Select(x => x.Item1).Should().Equal(101);
// filter3.CalledWhenFalse.Select(x => x.Item1).Should().Equal(101);
list.Insert(2, 99);
Equal(10, 20, 99, 50, 30, 40);
// filter1.CalledOnCollectionChanged.Select(x => (x.Action, x.NewValue, x.NewViewIndex)).Should().Equal((NotifyCollectionChangedAction.Add, 100, 7), (NotifyCollectionChangedAction.Add, 101, 8));
// filter2.CalledOnCollectionChanged.Select(x => (x.Action, x.NewValue, x.NewViewIndex)).Should().Equal((NotifyCollectionChangedAction.Add, 100, 7), (NotifyCollectionChangedAction.Add, 101, 8));
// filter3.CalledOnCollectionChanged.Select(x => (x.Action, x.NewValue, x.NewViewIndex)).Should().Equal((NotifyCollectionChangedAction.Add, 100, 7), (NotifyCollectionChangedAction.Add, 101, 8));
list.RemoveAt(2);
Equal(10, 20, 50, 30, 40);
// foreach (var item in new[] { filter1, filter2, filter3 }) item.CalledOnCollectionChanged.Clear();
list[3] = 88;
Equal(10, 20, 50, 88, 40);
// list.Insert(0, 1000);
// list.InsertRange(0, new[] { 999 });
list.Clear();
Equal(new int[0]);
// filter1.CalledOnCollectionChanged.Select(x => (x.Action, x.NewValue, x.NewViewIndex)).Should().Equal((NotifyCollectionChangedAction.Add, 1000, 0), (NotifyCollectionChangedAction.Add, 999, 0));
// filter2.CalledOnCollectionChanged.Select(x => (x.Action, x.NewValue, x.NewViewIndex)).Should().Equal((NotifyCollectionChangedAction.Add, 1000, 9), (NotifyCollectionChangedAction.Add, 999, 9)); // sorted index
// filter3.CalledOnCollectionChanged.Select(x => (x.Action, x.NewValue, x.NewViewIndex)).Should().Equal((NotifyCollectionChangedAction.Add, 1000, 9), (NotifyCollectionChangedAction.Add, 999, 9)); // sorted index
// foreach (var item in new[] { filter1, filter2, filter3 }) item.CalledOnCollectionChanged.Clear();
list.AddRange(new[] { 100, 200, 300 });
Equal(100, 200, 300);
// list.RemoveAt(0);
// list.RemoveRange(0, 1);
list.InsertRange(1, new[] { 400, 500, 600 });
Equal(100, 400, 500, 600, 200, 300);
// filter1.CalledOnCollectionChanged.Select(x => (x.Action, x.OldValue, x.OldViewIndex)).Should().Equal((NotifyCollectionChangedAction.Remove, 999, 0), (NotifyCollectionChangedAction.Remove, 1000, 0));
// filter2.CalledOnCollectionChanged.Select(x => (x.Action, x.OldValue, x.OldViewIndex)).Should().Equal((NotifyCollectionChangedAction.Remove, 999, 9), (NotifyCollectionChangedAction.Remove, 1000, 9));
// filter3.CalledOnCollectionChanged.Select(x => (x.Action, x.OldValue, x.OldViewIndex)).Should().Equal((NotifyCollectionChangedAction.Remove, 999, 9), (NotifyCollectionChangedAction.Remove, 1000, 9));
// foreach (var item in new[] { filter1, filter2, filter3 }) item.CalledOnCollectionChanged.Clear();
list.RemoveRange(2, 2);
Equal(100, 400, 200, 300);
}
// list[0] = 9999;
[Fact]
public void Freezed()
{
var list = new FreezedList<int>(new[] { 10, 20, 50, 30, 40, 60 });
// filter1.CalledOnCollectionChanged.Select(x => (x.Action, x.NewValue, x.NewViewIndex, x.OldViewIndex)).Should().Equal((NotifyCollectionChangedAction.Replace, 9999, 0, 0));
// filter2.CalledOnCollectionChanged.Select(x => (x.Action, x.NewValue, x.NewViewIndex, x.OldViewIndex)).Should().Equal((NotifyCollectionChangedAction.Replace, 9999, 8, 0));
// filter3.CalledOnCollectionChanged.Select(x => (x.Action, x.NewValue, x.NewViewIndex, x.OldViewIndex)).Should().Equal((NotifyCollectionChangedAction.Replace, 9999, 8, 0));
// foreach (var item in new[] { filter1, filter2, filter3 }) item.CalledOnCollectionChanged.Clear();
var view = list.CreateSortableView(x => new ViewContainer<int>(x));
// list.Move(3, 0);
// filter1.CalledOnCollectionChanged.Select(x => (x.Action, x.NewValue, x.NewViewIndex, x.OldViewIndex)).Should().Equal((NotifyCollectionChangedAction.Move, 44, 0, 3));
// filter2.CalledOnCollectionChanged.Select(x => (x.Action, x.NewValue, x.NewViewIndex, x.OldViewIndex)).Should().Equal((NotifyCollectionChangedAction.Move, 44, 2, 2));
// filter3.CalledOnCollectionChanged.Select(x => (x.Action, x.NewValue, x.NewViewIndex, x.OldViewIndex)).Should().Equal((NotifyCollectionChangedAction.Move, 44, 2, 2));
// foreach (var item in new[] { filter1, filter2, filter3 }) item.CalledOnCollectionChanged.Clear();
view.Sort(x => x, true);
view.Select(x => x.Value).Should().Equal(10, 20, 30, 40, 50, 60);
view.Select(x => x.View).Should().Equal(10, 20, 30, 40, 50, 60);
// list.Clear();
// filter1.CalledOnCollectionChanged.Select(x => x.Action).Should().Equal(NotifyCollectionChangedAction.Reset);
// filter2.CalledOnCollectionChanged.Select(x => x.Action).Should().Equal(NotifyCollectionChangedAction.Reset);
// filter3.CalledOnCollectionChanged.Select(x => x.Action).Should().Equal(NotifyCollectionChangedAction.Reset);
//}
view.Sort(x => x, false);
view.Select(x => x.Value).Should().Equal(60, 50, 40, 30, 20, 10);
view.Select(x => x.View).Should().Equal(60, 50, 40, 30, 20, 10);
}
//[Fact]
//public void FilterAndInvokeAddEvent()
//{
// var list = new ObservableList<int>();
// var view1 = list.CreateView(x => new ViewContainer<int>(x));
// list.AddRange(new[] { 10, 21, 30, 44 });
[Fact]
public void FilterTest()
{
var list = new ObservableList<int>();
var view1 = list.CreateView(x => new ViewContainer<int>(x));
var view2 = list.CreateSortedView(x => x, x => new ViewContainer<int>(x), comparer: Comparer<int>.Default);
var view3 = list.CreateSortedView(x => x, x => new ViewContainer<int>(x), viewComparer: Comparer<ViewContainer<int>>.Default);
list.AddRange(new[] { 10, 21, 30, 44, 45, 66, 90 });
// var filter1 = new TestFilter<int>((x, v) => x % 2 == 0);
// view1.AttachFilter((x, v) => x % 2 == 0));
var filter1 = new TestFilter<int>((x, v) => x % 2 == 0);
var filter2 = new TestFilter<int>((x, v) => x % 2 == 0);
var filter3 = new TestFilter<int>((x, v) => x % 2 == 0);
view1.AttachFilter(filter1);
view2.AttachFilter(filter2);
view3.AttachFilter(filter3);
// filter1.CalledOnCollectionChanged[0].Action.Should().Be(NotifyCollectionChangedAction.Add);
// filter1.CalledOnCollectionChanged[0].NewValue.Should().Be(10);
// filter1.CalledOnCollectionChanged[1].Action.Should().Be(NotifyCollectionChangedAction.Add);
// filter1.CalledOnCollectionChanged[1].NewValue.Should().Be(21);
// filter1.CalledOnCollectionChanged[2].Action.Should().Be(NotifyCollectionChangedAction.Add);
// filter1.CalledOnCollectionChanged[2].NewValue.Should().Be(30);
// filter1.CalledOnCollectionChanged[3].Action.Should().Be(NotifyCollectionChangedAction.Add);
// filter1.CalledOnCollectionChanged[3].NewValue.Should().Be(44);
filter1.CalledWhenTrue.Select(x => x.Item1).Should().Equal(10, 30, 44, 66, 90);
filter2.CalledWhenTrue.Select(x => x.Item1).Should().Equal(10, 30, 44, 66, 90);
filter3.CalledWhenTrue.Select(x => x.Item1).Should().Equal(10, 30, 44, 66, 90);
// filter1.CalledWhenTrue.Count.Should().Be(3);
// filter1.CalledWhenFalse.Count.Should().Be(1);
//}
filter1.CalledWhenFalse.Select(x => x.Item1).Should().Equal(21, 45);
filter2.CalledWhenFalse.Select(x => x.Item1).Should().Equal(21, 45);
filter3.CalledWhenFalse.Select(x => x.Item1).Should().Equal(21, 45);
view1.Select(x => x.Value).Should().Equal(10, 30, 44, 66, 90);
view2.Select(x => x.Value).Should().Equal(10, 30, 44, 66, 90);
view3.Select(x => x.Value).Should().Equal(10, 30, 44, 66, 90);
filter1.Clear();
filter2.Clear();
filter3.Clear();
list.Add(100);
list.AddRange(new[] { 101 });
filter1.CalledWhenTrue.Select(x => x.Item1).Should().Equal(100);
filter2.CalledWhenTrue.Select(x => x.Item1).Should().Equal(100);
filter3.CalledWhenTrue.Select(x => x.Item1).Should().Equal(100);
filter1.CalledWhenFalse.Select(x => x.Item1).Should().Equal(101);
filter2.CalledWhenFalse.Select(x => x.Item1).Should().Equal(101);
filter3.CalledWhenFalse.Select(x => x.Item1).Should().Equal(101);
filter1.CalledOnCollectionChanged.Select(x => (x.changedKind, x.value)).Should().Equal((ChangedKind.Add, 100), (ChangedKind.Add, 101));
filter2.CalledOnCollectionChanged.Select(x => (x.changedKind, x.value)).Should().Equal((ChangedKind.Add, 100), (ChangedKind.Add, 101));
filter3.CalledOnCollectionChanged.Select(x => (x.changedKind, x.value)).Should().Equal((ChangedKind.Add, 100), (ChangedKind.Add, 101));
foreach (var item in new[] { filter1, filter2, filter3 }) item.CalledOnCollectionChanged.Clear();
list.Insert(0, 1000);
list.InsertRange(0, new[] { 999 });
filter1.CalledOnCollectionChanged.Select(x => (x.changedKind, x.value)).Should().Equal((ChangedKind.Add, 1000), (ChangedKind.Add, 999));
filter2.CalledOnCollectionChanged.Select(x => (x.changedKind, x.value)).Should().Equal((ChangedKind.Add, 1000), (ChangedKind.Add, 999));
filter3.CalledOnCollectionChanged.Select(x => (x.changedKind, x.value)).Should().Equal((ChangedKind.Add, 1000), (ChangedKind.Add, 999));
foreach (var item in new[] { filter1, filter2, filter3 }) item.CalledOnCollectionChanged.Clear();
list.RemoveAt(0);
list.RemoveRange(0, 1);
filter1.CalledOnCollectionChanged.Select(x => (x.changedKind, x.value)).Should().Equal((ChangedKind.Remove, 999), (ChangedKind.Remove, 1000));
filter2.CalledOnCollectionChanged.Select(x => (x.changedKind, x.value)).Should().Equal((ChangedKind.Remove, 999), (ChangedKind.Remove, 1000));
filter3.CalledOnCollectionChanged.Select(x => (x.changedKind, x.value)).Should().Equal((ChangedKind.Remove, 999), (ChangedKind.Remove, 1000));
foreach (var item in new[] { filter1, filter2, filter3 }) item.CalledOnCollectionChanged.Clear();
list[0] = 9999;
filter1.CalledOnCollectionChanged.Select(x => (x.changedKind, x.value)).Should().Equal((ChangedKind.Remove, 10), (ChangedKind.Add, 9999));
filter2.CalledOnCollectionChanged.Select(x => (x.changedKind, x.value)).Should().Equal((ChangedKind.Remove, 10), (ChangedKind.Add, 9999));
filter3.CalledOnCollectionChanged.Select(x => (x.changedKind, x.value)).Should().Equal((ChangedKind.Remove, 10), (ChangedKind.Add, 9999));
foreach (var item in new[] { filter1, filter2, filter3 }) item.CalledOnCollectionChanged.Clear();
list.Move(3, 0);
filter1.CalledOnCollectionChanged.Select(x => (x.changedKind, x.value)).Should().Equal((ChangedKind.Move, 44));
filter2.CalledOnCollectionChanged.Select(x => (x.changedKind, x.value)).Should().Equal((ChangedKind.Move, 44));
filter3.CalledOnCollectionChanged.Select(x => (x.changedKind, x.value)).Should().Equal((ChangedKind.Move, 44));
foreach (var item in new[] { filter1, filter2, filter3 }) item.CalledOnCollectionChanged.Clear();
list.Clear();
filter1.CalledOnCollectionChanged.Select(x => (x.changedKind, x.value)).Should().Equal((ChangedKind.Remove, 44), (ChangedKind.Remove, 9999), (ChangedKind.Remove, 21), (ChangedKind.Remove, 30), (ChangedKind.Remove, 45), (ChangedKind.Remove, 66), (ChangedKind.Remove, 90), (ChangedKind.Remove, 100), (ChangedKind.Remove, 101));
filter2.CalledOnCollectionChanged.Select(x => (x.changedKind, x.value)).Should().Equal((ChangedKind.Remove, 21), (ChangedKind.Remove, 30), (ChangedKind.Remove, 44), (ChangedKind.Remove, 45), (ChangedKind.Remove, 66), (ChangedKind.Remove, 90), (ChangedKind.Remove, 100), (ChangedKind.Remove, 101), (ChangedKind.Remove, 9999));
filter3.CalledOnCollectionChanged.Select(x => (x.changedKind, x.value)).Should().Equal((ChangedKind.Remove, 21), (ChangedKind.Remove, 30), (ChangedKind.Remove, 44), (ChangedKind.Remove, 45), (ChangedKind.Remove, 66), (ChangedKind.Remove, 90), (ChangedKind.Remove, 100), (ChangedKind.Remove, 101), (ChangedKind.Remove, 9999));
}
[Fact]
public void FilterAndInvokeAddEvent()
{
var list = new ObservableList<int>();
var view1 = list.CreateView(x => new ViewContainer<int>(x));
list.AddRange(new[] { 10, 21, 30, 44 });
var filter1 = new TestFilter<int>((x, v) => x % 2 == 0);
view1.AttachFilter(filter1, true);
filter1.CalledOnCollectionChanged[0].changedKind.Should().Be(ChangedKind.Add);
filter1.CalledOnCollectionChanged[0].value.Should().Be(10);
filter1.CalledOnCollectionChanged[1].changedKind.Should().Be(ChangedKind.Add);
filter1.CalledOnCollectionChanged[1].value.Should().Be(21);
filter1.CalledOnCollectionChanged[2].changedKind.Should().Be(ChangedKind.Add);
filter1.CalledOnCollectionChanged[2].value.Should().Be(30);
filter1.CalledOnCollectionChanged[3].changedKind.Should().Be(ChangedKind.Add);
filter1.CalledOnCollectionChanged[3].value.Should().Be(44);
filter1.CalledWhenTrue.Count.Should().Be(3);
filter1.CalledWhenFalse.Count.Should().Be(1);
}
}
}

View File

@ -1,7 +1,6 @@
using FluentAssertions;
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
@ -27,6 +26,7 @@ namespace ObservableCollections.Tests
{
queue.Should().Equal(expected);
view.Select(x => x.Value).Should().Equal(expected);
view.Select(x => x.View).Should().Equal(expected.Select(x => new ViewContainer<int>(x)));
}
Equal(10, 50, 30, 20, 40);
@ -49,72 +49,67 @@ namespace ObservableCollections.Tests
Equal();
}
//[Fact]
//public void Filter()
//{
// var queue = new ObservableQueue<int>();
// var view = queue.CreateView(x => new ViewContainer<int>(x));
// var filter = new TestFilter<int>((x, v) => x % 3 == 0);
[Fact]
public void Filter()
{
var queue = new ObservableQueue<int>();
var view = queue.CreateView(x => new ViewContainer<int>(x));
var filter = new TestFilter<int>((x, v) => x % 3 == 0);
// queue.Enqueue(10);
// queue.Enqueue(50);
// queue.Enqueue(30);
// queue.Enqueue(20);
// queue.Enqueue(40);
queue.Enqueue(10);
queue.Enqueue(50);
queue.Enqueue(30);
queue.Enqueue(20);
queue.Enqueue(40);
// view.AttachFilter(filter);
// filter.CalledWhenTrue.Select(x => x.Item1).Should().Equal(30);
// filter.CalledWhenFalse.Select(x => x.Item1).Should().Equal(10, 50, 20, 40);
view.AttachFilter(filter);
filter.CalledWhenTrue.Select(x => x.Item1).Should().Equal(30);
filter.CalledWhenFalse.Select(x => x.Item1).Should().Equal(10, 50, 20, 40);
// view.Select(x => x.Value).Should().Equal(30);
view.Select(x => x.Value).Should().Equal(30);
// filter.Clear();
filter.Clear();
// queue.Enqueue(33);
// queue.EnqueueRange(new[] { 98 });
queue.Enqueue(33);
queue.EnqueueRange(new[] { 98 });
// filter.CalledOnCollectionChanged.Select(x => (x.Action, x.NewValue, x.NewViewIndex)).Should().Equal((NotifyCollectionChangedAction.Add, 33, 5), (NotifyCollectionChangedAction.Add, 98, 6));
// filter.Clear();
filter.CalledOnCollectionChanged.Select(x => (x.changedKind, x.value)).Should().Equal((ChangedKind.Add, 33), (ChangedKind.Add, 98));
filter.Clear();
// queue.Dequeue();
// queue.DequeueRange(2);
// filter.CalledOnCollectionChanged.Select(x => (x.Action, x.OldValue, x.OldViewIndex)).Should().Equal((NotifyCollectionChangedAction.Remove, 10, 0), (NotifyCollectionChangedAction.Remove, 50, 0), (NotifyCollectionChangedAction.Remove, 30, 0));
//}
queue.Dequeue();
queue.DequeueRange(2);
filter.CalledOnCollectionChanged.Select(x => (x.changedKind, x.value)).Should().Equal((ChangedKind.Remove, 10), (ChangedKind.Remove, 50), (ChangedKind.Remove, 30));
}
//[Fact]
//public void FilterAndInvokeAddEvent()
//{
// var queue = new ObservableQueue<int>();
// var view = queue.CreateView(x => new ViewContainer<int>(x));
// var filter = new TestFilter<int>((x, v) => x % 3 == 0);
[Fact]
public void FilterAndInvokeAddEvent()
{
var queue = new ObservableQueue<int>();
var view = queue.CreateView(x => new ViewContainer<int>(x));
var filter = new TestFilter<int>((x, v) => x % 3 == 0);
// queue.Enqueue(10);
// queue.Enqueue(50);
// queue.Enqueue(30);
// queue.Enqueue(20);
// queue.Enqueue(40);
queue.Enqueue(10);
queue.Enqueue(50);
queue.Enqueue(30);
queue.Enqueue(20);
queue.Enqueue(40);
// view.AttachFilter(filter, true);
view.AttachFilter(filter, true);
// filter.CalledOnCollectionChanged.Count.Should().Be(5);
// filter.CalledOnCollectionChanged[0].Action.Should().Be(NotifyCollectionChangedAction.Add);
// filter.CalledOnCollectionChanged[0].NewValue.Should().Be(10);
// filter.CalledOnCollectionChanged[0].NewViewIndex.Should().Be(0);
// filter.CalledOnCollectionChanged[1].Action.Should().Be(NotifyCollectionChangedAction.Add);
// filter.CalledOnCollectionChanged[1].NewValue.Should().Be(50);
// filter.CalledOnCollectionChanged[1].NewViewIndex.Should().Be(1);
// filter.CalledOnCollectionChanged[2].Action.Should().Be(NotifyCollectionChangedAction.Add);
// filter.CalledOnCollectionChanged[2].NewValue.Should().Be(30);
// filter.CalledOnCollectionChanged[2].NewViewIndex.Should().Be(2);
// filter.CalledOnCollectionChanged[3].Action.Should().Be(NotifyCollectionChangedAction.Add);
// filter.CalledOnCollectionChanged[3].NewValue.Should().Be(20);
// filter.CalledOnCollectionChanged[3].NewViewIndex.Should().Be(3);
// filter.CalledOnCollectionChanged[4].Action.Should().Be(NotifyCollectionChangedAction.Add);
// filter.CalledOnCollectionChanged[4].NewValue.Should().Be(40);
// filter.CalledOnCollectionChanged[4].NewViewIndex.Should().Be(4);
filter.CalledOnCollectionChanged.Count.Should().Be(5);
filter.CalledOnCollectionChanged[0].changedKind.Should().Be(ChangedKind.Add);
filter.CalledOnCollectionChanged[0].value.Should().Be(10);
filter.CalledOnCollectionChanged[1].changedKind.Should().Be(ChangedKind.Add);
filter.CalledOnCollectionChanged[1].value.Should().Be(50);
filter.CalledOnCollectionChanged[2].changedKind.Should().Be(ChangedKind.Add);
filter.CalledOnCollectionChanged[2].value.Should().Be(30);
filter.CalledOnCollectionChanged[3].changedKind.Should().Be(ChangedKind.Add);
filter.CalledOnCollectionChanged[3].value.Should().Be(20);
filter.CalledOnCollectionChanged[4].changedKind.Should().Be(ChangedKind.Add);
filter.CalledOnCollectionChanged[4].value.Should().Be(40);
// filter.CalledWhenTrue.Count.Should().Be(1);
// filter.CalledWhenFalse.Count.Should().Be(4);
//}
filter.CalledWhenTrue.Count.Should().Be(1);
filter.CalledWhenFalse.Count.Should().Be(4);
}
}
}

View File

@ -24,6 +24,7 @@ namespace ObservableCollections.Tests
{
buf.Should().Equal(expected);
view.Select(x => x.Value).Should().Equal(expected);
view.Select(x => x.View).Should().Equal(expected.Select(x => new ViewContainer<int>(x)));
}
Equal(10, 50, 30, 20, 40);
@ -64,6 +65,7 @@ namespace ObservableCollections.Tests
{
buf.Should().Equal(expected);
view.Select(x => x.Value).Should().Equal(expected);
view.Select(x => x.View).Should().Equal(expected.Select(x => new ViewContainer<int>(x)));
}
buf.AddLast(10);

View File

@ -1,7 +1,6 @@
using FluentAssertions;
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
@ -27,6 +26,7 @@ namespace ObservableCollections.Tests
{
stack.Should().Equal(expected);
view.Select(x => x.Value).Should().Equal(expected);
view.Select(x => x.View).Should().Equal(expected.Select(x => new ViewContainer<int>(x)));
}
Equal(40, 20, 30, 50, 10);
@ -49,6 +49,67 @@ namespace ObservableCollections.Tests
Equal();
}
[Fact]
public void Filter()
{
var stack = new ObservableStack<int>();
var view = stack.CreateView(x => new ViewContainer<int>(x));
var filter = new TestFilter<int>((x, v) => x % 3 == 0);
stack.Push(10);
stack.Push(50);
stack.Push(30);
stack.Push(20);
stack.Push(40);
view.AttachFilter(filter);
filter.CalledWhenTrue.Select(x => x.Item1).Should().Equal(30);
filter.CalledWhenFalse.Select(x => x.Item1).Should().Equal(40, 20, 50, 10);
view.Select(x => x.Value).Should().Equal(30);
filter.Clear();
stack.Push(33);
stack.PushRange(new[] { 98 });
filter.CalledOnCollectionChanged.Select(x => (x.changedKind, x.value)).Should().Equal((ChangedKind.Add, 33), (ChangedKind.Add, 98));
filter.Clear();
stack.Pop();
stack.PopRange(2);
filter.CalledOnCollectionChanged.Select(x => (x.changedKind, x.value)).Should().Equal((ChangedKind.Remove, 98), (ChangedKind.Remove, 33), (ChangedKind.Remove, 40));
}
[Fact]
public void FilterAndInvokeAddEvent()
{
var stack = new ObservableStack<int>();
var view = stack.CreateView(x => new ViewContainer<int>(x));
var filter = new TestFilter<int>((x, v) => x % 3 == 0);
stack.Push(10);
stack.Push(50);
stack.Push(30);
stack.Push(20);
stack.Push(40);
view.AttachFilter(filter, true);
filter.CalledOnCollectionChanged.Count.Should().Be(5);
filter.CalledOnCollectionChanged[4].changedKind.Should().Be(ChangedKind.Add);
filter.CalledOnCollectionChanged[4].value.Should().Be(10);
filter.CalledOnCollectionChanged[3].changedKind.Should().Be(ChangedKind.Add);
filter.CalledOnCollectionChanged[3].value.Should().Be(50);
filter.CalledOnCollectionChanged[2].changedKind.Should().Be(ChangedKind.Add);
filter.CalledOnCollectionChanged[2].value.Should().Be(30);
filter.CalledOnCollectionChanged[1].changedKind.Should().Be(ChangedKind.Add);
filter.CalledOnCollectionChanged[1].value.Should().Be(20);
filter.CalledOnCollectionChanged[0].changedKind.Should().Be(ChangedKind.Add);
filter.CalledOnCollectionChanged[0].value.Should().Be(40);
filter.CalledWhenTrue.Count.Should().Be(1);
filter.CalledWhenFalse.Count.Should().Be(4);
}
}
}

View File

@ -0,0 +1,69 @@
using System.Collections.Generic;
namespace ObservableCollections.Tests;
public class SortedViewTest
{
[Fact]
public void Sort()
{
var list = new ObservableList<int>();
var sortedView = list.CreateSortedView(
x => x,
x => new ViewContainer<int>(x),
Comparer<int>.Default);
list.Add(10);
list.Add(50);
list.Add(30);
list.Add(20);
list.Add(40);
using var e = sortedView.GetEnumerator();
e.MoveNext().Should().BeTrue();
e.Current.Value.Should().Be(10);
e.MoveNext().Should().BeTrue();
e.Current.Value.Should().Be(20);
e.MoveNext().Should().BeTrue();
e.Current.Value.Should().Be(30);
e.MoveNext().Should().BeTrue();
e.Current.Value.Should().Be(40);
e.MoveNext().Should().BeTrue();
e.Current.Value.Should().Be(50);
e.MoveNext().Should().BeFalse();
}
[Fact]
public void ObserveIndex()
{
var list = new ObservableList<int>();
var sortedView = list.CreateSortedView(
x => x,
x => new ViewContainer<int>(x),
Comparer<int>.Default);
var filter = new TestFilter<int>((value, view) => value % 2 == 0);
list.Add(50);
list.Add(10);
sortedView.AttachFilter(filter);
list.Add(20);
filter.CalledOnCollectionChanged[0].changedKind.Should().Be(ChangedKind.Add);
filter.CalledOnCollectionChanged[0].value.Should().Be(20);
filter.CalledOnCollectionChanged[0].index.Should().Be(1);
list.Remove(20);
filter.CalledOnCollectionChanged[1].changedKind.Should().Be(ChangedKind.Remove);
filter.CalledOnCollectionChanged[1].value.Should().Be(20);
filter.CalledOnCollectionChanged[1].oldIndex.Should().Be(1);
list[1] = 999; // from 10(at 0 in original) to 999
filter.CalledOnCollectionChanged[2].changedKind.Should().Be(ChangedKind.Remove);
filter.CalledOnCollectionChanged[2].value.Should().Be(10);
filter.CalledOnCollectionChanged[2].oldIndex.Should().Be(0);
filter.CalledOnCollectionChanged[3].changedKind.Should().Be(ChangedKind.Add);
filter.CalledOnCollectionChanged[3].value.Should().Be(999);
filter.CalledOnCollectionChanged[3].index.Should().Be(1);
}
}

View File

@ -0,0 +1,69 @@
using System.Collections.Generic;
namespace ObservableCollections.Tests;
public class SortedViewViewComparerTest
{
[Fact]
public void Sort()
{
var list = new ObservableList<int>();
var sortedView = list.CreateSortedView(
x => x,
x => new ViewContainer<int>(x),
Comparer<ViewContainer<int>>.Default);
list.Add(10);
list.Add(50);
list.Add(30);
list.Add(20);
list.Add(40);
using var e = sortedView.GetEnumerator();
e.MoveNext().Should().BeTrue();
e.Current.Value.Should().Be(10);
e.MoveNext().Should().BeTrue();
e.Current.Value.Should().Be(20);
e.MoveNext().Should().BeTrue();
e.Current.Value.Should().Be(30);
e.MoveNext().Should().BeTrue();
e.Current.Value.Should().Be(40);
e.MoveNext().Should().BeTrue();
e.Current.Value.Should().Be(50);
e.MoveNext().Should().BeFalse();
}
[Fact]
public void ObserveIndex()
{
var list = new ObservableList<int>();
var sortedView = list.CreateSortedView(
x => x,
x => new ViewContainer<int>(x),
Comparer<ViewContainer<int>>.Default);
var filter = new TestFilter<int>((value, view) => value % 2 == 0);
list.Add(50);
list.Add(10);
sortedView.AttachFilter(filter);
list.Add(20);
filter.CalledOnCollectionChanged[0].changedKind.Should().Be(ChangedKind.Add);
filter.CalledOnCollectionChanged[0].value.Should().Be(20);
filter.CalledOnCollectionChanged[0].index.Should().Be(1);
list.Remove(20);
filter.CalledOnCollectionChanged[1].changedKind.Should().Be(ChangedKind.Remove);
filter.CalledOnCollectionChanged[1].value.Should().Be(20);
filter.CalledOnCollectionChanged[1].oldIndex.Should().Be(1);
list[1] = 999; // from 10(at 0 in original) to 999
filter.CalledOnCollectionChanged[2].changedKind.Should().Be(ChangedKind.Remove);
filter.CalledOnCollectionChanged[2].value.Should().Be(10);
filter.CalledOnCollectionChanged[2].oldIndex.Should().Be(0);
filter.CalledOnCollectionChanged[3].changedKind.Should().Be(ChangedKind.Add);
filter.CalledOnCollectionChanged[3].value.Should().Be(999);
filter.CalledOnCollectionChanged[3].index.Should().Be(1);
}
}

View File

@ -1,5 +1,3 @@
using ObservableCollections;
namespace ObservableCollections.Tests;
public class ToNotifyCollectionChangedTest
@ -45,7 +43,7 @@ public class ToNotifyCollectionChangedTest
var view = list.CreateView(x => $"${x}");
var notify = view.ToNotifyCollectionChanged();
view.AttachFilter((value) => value % 2 == 0);
view.AttachFilter((value, view) => value % 2 == 0);
list.Add(4);

View File

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
namespace ObservableCollections.Tests
{
@ -13,7 +12,7 @@ namespace ObservableCollections.Tests
public T Value { get; }
public static implicit operator ViewContainer<T>(T value) => new(value);
public static implicit operator ViewContainer<T>(T value) => new ViewContainer<T>(value);
public override int GetHashCode()
{
@ -31,81 +30,83 @@ namespace ObservableCollections.Tests
}
}
//public class TestFilter<T> : ISynchronizedViewFilter<T>
//{
// readonly Func<T, bool> filter;
// public List<SynchronizedViewChangedEventArgs<T, ViewContainer<T>>> CalledOnCollectionChanged = new();
public class TestFilter<T> : ISynchronizedViewFilter<T, ViewContainer<T>>
{
readonly Func<T, ViewContainer<T>, bool> filter;
public List<(T, ViewContainer<T>)> CalledWhenTrue = new();
public List<(T, ViewContainer<T>)> CalledWhenFalse = new();
public List<(ChangedKind changedKind, T value, ViewContainer<T> view, int index, int oldIndex)> CalledOnCollectionChanged = new();
// public TestFilter(Func<T, bool> filter)
// {
// this.filter = filter;
// }
public TestFilter(Func<T, ViewContainer<T>, bool> filter)
{
this.filter = filter;
}
// public void Clear()
// {
// CalledWhenTrue.Clear();
// CalledWhenFalse.Clear();
// CalledOnCollectionChanged.Clear();
// }
public void Clear()
{
CalledWhenTrue.Clear();
CalledWhenFalse.Clear();
CalledOnCollectionChanged.Clear();
}
// public bool IsMatch(T value)
// {
// return this.filter.Invoke(value);
// }
public bool IsMatch(T value, ViewContainer<T> view)
{
return this.filter.Invoke(value, view);
}
// public void OnCollectionChanged(in SynchronizedViewChangedEventArgs<T, ViewContainer<T>> args)
// {
// CalledOnCollectionChanged.Add(args);
// }
public void OnCollectionChanged(ChangedKind changedKind, T value, ViewContainer<T> view, in NotifyCollectionChangedEventArgs<T> eventArgs)
{
CalledOnCollectionChanged.Add((changedKind, value, view, eventArgs.NewStartingIndex, eventArgs.OldStartingIndex));
}
// public void WhenTrue(T value, ViewContainer<T> view)
// {
// CalledWhenTrue.Add((value, view));
// }
public void WhenTrue(T value, ViewContainer<T> view)
{
CalledWhenTrue.Add((value, view));
}
// public void WhenFalse(T value, ViewContainer<T> view)
// {
// CalledWhenFalse.Add((value, view));
// }
//}
public void WhenFalse(T value, ViewContainer<T> view)
{
CalledWhenFalse.Add((value, view));
}
}
//public class TestFilter2<T> : ISynchronizedViewFilter<KeyValuePair<T, T>, ViewContainer<T>>
//{
// readonly Func<KeyValuePair<T, T>, ViewContainer<T>, bool> filter;
// public List<(KeyValuePair<T, T>, ViewContainer<T>)> CalledWhenTrue = new();
// public List<(KeyValuePair<T, T>, ViewContainer<T>)> CalledWhenFalse = new();
// public List<SynchronizedViewChangedEventArgs<KeyValuePair<T, T>, ViewContainer<T>>> CalledOnCollectionChanged = new();
public class TestFilter2<T> : ISynchronizedViewFilter<KeyValuePair<T, T>, ViewContainer<T>>
{
readonly Func<KeyValuePair<T, T>, ViewContainer<T>, bool> filter;
public List<(KeyValuePair<T, T>, ViewContainer<T>)> CalledWhenTrue = new();
public List<(KeyValuePair<T, T>, ViewContainer<T>)> CalledWhenFalse = new();
public List<(ChangedKind changedKind, KeyValuePair<T, T> value, ViewContainer<T> view)> CalledOnCollectionChanged = new();
// public TestFilter2(Func<KeyValuePair<T, T>, ViewContainer<T>, bool> filter)
// {
// this.filter = filter;
// }
public TestFilter2(Func<KeyValuePair<T, T>, ViewContainer<T>, bool> filter)
{
this.filter = filter;
}
// public void Clear()
// {
// CalledWhenTrue.Clear();
// CalledWhenFalse.Clear();
// CalledOnCollectionChanged.Clear();
// }
public void Clear()
{
CalledWhenTrue.Clear();
CalledWhenFalse.Clear();
CalledOnCollectionChanged.Clear();
}
// public bool IsMatch(KeyValuePair<T, T> value, ViewContainer<T> view)
// {
// return this.filter.Invoke(value, view);
// }
public bool IsMatch(KeyValuePair<T, T> value, ViewContainer<T> view)
{
return this.filter.Invoke(value, view);
}
// public void OnCollectionChanged(in SynchronizedViewChangedEventArgs<KeyValuePair<T, T>, ViewContainer<T>> args)
// {
// CalledOnCollectionChanged.Add(args);
// }
public void OnCollectionChanged(ChangedKind changedKind, KeyValuePair<T, T> value, ViewContainer<T> view, in NotifyCollectionChangedEventArgs<KeyValuePair<T, T>> eventArgs)
{
CalledOnCollectionChanged.Add((changedKind, value, view));
}
// public void WhenTrue(KeyValuePair<T, T> value, ViewContainer<T> view)
// {
// CalledWhenTrue.Add((value, view));
// }
public void WhenTrue(KeyValuePair<T, T> value, ViewContainer<T> view)
{
CalledWhenTrue.Add((value, view));
}
// public void WhenFalse(KeyValuePair<T, T> value, ViewContainer<T> view)
// {
// CalledWhenFalse.Add((value, view));
// }
//}
public void WhenFalse(KeyValuePair<T, T> value, ViewContainer<T> view)
{
CalledWhenFalse.Add((value, view));
}
}
}