diff --git a/README.md b/README.md
index 761ccca..21cf6b9 100644
--- a/README.md
+++ b/README.md
@@ -355,6 +355,10 @@ public class WpfDispatcherCollection(Dispatcher dispatcher) : ICollectionEventDi
}
```
+`ToNotifyCollectionChanged()` can also be called without going through a View. In this case, it's guaranteed that no filters will be applied, making it faster. If you want to apply filters, please generate a View before calling it. Additionally, `ObservableList` has a variation called `ToNotifyCollectionChangedSlim()`. This option doesn't generate a list for the View and shares the actual data, making it the fastest and most memory-efficient option. However, range operations such as `AddRange`, `InsertRange` and `RemoveRange` are not supported by WPF (or Avalonia), so they will throw runtime exceptions.
+
+Views and ToNotifyCollectionChanged are internally connected by events, so they need to be `Dispose` to release those connections.
+
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.
diff --git a/sandbox/AvaloniaApp/MainWindow.axaml b/sandbox/AvaloniaApp/MainWindow.axaml
index cfd3b92..9530d9b 100644
--- a/sandbox/AvaloniaApp/MainWindow.axaml
+++ b/sandbox/AvaloniaApp/MainWindow.axaml
@@ -7,7 +7,14 @@
Title="AvaloniaApp">
-
-
+
+
+
+
+
+
+
+
+
diff --git a/sandbox/AvaloniaApp/MainWindow.axaml.cs b/sandbox/AvaloniaApp/MainWindow.axaml.cs
index b6c6dc6..92d9326 100644
--- a/sandbox/AvaloniaApp/MainWindow.axaml.cs
+++ b/sandbox/AvaloniaApp/MainWindow.axaml.cs
@@ -26,22 +26,27 @@ namespace AvaloniaApp
private ObservableList observableList { get; } = new ObservableList();
public INotifyCollectionChangedSynchronizedViewList ItemsView { get; }
public ReactiveCommand AddCommand { get; } = new ReactiveCommand();
+ public ReactiveCommand AddRangeCommand { get; } = new ReactiveCommand();
+ public ReactiveCommand InsertAtRandomCommand { get; } = new ReactiveCommand();
+ public ReactiveCommand RemoveAtRandomCommand { get; } = new ReactiveCommand();
public ReactiveCommand ClearCommand { get; } = new ReactiveCommand();
+ public ReactiveCommand ReverseCommand { get; } = new ReactiveCommand();
+ public ReactiveCommand SortCommand { get; } = new ReactiveCommand();
+ public ReactiveCommand AttachFilterCommand { get; } = new ReactiveCommand();
+ public ReactiveCommand ResetFilterCommand { get; } = new ReactiveCommand();
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();
- //var test = ItemsView.ToArray();
- //INotifyCollectionChangedSynchronizedView
- // ItemsView = observableList.CreateView(x => x).ToNotifyCollectionChanged();
+ // check for optimize list
+ // ItemsView = observableList.ToNotifyCollectionChanged(SynchronizationContextCollectionEventDispatcher.Current);
- // BindingOperations.EnableCollectionSynchronization(ItemsView, new object());
AddCommand.Subscribe(_ =>
{
@@ -51,12 +56,49 @@ namespace AvaloniaApp
});
});
- // var iii = 10;
+ AddRangeCommand.Subscribe(_ =>
+ {
+ var xs = Enumerable.Range(1, 5).Select(_ => Random.Shared.Next()).ToArray();
+ observableList.AddRange(xs);
+ });
+
+ InsertAtRandomCommand.Subscribe(_ =>
+ {
+ var from = Random.Shared.Next(0, view.Count);
+ observableList.Insert(from, Random.Shared.Next());
+ });
+
+ RemoveAtRandomCommand.Subscribe(_ =>
+ {
+ var from = Random.Shared.Next(0, view.Count);
+ observableList.RemoveAt(from);
+ });
+
ClearCommand.Subscribe(_ =>
{
- // observableList.Add(iii++);
observableList.Clear();
});
+
+
+ ReverseCommand.Subscribe(_ =>
+ {
+ observableList.Reverse();
+ });
+
+ SortCommand.Subscribe(_ =>
+ {
+ observableList.Sort();
+ });
+
+ AttachFilterCommand.Subscribe(_ =>
+ {
+ view.AttachFilter(x => x % 2 == 0);
+ });
+
+ ResetFilterCommand.Subscribe(_ =>
+ {
+ view.ResetFilter();
+ });
}
}
}
\ No newline at end of file
diff --git a/sandbox/WpfApp/MainWindow.xaml b/sandbox/WpfApp/MainWindow.xaml
index e5b8ead..e9d7411 100644
--- a/sandbox/WpfApp/MainWindow.xaml
+++ b/sandbox/WpfApp/MainWindow.xaml
@@ -20,8 +20,10 @@
+
+
diff --git a/sandbox/WpfApp/MainWindow.xaml.cs b/sandbox/WpfApp/MainWindow.xaml.cs
index 148e950..8c476e2 100644
--- a/sandbox/WpfApp/MainWindow.xaml.cs
+++ b/sandbox/WpfApp/MainWindow.xaml.cs
@@ -2,6 +2,7 @@
using R3;
using System;
using System.Collections.Generic;
+using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Linq;
@@ -76,8 +77,10 @@ namespace WpfApp
private ObservableList observableList { get; } = new ObservableList();
public INotifyCollectionChangedSynchronizedViewList ItemsView { get; }
public ReactiveCommand AddCommand { get; } = new ReactiveCommand();
+ public ReactiveCommand AddRangeCommand { get; } = new ReactiveCommand();
public ReactiveCommand InsertAtRandomCommand { get; } = new ReactiveCommand();
public ReactiveCommand RemoveAtRandomCommand { get; } = new ReactiveCommand();
+ public ReactiveCommand RemoveRangeCommand { get; } = new ReactiveCommand();
public ReactiveCommand ClearCommand { get; } = new ReactiveCommand();
public ReactiveCommand ReverseCommand { get; } = new ReactiveCommand();
public ReactiveCommand SortCommand { get; } = new ReactiveCommand();
@@ -90,8 +93,8 @@ namespace WpfApp
observableList.Add(2);
var view = observableList.CreateView(x => x);
- ItemsView = view.ToNotifyCollectionChanged(SynchronizationContextCollectionEventDispatcher.Current);
-
+ //ItemsView = view.ToNotifyCollectionChanged();
+ ItemsView = observableList.ToNotifyCollectionChanged();
// check for optimize list
// ItemsView = observableList.ToNotifyCollectionChanged(SynchronizationContextCollectionEventDispatcher.Current);
@@ -99,10 +102,16 @@ namespace WpfApp
AddCommand.Subscribe(_ =>
{
- ThreadPool.QueueUserWorkItem(_ =>
+ // ThreadPool.QueueUserWorkItem(_ =>
{
observableList.Add(Random.Shared.Next());
- });
+ }
+ });
+
+ AddRangeCommand.Subscribe(_ =>
+ {
+ var xs = Enumerable.Range(1, 5).Select(_ => Random.Shared.Next()).ToArray();
+ observableList.AddRange(xs);
});
InsertAtRandomCommand.Subscribe(_ =>
@@ -117,6 +126,11 @@ namespace WpfApp
observableList.RemoveAt(from);
});
+ RemoveRangeCommand.Subscribe(_ =>
+ {
+ observableList.RemoveRange(2, 5);
+ });
+
ClearCommand.Subscribe(_ =>
{
observableList.Clear();
diff --git a/src/ObservableCollections/AlternateIndexList.cs b/src/ObservableCollections/AlternateIndexList.cs
index 19d3110..23c2e28 100644
--- a/src/ObservableCollections/AlternateIndexList.cs
+++ b/src/ObservableCollections/AlternateIndexList.cs
@@ -178,7 +178,7 @@ public class AlternateIndexList : IEnumerable
object IEnumerator.Current => Current;
- public void Dispose() => iter.Reset();
+ public void Dispose() => iter.Dispose();
public bool MoveNext()
{
@@ -191,7 +191,7 @@ public class AlternateIndexList : IEnumerable
return false;
}
- public void Reset() => iter.Reset();
+ public void Reset() { }
public IEnumerator GetEnumerator() => this;
diff --git a/src/ObservableCollections/ObservableList.OptimizeView.cs b/src/ObservableCollections/ObservableList.OptimizeView.cs
index 15c5ce0..c5052da 100644
--- a/src/ObservableCollections/ObservableList.OptimizeView.cs
+++ b/src/ObservableCollections/ObservableList.OptimizeView.cs
@@ -21,12 +21,18 @@ public sealed partial class ObservableList : IList, IReadOnlyObservableLis
// return new NonFilteredSynchronizedViewList(collection.CreateView(transform));
//}
- public INotifyCollectionChangedSynchronizedViewList ToNotifyCollectionChanged()
+ ///
+ /// Create faster, compact INotifyCollectionChanged view, however it does not support ***Range.
+ ///
+ public INotifyCollectionChangedSynchronizedViewList ToNotifyCollectionChangedSlim()
{
return new ObservableListSynchronizedViewList(this, null);
}
- public INotifyCollectionChangedSynchronizedViewList ToNotifyCollectionChanged(ICollectionEventDispatcher? collectionEventDispatcher)
+ ///
+ /// Create faster, compact INotifyCollectionChanged view, however it does not support ***Range.
+ ///
+ public INotifyCollectionChangedSynchronizedViewList ToNotifyCollectionChangedSlim(ICollectionEventDispatcher? collectionEventDispatcher)
{
return new ObservableListSynchronizedViewList(this, collectionEventDispatcher);
}
diff --git a/src/ObservableCollections/SynchronizedViewList.cs b/src/ObservableCollections/SynchronizedViewList.cs
index eb16d64..fedec6c 100644
--- a/src/ObservableCollections/SynchronizedViewList.cs
+++ b/src/ObservableCollections/SynchronizedViewList.cs
@@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
+using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@@ -16,6 +17,8 @@ internal class FiltableSynchronizedViewList : ISynchronizedViewList listView;
protected readonly object gate = new object();
+ protected virtual bool IsSupportRangeFeature => true;
+
public FiltableSynchronizedViewList(ISynchronizedView parent)
{
this.parent = parent;
@@ -67,9 +70,22 @@ internal class FiltableSynchronizedViewList : ISynchronizedViewList(e.NewViews);
- var index = listView.InsertRange(e.NewStartingIndex, array.AsEnumerable());
- OnCollectionChanged(e.WithNewStartingIndex(index));
+ if (IsSupportRangeFeature)
+ {
+ using var array = new CloneCollection(e.NewViews);
+ var index = listView.InsertRange(e.NewStartingIndex, array.AsEnumerable());
+ OnCollectionChanged(e.WithNewStartingIndex(index));
+ }
+ else
+ {
+ var span = e.NewViews;
+ for (int i = 0; i < span.Length; i++)
+ {
+ var index = listView.Insert(e.NewStartingIndex + i, span[i]);
+ var ev = new SynchronizedViewChangedEventArgs(e.Action, true, newItem: (e.NewValues[i], span[i]), newStartingIndex: index);
+ OnCollectionChanged(ev);
+ }
+ }
return;
}
case NotifyCollectionChangedAction.Remove: // Remove
@@ -99,7 +115,21 @@ internal class FiltableSynchronizedViewList : ISynchronizedViewList(e.Action, true, oldItem: (e.OldValues[i], span[i]), oldStartingIndex: index);
+ OnCollectionChanged(ev);
+ }
+ return;
+ }
}
}
OnCollectionChanged(e.WithOldStartingIndex(index));
@@ -192,7 +222,7 @@ internal class FiltableSynchronizedViewList : ISynchronizedViewList : ISynchronizedViewList
readonly ISynchronizedView parent;
protected readonly List listView; // no filter can be faster
protected readonly object gate = new object();
+
+ protected virtual bool IsSupportRangeFeature => true;
public NonFilteredSynchronizedViewList(ISynchronizedView parent)
{
@@ -231,12 +263,27 @@ internal class NonFilteredSynchronizedViewList : ISynchronizedViewList
}
else
{
+ if (IsSupportRangeFeature)
+ {
#if NET8_0_OR_GREATER
- listView.InsertRange(e.NewStartingIndex, e.NewViews);
+ listView.InsertRange(e.NewStartingIndex, e.NewViews);
#else
- using var array = new CloneCollection(e.NewViews);
- listView.InsertRange(e.NewStartingIndex, array.AsEnumerable());
+ using var array = new CloneCollection(e.NewViews);
+ listView.InsertRange(e.NewStartingIndex, array.AsEnumerable());
#endif
+ }
+ else
+ {
+ var span = e.NewViews;
+ for (int i = 0; i < span.Length; i++)
+ {
+ var index = e.NewStartingIndex + i;
+ listView.Insert(index, span[i]);
+ var ev = new SynchronizedViewChangedEventArgs(e.Action, true, newItem: (e.NewValues[i], span[i]), newStartingIndex: index);
+ OnCollectionChanged(ev);
+ }
+ return;
+ }
}
break;
case NotifyCollectionChangedAction.Remove: // Remove
@@ -269,7 +316,21 @@ internal class NonFilteredSynchronizedViewList : ISynchronizedViewList
}
else
{
- listView.RemoveRange(e.OldStartingIndex, e.OldViews.Length);
+ if (IsSupportRangeFeature)
+ {
+ listView.RemoveRange(e.OldStartingIndex, e.OldViews.Length);
+ }
+ else
+ {
+ var span = e.OldViews;
+ for (int i = 0; i < span.Length; i++)
+ {
+ listView.RemoveAt(e.OldStartingIndex); // when removed, next remove index is same.
+ var ev = new SynchronizedViewChangedEventArgs(e.Action, true, oldItem: (e.OldValues[i], span[i]), oldStartingIndex: e.OldStartingIndex);
+ OnCollectionChanged(ev);
+ }
+ return;
+ }
}
}
break;
@@ -320,9 +381,6 @@ internal class NonFilteredSynchronizedViewList : ISynchronizedViewList
}
else
{
-
-
-
#if NET6_0_OR_GREATER
#pragma warning disable CS0436
if (parent is ObservableList.View observableListView && typeof(T) == typeof(TView))
@@ -408,7 +466,7 @@ internal class NonFilteredSynchronizedViewList : ISynchronizedViewList
IEnumerator IEnumerable.GetEnumerator()
{
- return listView.GetEnumerator();
+ return GetEnumerator();
}
public void Dispose()
@@ -428,6 +486,8 @@ internal class NotifyCollectionChangedSynchronizedViewList :
readonly ICollectionEventDispatcher eventDispatcher;
+ protected override bool IsSupportRangeFeature => false; // WPF, Avalonia etc does not support range notification
+
public event NotifyCollectionChangedEventHandler? CollectionChanged;
public event PropertyChangedEventHandler? PropertyChanged;
@@ -676,6 +736,8 @@ internal class NonFilteredNotifyCollectionChangedSynchronizedViewList
public event NotifyCollectionChangedEventHandler? CollectionChanged;
public event PropertyChangedEventHandler? PropertyChanged;
+ protected override bool IsSupportRangeFeature => false; // WPF, Avalonia etc does not support range notification
+
public NonFilteredNotifyCollectionChangedSynchronizedViewList(ISynchronizedView parent, ICollectionEventDispatcher? eventDispatcher)
: base(parent)
{