Merge pull request #65 from Cysharp/rework-filter-and-view
Rework filter and view(v3)
This commit is contained in:
commit
074f52a15d
551
README.md
551
README.md
@ -3,11 +3,9 @@
|
||||
|
||||
ObservableCollections is a high performance observable collections(`ObservableList<T>`, `ObservableDictionary<TKey, TValue>`, `ObservableHashSet<T>`, `ObservableQueue<T>`, `ObservableStack<T>`, `ObservableRingBuffer<T>`, `ObservableFixedSizeRingBuffer<T>`) with synchronized views and Observe Extension for [R3](https://github.com/Cysharp/R3).
|
||||
|
||||
.NET has [`ObservableCollection<T>`](https://docs.microsoft.com/en-us/dotnet/api/system.collections.objectmodel.observablecollection-1), however it has many lacks of features.
|
||||
.NET has [`ObservableCollection<T>`](https://docs.microsoft.com/en-us/dotnet/api/system.collections.objectmodel.observablecollection-1), however it has many lacks of features. It based `INotifyCollectionChanged`, `NotifyCollectionChangedEventHandler` and `NotifyCollectionChangedEventArgs`. There are no generics so everything boxed, allocate memory every time. Also `NotifyCollectionChangedEventArgs` holds all values to `IList` even if it is single value, this also causes allocations. `ObservableCollection<T>` has no Range feature so a lot of wastage occurs when adding multiple values, because it is a single value notification. Also, it is not thread-safe is hard to do linkage with the notifier.
|
||||
|
||||
It based `INotifyCollectionChanged`, `NotifyCollectionChangedEventHandler` and `NotifyCollectionChangedEventArgs`. There are no generics so everything boxed, allocate memory every time. Also `NotifyCollectionChangedEventArgs` holds all values to `IList` even if it is single value, this also causes allocations. `ObservableCollection<T>` has no Range feature so a lot of wastage occurs when adding multiple values, because it is a single value notification. Also, it is not thread-safe is hard to do linkage with the notifier.
|
||||
|
||||
ObservableCollections introduces generics version of `NotifyCollectionChangedEventHandler` and `NotifyCollectionChangedEventArgs`, it using latest C# features(`in`, `readonly ref struct`, `ReadOnlySpan<T>`).
|
||||
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.
|
||||
|
||||
```csharp
|
||||
public delegate void NotifyCollectionChangedEventHandler<T>(in NotifyCollectionChangedEventArgs<T> e);
|
||||
@ -22,6 +20,7 @@ public readonly ref struct NotifyCollectionChangedEventArgs<T>
|
||||
public readonly ReadOnlySpan<T> OldItems;
|
||||
public readonly int NewStartingIndex;
|
||||
public readonly int OldStartingIndex;
|
||||
public readonly SortOperation<T> SortOperation;
|
||||
}
|
||||
```
|
||||
|
||||
@ -30,20 +29,22 @@ 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, bool reverse = false);
|
||||
ISynchronizedView<T, TView> CreateView<TView>(Func<T, TView> transform);
|
||||
}
|
||||
|
||||
// also exists SortedView
|
||||
public static ISynchronizedView<T, TView> CreateSortedView<T, TKey, TView>(this IObservableCollection<T> source, Func<T, TKey> identitySelector, Func<T, TView> transform, IComparer<T> comparer);
|
||||
public static ISynchronizedView<T, TView> CreateSortedView<T, TKey, TView>(this IObservableCollection<T> source, Func<T, TKey> identitySelector, Func<T, TView> transform, IComparer<TView> viewComparer);
|
||||
```
|
||||
|
||||
SynchronizedView helps to separate between Model and View (ViewModel). We will use ObservableCollections as the Model and generate SynchronizedView as the View (ViewModel). This architecture can be applied not only to WPF, but also to Blazor, Unity, etc.
|
||||
|
||||

|
||||
|
||||
The View retains the transformed values. The transform function is called only once during Add, so costly objects that are linked can also be instantiated. Additionally, it has a feature to dynamically show or hide values using filters.
|
||||
|
||||
Observable Collections themselves do not implement `INotifyCollectionChanged`, so they cannot be bound on XAML platforms and the like. However, they can be converted to collections that implement `INotifyCollectionChanged` using `ToNotifyCollectionChanged()`, making them suitable for binding.
|
||||
|
||||

|
||||
|
||||
ObservableCollections has not just a simple list, there are many more data structures. `ObservableList<T>`, `ObservableDictionary<TKey, TValue>`, `ObservableHashSet<T>`, `ObservableQueue<T>`, `ObservableStack<T>`, `ObservableRingBuffer<T>`, `ObservableFixedSizeRingBuffer<T>`. `RingBuffer`, especially `FixedSizeRingBuffer`, can be achieved with efficient performance when there is rotation (e.g., displaying up to 1000 logs, where old ones are deleted when new ones are added). Of course, the AddRange allows for efficient batch processing of large numbers of additions.
|
||||
|
||||
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):
|
||||
@ -54,13 +55,18 @@ 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<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.
|
||||
|
||||
PM> Install-Package [ObservableCollections](https://www.nuget.org/packages/ObservableCollections)
|
||||
> dotnet add package [ObservableCollections](https://www.nuget.org/packages/ObservableCollections)
|
||||
|
||||
create new `ObservableList<T>`, `ObservableDictionary<TKey, TValue>`, `ObservableHashSet<T>`, `ObservableQueue<T>`, `ObservableStack<T>`, `ObservableRingBuffer<T>`, `ObservableFixedSizeRingBuffer<T>`.
|
||||
|
||||
@ -98,7 +104,7 @@ static void List_CollectionChanged(in NotifyCollectionChangedEventArgs<int> e)
|
||||
}
|
||||
```
|
||||
|
||||
Handling all `CollectionChanged` event manually is hard. We recommend to use `SynchronizedView` that transform element and handling all collection changed event for view synchronize.
|
||||
While it is possible to manually handle the `CollectionChanged` event as shown in the example above, you can also create a `SynchronizedView` as a collection that holds a separate synchronized value.
|
||||
|
||||
```csharp
|
||||
var list = new ObservableList<int>();
|
||||
@ -110,7 +116,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);
|
||||
@ -120,13 +126,120 @@ foreach (var (_, v) in view)
|
||||
view.Dispose();
|
||||
```
|
||||
|
||||
The basic idea behind using ObservableCollections is to create a View. In order to automate this pipeline, the view can be sortable, filtered, and have side effects on the values when they are changed.
|
||||
The view can modify the objects being enumerated by attaching a Filter.
|
||||
|
||||
```csharp
|
||||
var list = new ObservableList<int>();
|
||||
using var view = list.CreateView(x => x.ToString() + "$");
|
||||
|
||||
list.Add(1);
|
||||
list.Add(20);
|
||||
list.AddRange(new[] { 30, 31, 32 });
|
||||
|
||||
// attach filter
|
||||
view.AttachFilter(x => x % 2 == 0);
|
||||
|
||||
foreach (var v in view)
|
||||
{
|
||||
// 20$, 30$, 32$
|
||||
Console.WriteLine(v);
|
||||
}
|
||||
|
||||
// attach other filter(removed previous filter)
|
||||
view.AttachFilter(x => x % 2 == 1);
|
||||
|
||||
foreach (var v in view)
|
||||
{
|
||||
// 1$, 31$
|
||||
Console.WriteLine(v);
|
||||
}
|
||||
|
||||
// Count shows filtered length
|
||||
Console.WriteLine(view.Count); // 2
|
||||
```
|
||||
|
||||
The View only allows iteration and Count; it cannot be accessed via an indexer. If indexer access is required, you need to convert it using `ToViewList()`. Additionally, `ToNotifyCollectionChanged()` converts it to a synchronized view that implements `INotifyCollectionChanged`, which is necessary for XAML binding, in addition to providing indexer access.
|
||||
|
||||
```csharp
|
||||
// Queue <-> List Synchronization
|
||||
var queue = new ObservableQueue<int>();
|
||||
|
||||
queue.Enqueue(1);
|
||||
queue.Enqueue(10);
|
||||
queue.Enqueue(100);
|
||||
queue.Enqueue(1000);
|
||||
queue.Enqueue(10000);
|
||||
|
||||
using var view = queue.CreateView(x => x.ToString() + "$");
|
||||
|
||||
using var viewList = view.ToViewList();
|
||||
|
||||
Console.WriteLine(viewList[2]); // 100$
|
||||
```
|
||||
|
||||
In the case of ObservableList, calls to `Sort` and `Reverse` can also be synchronized with the view.
|
||||
|
||||
```csharp
|
||||
var list = new ObservableList<int> { 1, 301, 20, 50001, 4000 };
|
||||
using var view = list.CreateView(x => x.ToString() + "$");
|
||||
|
||||
view.AttachFilter(x => x % 2 == 0);
|
||||
|
||||
foreach (var v in view)
|
||||
{
|
||||
// 20$, 4000$
|
||||
Console.WriteLine(v);
|
||||
}
|
||||
|
||||
// Reverse operations on the list will affect the view
|
||||
list.Reverse();
|
||||
|
||||
foreach (var v in view)
|
||||
{
|
||||
// 4000$, 20$
|
||||
Console.WriteLine(v);
|
||||
}
|
||||
|
||||
// remove filter
|
||||
view.ResetFilter();
|
||||
|
||||
// The reverse operation is also reflected in the values hidden by the filter
|
||||
foreach (var v in view)
|
||||
{
|
||||
// 4000$, 50001$, 20$, 301$, 1$
|
||||
Console.WriteLine(v);
|
||||
}
|
||||
|
||||
// also affect Sort Operations
|
||||
list.Sort();
|
||||
foreach (var v in view)
|
||||
{
|
||||
// 1$, 20$, 301$, 4000$, 50001$
|
||||
Console.WriteLine(v);
|
||||
}
|
||||
|
||||
// you can use custom comparer
|
||||
list.Sort(new DescendantComaprer());
|
||||
foreach (var v in view)
|
||||
{
|
||||
// 50001$, 4000$, 301$, 20$, 1$
|
||||
Console.WriteLine(v);
|
||||
}
|
||||
|
||||
class DescendantComaprer : IComparer<int>
|
||||
{
|
||||
public int Compare(int x, int y)
|
||||
{
|
||||
return y.CompareTo(x);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Reactive Extensions with R3
|
||||
---
|
||||
Once the R3 extension package is installed, you can subscribe to `ObserveAdd`, `ObserveRemove`, `ObserveReplace`, `ObserveMove`, and `ObserveReset` events as Rx, allowing you to compose events individually.
|
||||
Once the R3 extension package is installed, you can subscribe to `ObserveAdd`, `ObserveRemove`, `ObserveReplace`, `ObserveMove`, `ObserveReset`, `ObserveClear`, `ObserveReverse`, `ObserveSort` events as Rx, allowing you to compose events individually.
|
||||
|
||||
PM> Install-Package [ObservableCollections.R3](https://www.nuget.org/packages/ObservableCollections.R3)
|
||||
> dotnet add package [ObservableCollections.R3](https://www.nuget.org/packages/ObservableCollections.R3)
|
||||
|
||||
```csharp
|
||||
using R3;
|
||||
@ -144,69 +257,59 @@ list.Add(20);
|
||||
list.AddRange(new[] { 10, 20, 30 });
|
||||
```
|
||||
|
||||
Note that `ObserveReset` is used to subscribe to Clear, Reverse, and Sort operations in bulk.
|
||||
|
||||
Since it is not supported by dotnet/reactive, please use the Rx library [R3](https://github.com/Cysharp/R3).
|
||||
|
||||
Blazor
|
||||
---
|
||||
Since Blazor re-renders the whole thing by StateHasChanged, you may think that Observable collections are unnecessary. However, when you split it into Components, it is beneficial for Component confidence to detect the change and change its own State.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
```csharp
|
||||
public partial class DataTable<T> : ComponentBase, IDisposable
|
||||
public partial class Index : IDisposable
|
||||
{
|
||||
[Parameter, EditorRequired]
|
||||
public IReadOnlyList<T> Items { get; set; } = default!;
|
||||
|
||||
[Parameter, EditorRequired]
|
||||
public Func<T, Cell[]> DataTemplate { get; set; } = default!;
|
||||
|
||||
ISynchronizedView<T, Cell[]> view = default!;
|
||||
ObservableList<int> list;
|
||||
public ISynchronizedView<int, int> ItemsView { get; set; }
|
||||
int count = 0;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
if (Items is IObservableCollection<T> observableCollection)
|
||||
{
|
||||
// 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);
|
||||
}
|
||||
list = new ObservableList<int>();
|
||||
ItemsView = list.CreateView(x => x);
|
||||
|
||||
// View also has a change notification.
|
||||
view.CollectionStateChanged += async _ =>
|
||||
ItemsView.CollectionStateChanged += action =>
|
||||
{
|
||||
await InvokeAsync(StateHasChanged);
|
||||
InvokeAsync(StateHasChanged);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
void OnClick()
|
||||
{
|
||||
list.Add(count++);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// unsubscribe.
|
||||
view.Dispose();
|
||||
ItemsView.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// .razor, iterate view
|
||||
@foreach (var (row, cells) in view)
|
||||
{
|
||||
<tr>
|
||||
@foreach (var item in cells)
|
||||
{
|
||||
<td>
|
||||
<CellView Item="item" />
|
||||
</td>
|
||||
}
|
||||
</tr>
|
||||
}
|
||||
@page "/"
|
||||
|
||||
<button @onclick=OnClick>button</button>
|
||||
|
||||
<table>
|
||||
@foreach (var item in ItemsView)
|
||||
{
|
||||
<tr>
|
||||
<td>@item</td>
|
||||
</tr>
|
||||
}
|
||||
</table>
|
||||
```
|
||||
|
||||
WPF(XAML based UI platforms)
|
||||
WPF/Avalonia/WinUI (XAML based UI platforms)
|
||||
---
|
||||
Because of data binding in WPF, it is important that the collection is Observable. ObservableCollections high-performance `IObservableCollection<T>` cannot be bind to WPF. Call `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.
|
||||
|
||||
@ -214,7 +317,7 @@ Because of data binding in WPF, it is important that the collection is Observabl
|
||||
// WPF simple sample.
|
||||
|
||||
ObservableList<int> list;
|
||||
public INotifyCollectionChangedSynchronizedView<int> ItemsView { get; set; }
|
||||
public INotifyCollectionChangedSynchronizedViewList<int> ItemsView { get; set; }
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
@ -224,10 +327,10 @@ public MainWindow()
|
||||
list = new ObservableList<int>();
|
||||
|
||||
// for ui synchronization safety of viewmodel
|
||||
ItemsView = list.CreateView(x => x).ToNotifyCollectionChanged(SynchronizationContextCollectionEventDispatcher.Current);
|
||||
ItemsView = list.ToNotifyCollectionChanged(SynchronizationContextCollectionEventDispatcher.Current);
|
||||
|
||||
// if collection is changed only from ui-thread, can use this overload
|
||||
// ItemsView = list.CreateView(x => x).ToNotifyCollectionChanged();
|
||||
// ItemsView = list.ToNotifyCollectionChanged();
|
||||
}
|
||||
|
||||
protected override void OnClosed(EventArgs e)
|
||||
@ -236,8 +339,6 @@ protected override void OnClosed(EventArgs e)
|
||||
}
|
||||
```
|
||||
|
||||
> WPF can not use SortedView because SortedView can not provide sort event to INotifyCollectionChanged.
|
||||
|
||||
`SynchronizationContextCollectionEventDispatcher.Current` is default implementation of `IColllectionEventDispatcher`, it is used `SynchronizationContext.Current` for dispatche ui thread. You can create custom `ICollectionEventDispatcher` to use custom dispatcher object. For example use WPF Dispatcher:
|
||||
|
||||
```csharp
|
||||
@ -258,12 +359,9 @@ 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 we need to have side effects on GameObjects, we will prepare a filter and apply an action on changes.
|
||||
In Unity, ObservableCollections and Views are useful as CollectionManagers, since they need to convert T to Prefab for display. Since View objects are generated only once, it's possible to complement GameObjects tied to the collection.
|
||||
|
||||
```csharp
|
||||
// Unity, with filter sample.
|
||||
public class SampleScript : MonoBehaviour
|
||||
{
|
||||
public Button prefab;
|
||||
@ -280,186 +378,187 @@ public class SampleScript : MonoBehaviour
|
||||
item.GetComponentInChildren<Text>().text = x.ToString();
|
||||
return item.gameObject;
|
||||
});
|
||||
view.AttachFilter(new GameObjectFilter(root));
|
||||
view.ViewChanged += View_ViewChanged;
|
||||
}
|
||||
|
||||
void View_ViewChanged(in SynchronizedViewChangedEventArgs<int, string> eventArgs)
|
||||
{
|
||||
if (eventArgs.Action == NotifyCollectionChangedAction.Add)
|
||||
{
|
||||
eventArgs.NewItem.View.transform.SetParent(root.transform);
|
||||
}
|
||||
else if (NotifyCollectionChangedAction.Remove)
|
||||
{
|
||||
GameObject.Destroy(eventArgs.OldItem.View);
|
||||
}
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
view.Dispose();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
public class GameObjectFilter : ISynchronizedViewFilter<int, GameObject>
|
||||
Reference
|
||||
---
|
||||
ObservableCollections provides these collections.
|
||||
|
||||
```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>> 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);
|
||||
}
|
||||
```
|
||||
|
||||
The notification event `NotifyCollectionChangedEventArgs<T>` has the following definition:
|
||||
|
||||
```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 interface ISynchronizedView<T, TView> : IReadOnlyCollection<TView>, IDisposable
|
||||
{
|
||||
object SyncRoot { get; }
|
||||
ISynchronizedViewFilter<T> 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<NotifyCollectionChangedAction>? CollectionStateChanged;
|
||||
|
||||
void AttachFilter(ISynchronizedViewFilter<T> filter);
|
||||
void ResetFilter();
|
||||
ISynchronizedViewList<TView> ToViewList();
|
||||
INotifyCollectionChangedSynchronizedViewList<TView> ToNotifyCollectionChanged();
|
||||
INotifyCollectionChangedSynchronizedViewList<TView> ToNotifyCollectionChanged(ICollectionEventDispatcher? collectionEventDispatcher);
|
||||
}
|
||||
```
|
||||
|
||||
The `Count` of the View returns the filtered value, but if you need the unfiltered value, use `UnfilteredCount`. Also, normal enumeration returns only `TView`, but if you need `T` or want to enumerate pre-filtered values, you can get them with `Filtered` and `Unfiltered`.
|
||||
|
||||
The View's notification event `SynchronizedViewChangedEventArgs<T>` has the following definition:
|
||||
|
||||
```csharp
|
||||
public readonly ref struct SynchronizedViewChangedEventArgs<T, TView>
|
||||
{
|
||||
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;
|
||||
}
|
||||
```
|
||||
|
||||
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 bool IsReverse { get; }
|
||||
public bool IsClear { get; }
|
||||
public bool IsSort { get; }
|
||||
}
|
||||
```
|
||||
|
||||
When `IsReverse` is true, you need to use `Index` and `Count`. When `IsSort` is true, you need to use `Index`, `Count`, and `Comparer` values.
|
||||
|
||||
For Filter, you can either create one that implements this interface or generate one from a lambda expression using extension methods.
|
||||
|
||||
```csharp
|
||||
public interface ISynchronizedViewFilter<T>
|
||||
{
|
||||
bool IsMatch(T value);
|
||||
}
|
||||
|
||||
public static class SynchronizedViewExtensions
|
||||
{
|
||||
public static void AttachFilter<T, TView>(this ISynchronizedView<T, TView> source, Func<T, bool> filter)
|
||||
{
|
||||
readonly GameObject root;
|
||||
|
||||
public GameObjectFilter(GameObject root)
|
||||
{
|
||||
this.root = root;
|
||||
}
|
||||
|
||||
public void OnCollectionChanged(in SynchronizedViewChangedEventArgs<int, GameObject> eventArgs)
|
||||
{
|
||||
if (eventArgs.Action == NotifyCollectionChangedAction.Add)
|
||||
{
|
||||
eventArgs.NewView.transform.SetParent(root.transform);
|
||||
}
|
||||
else if (NotifyCollectionChangedAction.Remove)
|
||||
{
|
||||
GameObject.Destroy(eventArgs.OldView);
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsMatch(int value, GameObject view)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public void WhenTrue(int value, GameObject view)
|
||||
{
|
||||
view.SetActive(true);
|
||||
}
|
||||
|
||||
public void WhenFalse(int value, GameObject view)
|
||||
{
|
||||
view.SetActive(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
It is also possible to manage Order by managing indexes inserted from eventArgs, but it is very difficult with many caveats. If you don't have major performance issues, you can foreach the View itself on CollectionStateChanged (like Blazor) and reorder the transforms. If you have such a architecture, you can also use SortedView.
|
||||
|
||||
View/SortedView
|
||||
---
|
||||
View can create from `IObservableCollection<T>`, it completely synchronized and thread-safe.
|
||||
Here are definitions for other collections:
|
||||
|
||||
```csharp
|
||||
public interface IObservableCollection<T> : IReadOnlyCollection<T>
|
||||
public interface IReadOnlyObservableList<T> :
|
||||
IReadOnlyList<T>, IObservableCollection<T>
|
||||
{
|
||||
// snip...
|
||||
ISynchronizedView<T, TView> CreateView<TView>(Func<T, TView> transform, bool reverse = false);
|
||||
}
|
||||
```
|
||||
|
||||
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
|
||||
public interface ISynchronizedView<T, TView> : IReadOnlyCollection<(T Value, TView View)>, IDisposable
|
||||
{
|
||||
object SyncRoot { get; }
|
||||
|
||||
event NotifyCollectionChangedEventHandler<T>? RoutingCollectionChanged;
|
||||
event Action<NotifyCollectionChangedAction>? CollectionStateChanged;
|
||||
|
||||
void AttachFilter(ISynchronizedViewFilter<T, TView> filter);
|
||||
void ResetFilter(Action<T, TView>? resetAction);
|
||||
INotifyCollectionChangedSynchronizedView<T, TView> ToNotifyCollectionChanged();
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
see [filter](#filter) section.
|
||||
|
||||
|
||||
|
||||
```csharp
|
||||
var view = transform(value);
|
||||
if (filter.IsMatch(value, view))
|
||||
{
|
||||
filter.WhenTrue(value, view);
|
||||
}
|
||||
else
|
||||
{
|
||||
filter.WhenFalse(value, view);
|
||||
}
|
||||
AddToCollectionInnerStructure(value, view);
|
||||
filter.OnCollectionChanged(ChangeKind.Add, value, view, eventArgs);
|
||||
RoutingCollectionChanged(eventArgs);
|
||||
CollectionStateChanged();
|
||||
```
|
||||
|
||||
|
||||
```csharp
|
||||
public static ISynchronizedView<T, TView> CreateSortedView<T, TKey, TView>(this IObservableCollection<T> source, Func<T, TKey> identitySelector, Func<T, TView> transform, IComparer<T> comparer)
|
||||
where TKey : notnull
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
> 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.
|
||||
|
||||
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(in SynchronizedViewChangedEventArgs<T, TView> eventArgs);
|
||||
}
|
||||
|
||||
public readonly struct SynchronizedViewChangedEventArgs<T, TView>
|
||||
public interface IReadOnlyObservableDictionary<TKey, TValue> :
|
||||
IReadOnlyDictionary<TKey, TValue>, IObservableCollection<KeyValuePair<TKey, TValue>>
|
||||
{
|
||||
public readonly NotifyCollectionChangedAction Action = action;
|
||||
public readonly T NewValue = newValue;
|
||||
public readonly T OldValue = oldValue;
|
||||
public readonly TView NewView = newView;
|
||||
public readonly TView OldView = oldView;
|
||||
public readonly int NewViewIndex = newViewIndex;
|
||||
public readonly int OldViewIndex = oldViewIndex;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
Collections
|
||||
---
|
||||
|
||||
```csharp
|
||||
public sealed partial class ObservableDictionary<TKey, TValue> : IDictionary<TKey, TValue>, IReadOnlyDictionary<TKey, TValue>, IObservableCollection<KeyValuePair<TKey, TValue>> where TKey : notnull
|
||||
public sealed partial class ObservableFixedSizeRingBuffer<T> : IList<T>, IReadOnlyList<T>, IObservableCollection<T>
|
||||
public sealed partial class ObservableHashSet<T> : IReadOnlySet<T>, IReadOnlyCollection<T>, IObservableCollection<T> where T : notnull
|
||||
|
||||
public sealed partial class ObservableHashSet<T> : IReadOnlySet<T>, IReadOnlyCollection<T>, IObservableCollection<T>
|
||||
where T : notnull
|
||||
|
||||
public sealed partial class ObservableList<T> : IList<T>, IReadOnlyList<T>, IObservableCollection<T>
|
||||
|
||||
public sealed partial class ObservableQueue<T> : IReadOnlyCollection<T>, IObservableCollection<T>
|
||||
public sealed partial class ObservableRingBuffer<T> : IList<T>, IReadOnlyList<T>, IObservableCollection<T>
|
||||
|
||||
public sealed partial class ObservableStack<T> : IReadOnlyCollection<T>, IObservableCollection<T>
|
||||
|
||||
public sealed class RingBuffer<T> : IList<T>, IReadOnlyList<T>
|
||||
```
|
||||
|
||||
Freezed
|
||||
---
|
||||
|
||||
|
||||
```csharp
|
||||
public sealed class FreezedList<T> : IReadOnlyList<T>, IFreezedCollection<T>
|
||||
public sealed class FreezedDictionary<TKey, TValue> : IReadOnlyDictionary<TKey, TValue>, IFreezedCollection<KeyValuePair<TKey, TValue>> where TKey : notnull
|
||||
|
||||
|
||||
public interface IFreezedCollection<T>
|
||||
{
|
||||
ISynchronizedView<T, TView> CreateView<TView>(Func<T, TView> transform, bool reverse = false);
|
||||
ISortableSynchronizedView<T, TView> CreateSortableView<TView>(Func<T, TView> transform);
|
||||
}
|
||||
|
||||
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)
|
||||
public interface ISynchronizedViewList<out TView> : IReadOnlyList<TView>, IDisposable
|
||||
{
|
||||
}
|
||||
|
||||
public interface INotifyCollectionChangedSynchronizedViewList<out TView> : ISynchronizedViewList<TView>, INotifyCollectionChanged, INotifyPropertyChanged
|
||||
{
|
||||
}
|
||||
```
|
||||
|
||||
License
|
||||
|
BIN
docs/assets.pptx
BIN
docs/assets.pptx
Binary file not shown.
@ -24,7 +24,7 @@ namespace AvaloniaApp
|
||||
public class ViewModel
|
||||
{
|
||||
private ObservableList<int> observableList { get; } = new ObservableList<int>();
|
||||
public INotifyCollectionChangedSynchronizedView<int> ItemsView { get; }
|
||||
public INotifyCollectionChangedSynchronizedViewList<int> ItemsView { get; }
|
||||
public ReactiveCommand<Unit> AddCommand { get; } = new ReactiveCommand<Unit>();
|
||||
public ReactiveCommand<Unit> ClearCommand { get; } = new ReactiveCommand<Unit>();
|
||||
|
||||
|
@ -7,7 +7,7 @@ public partial class Index : IDisposable
|
||||
{
|
||||
ObservableList<int> list;
|
||||
public ISynchronizedView<int, int> ItemsView { get; set; }
|
||||
int adder = 99;
|
||||
int count = 99;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
@ -20,19 +20,13 @@ public partial class Index : IDisposable
|
||||
};
|
||||
}
|
||||
|
||||
void OnClick()
|
||||
{
|
||||
list.Add(count++);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
ItemsView.Dispose();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
void OnClick()
|
||||
{
|
||||
ThreadPool.QueueUserWorkItem(_ =>
|
||||
{
|
||||
list.Add(adder++);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -3,43 +3,38 @@ using System.Collections.Specialized;
|
||||
using R3;
|
||||
using System.Linq;
|
||||
using ObservableCollections;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
|
||||
|
||||
|
||||
|
||||
var list = new ObservableList<int>();
|
||||
list.ObserveAdd()
|
||||
.Subscribe(x =>
|
||||
// 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$
|
||||
|
||||
|
||||
view.ViewChanged += View_ViewChanged;
|
||||
|
||||
void View_ViewChanged(in SynchronizedViewChangedEventArgs<int, string> eventArgs)
|
||||
{
|
||||
if (eventArgs.Action == NotifyCollectionChangedAction.Add)
|
||||
{
|
||||
Console.WriteLine(x);
|
||||
});
|
||||
// eventArgs.OldItem.View.
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
models.Add(100);
|
||||
|
||||
foreach (var (x, xs) in viewModels)
|
||||
{
|
||||
System.Console.WriteLine(xs.Value);
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
class ViewModel
|
||||
@ -48,38 +43,28 @@ class ViewModel
|
||||
public string Value { get; set; } = default!;
|
||||
}
|
||||
|
||||
class HogeFilter : ISynchronizedViewFilter<int, ViewModel>
|
||||
class HogeFilter : ISynchronizedViewFilter<int>
|
||||
{
|
||||
public bool IsMatch(int value, ViewModel view)
|
||||
public bool IsMatch(int value)
|
||||
{
|
||||
return value % 2 == 0;
|
||||
}
|
||||
|
||||
public void WhenTrue(int value, ViewModel view)
|
||||
{
|
||||
view.Value = $"@{value} (even)";
|
||||
}
|
||||
|
||||
public void WhenFalse(int value, ViewModel view)
|
||||
{
|
||||
view.Value = $"@{value} (odd)";
|
||||
}
|
||||
|
||||
public void OnCollectionChanged(in SynchronizedViewChangedEventArgs<int, ViewModel> eventArgs)
|
||||
{
|
||||
switch (eventArgs.Action)
|
||||
{
|
||||
case NotifyCollectionChangedAction.Add:
|
||||
eventArgs.NewView.Value += " Add";
|
||||
eventArgs.NewItem.View.Value += " Add";
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Remove:
|
||||
eventArgs.OldView.Value += " Remove";
|
||||
eventArgs.OldItem.View.Value += " Remove";
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Move:
|
||||
eventArgs.NewView.Value += $" Move {eventArgs.OldViewIndex} {eventArgs.NewViewIndex}";
|
||||
eventArgs.NewItem.View.Value += $" Move {eventArgs.OldStartingIndex} {eventArgs.NewStartingIndex}";
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Replace:
|
||||
eventArgs.NewView.Value += $" Replace {eventArgs.NewViewIndex}";
|
||||
eventArgs.NewItem.View.Value += $" Replace {eventArgs.NewStartingIndex}";
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Reset:
|
||||
break;
|
||||
|
@ -20,6 +20,12 @@
|
||||
<StackPanel>
|
||||
<ListBox ItemsSource="{Binding ItemsView}" />
|
||||
<Button Content="Add" Command="{Binding AddCommand}" />
|
||||
<Button Content="Insert" Command="{Binding InsertAtRandomCommand}" />
|
||||
<Button Content="Remove" Command="{Binding RemoveAtRandomCommand}" />
|
||||
<Button Content="Clear" Command="{Binding ClearCommand}" />
|
||||
<Button Content="Reverse" Command="{Binding ReverseCommand}" />
|
||||
<Button Content="Sort" Command="{Binding SortCommand}" />
|
||||
<Button Content="AttachFilter" Command="{Binding AttachFilterCommand}" />
|
||||
<Button Content="ResetFilter" Command="{Binding ResetFilterCommand}" />
|
||||
</StackPanel>
|
||||
</Window>
|
||||
|
@ -74,21 +74,28 @@ namespace WpfApp
|
||||
public class ViewModel
|
||||
{
|
||||
private ObservableList<int> observableList { get; } = new ObservableList<int>();
|
||||
public INotifyCollectionChangedSynchronizedView<int> ItemsView { get; }
|
||||
public INotifyCollectionChangedSynchronizedViewList<int> ItemsView { get; }
|
||||
public ReactiveCommand<Unit> AddCommand { 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);
|
||||
|
||||
ItemsView = observableList.CreateView(x => x).ToNotifyCollectionChanged(SynchronizationContextCollectionEventDispatcher.Current);
|
||||
var view = observableList.CreateView(x => x);
|
||||
ItemsView = view.ToNotifyCollectionChanged(SynchronizationContextCollectionEventDispatcher.Current);
|
||||
|
||||
|
||||
// ItemsView = observableList.CreateView(x => x).ToNotifyCollectionChanged();
|
||||
// check for optimize list
|
||||
// ItemsView = observableList.ToNotifyCollectionChanged(SynchronizationContextCollectionEventDispatcher.Current);
|
||||
|
||||
// BindingOperations.EnableCollectionSynchronization(ItemsView, new object());
|
||||
|
||||
AddCommand.Subscribe(_ =>
|
||||
{
|
||||
@ -98,12 +105,43 @@ namespace WpfApp
|
||||
});
|
||||
});
|
||||
|
||||
// var iii = 10;
|
||||
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.Add(iii++);
|
||||
observableList.Clear();
|
||||
});
|
||||
|
||||
|
||||
ReverseCommand.Subscribe(_ =>
|
||||
{
|
||||
observableList.Reverse();
|
||||
});
|
||||
|
||||
SortCommand.Subscribe(_ =>
|
||||
{
|
||||
observableList.Sort();
|
||||
});
|
||||
|
||||
AttachFilterCommand.Subscribe(_ =>
|
||||
{
|
||||
view.AttachFilter(x => x % 2 == 0);
|
||||
});
|
||||
|
||||
ResetFilterCommand.Subscribe(_ =>
|
||||
{
|
||||
view.ResetFilter();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -10,6 +10,22 @@ public readonly record struct CollectionAddEvent<T>(int Index, T Value);
|
||||
public readonly record struct CollectionRemoveEvent<T>(int Index, T Value);
|
||||
public readonly record struct CollectionReplaceEvent<T>(int Index, T OldValue, T NewValue);
|
||||
public readonly record struct CollectionMoveEvent<T>(int OldIndex, int NewIndex, T Value);
|
||||
public readonly record struct CollectionResetEvent<T>
|
||||
{
|
||||
readonly SortOperation<T> sortOperation;
|
||||
|
||||
public bool IsClear => sortOperation.IsClear;
|
||||
public bool IsSort => sortOperation.IsSort;
|
||||
public bool IsReverse => sortOperation.IsReverse;
|
||||
public int Index => sortOperation.Index;
|
||||
public int Count => sortOperation.Count;
|
||||
public IComparer<T>? Comparer => sortOperation.Comparer;
|
||||
|
||||
public CollectionResetEvent(SortOperation<T> sortOperation)
|
||||
{
|
||||
this.sortOperation = sortOperation;
|
||||
}
|
||||
}
|
||||
|
||||
public readonly record struct DictionaryAddEvent<TKey, TValue>(TKey Key, TValue Value);
|
||||
|
||||
@ -34,17 +50,32 @@ public static 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<Unit> ObserveReset<T>(this IObservableCollection<T> source, CancellationToken cancellationToken = default)
|
||||
|
||||
public static Observable<CollectionResetEvent<T>> ObserveReset<T>(this IObservableCollection<T> source, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return new ObservableCollectionReset<T>(source, cancellationToken);
|
||||
}
|
||||
|
||||
public static Observable<Unit> ObserveClear<T>(this IObservableCollection<T> source, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return new ObservableCollectionClear<T>(source, cancellationToken);
|
||||
}
|
||||
|
||||
public static Observable<(int Index, int Count)> ObserveReverse<T>(this IObservableCollection<T> source, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return new ObservableCollectionReverse<T>(source, cancellationToken);
|
||||
}
|
||||
|
||||
public static Observable<(int Index, int Count, IComparer<T> Comparer)> ObserveSort<T>(this IObservableCollection<T> source, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return new ObservableCollectionSort<T>(source, cancellationToken);
|
||||
}
|
||||
|
||||
public static Observable<int> ObserveCountChanged<T>(this IObservableCollection<T> source, bool notifyCurrentCount = false, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return new ObservableCollectionCountChanged<T>(source, notifyCurrentCount, cancellationToken);
|
||||
@ -148,7 +179,7 @@ sealed class ObservableCollectionReplace<T>(IObservableCollection<T> collection,
|
||||
{
|
||||
return new _ObservableCollectionReplace(collection, observer, cancellationToken);
|
||||
}
|
||||
|
||||
|
||||
sealed class _ObservableCollectionReplace(
|
||||
IObservableCollection<T> collection,
|
||||
Observer<CollectionReplaceEvent<T>> observer,
|
||||
@ -172,7 +203,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,
|
||||
@ -188,16 +219,39 @@ 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 _ObservableCollectionReset(collection, observer, cancellationToken);
|
||||
return new _ObservableCollectionClear(collection, observer, cancellationToken);
|
||||
}
|
||||
|
||||
sealed class _ObservableCollectionReset(
|
||||
|
||||
sealed class _ObservableCollectionClear(
|
||||
IObservableCollection<T> collection,
|
||||
Observer<Unit> observer,
|
||||
CancellationToken cancellationToken)
|
||||
@ -205,7 +259,7 @@ sealed class ObservableCollectionReset<T>(IObservableCollection<T> collection, C
|
||||
{
|
||||
protected override void Handler(in NotifyCollectionChangedEventArgs<T> eventArgs)
|
||||
{
|
||||
if (eventArgs.Action == NotifyCollectionChangedAction.Reset)
|
||||
if (eventArgs.Action == NotifyCollectionChangedAction.Reset && eventArgs.SortOperation.IsClear)
|
||||
{
|
||||
observer.OnNext(Unit.Default);
|
||||
}
|
||||
@ -213,6 +267,52 @@ sealed class ObservableCollectionReset<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>
|
||||
{
|
||||
@ -220,7 +320,7 @@ sealed class ObservableCollectionCountChanged<T>(IObservableCollection<T> collec
|
||||
{
|
||||
return new _ObservableCollectionCountChanged(collection, notifyCurrentCount, observer, cancellationToken);
|
||||
}
|
||||
|
||||
|
||||
sealed class _ObservableCollectionCountChanged : ObservableCollectionObserverBase<T, int>
|
||||
{
|
||||
int countPrev;
|
||||
@ -372,7 +472,7 @@ abstract class ObservableCollectionObserverBase<T, TEvent> : IDisposable
|
||||
this.handlerDelegate = Handler;
|
||||
|
||||
collection.CollectionChanged += handlerDelegate;
|
||||
|
||||
|
||||
if (cancellationToken.CanBeCanceled)
|
||||
{
|
||||
cancellationTokenRegistration = cancellationToken.UnsafeRegister(static state =>
|
||||
|
227
src/ObservableCollections/AlternateIndexList.cs
Normal file
227
src/ObservableCollections/AlternateIndexList.cs
Normal file
@ -0,0 +1,227 @@
|
||||
#pragma warning disable CS0436
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
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();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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 != -1)
|
||||
{
|
||||
list.RemoveAt(index);
|
||||
UpdateAlternateIndex(index, -1);
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
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.Reset();
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
if (iter.MoveNext())
|
||||
{
|
||||
ConsumedCount++;
|
||||
current = new(startingIndex++, iter.Current);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Reset() => iter.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();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
#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);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,61 +0,0 @@
|
||||
using ObservableCollections.Internal;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace ObservableCollections
|
||||
{
|
||||
public sealed class FreezedList<T> : IReadOnlyList<T>, IFreezedCollection<T>
|
||||
{
|
||||
readonly IReadOnlyList<T> list;
|
||||
|
||||
public T this[int index]
|
||||
{
|
||||
get
|
||||
{
|
||||
return list[index];
|
||||
}
|
||||
}
|
||||
|
||||
public int Count
|
||||
{
|
||||
get
|
||||
{
|
||||
return list.Count;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsReadOnly => true;
|
||||
|
||||
public FreezedList(IReadOnlyList<T> list)
|
||||
{
|
||||
this.list = list;
|
||||
}
|
||||
|
||||
public ISynchronizedView<T, TView> CreateView<TView>(Func<T, TView> transform, bool reverse = false)
|
||||
{
|
||||
return new FreezedView<T, TView>(list, transform, reverse);
|
||||
}
|
||||
|
||||
public ISortableSynchronizedView<T, TView> CreateSortableView<TView>(Func<T, TView> transform)
|
||||
{
|
||||
return new FreezedSortableView<T, TView>(list, transform);
|
||||
}
|
||||
|
||||
public bool Contains(T item)
|
||||
{
|
||||
return list.Contains(item);
|
||||
}
|
||||
|
||||
public IEnumerator<T> GetEnumerator()
|
||||
{
|
||||
return list.GetEnumerator();
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
}
|
||||
}
|
@ -45,11 +45,11 @@ namespace ObservableCollections
|
||||
}
|
||||
}
|
||||
|
||||
internal class DirectCollectionEventDispatcher : ICollectionEventDispatcher
|
||||
internal class InlineCollectionEventDispatcher : ICollectionEventDispatcher
|
||||
{
|
||||
public static readonly ICollectionEventDispatcher Instance = new DirectCollectionEventDispatcher();
|
||||
public static readonly ICollectionEventDispatcher Instance = new InlineCollectionEventDispatcher();
|
||||
|
||||
DirectCollectionEventDispatcher()
|
||||
InlineCollectionEventDispatcher()
|
||||
{
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,4 @@
|
||||
using ObservableCollections.Internal;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.ComponentModel;
|
||||
@ -8,12 +6,13 @@ 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 interface IObservableCollection<T> : IReadOnlyCollection<T>
|
||||
{
|
||||
event NotifyCollectionChangedEventHandler<T>? CollectionChanged;
|
||||
object SyncRoot { get; }
|
||||
ISynchronizedView<T, TView> CreateView<TView>(Func<T, TView> transform, bool reverse = false);
|
||||
ISynchronizedView<T, TView> CreateView<TView>(Func<T, TView> transform);
|
||||
}
|
||||
|
||||
public interface IReadOnlyObservableList<T> :
|
||||
@ -21,112 +20,69 @@ namespace ObservableCollections
|
||||
{
|
||||
}
|
||||
|
||||
public interface IReadOnlyObservableDictionary<TKey, TValue> :
|
||||
public interface IReadOnlyObservableDictionary<TKey, TValue> :
|
||||
IReadOnlyDictionary<TKey, TValue>, IObservableCollection<KeyValuePair<TKey, TValue>>
|
||||
{
|
||||
}
|
||||
|
||||
public interface IFreezedCollection<T>
|
||||
{
|
||||
ISynchronizedView<T, TView> CreateView<TView>(Func<T, TView> transform, bool reverse = false);
|
||||
ISortableSynchronizedView<T, TView> CreateSortableView<TView>(Func<T, TView> transform);
|
||||
}
|
||||
|
||||
public interface ISynchronizedView<T, TView> : IReadOnlyCollection<(T Value, TView View)>, IDisposable
|
||||
public interface ISynchronizedView<T, TView> : IReadOnlyCollection<TView>, IDisposable
|
||||
{
|
||||
object SyncRoot { get; }
|
||||
ISynchronizedViewFilter<T, TView> CurrentFilter { get; }
|
||||
ISynchronizedViewFilter<T> Filter { get; }
|
||||
IEnumerable<(T Value, TView View)> Filtered { get; }
|
||||
IEnumerable<(T Value, TView View)> Unfiltered { get; }
|
||||
int UnfilteredCount { get; }
|
||||
|
||||
event NotifyCollectionChangedEventHandler<T>? RoutingCollectionChanged;
|
||||
event NotifyViewChangedEventHandler<T, TView>? ViewChanged;
|
||||
event Action<NotifyCollectionChangedAction>? CollectionStateChanged;
|
||||
|
||||
void AttachFilter(ISynchronizedViewFilter<T, TView> filter, bool invokeAddEventForInitialElements = false);
|
||||
void ResetFilter(Action<T, TView>? resetAction);
|
||||
INotifyCollectionChangedSynchronizedView<TView> ToNotifyCollectionChanged();
|
||||
INotifyCollectionChangedSynchronizedView<TView> ToNotifyCollectionChanged(ICollectionEventDispatcher? collectionEventDispatcher);
|
||||
void AttachFilter(ISynchronizedViewFilter<T> filter);
|
||||
void ResetFilter();
|
||||
ISynchronizedViewList<TView> ToViewList();
|
||||
INotifyCollectionChangedSynchronizedViewList<TView> ToNotifyCollectionChanged();
|
||||
INotifyCollectionChangedSynchronizedViewList<TView> ToNotifyCollectionChanged(ICollectionEventDispatcher? collectionEventDispatcher);
|
||||
}
|
||||
|
||||
public interface ISortableSynchronizedView<T, TView> : ISynchronizedView<T, TView>
|
||||
public interface ISynchronizedViewList<out TView> : IReadOnlyList<TView>, IDisposable
|
||||
{
|
||||
void Sort(IComparer<T> comparer);
|
||||
void Sort(IComparer<TView> viewComparer);
|
||||
}
|
||||
|
||||
// will be implemented in the future?
|
||||
//public interface IGroupedSynchoronizedView<T, TKey, TView> : ILookup<TKey, (T, TView)>, ISynchronizedView<T, TView>
|
||||
//{
|
||||
//}
|
||||
|
||||
public interface INotifyCollectionChangedSynchronizedView<out TView> : IReadOnlyCollection<TView>, INotifyCollectionChanged, INotifyPropertyChanged, IDisposable
|
||||
public interface INotifyCollectionChangedSynchronizedViewList<out TView> : ISynchronizedViewList<TView>, INotifyCollectionChanged, INotifyPropertyChanged
|
||||
{
|
||||
void Refresh();
|
||||
}
|
||||
|
||||
public static class ObservableCollectionsExtensions
|
||||
public static class ObservableCollectionExtensions
|
||||
{
|
||||
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 static ISynchronizedViewList<T> ToViewList<T>(this IObservableCollection<T> collection)
|
||||
{
|
||||
return new SortedView<T, TKey, TView>(source, identitySelector, transform, comparer);
|
||||
return ToViewList(collection, static x => x);
|
||||
}
|
||||
|
||||
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 ISynchronizedViewList<TView> ToViewList<T, TView>(this IObservableCollection<T> collection, Func<T, TView> transform)
|
||||
{
|
||||
return new SortedViewViewComparer<T, TKey, TView>(source, identitySelector, transform, viewComparer);
|
||||
// Optimized for non filtered
|
||||
return new NonFilteredSynchronizedViewList<T, TView>(collection.CreateView(transform));
|
||||
}
|
||||
|
||||
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
|
||||
public static INotifyCollectionChangedSynchronizedViewList<T> ToNotifyCollectionChanged<T>(this IObservableCollection<T> collection)
|
||||
{
|
||||
return source.CreateSortedView(identitySelector, transform, new AnonymousComparer<T, TCompare>(compareSelector, ascending));
|
||||
return ToNotifyCollectionChanged(collection, null);
|
||||
}
|
||||
|
||||
public static ISortableSynchronizedView<T, TView> CreateSortableView<T, TView>(this IFreezedCollection<T> source, Func<T, TView> transform, IComparer<T> initialSort)
|
||||
public static INotifyCollectionChangedSynchronizedViewList<T> ToNotifyCollectionChanged<T>(this IObservableCollection<T> collection, ICollectionEventDispatcher? collectionEventDispatcher)
|
||||
{
|
||||
var view = source.CreateSortableView(transform);
|
||||
view.Sort(initialSort);
|
||||
return view;
|
||||
return ToNotifyCollectionChanged(collection, static x => x, collectionEventDispatcher);
|
||||
}
|
||||
|
||||
public static ISortableSynchronizedView<T, TView> CreateSortableView<T, TView>(this IFreezedCollection<T> source, Func<T, TView> transform, IComparer<TView> initialViewSort)
|
||||
public static INotifyCollectionChangedSynchronizedViewList<TView> ToNotifyCollectionChanged<T, TView>(this IObservableCollection<T> collection, Func<T, TView> transform)
|
||||
{
|
||||
var view = source.CreateSortableView(transform);
|
||||
view.Sort(initialViewSort);
|
||||
return view;
|
||||
return ToNotifyCollectionChanged(collection, transform, null!);
|
||||
}
|
||||
|
||||
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 INotifyCollectionChangedSynchronizedViewList<TView> ToNotifyCollectionChanged<T, TView>(this IObservableCollection<T> collection, Func<T, TView> transform, ICollectionEventDispatcher? collectionEventDispatcher)
|
||||
{
|
||||
var view = source.CreateSortableView(transform);
|
||||
view.Sort(initialCompareSelector, ascending);
|
||||
return view;
|
||||
}
|
||||
|
||||
public static void Sort<T, TView, TCompare>(this ISortableSynchronizedView<T, TView> source, Func<T, TCompare> compareSelector, bool ascending = true)
|
||||
{
|
||||
source.Sort(new AnonymousComparer<T, TCompare>(compareSelector, ascending));
|
||||
}
|
||||
|
||||
class AnonymousComparer<T, TCompare> : IComparer<T>
|
||||
{
|
||||
readonly Func<T, TCompare> selector;
|
||||
readonly int f;
|
||||
|
||||
public AnonymousComparer(Func<T, TCompare> selector, bool ascending)
|
||||
{
|
||||
this.selector = selector;
|
||||
this.f = ascending ? 1 : -1;
|
||||
}
|
||||
|
||||
public int Compare(T? x, T? y)
|
||||
{
|
||||
if (x == null && y == null) return 0;
|
||||
if (x == null) return 1 * f;
|
||||
if (y == null) return -1 * f;
|
||||
|
||||
return Comparer<TCompare>.Default.Compare(selector(x), selector(y)) * f;
|
||||
}
|
||||
// Optimized for non filtered
|
||||
return new NonFilteredNotifyCollectionChangedSynchronizedViewList<T, TView>(collection.CreateView(transform), collectionEventDispatcher);
|
||||
}
|
||||
}
|
||||
}
|
@ -3,141 +3,22 @@ using System.Collections.Specialized;
|
||||
|
||||
namespace ObservableCollections
|
||||
{
|
||||
public readonly struct SynchronizedViewChangedEventArgs<T, TView>(
|
||||
NotifyCollectionChangedAction action,
|
||||
T newValue = default!,
|
||||
T oldValue = default!,
|
||||
TView newView = default!,
|
||||
TView oldView = default!,
|
||||
int newViewIndex = -1,
|
||||
int oldViewIndex = -1)
|
||||
public interface ISynchronizedViewFilter<T>
|
||||
{
|
||||
public readonly NotifyCollectionChangedAction Action = action;
|
||||
public readonly T NewValue = newValue;
|
||||
public readonly T OldValue = oldValue;
|
||||
public readonly TView NewView = newView;
|
||||
public readonly TView OldView = oldView;
|
||||
public readonly int NewViewIndex = newViewIndex;
|
||||
public readonly int OldViewIndex = oldViewIndex;
|
||||
bool IsMatch(T value);
|
||||
}
|
||||
|
||||
public interface ISynchronizedViewFilter<T, TView>
|
||||
public class SynchronizedViewFilter<T>(Func<T, bool> isMatch) : ISynchronizedViewFilter<T>
|
||||
{
|
||||
bool IsMatch(T value, TView view);
|
||||
void WhenTrue(T value, TView view);
|
||||
void WhenFalse(T value, TView view);
|
||||
void OnCollectionChanged(in SynchronizedViewChangedEventArgs<T, TView> eventArgs);
|
||||
}
|
||||
public static readonly ISynchronizedViewFilter<T> Null = new NullViewFilter();
|
||||
|
||||
public class SynchronizedViewFilter<T, TView>(
|
||||
Func<T, TView, bool> isMatch,
|
||||
Action<T, TView>? whenTrue,
|
||||
Action<T, TView>? whenFalse,
|
||||
Action<SynchronizedViewChangedEventArgs<T, TView>>? onCollectionChanged)
|
||||
: ISynchronizedViewFilter<T, TView>
|
||||
{
|
||||
public static readonly ISynchronizedViewFilter<T, TView> Null = new NullViewFilter();
|
||||
public bool IsMatch(T value) => isMatch(value);
|
||||
|
||||
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(in SynchronizedViewChangedEventArgs<T, TView> eventArgs) => onCollectionChanged?.Invoke(eventArgs);
|
||||
|
||||
class NullViewFilter : ISynchronizedViewFilter<T, TView>
|
||||
class NullViewFilter : ISynchronizedViewFilter<T>
|
||||
{
|
||||
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(in SynchronizedViewChangedEventArgs<T, TView> eventArgs) { }
|
||||
public bool IsMatch(T value) => true;
|
||||
}
|
||||
}
|
||||
|
||||
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<SynchronizedViewChangedEventArgs<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, int index)
|
||||
{
|
||||
filter.InvokeOnAdd(value.value, value.view, index);
|
||||
}
|
||||
|
||||
internal static void InvokeOnAdd<T, TView>(this ISynchronizedViewFilter<T, TView> filter, T value, TView view, int index)
|
||||
{
|
||||
if (filter.IsMatch(value, view))
|
||||
{
|
||||
filter.WhenTrue(value, view);
|
||||
}
|
||||
else
|
||||
{
|
||||
filter.WhenFalse(value, view);
|
||||
}
|
||||
filter.OnCollectionChanged(new SynchronizedViewChangedEventArgs<T, TView>(NotifyCollectionChangedAction.Add, newValue: value, newView: view, newViewIndex: index));
|
||||
}
|
||||
|
||||
internal static void InvokeOnRemove<T, TView>(this ISynchronizedViewFilter<T, TView> filter, (T value, TView view) value, int oldIndex)
|
||||
{
|
||||
filter.InvokeOnRemove(value.value, value.view, oldIndex);
|
||||
}
|
||||
|
||||
internal static void InvokeOnRemove<T, TView>(this ISynchronizedViewFilter<T, TView> filter, T value, TView view, int oldIndex)
|
||||
{
|
||||
filter.OnCollectionChanged(new SynchronizedViewChangedEventArgs<T, TView>(NotifyCollectionChangedAction.Remove, oldValue: value, oldView: view, oldViewIndex: oldIndex));
|
||||
}
|
||||
|
||||
internal static void InvokeOnMove<T, TView>(this ISynchronizedViewFilter<T, TView> filter, (T value, TView view) value, int index, int oldIndex)
|
||||
{
|
||||
InvokeOnMove(filter, value.value, value.view, index, oldIndex);
|
||||
}
|
||||
|
||||
internal static void InvokeOnMove<T, TView>(this ISynchronizedViewFilter<T, TView> filter, T value, TView view, int index, int oldIndex)
|
||||
{
|
||||
filter.OnCollectionChanged(new SynchronizedViewChangedEventArgs<T, TView>(NotifyCollectionChangedAction.Move, newValue: value, newView: view, newViewIndex: index, oldViewIndex: oldIndex));
|
||||
}
|
||||
|
||||
internal static void InvokeOnReplace<T, TView>(this ISynchronizedViewFilter<T, TView> filter, (T value, TView view) value, (T value, TView view) oldValue, int index, int oldIndex = -1)
|
||||
{
|
||||
filter.InvokeOnReplace(value.value, value.view, oldValue.value, oldValue.view, index, oldIndex);
|
||||
}
|
||||
|
||||
internal static void InvokeOnReplace<T, TView>(this ISynchronizedViewFilter<T, TView> filter, T value, TView view, T oldValue, TView oldView, int index, int oldIndex = -1)
|
||||
{
|
||||
filter.OnCollectionChanged(new SynchronizedViewChangedEventArgs<T, TView>(NotifyCollectionChangedAction.Replace, newValue: value, newView: view, oldValue: oldValue, oldView: oldView, newViewIndex: index, oldViewIndex: oldIndex >= 0 ? oldIndex : index));
|
||||
}
|
||||
|
||||
internal static void InvokeOnReset<T, TView>(this ISynchronizedViewFilter<T, TView> filter)
|
||||
{
|
||||
filter.OnCollectionChanged(new SynchronizedViewChangedEventArgs<T, TView>(NotifyCollectionChangedAction.Reset));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
55
src/ObservableCollections/Internal/FixedArray.cs
Normal file
55
src/ObservableCollections/Internal/FixedArray.cs
Normal file
@ -0,0 +1,55 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,264 +0,0 @@
|
||||
#pragma warning disable CS0067
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
|
||||
namespace ObservableCollections.Internal
|
||||
{
|
||||
internal sealed class FreezedView<T, TView> : ISynchronizedView<T, TView>
|
||||
{
|
||||
readonly bool reverse;
|
||||
readonly List<(T, TView)> list;
|
||||
|
||||
ISynchronizedViewFilter<T, TView> filter;
|
||||
|
||||
public 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, 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, null);
|
||||
}
|
||||
|
||||
public INotifyCollectionChangedSynchronizedView<TView> ToNotifyCollectionChanged(ICollectionEventDispatcher? collectionEventDispatcher)
|
||||
{
|
||||
return new NotifyCollectionChangedSynchronizedView<T, TView>(this, collectionEventDispatcher);
|
||||
}
|
||||
}
|
||||
|
||||
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, 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, null);
|
||||
}
|
||||
|
||||
public INotifyCollectionChangedSynchronizedView<TView> ToNotifyCollectionChanged(ICollectionEventDispatcher? collectionEventDispatcher)
|
||||
{
|
||||
return new NotifyCollectionChangedSynchronizedView<T, TView>(this, collectionEventDispatcher);
|
||||
}
|
||||
|
||||
class TComparer : IComparer<(T, TView)>
|
||||
{
|
||||
readonly IComparer<T> comparer;
|
||||
|
||||
public TComparer(IComparer<T> comparer)
|
||||
{
|
||||
this.comparer = comparer;
|
||||
}
|
||||
|
||||
public int Compare((T, TView) x, (T, TView) y)
|
||||
{
|
||||
return comparer.Compare(x.Item1, y.Item1);
|
||||
}
|
||||
}
|
||||
|
||||
class TViewComparer : IComparer<(T, TView)>
|
||||
{
|
||||
readonly IComparer<TView> comparer;
|
||||
|
||||
public TViewComparer(IComparer<TView> comparer)
|
||||
{
|
||||
this.comparer = comparer;
|
||||
}
|
||||
|
||||
public int Compare((T, TView) x, (T, TView) y)
|
||||
{
|
||||
return comparer.Compare(x.Item2, y.Item2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,288 +0,0 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace ObservableCollections.Internal
|
||||
{
|
||||
internal class NotifyCollectionChangedSynchronizedView<T, TView> :
|
||||
INotifyCollectionChangedSynchronizedView<TView>,
|
||||
ISynchronizedViewFilter<T, TView>
|
||||
{
|
||||
static readonly PropertyChangedEventArgs CountPropertyChangedEventArgs = new("Count");
|
||||
static readonly Action<NotifyCollectionChangedEventArgs> raiseChangedEventInvoke = RaiseChangedEvent;
|
||||
|
||||
readonly ISynchronizedView<T, TView> parent;
|
||||
readonly ISynchronizedViewFilter<T, TView> currentFilter;
|
||||
readonly ICollectionEventDispatcher eventDispatcher;
|
||||
|
||||
public NotifyCollectionChangedSynchronizedView(ISynchronizedView<T, TView> parent, ICollectionEventDispatcher? eventDispatcher)
|
||||
{
|
||||
this.parent = parent;
|
||||
this.eventDispatcher = eventDispatcher ?? DirectCollectionEventDispatcher.Instance;
|
||||
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(in SynchronizedViewChangedEventArgs<T, TView> args)
|
||||
{
|
||||
currentFilter.OnCollectionChanged(args);
|
||||
|
||||
if (CollectionChanged == null && PropertyChanged == null) return;
|
||||
|
||||
switch (args.Action)
|
||||
{
|
||||
case NotifyCollectionChangedAction.Add:
|
||||
eventDispatcher.Post(new CollectionEventDispatcherEventArgs(NotifyCollectionChangedAction.Add, args.NewView, args.NewViewIndex)
|
||||
{
|
||||
Collection = this,
|
||||
Invoker = raiseChangedEventInvoke,
|
||||
IsInvokeCollectionChanged = true,
|
||||
IsInvokePropertyChanged = true
|
||||
});
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Remove:
|
||||
eventDispatcher.Post(new CollectionEventDispatcherEventArgs(NotifyCollectionChangedAction.Remove, args.OldView, args.OldViewIndex)
|
||||
{
|
||||
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.NewView, args.OldView, args.NewViewIndex)
|
||||
{
|
||||
Collection = this,
|
||||
Invoker = raiseChangedEventInvoke,
|
||||
IsInvokeCollectionChanged = true,
|
||||
IsInvokePropertyChanged = false
|
||||
});
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Move:
|
||||
eventDispatcher.Post(new CollectionEventDispatcherEventArgs(NotifyCollectionChangedAction.Move, args.NewView, args.NewViewIndex, args.OldViewIndex)
|
||||
{
|
||||
Collection = this,
|
||||
Invoker = raiseChangedEventInvoke,
|
||||
IsInvokeCollectionChanged = true,
|
||||
IsInvokePropertyChanged = false
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void RaiseChangedEvent(NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
var e2 = (CollectionEventDispatcherEventArgs)e;
|
||||
var self = (NotifyCollectionChangedSynchronizedView<T, TView>)e2.Collection;
|
||||
|
||||
if (e2.IsInvokeCollectionChanged)
|
||||
{
|
||||
self.CollectionChanged?.Invoke(self, e);
|
||||
}
|
||||
if (e2.IsInvokePropertyChanged)
|
||||
{
|
||||
self.PropertyChanged?.Invoke(self, CountPropertyChangedEventArgs);
|
||||
}
|
||||
}
|
||||
|
||||
public void Refresh()
|
||||
{
|
||||
OnCollectionChanged(new SynchronizedViewChangedEventArgs<T, TView>(NotifyCollectionChangedAction.Reset));
|
||||
}
|
||||
}
|
||||
|
||||
internal class ListNotifyCollectionChangedSynchronizedView<T, TView>
|
||||
: NotifyCollectionChangedSynchronizedView<T, TView>
|
||||
, IList<TView>, IReadOnlyList<TView>
|
||||
, IList
|
||||
{
|
||||
readonly ObservableList<T>.View<TView> view;
|
||||
|
||||
public ListNotifyCollectionChangedSynchronizedView(ObservableList<T>.View<TView> parent, ICollectionEventDispatcher? eventDispatcher)
|
||||
: base(parent, eventDispatcher)
|
||||
{
|
||||
this.view = parent;
|
||||
}
|
||||
|
||||
public TView this[int index]
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (view.SyncRoot)
|
||||
{
|
||||
return view.list[index].Item2;
|
||||
}
|
||||
}
|
||||
set => throw new NotSupportedException();
|
||||
}
|
||||
|
||||
object? IList.this[int index]
|
||||
{
|
||||
get
|
||||
{
|
||||
return this[index];
|
||||
}
|
||||
set => throw new NotSupportedException();
|
||||
}
|
||||
|
||||
static bool IsCompatibleObject(object? value)
|
||||
{
|
||||
return (value is TView) || (value is T) || (value == null && default(T) == null);
|
||||
}
|
||||
|
||||
public bool IsReadOnly => true;
|
||||
|
||||
public bool IsFixedSize => false;
|
||||
|
||||
public bool IsSynchronized => true;
|
||||
|
||||
public object SyncRoot => view.SyncRoot;
|
||||
|
||||
public void Add(TView item)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public int Add(object? value)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public bool Contains(TView item)
|
||||
{
|
||||
lock (view.SyncRoot)
|
||||
{
|
||||
foreach (var listItem in view.list)
|
||||
{
|
||||
if (EqualityComparer<TView>.Default.Equals(listItem.Item2, item))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool Contains(object? value)
|
||||
{
|
||||
if (IsCompatibleObject(value))
|
||||
{
|
||||
return Contains((TView)value!);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void CopyTo(TView[] array, int arrayIndex)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public void CopyTo(Array array, int index)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public int IndexOf(TView item)
|
||||
{
|
||||
lock (view.SyncRoot)
|
||||
{
|
||||
var index = 0;
|
||||
foreach (var listItem in view.list)
|
||||
{
|
||||
if (EqualityComparer<TView>.Default.Equals(listItem.Item2, item))
|
||||
{
|
||||
return index;
|
||||
}
|
||||
index++;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public int IndexOf(object? item)
|
||||
{
|
||||
if (IsCompatibleObject(item))
|
||||
{
|
||||
return IndexOf((TView)item!);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public void Insert(int index, TView item)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public void Insert(int index, object? value)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public bool Remove(TView item)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public void Remove(object? value)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void RemoveAt(int index)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,58 +1,58 @@
|
||||
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!;
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,259 +0,0 @@
|
||||
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;
|
||||
var i = 0;
|
||||
foreach (var (_, (value, view)) in list)
|
||||
{
|
||||
if (invokeAddEventForCurrentElements)
|
||||
{
|
||||
filter.InvokeOnAdd(value, view, 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 (_, (value, view)) in list)
|
||||
{
|
||||
resetAction(value, view);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public INotifyCollectionChangedSynchronizedView<TView> ToNotifyCollectionChanged()
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
return new NotifyCollectionChangedSynchronizedView<T, TView>(this, null);
|
||||
}
|
||||
}
|
||||
|
||||
public INotifyCollectionChangedSynchronizedView<TView> ToNotifyCollectionChanged(ICollectionEventDispatcher? collectionEventDispatcher)
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
return new NotifyCollectionChangedSynchronizedView<T, TView>(this, collectionEventDispatcher);
|
||||
}
|
||||
}
|
||||
|
||||
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, 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, 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, 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, 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));
|
||||
var oldIndex = -1;
|
||||
if (list.TryGetValue(oldKey, out var o))
|
||||
{
|
||||
oldIndex = list.IndexOfKey(oldKey);
|
||||
list.RemoveAt(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.InvokeOnReplace((value, view), o, newIndex, oldIndex: oldIndex);
|
||||
}
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Move:
|
||||
{
|
||||
// Move(index change) does not affect sorted list.
|
||||
var oldValue = e.OldItem;
|
||||
var oldKey = (oldValue, identitySelector(oldValue));
|
||||
if (list.TryGetValue(oldKey, out var v))
|
||||
{
|
||||
var index = list.IndexOfKey(oldKey);
|
||||
filter.InvokeOnMove(v, index, index);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Reset:
|
||||
list.Clear();
|
||||
filter.InvokeOnReset();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
RoutingCollectionChanged?.Invoke(e);
|
||||
CollectionStateChanged?.Invoke(e.Action);
|
||||
}
|
||||
}
|
||||
|
||||
sealed class Comparer : IComparer<(T value, TKey id)>
|
||||
{
|
||||
readonly IComparer<T> comparer;
|
||||
|
||||
public Comparer(IComparer<T> comparer)
|
||||
{
|
||||
this.comparer = comparer;
|
||||
}
|
||||
|
||||
public int Compare((T value, TKey id) x, (T value, TKey id) y)
|
||||
{
|
||||
var compare = comparer.Compare(x.value, y.value);
|
||||
if (compare == 0)
|
||||
{
|
||||
compare = Comparer<TKey>.Default.Compare(x.id, y.id);
|
||||
}
|
||||
|
||||
return compare;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,278 +0,0 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
|
||||
namespace ObservableCollections.Internal
|
||||
{
|
||||
internal class SortedViewViewComparer<T, TKey, TView> : ISynchronizedView<T, TView>
|
||||
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;
|
||||
var i = 0;
|
||||
foreach (var (_, (value, view)) in list)
|
||||
{
|
||||
if (invokeAddEventForCurrentElements)
|
||||
{
|
||||
filter.InvokeOnAdd(value, view, 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 (_, (value, view)) in list)
|
||||
{
|
||||
resetAction(value, view);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public INotifyCollectionChangedSynchronizedView<TView> ToNotifyCollectionChanged()
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
return new NotifyCollectionChangedSynchronizedView<T, TView>(this, null);
|
||||
}
|
||||
}
|
||||
|
||||
public INotifyCollectionChangedSynchronizedView<TView> ToNotifyCollectionChanged(ICollectionEventDispatcher? collectionEventDispatcher)
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
return new NotifyCollectionChangedSynchronizedView<T, TView>(this, collectionEventDispatcher);
|
||||
}
|
||||
}
|
||||
|
||||
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, 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, 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, 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, index);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case NotifyCollectionChangedAction.Replace:
|
||||
// Replace is remove old item and insert new item.
|
||||
{
|
||||
var oldValue = e.OldItem;
|
||||
var oldId = identitySelector(oldValue);
|
||||
var oldIndex = -1;
|
||||
if (viewMap.Remove(oldId, out var oldView))
|
||||
{
|
||||
var oldKey = (oldView, oldId);
|
||||
if (list.TryGetValue(oldKey, out var v))
|
||||
{
|
||||
oldIndex = list.IndexOfKey(oldKey);
|
||||
list.RemoveAt(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.InvokeOnReplace(value, view, oldValue, oldView!, index, oldIndex);
|
||||
break;
|
||||
}
|
||||
case NotifyCollectionChangedAction.Move:
|
||||
// Move(index change) does not affect soreted dict.
|
||||
{
|
||||
var value = e.OldItem;
|
||||
var id = identitySelector(value);
|
||||
if (viewMap.TryGetValue(id, out var view))
|
||||
{
|
||||
var index = list.IndexOfKey((view, id));
|
||||
filter.InvokeOnMove(value, view, index, index);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case NotifyCollectionChangedAction.Reset:
|
||||
list.Clear();
|
||||
viewMap.Clear();
|
||||
filter.InvokeOnReset();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
RoutingCollectionChanged?.Invoke(e);
|
||||
CollectionStateChanged?.Invoke(e.Action);
|
||||
}
|
||||
}
|
||||
|
||||
sealed class Comparer : IComparer<(TView view, TKey id)>
|
||||
{
|
||||
readonly IComparer<TView> comparer;
|
||||
|
||||
public Comparer(IComparer<TView> comparer)
|
||||
{
|
||||
this.comparer = comparer;
|
||||
}
|
||||
|
||||
public int Compare((TView view, TKey id) x, (TView view, TKey id) y)
|
||||
{
|
||||
var compare = comparer.Compare(x.view, y.view);
|
||||
if (compare == 0)
|
||||
{
|
||||
compare = Comparer<TKey>.Default.Compare(x.id, y.id);
|
||||
}
|
||||
|
||||
return compare;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,9 +1,61 @@
|
||||
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)
|
||||
@ -16,7 +68,7 @@ namespace ObservableCollections
|
||||
/// Action.Move
|
||||
/// NewStartingIndex, OldStartingIndex
|
||||
/// Action.Reset
|
||||
/// -
|
||||
/// SortOperation(IsClear, IsReverse, Comparer)
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
public readonly ref struct NotifyCollectionChangedEventArgs<T>
|
||||
@ -29,8 +81,9 @@ 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)
|
||||
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)
|
||||
{
|
||||
Action = action;
|
||||
IsSingleItem = isSingleItem;
|
||||
@ -40,6 +93,7 @@ namespace ObservableCollections
|
||||
OldItems = oldItems;
|
||||
NewStartingIndex = newStartingIndex;
|
||||
OldStartingIndex = oldStartingIndex;
|
||||
SortOperation = sortOperation;
|
||||
}
|
||||
|
||||
public static NotifyCollectionChangedEventArgs<T> Add(T newItem, int newStartingIndex)
|
||||
@ -81,5 +135,15 @@ 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));
|
||||
}
|
||||
}
|
||||
}
|
@ -1,24 +1,35 @@
|
||||
<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" />
|
||||
</ItemGroup>
|
||||
<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>
|
||||
<ItemGroup>
|
||||
<None Include="../../Icon.png" Pack="true" PackagePath="/" />
|
||||
<EmbeddedResource Include="..\..\LICENSE" />
|
||||
</ItemGroup>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="PolySharp" Version="1.14.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
@ -9,7 +9,7 @@ namespace ObservableCollections
|
||||
{
|
||||
public sealed partial class ObservableDictionary<TKey, TValue>
|
||||
{
|
||||
public ISynchronizedView<KeyValuePair<TKey, TValue>, TView> CreateView<TView>(Func<KeyValuePair<TKey, TValue>, TView> transform, bool _ = false)
|
||||
public ISynchronizedView<KeyValuePair<TKey, TValue>, TView> CreateView<TView>(Func<KeyValuePair<TKey, TValue>, TView> transform)
|
||||
{
|
||||
// reverse is no used.
|
||||
return new View<TView>(this, transform);
|
||||
@ -19,32 +19,45 @@ namespace ObservableCollections
|
||||
{
|
||||
readonly ObservableDictionary<TKey, TValue> source;
|
||||
readonly Func<KeyValuePair<TKey, TValue>, TView> selector;
|
||||
ISynchronizedViewFilter<KeyValuePair<TKey, TValue>, TView> filter;
|
||||
ISynchronizedViewFilter<KeyValuePair<TKey, TValue>> filter;
|
||||
readonly Dictionary<TKey, (TValue, TView)> dict;
|
||||
int filteredCount;
|
||||
|
||||
public View(ObservableDictionary<TKey, TValue> source, Func<KeyValuePair<TKey, TValue>, TView> selector)
|
||||
{
|
||||
this.source = source;
|
||||
this.selector = selector;
|
||||
this.filter = SynchronizedViewFilter<KeyValuePair<TKey, TValue>, TView>.Null;
|
||||
this.filter = SynchronizedViewFilter<KeyValuePair<TKey, TValue>>.Null;
|
||||
this.SyncRoot = new object();
|
||||
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 NotifyCollectionChangedEventHandler<KeyValuePair<TKey, TValue>>? RoutingCollectionChanged;
|
||||
public event NotifyViewChangedEventHandler<KeyValuePair<TKey, TValue>, TView>? ViewChanged;
|
||||
public event Action<NotifyCollectionChangedAction>? CollectionStateChanged;
|
||||
|
||||
public ISynchronizedViewFilter<KeyValuePair<TKey, TValue>, TView> CurrentFilter
|
||||
public ISynchronizedViewFilter<KeyValuePair<TKey, TValue>> Filter
|
||||
{
|
||||
get { lock (SyncRoot) return filter; }
|
||||
}
|
||||
|
||||
public int Count
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
return filteredCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int UnfilteredCount
|
||||
{
|
||||
get
|
||||
{
|
||||
@ -60,76 +73,104 @@ namespace ObservableCollections
|
||||
this.source.CollectionChanged -= SourceCollectionChanged;
|
||||
}
|
||||
|
||||
public void AttachFilter(ISynchronizedViewFilter<KeyValuePair<TKey, TValue>, TView> filter, bool invokeAddEventForCurrentElements = false)
|
||||
public void AttachFilter(ISynchronizedViewFilter<KeyValuePair<TKey, TValue>> filter)
|
||||
{
|
||||
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);
|
||||
var view = v.Value.Item2;
|
||||
if (invokeAddEventForCurrentElements)
|
||||
if (filter.IsMatch(value))
|
||||
{
|
||||
filter.InvokeOnAdd(value, view, -1);
|
||||
}
|
||||
else
|
||||
{
|
||||
filter.InvokeOnAttach(value, view);
|
||||
filteredCount++;
|
||||
}
|
||||
}
|
||||
|
||||
ViewChanged?.Invoke(new SynchronizedViewChangedEventArgs<KeyValuePair<TKey, TValue>, TView>(NotifyCollectionChangedAction.Reset, true));
|
||||
}
|
||||
}
|
||||
|
||||
public void ResetFilter(Action<KeyValuePair<TKey, TValue>, TView>? resetAction)
|
||||
public void ResetFilter()
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
this.filter = SynchronizedViewFilter<KeyValuePair<TKey, TValue>, TView>.Null;
|
||||
if (resetAction != null)
|
||||
{
|
||||
foreach (var v in dict)
|
||||
{
|
||||
resetAction(new KeyValuePair<TKey, TValue>(v.Key, v.Value.Item1), v.Value.Item2);
|
||||
}
|
||||
}
|
||||
this.filter = SynchronizedViewFilter<KeyValuePair<TKey, TValue>>.Null;
|
||||
this.filteredCount = dict.Count;
|
||||
ViewChanged?.Invoke(new SynchronizedViewChangedEventArgs<KeyValuePair<TKey, TValue>, TView>(NotifyCollectionChangedAction.Reset, true));
|
||||
}
|
||||
}
|
||||
|
||||
public INotifyCollectionChangedSynchronizedView<TView> ToNotifyCollectionChanged()
|
||||
public ISynchronizedViewList<TView> ToViewList()
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
return new NotifyCollectionChangedSynchronizedView<KeyValuePair<TKey, TValue>, TView>(this, null);
|
||||
}
|
||||
return new FiltableSynchronizedViewList<KeyValuePair<TKey, TValue>, TView>(this);
|
||||
}
|
||||
|
||||
public INotifyCollectionChangedSynchronizedView<TView> ToNotifyCollectionChanged(ICollectionEventDispatcher? collectionEventDispatcher)
|
||||
public INotifyCollectionChangedSynchronizedViewList<TView> ToNotifyCollectionChanged()
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
return new NotifyCollectionChangedSynchronizedView<KeyValuePair<TKey, TValue>, TView>(this, collectionEventDispatcher);
|
||||
}
|
||||
return new NotifyCollectionChangedSynchronizedViewList<KeyValuePair<TKey, TValue>, TView>(this, null);
|
||||
}
|
||||
|
||||
public IEnumerator<(KeyValuePair<TKey, TValue>, TView)> GetEnumerator()
|
||||
public INotifyCollectionChangedSynchronizedViewList<TView> ToNotifyCollectionChanged(ICollectionEventDispatcher? collectionEventDispatcher)
|
||||
{
|
||||
return new NotifyCollectionChangedSynchronizedViewList<KeyValuePair<TKey, TValue>, TView>(this, collectionEventDispatcher);
|
||||
}
|
||||
|
||||
public IEnumerator<TView> GetEnumerator()
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
foreach (var item in dict)
|
||||
{
|
||||
var v = (new KeyValuePair<TKey, TValue>(item.Key, item.Value.Item1), item.Value.Item2);
|
||||
if (filter.IsMatch(v.Item1, v.Item2))
|
||||
if (filter.IsMatch(v.Item1))
|
||||
{
|
||||
yield return v;
|
||||
yield return v.Item2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
|
||||
public IEnumerable<(KeyValuePair<TKey, TValue> Value, TView View)> Filtered
|
||||
{
|
||||
return GetEnumerator();
|
||||
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.Item1))
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SourceCollectionChanged(in NotifyCollectionChangedEventArgs<KeyValuePair<TKey, TValue>> e)
|
||||
@ -140,41 +181,40 @@ namespace ObservableCollections
|
||||
switch (e.Action)
|
||||
{
|
||||
case NotifyCollectionChangedAction.Add:
|
||||
{
|
||||
var v = selector(e.NewItem);
|
||||
dict.Add(e.NewItem.Key, (e.NewItem.Value, v));
|
||||
filter.InvokeOnAdd(e.NewItem, v, -1);
|
||||
}
|
||||
{
|
||||
var v = selector(e.NewItem);
|
||||
dict.Add(e.NewItem.Key, (e.NewItem.Value, v));
|
||||
this.InvokeOnAdd(ref filteredCount, ViewChanged, e.NewItem, v, -1);
|
||||
}
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Remove:
|
||||
{
|
||||
if (dict.Remove(e.OldItem.Key, out var v))
|
||||
{
|
||||
filter.InvokeOnRemove(e.OldItem, v.Item2, -1);
|
||||
if (dict.Remove(e.OldItem.Key, out var v))
|
||||
{
|
||||
this.InvokeOnRemove(ref filteredCount, ViewChanged, e.OldItem, v.Item2, -1);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Replace:
|
||||
{
|
||||
var v = selector(e.NewItem);
|
||||
dict.Remove(e.OldItem.Key, out var ov);
|
||||
dict[e.NewItem.Key] = (e.NewItem.Value, v);
|
||||
|
||||
filter.InvokeOnReplace(e.NewItem, v, e.OldItem, ov.Item2, -1);
|
||||
}
|
||||
{
|
||||
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);
|
||||
}
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Reset:
|
||||
{
|
||||
dict.Clear();
|
||||
filter.InvokeOnReset();
|
||||
}
|
||||
{
|
||||
dict.Clear();
|
||||
this.InvokeOnReset(ref filteredCount, ViewChanged);
|
||||
}
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Move: // ObservableDictionary have no Move operation.
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
RoutingCollectionChanged?.Invoke(e);
|
||||
CollectionStateChanged?.Invoke(e.Action);
|
||||
}
|
||||
}
|
||||
|
@ -7,8 +7,7 @@ using System.Linq;
|
||||
|
||||
namespace ObservableCollections
|
||||
{
|
||||
public sealed partial class ObservableDictionary<TKey, TValue> : IDictionary<TKey, TValue>,
|
||||
IReadOnlyObservableDictionary<TKey, TValue>
|
||||
public sealed partial class ObservableDictionary<TKey, TValue> : IDictionary<TKey, TValue>, IReadOnlyObservableDictionary<TKey, TValue>
|
||||
where TKey : notnull
|
||||
{
|
||||
readonly Dictionary<TKey, TValue> dictionary;
|
||||
|
@ -320,9 +320,9 @@ namespace ObservableCollections
|
||||
return GetEnumerator();
|
||||
}
|
||||
|
||||
public ISynchronizedView<T, TView> CreateView<TView>(Func<T, TView> transform, bool reverse = false)
|
||||
public ISynchronizedView<T, TView> CreateView<TView>(Func<T, TView> transform)
|
||||
{
|
||||
return new ObservableRingBuffer<T>.View<TView>(this, transform, reverse);
|
||||
return new ObservableRingBuffer<T>.View<TView>(this, transform);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,19 +4,20 @@ using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ObservableCollections
|
||||
{
|
||||
public sealed partial class ObservableHashSet<T> : IReadOnlyCollection<T>, IObservableCollection<T>
|
||||
{
|
||||
public ISynchronizedView<T, TView> CreateView<TView>(Func<T, TView> transform, bool _ = false)
|
||||
public ISynchronizedView<T, TView> CreateView<TView>(Func<T, TView> transform)
|
||||
{
|
||||
return new View<TView>(this, transform);
|
||||
}
|
||||
|
||||
sealed class View<TView> : ISynchronizedView<T, TView>
|
||||
{
|
||||
public ISynchronizedViewFilter<T, TView> CurrentFilter
|
||||
public ISynchronizedViewFilter<T> Filter
|
||||
{
|
||||
get { lock (SyncRoot) return filter; }
|
||||
}
|
||||
@ -24,10 +25,11 @@ namespace ObservableCollections
|
||||
readonly ObservableHashSet<T> source;
|
||||
readonly Func<T, TView> selector;
|
||||
readonly Dictionary<T, (T, TView)> dict;
|
||||
int filteredCount;
|
||||
|
||||
ISynchronizedViewFilter<T, TView> filter;
|
||||
ISynchronizedViewFilter<T> filter;
|
||||
|
||||
public event NotifyCollectionChangedEventHandler<T>? RoutingCollectionChanged;
|
||||
public event NotifyViewChangedEventHandler<T, TView>? ViewChanged;
|
||||
public event Action<NotifyCollectionChangedAction>? CollectionStateChanged;
|
||||
|
||||
public object SyncRoot { get; }
|
||||
@ -36,16 +38,28 @@ namespace ObservableCollections
|
||||
{
|
||||
this.source = source;
|
||||
this.selector = selector;
|
||||
this.filter = SynchronizedViewFilter<T, TView>.Null;
|
||||
this.filter = SynchronizedViewFilter<T>.Null;
|
||||
this.SyncRoot = new object();
|
||||
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
|
||||
{
|
||||
@ -56,65 +70,63 @@ namespace ObservableCollections
|
||||
}
|
||||
}
|
||||
|
||||
public void AttachFilter(ISynchronizedViewFilter<T, TView> filter, bool invokeAddEventForCurrentElements = false)
|
||||
public void AttachFilter(ISynchronizedViewFilter<T> filter)
|
||||
{
|
||||
if (filter.IsNullFilter())
|
||||
{
|
||||
ResetFilter();
|
||||
return;
|
||||
}
|
||||
|
||||
lock (SyncRoot)
|
||||
{
|
||||
this.filter = filter;
|
||||
this.filteredCount = 0;
|
||||
foreach (var (_, (value, view)) in dict)
|
||||
{
|
||||
if (invokeAddEventForCurrentElements)
|
||||
if (filter.IsMatch(value))
|
||||
{
|
||||
filter.InvokeOnAdd((value, view), -1);
|
||||
}
|
||||
else
|
||||
{
|
||||
filter.InvokeOnAttach(value, view);
|
||||
filteredCount++;
|
||||
}
|
||||
}
|
||||
ViewChanged?.Invoke(new SynchronizedViewChangedEventArgs<T, TView>(NotifyCollectionChangedAction.Reset, true));
|
||||
}
|
||||
}
|
||||
|
||||
public void ResetFilter(Action<T, TView>? resetAction)
|
||||
public void ResetFilter()
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
this.filter = SynchronizedViewFilter<T, TView>.Null;
|
||||
if (resetAction != null)
|
||||
{
|
||||
foreach (var (_, (value, view)) in dict)
|
||||
{
|
||||
resetAction(value, view);
|
||||
}
|
||||
}
|
||||
this.filter = SynchronizedViewFilter<T>.Null;
|
||||
this.filteredCount = dict.Count;
|
||||
ViewChanged?.Invoke(new SynchronizedViewChangedEventArgs<T, TView>(NotifyCollectionChangedAction.Reset, true));
|
||||
}
|
||||
}
|
||||
|
||||
public INotifyCollectionChangedSynchronizedView<TView> ToNotifyCollectionChanged()
|
||||
public ISynchronizedViewList<TView> ToViewList()
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
return new NotifyCollectionChangedSynchronizedView<T, TView>(this, null);
|
||||
}
|
||||
return new FiltableSynchronizedViewList<T, TView>(this);
|
||||
}
|
||||
|
||||
public INotifyCollectionChangedSynchronizedView<TView> ToNotifyCollectionChanged(ICollectionEventDispatcher? collectionEventDispatcher)
|
||||
public INotifyCollectionChangedSynchronizedViewList<TView> ToNotifyCollectionChanged()
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
return new NotifyCollectionChangedSynchronizedView<T, TView>(this, collectionEventDispatcher);
|
||||
}
|
||||
return new NotifyCollectionChangedSynchronizedViewList<T, TView>(this, null);
|
||||
}
|
||||
|
||||
public IEnumerator<(T, TView)> GetEnumerator()
|
||||
public INotifyCollectionChangedSynchronizedViewList<TView> ToNotifyCollectionChanged(ICollectionEventDispatcher? collectionEventDispatcher)
|
||||
{
|
||||
return new NotifyCollectionChangedSynchronizedViewList<T, TView>(this, collectionEventDispatcher);
|
||||
}
|
||||
|
||||
public IEnumerator<TView> GetEnumerator()
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
foreach (var item in dict)
|
||||
{
|
||||
if (filter.IsMatch(item.Value.Item1, item.Value.Item2))
|
||||
if (filter.IsMatch(item.Value.Item1))
|
||||
{
|
||||
yield return item.Value;
|
||||
yield return item.Value.Item2;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -122,6 +134,37 @@ namespace ObservableCollections
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
|
||||
public IEnumerable<(T Value, TView View)> Filtered
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
foreach (var item in dict)
|
||||
{
|
||||
if (filter.IsMatch(item.Value.Item1))
|
||||
{
|
||||
yield return item.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<(T Value, TView View)> Unfiltered
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
foreach (var item in dict)
|
||||
{
|
||||
yield return item.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
this.source.CollectionChanged -= SourceCollectionChanged;
|
||||
@ -138,7 +181,7 @@ namespace ObservableCollections
|
||||
{
|
||||
var v = (e.NewItem, selector(e.NewItem));
|
||||
dict.Add(e.NewItem, v);
|
||||
filter.InvokeOnAdd(v, -1);
|
||||
this.InvokeOnAdd(ref filteredCount, ViewChanged, v, -1);
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -147,7 +190,7 @@ namespace ObservableCollections
|
||||
{
|
||||
var v = (item, selector(item));
|
||||
dict.Add(item, v);
|
||||
filter.InvokeOnAdd(v, i++);
|
||||
this.InvokeOnAdd(ref filteredCount, ViewChanged, v, i++);
|
||||
}
|
||||
}
|
||||
break;
|
||||
@ -156,7 +199,7 @@ namespace ObservableCollections
|
||||
{
|
||||
if (dict.Remove(e.OldItem, out var value))
|
||||
{
|
||||
filter.InvokeOnRemove(value, -1);
|
||||
this.InvokeOnRemove(ref filteredCount, ViewChanged, value, -1);
|
||||
}
|
||||
}
|
||||
else
|
||||
@ -165,14 +208,14 @@ namespace ObservableCollections
|
||||
{
|
||||
if (dict.Remove(item, out var value))
|
||||
{
|
||||
filter.InvokeOnRemove(value, -1);
|
||||
this.InvokeOnRemove(ref filteredCount, ViewChanged, value, -1);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Reset:
|
||||
dict.Clear();
|
||||
filter.InvokeOnReset();
|
||||
this.InvokeOnReset(ref filteredCount, ViewChanged);
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Replace:
|
||||
case NotifyCollectionChangedAction.Move:
|
||||
@ -180,7 +223,6 @@ namespace ObservableCollections
|
||||
break;
|
||||
}
|
||||
|
||||
RoutingCollectionChanged?.Invoke(e);
|
||||
CollectionStateChanged?.Invoke(e.Action);
|
||||
}
|
||||
}
|
||||
|
284
src/ObservableCollections/ObservableList.OptimizeView.cs
Normal file
284
src/ObservableCollections/ObservableList.OptimizeView.cs
Normal file
@ -0,0 +1,284 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.ComponentModel;
|
||||
using System.Text;
|
||||
|
||||
namespace ObservableCollections;
|
||||
|
||||
public sealed 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));
|
||||
//}
|
||||
|
||||
public INotifyCollectionChangedSynchronizedViewList<T> ToNotifyCollectionChanged()
|
||||
{
|
||||
return new ObservableListSynchronizedViewList<T>(this, null);
|
||||
}
|
||||
|
||||
public INotifyCollectionChangedSynchronizedViewList<T> ToNotifyCollectionChanged(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> : INotifyCollectionChangedSynchronizedViewList<T>, IList<T>, IList
|
||||
{
|
||||
static readonly PropertyChangedEventArgs CountPropertyChangedEventArgs = new("Count");
|
||||
static readonly Action<NotifyCollectionChangedEventArgs> raiseChangedEventInvoke = RaiseChangedEvent;
|
||||
|
||||
readonly ObservableList<T> parent;
|
||||
readonly ICollectionEventDispatcher eventDispatcher;
|
||||
|
||||
public event NotifyCollectionChangedEventHandler? CollectionChanged;
|
||||
public 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 T this[int index] => parent[index];
|
||||
|
||||
public int Count => parent.Count;
|
||||
|
||||
public IEnumerator<T> GetEnumerator()
|
||||
{
|
||||
return parent.GetEnumerator();
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return parent.GetEnumerator();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
parent.CollectionChanged -= Parent_CollectionChanged;
|
||||
}
|
||||
|
||||
// IList<T>, IList implementation
|
||||
|
||||
T IList<T>.this[int index]
|
||||
{
|
||||
get => ((IReadOnlyList<T>)this)[index];
|
||||
set => throw new NotSupportedException();
|
||||
}
|
||||
|
||||
object? IList.this[int index]
|
||||
{
|
||||
get
|
||||
{
|
||||
return this[index];
|
||||
}
|
||||
set => throw new NotSupportedException();
|
||||
}
|
||||
|
||||
static bool IsCompatibleObject(object? value)
|
||||
{
|
||||
return value is T || value == null && default(T) == null;
|
||||
}
|
||||
|
||||
public bool IsReadOnly => true;
|
||||
|
||||
public bool IsFixedSize => false;
|
||||
|
||||
public bool IsSynchronized => true;
|
||||
|
||||
public object SyncRoot => parent.SyncRoot;
|
||||
|
||||
public void Add(T item)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public int Add(object? value)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public bool Contains(T item)
|
||||
{
|
||||
return parent.Contains(item);
|
||||
}
|
||||
|
||||
public bool Contains(object? value)
|
||||
{
|
||||
if (IsCompatibleObject(value))
|
||||
{
|
||||
return Contains((T)value!);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void CopyTo(T[] array, int arrayIndex)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public void CopyTo(Array array, int index)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public int IndexOf(T item)
|
||||
{
|
||||
return parent.IndexOf(item);
|
||||
}
|
||||
|
||||
public int IndexOf(object? item)
|
||||
{
|
||||
if (IsCompatibleObject(item))
|
||||
{
|
||||
return IndexOf((T)item!);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public void Insert(int index, T item)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public void Insert(int index, object? value)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public bool Remove(T item)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public void Remove(object? value)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void RemoveAt(int index)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
@ -9,14 +9,14 @@ namespace ObservableCollections
|
||||
{
|
||||
public sealed partial class ObservableList<T> : IList<T>, IReadOnlyObservableList<T>
|
||||
{
|
||||
public ISynchronizedView<T, TView> CreateView<TView>(Func<T, TView> transform, bool reverse = false)
|
||||
public ISynchronizedView<T, TView> CreateView<TView>(Func<T, TView> transform)
|
||||
{
|
||||
return new View<TView>(this, transform, reverse);
|
||||
return new View<TView>(this, transform);
|
||||
}
|
||||
|
||||
internal sealed class View<TView> : ISynchronizedView<T, TView>
|
||||
{
|
||||
public ISynchronizedViewFilter<T, TView> CurrentFilter
|
||||
public ISynchronizedViewFilter<T> Filter
|
||||
{
|
||||
get
|
||||
{
|
||||
@ -26,31 +26,42 @@ namespace ObservableCollections
|
||||
|
||||
readonly ObservableList<T> source;
|
||||
readonly Func<T, TView> selector;
|
||||
readonly bool reverse;
|
||||
internal readonly List<(T, TView)> list; // be careful to use
|
||||
internal readonly List<(T, TView)> list; // unsafe, be careful to use
|
||||
int filteredCount;
|
||||
|
||||
ISynchronizedViewFilter<T, TView> filter;
|
||||
ISynchronizedViewFilter<T> filter;
|
||||
|
||||
public event NotifyCollectionChangedEventHandler<T>? RoutingCollectionChanged;
|
||||
public event NotifyViewChangedEventHandler<T, TView>? ViewChanged;
|
||||
public event Action<NotifyCollectionChangedAction>? CollectionStateChanged;
|
||||
|
||||
public object SyncRoot { get; }
|
||||
|
||||
public View(ObservableList<T> source, Func<T, TView> selector, bool reverse)
|
||||
public View(ObservableList<T> source, Func<T, TView> selector)
|
||||
{
|
||||
this.source = source;
|
||||
this.selector = selector;
|
||||
this.reverse = reverse;
|
||||
this.filter = SynchronizedViewFilter<T, TView>.Null;
|
||||
this.filter = SynchronizedViewFilter<T>.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
|
||||
{
|
||||
@ -61,79 +72,64 @@ namespace ObservableCollections
|
||||
}
|
||||
}
|
||||
|
||||
public void AttachFilter(ISynchronizedViewFilter<T, TView> filter, bool invokeAddEventForCurrentElements = false)
|
||||
public void AttachFilter(ISynchronizedViewFilter<T> filter)
|
||||
{
|
||||
if (filter.IsNullFilter())
|
||||
{
|
||||
ResetFilter();
|
||||
return;
|
||||
}
|
||||
|
||||
lock (SyncRoot)
|
||||
{
|
||||
this.filter = filter;
|
||||
this.filteredCount = 0;
|
||||
for (var i = 0; i < list.Count; i++)
|
||||
{
|
||||
var (value, view) = list[i];
|
||||
if (invokeAddEventForCurrentElements)
|
||||
if (filter.IsMatch(list[i].Item1))
|
||||
{
|
||||
filter.InvokeOnAdd(value, view, i);
|
||||
}
|
||||
else
|
||||
{
|
||||
filter.InvokeOnAttach(value, view);
|
||||
filteredCount++;
|
||||
}
|
||||
}
|
||||
|
||||
ViewChanged?.Invoke(new SynchronizedViewChangedEventArgs<T, TView>(NotifyCollectionChangedAction.Reset, true));
|
||||
}
|
||||
}
|
||||
|
||||
public void ResetFilter(Action<T, TView>? resetAction)
|
||||
public void ResetFilter()
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
this.filter = SynchronizedViewFilter<T, TView>.Null;
|
||||
if (resetAction != null)
|
||||
this.filter = SynchronizedViewFilter<T>.Null;
|
||||
this.filteredCount = list.Count;
|
||||
ViewChanged?.Invoke(new SynchronizedViewChangedEventArgs<T, TView>(NotifyCollectionChangedAction.Reset, true));
|
||||
}
|
||||
}
|
||||
|
||||
public ISynchronizedViewList<TView> ToViewList()
|
||||
{
|
||||
return new FiltableSynchronizedViewList<T, TView>(this);
|
||||
}
|
||||
|
||||
public INotifyCollectionChangedSynchronizedViewList<TView> ToNotifyCollectionChanged()
|
||||
{
|
||||
return new NotifyCollectionChangedSynchronizedViewList<T, TView>(this, null);
|
||||
}
|
||||
|
||||
public INotifyCollectionChangedSynchronizedViewList<TView> ToNotifyCollectionChanged(ICollectionEventDispatcher? collectionEventDispatcher)
|
||||
{
|
||||
return new NotifyCollectionChangedSynchronizedViewList<T, TView>(this, collectionEventDispatcher);
|
||||
}
|
||||
|
||||
public IEnumerator<TView> GetEnumerator()
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
foreach (var item in list)
|
||||
{
|
||||
foreach (var (item, view) in list)
|
||||
if (filter.IsMatch(item.Item1))
|
||||
{
|
||||
resetAction(item, view);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public INotifyCollectionChangedSynchronizedView<TView> ToNotifyCollectionChanged()
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
return new ListNotifyCollectionChangedSynchronizedView<T, TView>(this, null);
|
||||
}
|
||||
}
|
||||
|
||||
public INotifyCollectionChangedSynchronizedView<TView> ToNotifyCollectionChanged(ICollectionEventDispatcher? collectionEventDispatcher)
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
return new ListNotifyCollectionChangedSynchronizedView<T, TView>(this, collectionEventDispatcher);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
yield return item.Item2;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -141,6 +137,37 @@ namespace ObservableCollections
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
|
||||
public IEnumerable<(T Value, TView View)> Filtered
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
foreach (var item in list)
|
||||
{
|
||||
if (filter.IsMatch(item.Item1))
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<(T Value, TView View)> Unfiltered
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
foreach (var item in list)
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
this.source.CollectionChanged -= SourceCollectionChanged;
|
||||
@ -153,48 +180,41 @@ namespace ObservableCollections
|
||||
switch (e.Action)
|
||||
{
|
||||
case NotifyCollectionChangedAction.Add:
|
||||
// Add
|
||||
if (e.NewStartingIndex == list.Count)
|
||||
// Add or Insert
|
||||
if (e.IsSingleItem)
|
||||
{
|
||||
if (e.IsSingleItem)
|
||||
{
|
||||
var v = (e.NewItem, selector(e.NewItem));
|
||||
list.Add(v);
|
||||
filter.InvokeOnAdd(v, e.NewStartingIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
var i = e.NewStartingIndex;
|
||||
foreach (var item in e.NewItems)
|
||||
{
|
||||
var v = (item, selector(item));
|
||||
list.Add(v);
|
||||
filter.InvokeOnAdd(v, i++);
|
||||
}
|
||||
}
|
||||
var v = (e.NewItem, selector(e.NewItem));
|
||||
list.Insert(e.NewStartingIndex, v);
|
||||
this.InvokeOnAdd(ref filteredCount, ViewChanged, v, e.NewStartingIndex);
|
||||
}
|
||||
// Insert
|
||||
else
|
||||
{
|
||||
if (e.IsSingleItem)
|
||||
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++)
|
||||
{
|
||||
var v = (e.NewItem, selector(e.NewItem));
|
||||
list.Insert(e.NewStartingIndex, v);
|
||||
filter.InvokeOnAdd(v, e.NewStartingIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
// inefficient copy, need refactoring
|
||||
var newArray = new (T, TView)[e.NewItems.Length];
|
||||
var span = e.NewItems;
|
||||
for (var i = 0; i < span.Length; i++)
|
||||
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);
|
||||
if (isMatch)
|
||||
{
|
||||
var v = (span[i], selector(span[i]));
|
||||
newArray[i] = v;
|
||||
filter.InvokeOnAdd(v, e.NewStartingIndex + i);
|
||||
filteredCount++; // increment in this process
|
||||
}
|
||||
else
|
||||
{
|
||||
isMatchAll = false;
|
||||
}
|
||||
list.InsertRange(e.NewStartingIndex, newArray);
|
||||
}
|
||||
|
||||
list.InsertRange(e.NewStartingIndex, valueViews.Span);
|
||||
this.InvokeOnAddRange(ViewChanged, e.NewItems, views.Span, isMatchAll, matches.Span, e.NewStartingIndex);
|
||||
}
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Remove:
|
||||
@ -202,18 +222,36 @@ namespace ObservableCollections
|
||||
{
|
||||
var v = list[e.OldStartingIndex];
|
||||
list.RemoveAt(e.OldStartingIndex);
|
||||
filter.InvokeOnRemove(v, e.OldStartingIndex);
|
||||
this.InvokeOnRemove(ref filteredCount, ViewChanged, v, e.OldStartingIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
var len = e.OldStartingIndex + e.OldItems.Length;
|
||||
for (var i = e.OldStartingIndex; i < len; i++)
|
||||
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 v = list[i];
|
||||
filter.InvokeOnRemove(v, e.OldStartingIndex + i);
|
||||
var item = list[i];
|
||||
values.Span[j] = item.Item1;
|
||||
views.Span[j] = item.Item2;
|
||||
var isMatch = matches.Span[j] = Filter.IsMatch(item.Item1);
|
||||
if (isMatch)
|
||||
{
|
||||
filteredCount--; // decrement in this process
|
||||
}
|
||||
else
|
||||
{
|
||||
isMatchAll = false;
|
||||
}
|
||||
j++;
|
||||
}
|
||||
|
||||
list.RemoveRange(e.OldStartingIndex, e.OldItems.Length);
|
||||
this.InvokeOnRemoveRange(ViewChanged, values.Span, views.Span, isMatchAll, matches.Span, e.OldStartingIndex);
|
||||
}
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Replace:
|
||||
@ -222,7 +260,7 @@ namespace ObservableCollections
|
||||
var v = (e.NewItem, selector(e.NewItem));
|
||||
var ov = (e.OldItem, list[e.OldStartingIndex].Item2);
|
||||
list[e.NewStartingIndex] = v;
|
||||
filter.InvokeOnReplace(v, ov, e.NewStartingIndex);
|
||||
this.InvokeOnReplace(ref filteredCount, ViewChanged, v, ov, e.NewStartingIndex);
|
||||
break;
|
||||
}
|
||||
case NotifyCollectionChangedAction.Move:
|
||||
@ -231,21 +269,51 @@ namespace ObservableCollections
|
||||
list.RemoveAt(e.OldStartingIndex);
|
||||
list.Insert(e.NewStartingIndex, removeItem);
|
||||
|
||||
filter.InvokeOnMove(removeItem, e.NewStartingIndex, e.OldStartingIndex);
|
||||
this.InvokeOnMove(ref filteredCount, ViewChanged, removeItem, e.NewStartingIndex, e.OldStartingIndex);
|
||||
}
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Reset:
|
||||
list.Clear();
|
||||
filter.InvokeOnReset();
|
||||
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);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
RoutingCollectionChanged?.Invoke(e);
|
||||
CollectionStateChanged?.Invoke(e.Action);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -100,11 +100,16 @@ namespace ObservableCollections
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
var index = list.Count;
|
||||
var index = list.Count; // starting index
|
||||
|
||||
#if NET8_0_OR_GREATER
|
||||
list.AddRange(items);
|
||||
#else
|
||||
foreach (var item in items)
|
||||
{
|
||||
list.Add(item);
|
||||
}
|
||||
#endif
|
||||
|
||||
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Add(items, index));
|
||||
}
|
||||
@ -204,11 +209,16 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
@ -245,12 +255,9 @@ namespace ObservableCollections
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
#if NET5_0_OR_GREATER
|
||||
#pragma warning disable CS0436
|
||||
var range = CollectionsMarshal.AsSpan(list).Slice(index, count);
|
||||
#else
|
||||
var range = list.GetRange(index, count);
|
||||
#endif
|
||||
|
||||
#pragma warning restore CS0436
|
||||
// require copy before remove
|
||||
using (var xs = new CloneCollection<T>(range))
|
||||
{
|
||||
@ -270,5 +277,50 @@ 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,50 +4,62 @@ using System.Collections.Specialized;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ObservableCollections
|
||||
{
|
||||
public sealed partial class ObservableQueue<T> : IReadOnlyCollection<T>, IObservableCollection<T>
|
||||
{
|
||||
public ISynchronizedView<T, TView> CreateView<TView>(Func<T, TView> transform, bool reverse = false)
|
||||
public ISynchronizedView<T, TView> CreateView<TView>(Func<T, TView> transform)
|
||||
{
|
||||
return new View<TView>(this, transform, reverse);
|
||||
return new View<TView>(this, transform);
|
||||
}
|
||||
|
||||
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;
|
||||
ISynchronizedViewFilter<T> filter;
|
||||
|
||||
public event NotifyCollectionChangedEventHandler<T>? RoutingCollectionChanged;
|
||||
public event NotifyViewChangedEventHandler<T, TView>? ViewChanged;
|
||||
public event Action<NotifyCollectionChangedAction>? CollectionStateChanged;
|
||||
|
||||
public object SyncRoot { get; }
|
||||
|
||||
public ISynchronizedViewFilter<T, TView> CurrentFilter
|
||||
public ISynchronizedViewFilter<T> Filter
|
||||
{
|
||||
get { lock (SyncRoot) return filter; }
|
||||
}
|
||||
|
||||
public View(ObservableQueue<T> source, Func<T, TView> selector, bool reverse)
|
||||
public View(ObservableQueue<T> source, Func<T, TView> selector)
|
||||
{
|
||||
this.source = source;
|
||||
this.selector = selector;
|
||||
this.reverse = reverse;
|
||||
this.filter = SynchronizedViewFilter<T, TView>.Null;
|
||||
this.filter = SynchronizedViewFilter<T>.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
|
||||
{
|
||||
@ -58,79 +70,63 @@ namespace ObservableCollections
|
||||
}
|
||||
}
|
||||
|
||||
public void AttachFilter(ISynchronizedViewFilter<T, TView> filter, bool invokeAddEventForCurrentElements = false)
|
||||
public void AttachFilter(ISynchronizedViewFilter<T> filter)
|
||||
{
|
||||
if (filter.IsNullFilter())
|
||||
{
|
||||
ResetFilter();
|
||||
return;
|
||||
}
|
||||
|
||||
lock (SyncRoot)
|
||||
{
|
||||
this.filter = filter;
|
||||
var i = 0;
|
||||
this.filteredCount = 0;
|
||||
foreach (var (value, view) in queue)
|
||||
{
|
||||
if (invokeAddEventForCurrentElements)
|
||||
if (filter.IsMatch(value))
|
||||
{
|
||||
filter.InvokeOnAdd(value, view, i++);
|
||||
}
|
||||
else
|
||||
{
|
||||
filter.InvokeOnAttach(value, view);
|
||||
filteredCount++;
|
||||
}
|
||||
}
|
||||
ViewChanged?.Invoke(new SynchronizedViewChangedEventArgs<T, TView>(NotifyCollectionChangedAction.Reset, true));
|
||||
}
|
||||
}
|
||||
|
||||
public void ResetFilter(Action<T, TView>? resetAction)
|
||||
public void ResetFilter()
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
this.filter = SynchronizedViewFilter<T, TView>.Null;
|
||||
if (resetAction != null)
|
||||
this.filter = SynchronizedViewFilter<T>.Null;
|
||||
this.filteredCount = queue.Count;
|
||||
ViewChanged?.Invoke(new SynchronizedViewChangedEventArgs<T, TView>(NotifyCollectionChangedAction.Reset, true));
|
||||
}
|
||||
}
|
||||
|
||||
public ISynchronizedViewList<TView> ToViewList()
|
||||
{
|
||||
return new FiltableSynchronizedViewList<T, TView>(this);
|
||||
}
|
||||
|
||||
public INotifyCollectionChangedSynchronizedViewList<TView> ToNotifyCollectionChanged()
|
||||
{
|
||||
return new NotifyCollectionChangedSynchronizedViewList<T, TView>(this, null);
|
||||
}
|
||||
|
||||
public INotifyCollectionChangedSynchronizedViewList<TView> ToNotifyCollectionChanged(ICollectionEventDispatcher? collectionEventDispatcher)
|
||||
{
|
||||
return new NotifyCollectionChangedSynchronizedViewList<T, TView>(this, collectionEventDispatcher);
|
||||
}
|
||||
|
||||
public IEnumerator<TView> GetEnumerator()
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
foreach (var item in queue)
|
||||
{
|
||||
foreach (var (item, view) in queue)
|
||||
if (filter.IsMatch(item.Item1))
|
||||
{
|
||||
resetAction(item, view);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public INotifyCollectionChangedSynchronizedView<TView> ToNotifyCollectionChanged()
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
return new NotifyCollectionChangedSynchronizedView<T, TView>(this, null);
|
||||
}
|
||||
}
|
||||
|
||||
public INotifyCollectionChangedSynchronizedView<TView> ToNotifyCollectionChanged(ICollectionEventDispatcher? collectionEventDispatcher)
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
return new NotifyCollectionChangedSynchronizedView<T, TView>(this, collectionEventDispatcher);
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerator<(T, TView)> GetEnumerator()
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
if (!reverse)
|
||||
{
|
||||
foreach (var item in queue)
|
||||
{
|
||||
if (filter.IsMatch(item.Item1, item.Item2))
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var item in queue.AsEnumerable().Reverse())
|
||||
{
|
||||
if (filter.IsMatch(item.Item1, item.Item2))
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
yield return item.Item2;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -138,6 +134,37 @@ namespace ObservableCollections
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
|
||||
public IEnumerable<(T Value, TView View)> Filtered
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
foreach (var item in queue)
|
||||
{
|
||||
if (filter.IsMatch(item.Item1))
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<(T Value, TView View)> Unfiltered
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
foreach (var item in queue)
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
this.source.CollectionChanged -= SourceCollectionChanged;
|
||||
@ -155,7 +182,7 @@ namespace ObservableCollections
|
||||
{
|
||||
var v = (e.NewItem, selector(e.NewItem));
|
||||
queue.Enqueue(v);
|
||||
filter.InvokeOnAdd(v, e.NewStartingIndex);
|
||||
this.InvokeOnAdd(ref filteredCount, ViewChanged, v, e.NewStartingIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -164,7 +191,7 @@ namespace ObservableCollections
|
||||
{
|
||||
var v = (item, selector(item));
|
||||
queue.Enqueue(v);
|
||||
filter.InvokeOnAdd(v, i++);
|
||||
this.InvokeOnAdd(ref filteredCount, ViewChanged, v, i++);
|
||||
}
|
||||
}
|
||||
break;
|
||||
@ -173,7 +200,7 @@ namespace ObservableCollections
|
||||
if (e.IsSingleItem)
|
||||
{
|
||||
var v = queue.Dequeue();
|
||||
filter.InvokeOnRemove(v.Item1, v.Item2, 0);
|
||||
this.InvokeOnRemove(ref filteredCount, ViewChanged, v.Item1, v.Item2, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -181,13 +208,13 @@ namespace ObservableCollections
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
var v = queue.Dequeue();
|
||||
filter.InvokeOnRemove(v.Item1, v.Item2, 0);
|
||||
this.InvokeOnRemove(ref filteredCount, ViewChanged, v.Item1, v.Item2, 0);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Reset:
|
||||
queue.Clear();
|
||||
filter.InvokeOnReset();
|
||||
this.InvokeOnReset(ref filteredCount, ViewChanged);
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Replace:
|
||||
case NotifyCollectionChangedAction.Move:
|
||||
@ -195,7 +222,6 @@ namespace ObservableCollections
|
||||
break;
|
||||
}
|
||||
|
||||
RoutingCollectionChanged?.Invoke(e);
|
||||
CollectionStateChanged?.Invoke(e.Action);
|
||||
}
|
||||
}
|
||||
|
@ -4,51 +4,63 @@ using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ObservableCollections
|
||||
{
|
||||
public sealed partial class ObservableRingBuffer<T>
|
||||
{
|
||||
public ISynchronizedView<T, TView> CreateView<TView>(Func<T, TView> transform, bool reverse = false)
|
||||
public ISynchronizedView<T, TView> CreateView<TView>(Func<T, TView> transform)
|
||||
{
|
||||
return new View<TView>(this, transform, reverse);
|
||||
return new View<TView>(this, transform);
|
||||
}
|
||||
|
||||
// used with ObservableFixedSizeRingBuffer
|
||||
internal sealed class View<TView> : ISynchronizedView<T, TView>
|
||||
{
|
||||
public ISynchronizedViewFilter<T, TView> CurrentFilter
|
||||
public ISynchronizedViewFilter<T> Filter
|
||||
{
|
||||
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;
|
||||
ISynchronizedViewFilter<T> filter;
|
||||
|
||||
public event NotifyCollectionChangedEventHandler<T>? RoutingCollectionChanged;
|
||||
public event NotifyViewChangedEventHandler<T, TView>? ViewChanged;
|
||||
public event Action<NotifyCollectionChangedAction>? CollectionStateChanged;
|
||||
|
||||
public object SyncRoot { get; }
|
||||
|
||||
public View(IObservableCollection<T> source, Func<T, TView> selector, bool reverse)
|
||||
public View(IObservableCollection<T> source, Func<T, TView> selector)
|
||||
{
|
||||
this.source = source;
|
||||
this.selector = selector;
|
||||
this.reverse = reverse;
|
||||
this.filter = SynchronizedViewFilter<T, TView>.Null;
|
||||
this.filter = SynchronizedViewFilter<T>.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
|
||||
{
|
||||
@ -59,79 +71,70 @@ namespace ObservableCollections
|
||||
}
|
||||
}
|
||||
|
||||
public void AttachFilter(ISynchronizedViewFilter<T, TView> filter, bool invokeAddEventForCurrentElements = false)
|
||||
public void AttachFilter(ISynchronizedViewFilter<T> filter)
|
||||
{
|
||||
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 (invokeAddEventForCurrentElements)
|
||||
if (filter.IsMatch(value))
|
||||
{
|
||||
filter.InvokeOnAdd(value, view, i);
|
||||
}
|
||||
else
|
||||
{
|
||||
filter.InvokeOnAttach(value, view);
|
||||
filteredCount++;
|
||||
}
|
||||
}
|
||||
ViewChanged?.Invoke(new SynchronizedViewChangedEventArgs<T, TView>(NotifyCollectionChangedAction.Reset, true));
|
||||
}
|
||||
}
|
||||
|
||||
public void ResetFilter(Action<T, TView>? resetAction)
|
||||
public void ResetFilter()
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
this.filter = SynchronizedViewFilter<T, TView>.Null;
|
||||
if (resetAction != null)
|
||||
this.filter = SynchronizedViewFilter<T>.Null;
|
||||
this.filteredCount = ringBuffer.Count;
|
||||
ViewChanged?.Invoke(new SynchronizedViewChangedEventArgs<T, TView>(NotifyCollectionChangedAction.Reset, true));
|
||||
}
|
||||
}
|
||||
|
||||
public ISynchronizedViewList<TView> ToViewList()
|
||||
{
|
||||
return new FiltableSynchronizedViewList<T, TView>(this);
|
||||
}
|
||||
|
||||
public INotifyCollectionChangedSynchronizedViewList<TView> ToNotifyCollectionChanged()
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
return new NotifyCollectionChangedSynchronizedViewList<T, TView>(this, null);
|
||||
}
|
||||
}
|
||||
|
||||
public INotifyCollectionChangedSynchronizedViewList<TView> ToNotifyCollectionChanged(ICollectionEventDispatcher? collectionEventDispatcher)
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
return new NotifyCollectionChangedSynchronizedViewList<T, TView>(this, collectionEventDispatcher);
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerator<TView> GetEnumerator()
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
foreach (var item in ringBuffer)
|
||||
{
|
||||
foreach (var (item, view) in ringBuffer)
|
||||
if (filter.IsMatch(item.Item1))
|
||||
{
|
||||
resetAction(item, view);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public INotifyCollectionChangedSynchronizedView<TView> ToNotifyCollectionChanged()
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
return new NotifyCollectionChangedSynchronizedView<T, TView>(this, null);
|
||||
}
|
||||
}
|
||||
|
||||
public INotifyCollectionChangedSynchronizedView<TView> ToNotifyCollectionChanged(ICollectionEventDispatcher? collectionEventDispatcher)
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
return new NotifyCollectionChangedSynchronizedView<T, TView>(this, collectionEventDispatcher);
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerator<(T, TView)> GetEnumerator()
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
if (!reverse)
|
||||
{
|
||||
foreach (var item in ringBuffer)
|
||||
{
|
||||
if (filter.IsMatch(item.Item1, item.Item2))
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var item in ringBuffer.AsEnumerable().Reverse())
|
||||
{
|
||||
if (filter.IsMatch(item.Item1, item.Item2))
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
yield return item.Item2;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -139,6 +142,37 @@ namespace ObservableCollections
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
|
||||
public IEnumerable<(T Value, TView View)> Filtered
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
foreach (var item in ringBuffer)
|
||||
{
|
||||
if (filter.IsMatch(item.Item1))
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<(T Value, TView View)> Unfiltered
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
foreach (var item in ringBuffer)
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
this.source.CollectionChanged -= SourceCollectionChanged;
|
||||
@ -162,7 +196,7 @@ namespace ObservableCollections
|
||||
{
|
||||
var v = (e.NewItem, selector(e.NewItem));
|
||||
ringBuffer.AddFirst(v);
|
||||
filter.InvokeOnAdd(v, 0);
|
||||
this.InvokeOnAdd(ref filteredCount, ViewChanged, v, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -170,7 +204,7 @@ namespace ObservableCollections
|
||||
{
|
||||
var v = (item, selector(item));
|
||||
ringBuffer.AddFirst(v);
|
||||
filter.InvokeOnAdd(v, 0);
|
||||
this.InvokeOnAdd(ref filteredCount, ViewChanged, v, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -181,7 +215,7 @@ namespace ObservableCollections
|
||||
{
|
||||
var v = (e.NewItem, selector(e.NewItem));
|
||||
ringBuffer.AddLast(v);
|
||||
filter.InvokeOnAdd(v, ringBuffer.Count - 1);
|
||||
this.InvokeOnAdd(ref filteredCount, ViewChanged, v, ringBuffer.Count - 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -189,7 +223,7 @@ namespace ObservableCollections
|
||||
{
|
||||
var v = (item, selector(item));
|
||||
ringBuffer.AddLast(v);
|
||||
filter.InvokeOnAdd(v, ringBuffer.Count - 1);
|
||||
this.InvokeOnAdd(ref filteredCount, ViewChanged, v, ringBuffer.Count - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -202,14 +236,14 @@ namespace ObservableCollections
|
||||
if (e.IsSingleItem)
|
||||
{
|
||||
var v = ringBuffer.RemoveFirst();
|
||||
filter.InvokeOnRemove(v, 0);
|
||||
this.InvokeOnRemove(ref filteredCount, ViewChanged, v, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int i = 0; i < e.OldItems.Length; i++)
|
||||
{
|
||||
var v = ringBuffer.RemoveFirst();
|
||||
filter.InvokeOnRemove(v, 0);
|
||||
this.InvokeOnRemove(ref filteredCount, ViewChanged, v, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -220,7 +254,7 @@ namespace ObservableCollections
|
||||
{
|
||||
var index = ringBuffer.Count - 1;
|
||||
var v = ringBuffer.RemoveLast();
|
||||
filter.InvokeOnRemove(v, index);
|
||||
this.InvokeOnRemove(ref filteredCount, ViewChanged, v, index);
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -228,14 +262,14 @@ namespace ObservableCollections
|
||||
{
|
||||
var index = ringBuffer.Count - 1;
|
||||
var v = ringBuffer.RemoveLast();
|
||||
filter.InvokeOnRemove(v, index);
|
||||
this.InvokeOnRemove(ref filteredCount, ViewChanged, v, index);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Reset:
|
||||
ringBuffer.Clear();
|
||||
filter.InvokeOnReset();
|
||||
this.InvokeOnReset(ref filteredCount, ViewChanged);
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Replace:
|
||||
// range is not supported
|
||||
@ -243,7 +277,7 @@ namespace ObservableCollections
|
||||
var ov = ringBuffer[e.OldStartingIndex];
|
||||
var v = (e.NewItem, selector(e.NewItem));
|
||||
ringBuffer[e.NewStartingIndex] = v;
|
||||
filter.InvokeOnReplace(v, ov, e.NewStartingIndex);
|
||||
this.InvokeOnReplace(ref filteredCount, ViewChanged, v, ov, e.NewStartingIndex);
|
||||
break;
|
||||
}
|
||||
case NotifyCollectionChangedAction.Move:
|
||||
@ -251,7 +285,6 @@ namespace ObservableCollections
|
||||
break;
|
||||
}
|
||||
|
||||
RoutingCollectionChanged?.Invoke(e);
|
||||
CollectionStateChanged?.Invoke(e.Action);
|
||||
}
|
||||
}
|
||||
|
@ -9,45 +9,56 @@ namespace ObservableCollections
|
||||
{
|
||||
public sealed partial class ObservableStack<T> : IReadOnlyCollection<T>, IObservableCollection<T>
|
||||
{
|
||||
public ISynchronizedView<T, TView> CreateView<TView>(Func<T, TView> transform, bool reverse = false)
|
||||
public ISynchronizedView<T, TView> CreateView<TView>(Func<T, TView> transform)
|
||||
{
|
||||
return new View<TView>(this, transform, reverse);
|
||||
return new View<TView>(this, transform);
|
||||
}
|
||||
|
||||
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;
|
||||
ISynchronizedViewFilter<T> filter;
|
||||
|
||||
public event NotifyCollectionChangedEventHandler<T>? RoutingCollectionChanged;
|
||||
public event NotifyViewChangedEventHandler<T, TView>? ViewChanged;
|
||||
public event Action<NotifyCollectionChangedAction>? CollectionStateChanged;
|
||||
|
||||
public object SyncRoot { get; }
|
||||
|
||||
public ISynchronizedViewFilter<T, TView> CurrentFilter
|
||||
public ISynchronizedViewFilter<T> Filter
|
||||
{
|
||||
get { lock (SyncRoot) return filter; }
|
||||
}
|
||||
|
||||
public View(ObservableStack<T> source, Func<T, TView> selector, bool reverse)
|
||||
public View(ObservableStack<T> source, Func<T, TView> selector)
|
||||
{
|
||||
this.source = source;
|
||||
this.selector = selector;
|
||||
this.reverse = reverse;
|
||||
this.filter = SynchronizedViewFilter<T, TView>.Null;
|
||||
this.filter = SynchronizedViewFilter<T>.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
|
||||
{
|
||||
@ -58,78 +69,69 @@ namespace ObservableCollections
|
||||
}
|
||||
}
|
||||
|
||||
public void AttachFilter(ISynchronizedViewFilter<T, TView> filter, bool invokeAddEventForCurrentElements = false)
|
||||
public void AttachFilter(ISynchronizedViewFilter<T> filter)
|
||||
{
|
||||
if (filter.IsNullFilter())
|
||||
{
|
||||
ResetFilter();
|
||||
return;
|
||||
}
|
||||
|
||||
lock (SyncRoot)
|
||||
{
|
||||
this.filter = filter;
|
||||
this.filteredCount = 0;
|
||||
foreach (var (value, view) in stack)
|
||||
{
|
||||
if (invokeAddEventForCurrentElements)
|
||||
if (filter.IsMatch(value))
|
||||
{
|
||||
filter.InvokeOnAdd(value, view, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
filter.InvokeOnAttach(value, view);
|
||||
filteredCount++;
|
||||
}
|
||||
}
|
||||
ViewChanged?.Invoke(new SynchronizedViewChangedEventArgs<T, TView>(NotifyCollectionChangedAction.Reset, true));
|
||||
}
|
||||
}
|
||||
|
||||
public void ResetFilter(Action<T, TView>? resetAction)
|
||||
public void ResetFilter()
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
this.filter = SynchronizedViewFilter<T, TView>.Null;
|
||||
if (resetAction != null)
|
||||
this.filter = SynchronizedViewFilter<T>.Null;
|
||||
this.filteredCount = stack.Count;
|
||||
ViewChanged?.Invoke(new SynchronizedViewChangedEventArgs<T, TView>(NotifyCollectionChangedAction.Reset, true));
|
||||
}
|
||||
}
|
||||
|
||||
public ISynchronizedViewList<TView> ToViewList()
|
||||
{
|
||||
return new FiltableSynchronizedViewList<T, TView>(this);
|
||||
}
|
||||
|
||||
public INotifyCollectionChangedSynchronizedViewList<TView> ToNotifyCollectionChanged()
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
return new NotifyCollectionChangedSynchronizedViewList<T, TView>(this, null);
|
||||
}
|
||||
}
|
||||
|
||||
public INotifyCollectionChangedSynchronizedViewList<TView> ToNotifyCollectionChanged(ICollectionEventDispatcher? collectionEventDispatcher)
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
return new NotifyCollectionChangedSynchronizedViewList<T, TView>(this, collectionEventDispatcher);
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerator<TView> GetEnumerator()
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
foreach (var item in stack)
|
||||
{
|
||||
foreach (var (item, view) in stack)
|
||||
if (filter.IsMatch(item.Item1))
|
||||
{
|
||||
resetAction(item, view);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public INotifyCollectionChangedSynchronizedView<TView> ToNotifyCollectionChanged()
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
return new NotifyCollectionChangedSynchronizedView<T, TView>(this, null);
|
||||
}
|
||||
}
|
||||
|
||||
public INotifyCollectionChangedSynchronizedView<TView> ToNotifyCollectionChanged(ICollectionEventDispatcher? collectionEventDispatcher)
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
return new NotifyCollectionChangedSynchronizedView<T, TView>(this, collectionEventDispatcher);
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerator<(T, TView)> GetEnumerator()
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
if (!reverse)
|
||||
{
|
||||
foreach (var item in stack)
|
||||
{
|
||||
if (filter.IsMatch(item.Item1, item.Item2))
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var item in stack.AsEnumerable().Reverse())
|
||||
{
|
||||
if (filter.IsMatch(item.Item1, item.Item2))
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
yield return item.Item2;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -137,6 +139,37 @@ namespace ObservableCollections
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
|
||||
public IEnumerable<(T Value, TView View)> Filtered
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
foreach (var item in stack)
|
||||
{
|
||||
if (filter.IsMatch(item.Item1))
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<(T Value, TView View)> Unfiltered
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
foreach (var item in stack)
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
this.source.CollectionChanged -= SourceCollectionChanged;
|
||||
@ -154,7 +187,7 @@ namespace ObservableCollections
|
||||
{
|
||||
var v = (e.NewItem, selector(e.NewItem));
|
||||
stack.Push(v);
|
||||
filter.InvokeOnAdd(v, 0);
|
||||
this.InvokeOnAdd(ref filteredCount, ViewChanged, v, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -162,7 +195,7 @@ namespace ObservableCollections
|
||||
{
|
||||
var v = (item, selector(item));
|
||||
stack.Push(v);
|
||||
filter.InvokeOnAdd(v, 0);
|
||||
this.InvokeOnAdd(ref filteredCount, ViewChanged, v, 0);
|
||||
}
|
||||
}
|
||||
break;
|
||||
@ -171,7 +204,7 @@ namespace ObservableCollections
|
||||
if (e.IsSingleItem)
|
||||
{
|
||||
var v = stack.Pop();
|
||||
filter.InvokeOnRemove(v.Item1, v.Item2, 0);
|
||||
this.InvokeOnRemove(ref filteredCount, ViewChanged, v.Item1, v.Item2, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -179,13 +212,13 @@ namespace ObservableCollections
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
var v = stack.Pop();
|
||||
filter.InvokeOnRemove(v.Item1, v.Item2, 0);
|
||||
this.InvokeOnRemove(ref filteredCount, ViewChanged, v.Item1, v.Item2, 0);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Reset:
|
||||
stack.Clear();
|
||||
filter.InvokeOnReset();
|
||||
this.InvokeOnReset(ref filteredCount, ViewChanged);
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Replace:
|
||||
case NotifyCollectionChangedAction.Move:
|
||||
@ -193,7 +226,6 @@ namespace ObservableCollections
|
||||
break;
|
||||
}
|
||||
|
||||
RoutingCollectionChanged?.Invoke(e);
|
||||
CollectionStateChanged?.Invoke(e.Action);
|
||||
}
|
||||
}
|
||||
|
@ -2,12 +2,17 @@
|
||||
|
||||
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;
|
||||
@ -32,6 +37,89 @@ 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)
|
||||
|
34
src/ObservableCollections/Shims/CollectionsMarshalEx.cs
Normal file
34
src/ObservableCollections/Shims/CollectionsMarshalEx.cs
Normal file
@ -0,0 +1,34 @@
|
||||
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
|
258
src/ObservableCollections/SynchronizedViewChangedEventArgs.cs
Normal file
258
src/ObservableCollections/SynchronizedViewChangedEventArgs.cs
Normal file
@ -0,0 +1,258 @@
|
||||
#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 SynchronizedViewFilter<T>(filter));
|
||||
}
|
||||
|
||||
public static bool IsNullFilter<T>(this ISynchronizedViewFilter<T> filter)
|
||||
{
|
||||
return filter == SynchronizedViewFilter<T>.Null;
|
||||
}
|
||||
|
||||
internal static void InvokeOnAdd<T, TView>(this ISynchronizedView<T, TView> collection, ref int filteredCount, NotifyViewChangedEventHandler<T, TView>? ev, (T value, TView view) value, int index)
|
||||
{
|
||||
InvokeOnAdd(collection, ref filteredCount, ev, value.value, value.view, index);
|
||||
}
|
||||
|
||||
internal static void InvokeOnAdd<T, TView>(this ISynchronizedView<T, TView> collection, ref int filteredCount, NotifyViewChangedEventHandler<T, TView>? ev, T value, TView view, int index)
|
||||
{
|
||||
var isMatch = collection.Filter.IsMatch(value);
|
||||
if (isMatch)
|
||||
{
|
||||
filteredCount++;
|
||||
if (ev != null)
|
||||
{
|
||||
ev.Invoke(new SynchronizedViewChangedEventArgs<T, TView>(NotifyCollectionChangedAction.Add, true, newItem: (value, view), newStartingIndex: index));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static void InvokeOnAddRange<T, TView>(this ISynchronizedView<T, TView> collection, NotifyViewChangedEventHandler<T, TView>? ev, ReadOnlySpan<T> values, ReadOnlySpan<TView> views, bool isMatchAll, ReadOnlySpan<bool> matches, int index)
|
||||
{
|
||||
if (ev != null)
|
||||
{
|
||||
if (isMatchAll)
|
||||
{
|
||||
ev.Invoke(new SynchronizedViewChangedEventArgs<T, TView>(NotifyCollectionChangedAction.Add, isSingleItem: false, newValues: values, newViews: views, newStartingIndex: index));
|
||||
}
|
||||
else
|
||||
{
|
||||
var startingIndex = index;
|
||||
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: startingIndex++));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static void InvokeOnRemove<T, TView>(this ISynchronizedView<T, TView> collection, ref int filteredCount, NotifyViewChangedEventHandler<T, TView>? ev, (T value, TView view) value, int oldIndex)
|
||||
{
|
||||
InvokeOnRemove(collection, ref filteredCount, ev, value.value, value.view, oldIndex);
|
||||
}
|
||||
|
||||
internal static void InvokeOnRemove<T, TView>(this ISynchronizedView<T, TView> collection, ref int filteredCount, NotifyViewChangedEventHandler<T, TView>? ev, T value, TView view, int oldIndex)
|
||||
{
|
||||
var isMatch = collection.Filter.IsMatch(value);
|
||||
if (isMatch)
|
||||
{
|
||||
filteredCount--;
|
||||
if (ev != null)
|
||||
{
|
||||
ev.Invoke(new SynchronizedViewChangedEventArgs<T, TView>(NotifyCollectionChangedAction.Remove, true, oldItem: (value, view), oldStartingIndex: oldIndex));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// only use for ObservableList
|
||||
internal static void InvokeOnRemoveRange<T, TView>(this ISynchronizedView<T, TView> collection, NotifyViewChangedEventHandler<T, TView>? ev, ReadOnlySpan<T> values, ReadOnlySpan<TView> views, bool isMatchAll, ReadOnlySpan<bool> matches, int index)
|
||||
{
|
||||
if (ev != null)
|
||||
{
|
||||
if (isMatchAll)
|
||||
{
|
||||
ev.Invoke(new SynchronizedViewChangedEventArgs<T, TView>(NotifyCollectionChangedAction.Remove, isSingleItem: false, oldValues: values, oldViews: views, oldStartingIndex: index));
|
||||
}
|
||||
else
|
||||
{
|
||||
var startingIndex = index;
|
||||
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
|
||||
{
|
||||
index++; // not matched, skip index
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static void InvokeOnMove<T, TView>(this ISynchronizedView<T, TView> collection, ref int filteredCount, NotifyViewChangedEventHandler<T, TView>? ev, (T value, TView view) value, int index, int oldIndex)
|
||||
{
|
||||
InvokeOnMove(collection, ref filteredCount, ev, value.value, value.view, index, oldIndex);
|
||||
}
|
||||
|
||||
internal static void InvokeOnMove<T, TView>(this ISynchronizedView<T, TView> collection, ref int filteredCount, NotifyViewChangedEventHandler<T, TView>? ev, T value, TView view, int index, int oldIndex)
|
||||
{
|
||||
if (ev != null)
|
||||
{
|
||||
// move does not changes filtered-count
|
||||
var isMatch = collection.Filter.IsMatch(value);
|
||||
if (isMatch)
|
||||
{
|
||||
ev.Invoke(new SynchronizedViewChangedEventArgs<T, TView>(NotifyCollectionChangedAction.Move, true, newItem: (value, view), newStartingIndex: index, oldStartingIndex: 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);
|
||||
var newMatched = collection.Filter.IsMatch(value);
|
||||
var bothMatched = oldMatched && newMatched;
|
||||
|
||||
if (bothMatched)
|
||||
{
|
||||
if (ev != null)
|
||||
{
|
||||
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--;
|
||||
if (ev != null)
|
||||
{
|
||||
ev.Invoke(new SynchronizedViewChangedEventArgs<T, TView>(NotifyCollectionChangedAction.Remove, true, oldItem: (value, view), oldStartingIndex: oldIndex));
|
||||
}
|
||||
|
||||
}
|
||||
else if (newMatched)
|
||||
{
|
||||
// only-new is add
|
||||
filteredCount++;
|
||||
if (ev != null)
|
||||
{
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
909
src/ObservableCollections/SynchronizedViewList.cs
Normal file
909
src/ObservableCollections/SynchronizedViewList.cs
Normal file
@ -0,0 +1,909 @@
|
||||
using ObservableCollections.Internal;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace ObservableCollections;
|
||||
|
||||
internal class FiltableSynchronizedViewList<T, TView> : ISynchronizedViewList<TView>
|
||||
{
|
||||
readonly ISynchronizedView<T, TView> parent;
|
||||
protected readonly AlternateIndexList<TView> listView;
|
||||
protected readonly object gate = new object();
|
||||
|
||||
public FiltableSynchronizedViewList(ISynchronizedView<T, TView> parent)
|
||||
{
|
||||
this.parent = parent;
|
||||
lock (parent.SyncRoot)
|
||||
{
|
||||
listView = new AlternateIndexList<TView>(IterateFilteredIndexedViewsOfParent());
|
||||
parent.ViewChanged += Parent_ViewChanged;
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerable<(int, TView)> IterateFilteredIndexedViewsOfParent()
|
||||
{
|
||||
var filter = parent.Filter;
|
||||
var index = 0;
|
||||
if (filter.IsNullFilter())
|
||||
{
|
||||
foreach (var item in parent.Unfiltered) // use Unfiltered
|
||||
{
|
||||
yield return (index, item.View);
|
||||
index++;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var item in parent.Unfiltered) // use Unfiltered
|
||||
{
|
||||
if (filter.IsMatch(item.Value))
|
||||
{
|
||||
yield return (index, item.View);
|
||||
}
|
||||
index++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Parent_ViewChanged(in SynchronizedViewChangedEventArgs<T, TView> e)
|
||||
{
|
||||
// event is called inside parent lock
|
||||
lock (gate)
|
||||
{
|
||||
switch (e.Action)
|
||||
{
|
||||
case NotifyCollectionChangedAction.Add: // Add or Insert
|
||||
if (e.IsSingleItem)
|
||||
{
|
||||
var index = listView.Insert(e.NewStartingIndex, e.NewItem.View);
|
||||
OnCollectionChanged(e.WithNewStartingIndex(index));
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
using var array = new CloneCollection<TView>(e.NewViews);
|
||||
var index = listView.InsertRange(e.NewStartingIndex, array.AsEnumerable());
|
||||
OnCollectionChanged(e.WithNewStartingIndex(index));
|
||||
return;
|
||||
}
|
||||
case NotifyCollectionChangedAction.Remove: // Remove
|
||||
{
|
||||
int index = e.OldStartingIndex;
|
||||
if (e.IsSingleItem)
|
||||
{
|
||||
if (e.OldStartingIndex == -1) // can't gurantee correct remove if index is not provided
|
||||
{
|
||||
index = listView.Remove(e.OldItem.View);
|
||||
}
|
||||
else
|
||||
{
|
||||
index = listView.RemoveAt(e.OldStartingIndex);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (e.OldStartingIndex == -1)
|
||||
{
|
||||
foreach (var view in e.OldViews) // index is unknown, can't do batching
|
||||
{
|
||||
listView.Remove(view);
|
||||
OnCollectionChanged(e.WithOldStartingIndex(index));
|
||||
}
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
index = listView.RemoveRange(e.OldStartingIndex, e.OldViews.Length);
|
||||
}
|
||||
}
|
||||
OnCollectionChanged(e.WithOldStartingIndex(index));
|
||||
return;
|
||||
}
|
||||
case NotifyCollectionChangedAction.Replace: // Indexer
|
||||
if (e.NewStartingIndex == -1)
|
||||
{
|
||||
if (listView.TryReplaceByValue(e.OldItem.View, e.NewItem.View, out var replacedIndex))
|
||||
{
|
||||
OnCollectionChanged(e.WithNewAndOldStartingIndex(replacedIndex, replacedIndex));
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (listView.TrySetAtAlternateIndex(e.NewStartingIndex, e.NewItem.View, out var setIndex))
|
||||
{
|
||||
OnCollectionChanged(e.WithNewAndOldStartingIndex(setIndex, setIndex));
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
case NotifyCollectionChangedAction.Move: //Remove and Insert
|
||||
if (e.NewStartingIndex == -1)
|
||||
{
|
||||
return; // do nothing
|
||||
}
|
||||
else
|
||||
{
|
||||
var oldIndex = listView.RemoveAt(e.OldStartingIndex);
|
||||
var newIndex = listView.Insert(e.NewStartingIndex, e.NewItem.View);
|
||||
OnCollectionChanged(e.WithNewAndOldStartingIndex(newStartingIndex: newIndex, oldStartingIndex: oldIndex));
|
||||
}
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Reset: // Clear or drastic changes
|
||||
listView.Clear(IterateFilteredIndexedViewsOfParent()); // clear and fill refresh
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
OnCollectionChanged(e);
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void OnCollectionChanged(in SynchronizedViewChangedEventArgs<T, TView> args)
|
||||
{
|
||||
}
|
||||
|
||||
public TView this[int index]
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (gate)
|
||||
{
|
||||
return listView[index];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int Count
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (gate)
|
||||
{
|
||||
return listView.Count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerator<TView> GetEnumerator()
|
||||
{
|
||||
lock (gate)
|
||||
{
|
||||
foreach (var item in listView)
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return listView.GetEnumerator();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
parent.ViewChanged -= Parent_ViewChanged;
|
||||
}
|
||||
}
|
||||
|
||||
internal class NonFilteredSynchronizedViewList<T, TView> : ISynchronizedViewList<TView>
|
||||
{
|
||||
readonly ISynchronizedView<T, TView> parent;
|
||||
protected readonly List<TView> listView; // no filter can be faster
|
||||
protected readonly object gate = new object();
|
||||
|
||||
public NonFilteredSynchronizedViewList(ISynchronizedView<T, TView> parent)
|
||||
{
|
||||
this.parent = parent;
|
||||
lock (parent.SyncRoot)
|
||||
{
|
||||
listView = parent.ToList(); // iterate filtered
|
||||
parent.ViewChanged += Parent_ViewChanged;
|
||||
}
|
||||
}
|
||||
|
||||
private void Parent_ViewChanged(in SynchronizedViewChangedEventArgs<T, TView> e)
|
||||
{
|
||||
// event is called inside parent lock
|
||||
lock (gate)
|
||||
{
|
||||
switch (e.Action)
|
||||
{
|
||||
case NotifyCollectionChangedAction.Add: // Add or Insert
|
||||
if (e.IsSingleItem)
|
||||
{
|
||||
listView.Insert(e.NewStartingIndex, e.NewItem.View);
|
||||
}
|
||||
else
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
listView.InsertRange(e.NewStartingIndex, e.NewViews);
|
||||
#else
|
||||
using var array = new CloneCollection<TView>(e.NewViews);
|
||||
listView.InsertRange(e.NewStartingIndex, array.AsEnumerable());
|
||||
#endif
|
||||
}
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Remove: // Remove
|
||||
{
|
||||
if (e.IsSingleItem)
|
||||
{
|
||||
if (e.OldStartingIndex == -1) // can't gurantee correct remove if index is not provided
|
||||
{
|
||||
var index = listView.IndexOf(e.OldItem.View);
|
||||
listView.RemoveAt(index);
|
||||
OnCollectionChanged(e.WithOldStartingIndex(index));
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
listView.RemoveAt(e.OldStartingIndex);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (e.OldStartingIndex == -1)
|
||||
{
|
||||
foreach (var view in e.OldViews) // index is unknown, can't do batching
|
||||
{
|
||||
var index = listView.IndexOf(view);
|
||||
listView.RemoveAt(index);
|
||||
OnCollectionChanged(e.WithOldStartingIndex(index));
|
||||
}
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
listView.RemoveRange(e.OldStartingIndex, e.OldViews.Length);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case NotifyCollectionChangedAction.Replace: // Indexer
|
||||
if (e.NewStartingIndex == -1)
|
||||
{
|
||||
var index = listView.IndexOf(e.OldItem.View);
|
||||
if (index != -1)
|
||||
{
|
||||
listView[index] = e.NewItem.View;
|
||||
OnCollectionChanged(e.WithNewAndOldStartingIndex(index, index));
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
listView[e.NewStartingIndex] = e.NewItem.View;
|
||||
}
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Move: //Remove and Insert
|
||||
if (e.NewStartingIndex == -1)
|
||||
{
|
||||
return; // do nothing
|
||||
}
|
||||
else
|
||||
{
|
||||
listView.RemoveAt(e.OldStartingIndex);
|
||||
listView.Insert(e.NewStartingIndex, e.NewItem.View);
|
||||
}
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Reset: // Clear or drastic changes
|
||||
if (e.SortOperation.IsClear)
|
||||
{
|
||||
listView.Clear();
|
||||
foreach (var item in parent.Unfiltered) // refresh
|
||||
{
|
||||
listView.Add(item.View);
|
||||
}
|
||||
}
|
||||
else if (e.SortOperation.IsReverse)
|
||||
{
|
||||
listView.Reverse(e.SortOperation.Index, e.SortOperation.Count);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
|
||||
|
||||
#if NET6_0_OR_GREATER
|
||||
#pragma warning disable CS0436
|
||||
if (parent is ObservableList<T>.View<TView> observableListView && typeof(T) == typeof(TView))
|
||||
{
|
||||
var comparer = new ViewComparer(e.SortOperation.Comparer ?? Comparer<T>.Default);
|
||||
var viewSpan = CollectionsMarshal.AsSpan(listView).Slice(e.SortOperation.Index, e.SortOperation.Count);
|
||||
viewSpan.Sort(comparer);
|
||||
}
|
||||
else
|
||||
#pragma warning restore CS0436
|
||||
#endif
|
||||
{
|
||||
// can not get source Span, do Clear and Refresh
|
||||
listView.Clear();
|
||||
foreach (var item in parent.Unfiltered)
|
||||
{
|
||||
listView.Add(item.View);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
OnCollectionChanged(e);
|
||||
}
|
||||
}
|
||||
|
||||
sealed class ViewComparer : IComparer<TView>
|
||||
{
|
||||
readonly IComparer<T> comparer;
|
||||
|
||||
public ViewComparer(IComparer<T> comparer)
|
||||
{
|
||||
this.comparer = comparer;
|
||||
}
|
||||
|
||||
public int Compare(TView? x, TView? y)
|
||||
{
|
||||
var t1 = Unsafe.As<TView, T>(ref x!);
|
||||
var t2 = Unsafe.As<TView, T>(ref y!);
|
||||
return comparer.Compare(t1, t2);
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void OnCollectionChanged(in SynchronizedViewChangedEventArgs<T, TView> args)
|
||||
{
|
||||
}
|
||||
|
||||
public TView this[int index]
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (gate)
|
||||
{
|
||||
return listView[index];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int Count
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (gate)
|
||||
{
|
||||
return listView.Count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerator<TView> GetEnumerator()
|
||||
{
|
||||
lock (gate)
|
||||
{
|
||||
foreach (var item in listView)
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return listView.GetEnumerator();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
parent.ViewChanged -= Parent_ViewChanged;
|
||||
parent.Dispose(); // Dispose parent
|
||||
}
|
||||
}
|
||||
|
||||
internal class NotifyCollectionChangedSynchronizedViewList<T, TView> :
|
||||
FiltableSynchronizedViewList<T, TView>,
|
||||
INotifyCollectionChangedSynchronizedViewList<TView>,
|
||||
IList<TView>, IList
|
||||
{
|
||||
static readonly PropertyChangedEventArgs CountPropertyChangedEventArgs = new("Count");
|
||||
static readonly Action<NotifyCollectionChangedEventArgs> raiseChangedEventInvoke = RaiseChangedEvent;
|
||||
|
||||
readonly ICollectionEventDispatcher eventDispatcher;
|
||||
|
||||
public event NotifyCollectionChangedEventHandler? CollectionChanged;
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
public NotifyCollectionChangedSynchronizedViewList(ISynchronizedView<T, TView> parent, ICollectionEventDispatcher? eventDispatcher)
|
||||
: base(parent)
|
||||
{
|
||||
this.eventDispatcher = eventDispatcher ?? InlineCollectionEventDispatcher.Instance;
|
||||
}
|
||||
|
||||
protected override void OnCollectionChanged(in SynchronizedViewChangedEventArgs<T, TView> args)
|
||||
{
|
||||
if (CollectionChanged == null && PropertyChanged == null) return;
|
||||
|
||||
switch (args.Action)
|
||||
{
|
||||
case NotifyCollectionChangedAction.Add:
|
||||
if (args.IsSingleItem)
|
||||
{
|
||||
eventDispatcher.Post(new CollectionEventDispatcherEventArgs(NotifyCollectionChangedAction.Add, args.NewItem.View, args.NewStartingIndex)
|
||||
{
|
||||
Collection = this,
|
||||
Invoker = raiseChangedEventInvoke,
|
||||
IsInvokeCollectionChanged = true,
|
||||
IsInvokePropertyChanged = true
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
eventDispatcher.Post(new CollectionEventDispatcherEventArgs(NotifyCollectionChangedAction.Add, args.NewViews.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.View, args.OldStartingIndex)
|
||||
{
|
||||
Collection = this,
|
||||
Invoker = raiseChangedEventInvoke,
|
||||
IsInvokeCollectionChanged = true,
|
||||
IsInvokePropertyChanged = true
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
eventDispatcher.Post(new CollectionEventDispatcherEventArgs(NotifyCollectionChangedAction.Remove, args.OldViews.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.View, args.OldItem.View, args.NewStartingIndex)
|
||||
{
|
||||
Collection = this,
|
||||
Invoker = raiseChangedEventInvoke,
|
||||
IsInvokeCollectionChanged = true,
|
||||
IsInvokePropertyChanged = false
|
||||
});
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Move:
|
||||
eventDispatcher.Post(new CollectionEventDispatcherEventArgs(NotifyCollectionChangedAction.Move, args.NewItem.View, args.NewStartingIndex, args.OldStartingIndex)
|
||||
{
|
||||
Collection = this,
|
||||
Invoker = raiseChangedEventInvoke,
|
||||
IsInvokeCollectionChanged = true,
|
||||
IsInvokePropertyChanged = false
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void RaiseChangedEvent(NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
var e2 = (CollectionEventDispatcherEventArgs)e;
|
||||
var self = (NotifyCollectionChangedSynchronizedViewList<T, TView>)e2.Collection;
|
||||
|
||||
if (e2.IsInvokeCollectionChanged)
|
||||
{
|
||||
self.CollectionChanged?.Invoke(self, e);
|
||||
}
|
||||
if (e2.IsInvokePropertyChanged)
|
||||
{
|
||||
self.PropertyChanged?.Invoke(self, CountPropertyChangedEventArgs);
|
||||
}
|
||||
}
|
||||
|
||||
// IList<T>, IList implementation
|
||||
|
||||
TView IList<TView>.this[int index]
|
||||
{
|
||||
get => ((IReadOnlyList<TView>)this)[index];
|
||||
set => throw new NotSupportedException();
|
||||
}
|
||||
|
||||
object? IList.this[int index]
|
||||
{
|
||||
get
|
||||
{
|
||||
return this[index];
|
||||
}
|
||||
set => throw new NotSupportedException();
|
||||
}
|
||||
|
||||
static bool IsCompatibleObject(object? value)
|
||||
{
|
||||
return value is TView || value == null && default(TView) == null;
|
||||
}
|
||||
|
||||
public bool IsReadOnly => true;
|
||||
|
||||
public bool IsFixedSize => false;
|
||||
|
||||
public bool IsSynchronized => true;
|
||||
|
||||
public object SyncRoot => gate;
|
||||
|
||||
public void Add(TView item)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public int Add(object? value)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public bool Contains(TView item)
|
||||
{
|
||||
lock (gate)
|
||||
{
|
||||
foreach (var listItem in listView)
|
||||
{
|
||||
if (EqualityComparer<TView>.Default.Equals(listItem, item))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool Contains(object? value)
|
||||
{
|
||||
if (IsCompatibleObject(value))
|
||||
{
|
||||
return Contains((TView)value!);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void CopyTo(TView[] array, int arrayIndex)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public void CopyTo(Array array, int index)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public int IndexOf(TView item)
|
||||
{
|
||||
lock (gate)
|
||||
{
|
||||
var index = 0;
|
||||
foreach (var listItem in listView)
|
||||
{
|
||||
if (EqualityComparer<TView>.Default.Equals(listItem, item))
|
||||
{
|
||||
return index;
|
||||
}
|
||||
index++;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public int IndexOf(object? item)
|
||||
{
|
||||
if (IsCompatibleObject(item))
|
||||
{
|
||||
return IndexOf((TView)item!);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public void Insert(int index, TView item)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public void Insert(int index, object? value)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public bool Remove(TView item)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public void Remove(object? value)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void RemoveAt(int index)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
|
||||
internal class NonFilteredNotifyCollectionChangedSynchronizedViewList<T, TView> :
|
||||
NonFilteredSynchronizedViewList<T, TView>,
|
||||
INotifyCollectionChangedSynchronizedViewList<TView>,
|
||||
IList<TView>, IList
|
||||
{
|
||||
static readonly PropertyChangedEventArgs CountPropertyChangedEventArgs = new("Count");
|
||||
static readonly Action<NotifyCollectionChangedEventArgs> raiseChangedEventInvoke = RaiseChangedEvent;
|
||||
|
||||
readonly ICollectionEventDispatcher eventDispatcher;
|
||||
|
||||
public event NotifyCollectionChangedEventHandler? CollectionChanged;
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
public NonFilteredNotifyCollectionChangedSynchronizedViewList(ISynchronizedView<T, TView> parent, ICollectionEventDispatcher? eventDispatcher)
|
||||
: base(parent)
|
||||
{
|
||||
this.eventDispatcher = eventDispatcher ?? InlineCollectionEventDispatcher.Instance;
|
||||
}
|
||||
|
||||
protected override void OnCollectionChanged(in SynchronizedViewChangedEventArgs<T, TView> args)
|
||||
{
|
||||
if (CollectionChanged == null && PropertyChanged == null) return;
|
||||
|
||||
switch (args.Action)
|
||||
{
|
||||
case NotifyCollectionChangedAction.Add:
|
||||
if (args.IsSingleItem)
|
||||
{
|
||||
eventDispatcher.Post(new CollectionEventDispatcherEventArgs(NotifyCollectionChangedAction.Add, args.NewItem.View, args.NewStartingIndex)
|
||||
{
|
||||
Collection = this,
|
||||
Invoker = raiseChangedEventInvoke,
|
||||
IsInvokeCollectionChanged = true,
|
||||
IsInvokePropertyChanged = true
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
eventDispatcher.Post(new CollectionEventDispatcherEventArgs(NotifyCollectionChangedAction.Add, args.NewViews.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.View, args.OldStartingIndex)
|
||||
{
|
||||
Collection = this,
|
||||
Invoker = raiseChangedEventInvoke,
|
||||
IsInvokeCollectionChanged = true,
|
||||
IsInvokePropertyChanged = true
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
eventDispatcher.Post(new CollectionEventDispatcherEventArgs(NotifyCollectionChangedAction.Remove, args.OldViews.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.View, args.OldItem.View, args.NewStartingIndex)
|
||||
{
|
||||
Collection = this,
|
||||
Invoker = raiseChangedEventInvoke,
|
||||
IsInvokeCollectionChanged = true,
|
||||
IsInvokePropertyChanged = false
|
||||
});
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Move:
|
||||
eventDispatcher.Post(new CollectionEventDispatcherEventArgs(NotifyCollectionChangedAction.Move, args.NewItem.View, args.NewStartingIndex, args.OldStartingIndex)
|
||||
{
|
||||
Collection = this,
|
||||
Invoker = raiseChangedEventInvoke,
|
||||
IsInvokeCollectionChanged = true,
|
||||
IsInvokePropertyChanged = false
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void RaiseChangedEvent(NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
var e2 = (CollectionEventDispatcherEventArgs)e;
|
||||
var self = (NonFilteredNotifyCollectionChangedSynchronizedViewList<T, TView>)e2.Collection;
|
||||
|
||||
if (e2.IsInvokeCollectionChanged)
|
||||
{
|
||||
self.CollectionChanged?.Invoke(self, e);
|
||||
}
|
||||
if (e2.IsInvokePropertyChanged)
|
||||
{
|
||||
self.PropertyChanged?.Invoke(self, CountPropertyChangedEventArgs);
|
||||
}
|
||||
}
|
||||
|
||||
// IList<T>, IList implementation
|
||||
|
||||
TView IList<TView>.this[int index]
|
||||
{
|
||||
get => ((IReadOnlyList<TView>)this)[index];
|
||||
set => throw new NotSupportedException();
|
||||
}
|
||||
|
||||
object? IList.this[int index]
|
||||
{
|
||||
get
|
||||
{
|
||||
return this[index];
|
||||
}
|
||||
set => throw new NotSupportedException();
|
||||
}
|
||||
|
||||
static bool IsCompatibleObject(object? value)
|
||||
{
|
||||
return value is TView || value == null && default(TView) == null;
|
||||
}
|
||||
|
||||
public bool IsReadOnly => true;
|
||||
|
||||
public bool IsFixedSize => false;
|
||||
|
||||
public bool IsSynchronized => true;
|
||||
|
||||
public object SyncRoot => gate;
|
||||
|
||||
public void Add(TView item)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public int Add(object? value)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public bool Contains(TView item)
|
||||
{
|
||||
lock (gate)
|
||||
{
|
||||
foreach (var listItem in listView)
|
||||
{
|
||||
if (EqualityComparer<TView>.Default.Equals(listItem, item))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool Contains(object? value)
|
||||
{
|
||||
if (IsCompatibleObject(value))
|
||||
{
|
||||
return Contains((TView)value!);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void CopyTo(TView[] array, int arrayIndex)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public void CopyTo(Array array, int index)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public int IndexOf(TView item)
|
||||
{
|
||||
lock (gate)
|
||||
{
|
||||
var index = 0;
|
||||
foreach (var listItem in listView)
|
||||
{
|
||||
if (EqualityComparer<TView>.Default.Equals(listItem, item))
|
||||
{
|
||||
return index;
|
||||
}
|
||||
index++;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public int IndexOf(object? item)
|
||||
{
|
||||
if (IsCompatibleObject(item))
|
||||
{
|
||||
return IndexOf((TView)item!);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public void Insert(int index, TView item)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public void Insert(int index, object? value)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public bool Remove(TView item)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public void Remove(object? value)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void RemoveAt(int index)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
113
tests/ObservableCollections.Tests/AlternateIndexListTest.cs
Normal file
113
tests/ObservableCollections.Tests/AlternateIndexListTest.cs
Normal file
@ -0,0 +1,113 @@
|
||||
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"));
|
||||
}
|
||||
}
|
@ -24,7 +24,6 @@ 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);
|
||||
@ -42,146 +41,6 @@ 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.Action, x.NewValue.Value)).Should().Equal((NotifyCollectionChangedAction.Add, -100));
|
||||
filter2.CalledOnCollectionChanged.Select(x => (x.Action, x.NewValue.Value)).Should().Equal((NotifyCollectionChangedAction.Add, -100));
|
||||
filter3.CalledOnCollectionChanged.Select(x => (x.Action, x.NewValue.Value)).Should().Equal((NotifyCollectionChangedAction.Add, -100));
|
||||
foreach (var item in new[] { filter1, filter2, filter3 }) item.CalledOnCollectionChanged.Clear();
|
||||
|
||||
dict[10] = -1090;
|
||||
filter1.CalledOnCollectionChanged.Select(x => (x.Action, x.NewValue.Value, x.OldValue.Value)).Should().Equal((NotifyCollectionChangedAction.Replace, -1090, -12));
|
||||
filter2.CalledOnCollectionChanged.Select(x => (x.Action, x.NewValue.Value, x.OldValue.Value)).Should().Equal((NotifyCollectionChangedAction.Replace, -1090, -12));
|
||||
filter3.CalledOnCollectionChanged.Select(x => (x.Action, x.NewValue.Value, x.OldValue.Value)).Should().Equal((NotifyCollectionChangedAction.Replace, -1090, -12));
|
||||
foreach (var item in new[] { filter1, filter2, filter3 }) item.CalledOnCollectionChanged.Clear();
|
||||
|
||||
dict.Remove(20);
|
||||
filter1.CalledOnCollectionChanged.Select(x => (x.Action, x.OldValue.Value)).Should().Equal((NotifyCollectionChangedAction.Remove, -25));
|
||||
filter2.CalledOnCollectionChanged.Select(x => (x.Action, x.OldValue.Value)).Should().Equal((NotifyCollectionChangedAction.Remove, -25));
|
||||
filter3.CalledOnCollectionChanged.Select(x => (x.Action, x.OldValue.Value)).Should().Equal((NotifyCollectionChangedAction.Remove, -25));
|
||||
foreach (var item in new[] { filter1, filter2, filter3 }) item.CalledOnCollectionChanged.Clear();
|
||||
|
||||
dict.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);
|
||||
}
|
||||
|
||||
[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].Action.Should().Be(NotifyCollectionChangedAction.Add);
|
||||
filter1.CalledOnCollectionChanged[0].NewValue.Key.Should().Be(10);
|
||||
filter1.CalledOnCollectionChanged[1].Action.Should().Be(NotifyCollectionChangedAction.Add);
|
||||
filter1.CalledOnCollectionChanged[1].NewValue.Key.Should().Be(50);
|
||||
filter1.CalledOnCollectionChanged[2].Action.Should().Be(NotifyCollectionChangedAction.Add);
|
||||
filter1.CalledOnCollectionChanged[2].NewValue.Key.Should().Be(30);
|
||||
filter1.CalledOnCollectionChanged[3].Action.Should().Be(NotifyCollectionChangedAction.Add);
|
||||
filter1.CalledOnCollectionChanged[3].NewValue.Key.Should().Be(20);
|
||||
filter1.CalledOnCollectionChanged[4].Action.Should().Be(NotifyCollectionChangedAction.Add);
|
||||
filter1.CalledOnCollectionChanged[4].NewValue.Key.Should().Be(40);
|
||||
|
||||
filter1.CalledWhenTrue.Count.Should().Be(3);
|
||||
filter1.CalledWhenFalse.Count.Should().Be(2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -27,7 +27,6 @@ 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);
|
||||
@ -46,68 +45,7 @@ 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.Action, x.NewValue)).Should().Equal((NotifyCollectionChangedAction.Add, 33), (NotifyCollectionChangedAction.Add, 98));
|
||||
filter.Clear();
|
||||
|
||||
set.Remove(10);
|
||||
set.RemoveRange(new[] { 50, 30 });
|
||||
filter.CalledOnCollectionChanged.Select(x => (x.Action, x.OldValue)).Should().Equal((NotifyCollectionChangedAction.Remove, 10), (NotifyCollectionChangedAction.Remove, 50), (NotifyCollectionChangedAction.Remove, 30));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FilterAndInvokeAddEvent()
|
||||
{
|
||||
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, true);
|
||||
filter.CalledOnCollectionChanged.Count.Should().Be(5);
|
||||
filter.CalledOnCollectionChanged[0].Action.Should().Be(NotifyCollectionChangedAction.Add);
|
||||
filter.CalledOnCollectionChanged[0].NewValue.Should().Be(10);
|
||||
filter.CalledOnCollectionChanged[1].Action.Should().Be(NotifyCollectionChangedAction.Add);
|
||||
filter.CalledOnCollectionChanged[1].NewValue.Should().Be(50);
|
||||
filter.CalledOnCollectionChanged[2].Action.Should().Be(NotifyCollectionChangedAction.Add);
|
||||
filter.CalledOnCollectionChanged[2].NewValue.Should().Be(30);
|
||||
filter.CalledOnCollectionChanged[3].Action.Should().Be(NotifyCollectionChangedAction.Add);
|
||||
filter.CalledOnCollectionChanged[3].NewValue.Should().Be(20);
|
||||
filter.CalledOnCollectionChanged[4].Action.Should().Be(NotifyCollectionChangedAction.Add);
|
||||
filter.CalledOnCollectionChanged[4].NewValue.Should().Be(40);
|
||||
|
||||
filter.CalledWhenTrue.Count.Should().Be(1);
|
||||
filter.CalledWhenFalse.Count.Should().Be(4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IndexOutOfRange()
|
||||
{
|
||||
|
@ -27,14 +27,14 @@ namespace ObservableCollections.Tests
|
||||
reference.Should().Equal(expected);
|
||||
list.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)));
|
||||
view.Filtered.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.Select(x => x.View).Should().Equal(expected.Select(x => new ViewContainer<int>(x)));
|
||||
view.Filtered.Select(x => x.View).Should().Equal(expected.Select(x => new ViewContainer<int>(x)));
|
||||
}
|
||||
|
||||
Equal(10, 50, 30, 20, 40);
|
||||
@ -69,178 +69,107 @@ namespace ObservableCollections.Tests
|
||||
Equal2(100, 400, 200, 300);
|
||||
}
|
||||
|
||||
[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);
|
||||
//[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 });
|
||||
|
||||
list.Add(10); // 0
|
||||
list.Add(50); // 1
|
||||
list.Add(30); // 2
|
||||
list.Add(20); // 3
|
||||
list.Add(40); // 4
|
||||
// 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);
|
||||
|
||||
void Equal(params int[] expected)
|
||||
{
|
||||
list.Should().Equal(expected);
|
||||
// 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);
|
||||
|
||||
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());
|
||||
}
|
||||
// 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);
|
||||
|
||||
Equal(10, 50, 30, 20, 40);
|
||||
// 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);
|
||||
|
||||
list.Move(3, 1);
|
||||
Equal(10, 20, 50, 30, 40);
|
||||
// filter1.Clear();
|
||||
// filter2.Clear();
|
||||
// filter3.Clear();
|
||||
|
||||
list.Insert(2, 99);
|
||||
Equal(10, 20, 99, 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.RemoveAt(2);
|
||||
Equal(10, 20, 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[3] = 88;
|
||||
Equal(10, 20, 50, 88, 40);
|
||||
// foreach (var item in new[] { filter1, filter2, filter3 }) item.CalledOnCollectionChanged.Clear();
|
||||
|
||||
list.Clear();
|
||||
Equal(new int[0]);
|
||||
// list.Insert(0, 1000);
|
||||
// list.InsertRange(0, new[] { 999 });
|
||||
|
||||
list.AddRange(new[] { 100, 200, 300 });
|
||||
Equal(100, 200, 300);
|
||||
// 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.InsertRange(1, new[] { 400, 500, 600 });
|
||||
Equal(100, 400, 500, 600, 200, 300);
|
||||
// list.RemoveAt(0);
|
||||
// list.RemoveRange(0, 1);
|
||||
|
||||
list.RemoveRange(2, 2);
|
||||
Equal(100, 400, 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();
|
||||
|
||||
[Fact]
|
||||
public void Freezed()
|
||||
{
|
||||
var list = new FreezedList<int>(new[] { 10, 20, 50, 30, 40, 60 });
|
||||
// list[0] = 9999;
|
||||
|
||||
var view = list.CreateSortableView(x => new ViewContainer<int>(x));
|
||||
// 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();
|
||||
|
||||
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.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, 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);
|
||||
}
|
||||
// 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);
|
||||
//}
|
||||
|
||||
[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 });
|
||||
//[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);
|
||||
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);
|
||||
// var filter1 = new TestFilter<int>((x, v) => x % 2 == 0);
|
||||
// view1.AttachFilter((x, v) => x % 2 == 0));
|
||||
|
||||
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.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.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.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));
|
||||
|
||||
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.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.RemoveAt(0);
|
||||
list.RemoveRange(0, 1);
|
||||
|
||||
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[0] = 9999;
|
||||
|
||||
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();
|
||||
|
||||
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();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
[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].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.Count.Should().Be(3);
|
||||
filter1.CalledWhenFalse.Count.Should().Be(1);
|
||||
}
|
||||
// filter1.CalledWhenTrue.Count.Should().Be(3);
|
||||
// filter1.CalledWhenFalse.Count.Should().Be(1);
|
||||
//}
|
||||
}
|
||||
}
|
@ -27,7 +27,6 @@ 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);
|
||||
@ -50,72 +49,72 @@ 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.Action, x.NewValue, x.NewViewIndex)).Should().Equal((NotifyCollectionChangedAction.Add, 33, 5), (NotifyCollectionChangedAction.Add, 98, 6));
|
||||
// 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.Action, x.OldValue, x.OldViewIndex)).Should().Equal((NotifyCollectionChangedAction.Remove, 10, 0), (NotifyCollectionChangedAction.Remove, 50, 0), (NotifyCollectionChangedAction.Remove, 30, 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);
|
||||
//[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].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.CalledWhenTrue.Count.Should().Be(1);
|
||||
filter.CalledWhenFalse.Count.Should().Be(4);
|
||||
}
|
||||
// filter.CalledWhenTrue.Count.Should().Be(1);
|
||||
// filter.CalledWhenFalse.Count.Should().Be(4);
|
||||
//}
|
||||
}
|
||||
}
|
||||
|
@ -24,7 +24,6 @@ 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);
|
||||
@ -65,7 +64,6 @@ 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);
|
||||
|
@ -27,7 +27,6 @@ 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);
|
||||
@ -50,72 +49,6 @@ 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.Action, x.NewValue, x.NewViewIndex)).Should().Equal((NotifyCollectionChangedAction.Add, 33, 0), (NotifyCollectionChangedAction.Add, 98, 0));
|
||||
filter.Clear();
|
||||
|
||||
stack.Pop();
|
||||
stack.PopRange(2);
|
||||
filter.CalledOnCollectionChanged.Select(x => (x.Action, x.OldValue, x.OldViewIndex)).Should().Equal((NotifyCollectionChangedAction.Remove, 98, 0), (NotifyCollectionChangedAction.Remove, 33, 0), (NotifyCollectionChangedAction.Remove, 40, 0));
|
||||
}
|
||||
|
||||
[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].Action.Should().Be(NotifyCollectionChangedAction.Add);
|
||||
filter.CalledOnCollectionChanged[4].NewValue.Should().Be(10);
|
||||
filter.CalledOnCollectionChanged[4].NewViewIndex.Should().Be(0);
|
||||
filter.CalledOnCollectionChanged[3].Action.Should().Be(NotifyCollectionChangedAction.Add);
|
||||
filter.CalledOnCollectionChanged[3].NewValue.Should().Be(50);
|
||||
filter.CalledOnCollectionChanged[3].NewViewIndex.Should().Be(0);
|
||||
filter.CalledOnCollectionChanged[2].Action.Should().Be(NotifyCollectionChangedAction.Add);
|
||||
filter.CalledOnCollectionChanged[2].NewValue.Should().Be(30);
|
||||
filter.CalledOnCollectionChanged[2].NewViewIndex.Should().Be(0);
|
||||
filter.CalledOnCollectionChanged[1].Action.Should().Be(NotifyCollectionChangedAction.Add);
|
||||
filter.CalledOnCollectionChanged[1].NewValue.Should().Be(20);
|
||||
filter.CalledOnCollectionChanged[1].NewViewIndex.Should().Be(0);
|
||||
filter.CalledOnCollectionChanged[0].Action.Should().Be(NotifyCollectionChangedAction.Add);
|
||||
filter.CalledOnCollectionChanged[0].NewValue.Should().Be(40);
|
||||
filter.CalledOnCollectionChanged[0].NewViewIndex.Should().Be(0);
|
||||
|
||||
filter.CalledWhenTrue.Count.Should().Be(1);
|
||||
filter.CalledWhenFalse.Count.Should().Be(4);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -1,70 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
|
||||
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].Action.Should().Be(NotifyCollectionChangedAction.Add);
|
||||
filter.CalledOnCollectionChanged[0].NewValue.Should().Be(20);
|
||||
filter.CalledOnCollectionChanged[0].NewView.Should().Be(new ViewContainer<int>(20));
|
||||
filter.CalledOnCollectionChanged[0].NewViewIndex.Should().Be(1);
|
||||
|
||||
list.Remove(20);
|
||||
filter.CalledOnCollectionChanged[1].Action.Should().Be(NotifyCollectionChangedAction.Remove);
|
||||
filter.CalledOnCollectionChanged[1].OldValue.Should().Be(20);
|
||||
filter.CalledOnCollectionChanged[1].OldView.Should().Be(new ViewContainer<int>(20));
|
||||
filter.CalledOnCollectionChanged[1].OldViewIndex.Should().Be(1);
|
||||
|
||||
list[1] = 999; // from 10(at 0 in original) to 999
|
||||
filter.CalledOnCollectionChanged[2].Action.Should().Be(NotifyCollectionChangedAction.Replace);
|
||||
filter.CalledOnCollectionChanged[2].NewValue.Should().Be(999);
|
||||
filter.CalledOnCollectionChanged[2].OldValue.Should().Be(10);
|
||||
filter.CalledOnCollectionChanged[2].NewViewIndex.Should().Be(1);
|
||||
}
|
||||
}
|
@ -1,73 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
|
||||
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].Action.Should().Be(NotifyCollectionChangedAction.Add);
|
||||
filter.CalledOnCollectionChanged[0].NewValue.Should().Be(20);
|
||||
filter.CalledOnCollectionChanged[0].NewView.Should().Be(new ViewContainer<int>(20));
|
||||
filter.CalledOnCollectionChanged[0].NewViewIndex.Should().Be(1);
|
||||
|
||||
list.Remove(20);
|
||||
filter.CalledOnCollectionChanged[1].Action.Should().Be(NotifyCollectionChangedAction.Remove);
|
||||
filter.CalledOnCollectionChanged[1].OldValue.Should().Be(20);
|
||||
filter.CalledOnCollectionChanged[1].OldView.Should().Be(new ViewContainer<int>(20));
|
||||
filter.CalledOnCollectionChanged[1].OldViewIndex.Should().Be(1);
|
||||
|
||||
list[1] = 999; // from 10(at 0 in original) to 999
|
||||
filter.CalledOnCollectionChanged[2].Action.Should().Be(NotifyCollectionChangedAction.Replace);
|
||||
filter.CalledOnCollectionChanged[2].NewValue.Should().Be(999);
|
||||
filter.CalledOnCollectionChanged[2].OldValue.Should().Be(10);
|
||||
filter.CalledOnCollectionChanged[2].NewView.Should().Be(new ViewContainer<int>(999));
|
||||
filter.CalledOnCollectionChanged[2].OldView.Should().Be(new ViewContainer<int>(10));
|
||||
filter.CalledOnCollectionChanged[2].NewViewIndex.Should().Be(1);
|
||||
filter.CalledOnCollectionChanged[2].OldViewIndex.Should().Be(0);
|
||||
}
|
||||
}
|
@ -1,3 +1,5 @@
|
||||
using ObservableCollections;
|
||||
|
||||
namespace ObservableCollections.Tests;
|
||||
|
||||
public class ToNotifyCollectionChangedTest
|
||||
@ -43,7 +45,7 @@ public class ToNotifyCollectionChangedTest
|
||||
var view = list.CreateView(x => $"${x}");
|
||||
var notify = view.ToNotifyCollectionChanged();
|
||||
|
||||
view.AttachFilter((value, view) => value % 2 == 0);
|
||||
view.AttachFilter((value) => value % 2 == 0);
|
||||
|
||||
list.Add(4);
|
||||
|
||||
|
@ -31,83 +31,81 @@ namespace ObservableCollections.Tests
|
||||
}
|
||||
}
|
||||
|
||||
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<SynchronizedViewChangedEventArgs<T, ViewContainer<T>>> CalledOnCollectionChanged = new();
|
||||
//public class TestFilter<T> : ISynchronizedViewFilter<T>
|
||||
//{
|
||||
// readonly Func<T, bool> filter;
|
||||
// public List<SynchronizedViewChangedEventArgs<T, ViewContainer<T>>> CalledOnCollectionChanged = new();
|
||||
|
||||
public TestFilter(Func<T, ViewContainer<T>, bool> filter)
|
||||
{
|
||||
this.filter = filter;
|
||||
}
|
||||
// public TestFilter(Func<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, ViewContainer<T> view)
|
||||
{
|
||||
return this.filter.Invoke(value, view);
|
||||
}
|
||||
// public bool IsMatch(T value)
|
||||
// {
|
||||
// return this.filter.Invoke(value);
|
||||
// }
|
||||
|
||||
public void OnCollectionChanged(in SynchronizedViewChangedEventArgs<T, ViewContainer<T>> args)
|
||||
{
|
||||
CalledOnCollectionChanged.Add(args);
|
||||
}
|
||||
// public void OnCollectionChanged(in SynchronizedViewChangedEventArgs<T, ViewContainer<T>> args)
|
||||
// {
|
||||
// CalledOnCollectionChanged.Add(args);
|
||||
// }
|
||||
|
||||
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<SynchronizedViewChangedEventArgs<KeyValuePair<T, T>, ViewContainer<T>>> 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(in SynchronizedViewChangedEventArgs<KeyValuePair<T, T>, ViewContainer<T>> args)
|
||||
// {
|
||||
// CalledOnCollectionChanged.Add(args);
|
||||
// }
|
||||
|
||||
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));
|
||||
// }
|
||||
//}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user