• R/O
  • HTTP
  • SSH
  • HTTPS

Commit

Tags
Aucun tag

Frequently used words (click to add to your profile)

javac++androidlinuxc#windowsobjective-ccocoa誰得qtpythonphprubygameguibathyscaphec計画中(planning stage)翻訳omegatframeworktwitterdomtestvb.netdirectxゲームエンジンbtronarduinopreviewer

Cysharp/ObservableCollections をベースにしたUI表示用の同期ビュー


Commit MetaInfo

Révision8b29735bcf886a9b4ce5eb5ddbd27736eb67ba7e (tree)
l'heure2022-08-25 03:15:30
Auteuryoshy <yoshy.org.bitbucket@gz.j...>
Commiteryoshy

Message de Log

initial revision

Change Summary

Modification

--- /dev/null
+++ b/CleanAuLait.ObservableCollectionsMod.csproj
@@ -0,0 +1,21 @@
1+<Project Sdk="Microsoft.NET.Sdk">
2+
3+ <PropertyGroup>
4+ <TargetFramework>net6.0-windows10.0.19041.0</TargetFramework>
5+ <ImplicitUsings>enable</ImplicitUsings>
6+ <Nullable>enable</Nullable>
7+ </PropertyGroup>
8+
9+ <Choose>
10+ <When Condition=" '$(Configuration)'=='debug' ">
11+ <ItemGroup>
12+ <PackageReference Include="NLog" Version="5.0.2" />
13+ </ItemGroup>
14+ </When>
15+ </Choose>
16+
17+ <ItemGroup>
18+ <PackageReference Include="ObservableCollections" Version="1.1.2" />
19+ </ItemGroup>
20+
21+</Project>
--- /dev/null
+++ b/INotifyCollectionChangedListSynchronizedSingleView.cs
@@ -0,0 +1,11 @@
1+using System.Collections;
2+using System.Collections.Specialized;
3+using System.ComponentModel;
4+
5+namespace CleanAuLait.ObservableCollectionsMod
6+{
7+ public interface INotifyCollectionChangedListSynchronizedSingleView<T, TView>
8+ : ISynchronizedSingleView<T, TView>, IList, INotifyCollectionChanged, INotifyPropertyChanged
9+ {
10+ }
11+}
--- /dev/null
+++ b/ISortableSynchronizedCoupleView.cs
@@ -0,0 +1,12 @@
1+using ObservableCollections;
2+
3+namespace CleanAuLait.ObservableCollectionsMod
4+{
5+ public interface ISortableSynchronizedCoupleView<T, TKey, TView> : ISynchronizedCoupleView<T, TView>
6+ where TKey : notnull
7+ {
8+ void ConnectSource(IObservableCollection<T> source, IComparer<(TKey, T, TView)> comparer);
9+
10+ void Sort(IComparer<(TKey id, T value, TView view)> comparer);
11+ }
12+}
--- /dev/null
+++ b/ISynchronizedCoupleView.cs
@@ -0,0 +1,19 @@
1+using ObservableCollections;
2+using System.Collections.Specialized;
3+
4+namespace CleanAuLait.ObservableCollectionsMod
5+{
6+ public interface ISynchronizedCoupleView<T, TView> : IReadOnlyCollection<(T, TView)>, IDisposable
7+ {
8+ object SyncRoot { get; }
9+
10+ event NotifyCollectionChangedEventHandler<(T, TView)>? RoutingCollectionChanged;
11+ event Action<NotifyCollectionChangedAction>? CollectionStateChanged;
12+
13+ void ConnectSource(IObservableCollection<T> source);
14+ IObservableCollection<T>? DisconnectSource();
15+
16+ void AttachFilter(ISynchronizedViewFilter<T, TView> filter);
17+ void ResetFilter(Action<T, TView>? resetAction);
18+ }
19+}
--- /dev/null
+++ b/ISynchronizedSingleView.cs
@@ -0,0 +1,18 @@
1+using ObservableCollections;
2+using System.Collections.Specialized;
3+
4+namespace CleanAuLait.ObservableCollectionsMod
5+{
6+ public interface ISynchronizedSingleView<T, TView> : IReadOnlyCollection<TView>, IDisposable
7+ {
8+ object SyncRoot { get; }
9+
10+ event NotifyCollectionChangedEventHandler<TView>? RoutingCollectionChanged;
11+ event Action<NotifyCollectionChangedAction>? CollectionStateChanged;
12+
13+ void AttachFilter(ISynchronizedViewFilter<T, TView> filter);
14+ void ResetFilter(Action<T, TView>? resetAction);
15+
16+ INotifyCollectionChangedListSynchronizedSingleView<T, TView> WithINotifyCollectionChangedList();
17+ }
18+}
\ No newline at end of file
--- /dev/null
+++ b/Internal/CloneCollection.cs
@@ -0,0 +1,130 @@
1+using System.Buffers;
2+using System.Collections;
3+using System.Runtime.CompilerServices;
4+
5+// copy from ObservableCollections.Internal.CloneCollection
6+namespace CleanAuLait.ObservableCollectionsMod.Internal
7+{
8+ /// <summary>
9+ /// ReadOnly cloned collection.
10+ /// </summary>
11+ internal struct CloneCollection<T> : IDisposable
12+ {
13+ T[]? array;
14+ int length;
15+
16+ public ReadOnlySpan<T> Span => array.AsSpan(0, length);
17+
18+ public IEnumerable<T> AsEnumerable() => new EnumerableCollection(array, length);
19+
20+ public CloneCollection(T item)
21+ {
22+ this.array = ArrayPool<T>.Shared.Rent(1);
23+ this.length = 1;
24+ this.array[0] = item;
25+ }
26+
27+ public CloneCollection(IEnumerable<T> source)
28+ {
29+ if (source.TryGetNonEnumeratedCount(out var count))
30+ {
31+ var array = ArrayPool<T>.Shared.Rent(count);
32+
33+ if (source is ICollection<T> c)
34+ {
35+ c.CopyTo(array, 0);
36+ }
37+ else
38+ {
39+ var i = 0;
40+ foreach (var item in source)
41+ {
42+ array[i++] = item;
43+ }
44+ }
45+ this.array = array;
46+ this.length = count;
47+ }
48+ else
49+ {
50+ var array = ArrayPool<T>.Shared.Rent(count);
51+
52+ var i = 0;
53+ foreach (var item in source)
54+ {
55+ TryEnsureCapacity(ref array, i);
56+ array[i++] = item;
57+ }
58+ this.array = array;
59+ this.length = i;
60+ }
61+ }
62+
63+ public CloneCollection(ReadOnlySpan<T> source)
64+ {
65+ var array = ArrayPool<T>.Shared.Rent(source.Length);
66+ source.CopyTo(array);
67+ this.array = array;
68+ this.length = source.Length;
69+ }
70+
71+ static void TryEnsureCapacity(ref T[] array, int index)
72+ {
73+ if (array.Length == index)
74+ {
75+ ArrayPool<T>.Shared.Return(array, RuntimeHelpers.IsReferenceOrContainsReferences<T>());
76+ }
77+ array = ArrayPool<T>.Shared.Rent(index * 2);
78+ }
79+
80+ public void Dispose()
81+ {
82+ if (array != null)
83+ {
84+ ArrayPool<T>.Shared.Return(array, RuntimeHelpers.IsReferenceOrContainsReferences<T>());
85+ array = null;
86+ }
87+ }
88+
89+ // Optimize to use Count and CopyTo
90+ class EnumerableCollection : ICollection<T>
91+ {
92+ readonly T[] array;
93+ readonly int count;
94+
95+ public EnumerableCollection(T[]? array, int count)
96+ {
97+ if (array == null)
98+ {
99+ this.array = Array.Empty<T>();
100+ this.count = 0;
101+ }
102+ else
103+ {
104+ this.array = array;
105+ this.count = count;
106+ }
107+ }
108+
109+ public int Count => count;
110+
111+ public bool IsReadOnly => true;
112+
113+ public void Add(T item) => throw new NotSupportedException();
114+ public void Clear() => throw new NotSupportedException();
115+ public bool Contains(T item) => throw new NotSupportedException();
116+ public void CopyTo(T[] dest, int destIndex) => Array.Copy(array, 0, dest, destIndex, count);
117+
118+ public IEnumerator<T> GetEnumerator()
119+ {
120+ for (int i = 0; i < count; i++)
121+ {
122+ yield return array[i];
123+ }
124+ }
125+
126+ public bool Remove(T item) => throw new NotSupportedException();
127+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
128+ }
129+ }
130+}
\ No newline at end of file
--- /dev/null
+++ b/Internal/SortableSynchronizedCoupleView.Sort.cs
@@ -0,0 +1,66 @@
1+using ObservableCollections;
2+
3+namespace CleanAuLait.ObservableCollectionsMod.Internal
4+{
5+ internal sealed partial class SortableSynchronizedCoupleView<T, TKey, TView> where TKey : notnull
6+ {
7+ public void Sort(IComparer<(TKey id, T value, TView view)> comparer)
8+ {
9+ this.comparer = comparer;
10+
11+ lock (this.SyncRoot)
12+ {
13+ this.list.Sort(comparer);
14+ RoutingCollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<(T, TView)>.Reset());
15+ }
16+ }
17+
18+ public static IComparer<(TKey, T, TView)> CreateValueComparer<TValueColumn>(
19+ Func<T, TValueColumn> selector, bool ascending = true) => new ValueComparer<TValueColumn>(selector, ascending);
20+
21+ public static IComparer<(TKey, T, TView)> CreateViewComparer<TViewColumn>(
22+ Func<TView, TViewColumn> selector, bool ascending = true) => new ViewComparer<TViewColumn>(selector, ascending);
23+
24+ public static IComparer<(TKey, T, TView)> CreateMultiValueComparer<TValueColumn>(
25+ IEnumerable<Func<T, TValueColumn>> selectors, bool ascending = true) => new MultiValueComparer<TValueColumn>(selectors, ascending);
26+
27+ public static IComparer<(TKey, T, TView)> CreateMultiViewComparer<TViewColumn>(
28+ IEnumerable<Func<TView, TViewColumn>> selectors, bool ascending = true) => new MultiViewComparer<TViewColumn>(selectors, ascending);
29+
30+ public sealed class ValueComparer<TValueColumn> : SortableSynchronizedCoupleViewValueComparer<TKey, T, TView, TValueColumn>
31+ {
32+ public ValueComparer(Func<T, TValueColumn> selector, bool ascending = true) : base(selector, ascending)
33+ {
34+ }
35+ }
36+
37+ public sealed class ViewComparer<TViewColumn> : SortableSynchronizedCoupleViewViewComparer<TKey, T, TView, TViewColumn>
38+ {
39+ public ViewComparer(Func<TView, TViewColumn> selector, bool ascending = true) : base(selector, ascending)
40+ {
41+ }
42+ }
43+
44+ public sealed class MultiValueComparer<TValueColumn> : SortableSynchronizedCoupleViewMultiValueComparer<TKey, T, TView, TValueColumn>
45+ {
46+ public MultiValueComparer(IEnumerable<Func<T, TValueColumn>> selectors, bool ascending = true) : base(selectors, ascending)
47+ {
48+ }
49+ }
50+
51+ public sealed class MultiViewComparer<TViewColumn> : SortableSynchronizedCoupleViewMultiViewComparer<TKey, T, TView, TViewColumn>
52+ {
53+ public MultiViewComparer(IEnumerable<Func<TView, TViewColumn>> selectors, bool ascending = true) : base(selectors, ascending)
54+ {
55+ }
56+ }
57+
58+ private sealed class DefaultComparer : IComparer<(TKey id, T value, TView view)>
59+ {
60+ public int Compare((TKey id, T value, TView view) x, (TKey id, T value, TView view) y)
61+ {
62+ return Comparer<TKey>.Default.Compare(x.id, y.id);
63+ }
64+ }
65+ }
66+}
--- /dev/null
+++ b/Internal/SortableSynchronizedCoupleView.cs
@@ -0,0 +1,380 @@
1+#if DEBUG
2+using NLog;
3+#endif
4+using ObservableCollections;
5+using System.Collections;
6+using System.Collections.Specialized;
7+
8+namespace CleanAuLait.ObservableCollectionsMod.Internal
9+{
10+ internal sealed partial class SortableSynchronizedCoupleView<T, TKey, TView> :
11+ ISortableSynchronizedCoupleView<T, TKey, TView>
12+ where TKey : notnull
13+ {
14+#if DEBUG
15+ private static readonly ILogger logger = LogManager.GetCurrentClassLogger();
16+#endif
17+
18+ public event NotifyCollectionChangedEventHandler<(T, TView)>? RoutingCollectionChanged;
19+ public event Action<NotifyCollectionChangedAction>? CollectionStateChanged;
20+
21+ public object SyncRoot { get; } = new object();
22+
23+ private IObservableCollection<T>? source;
24+ private IComparer<(TKey id, T value, TView view)> comparer;
25+ private ISynchronizedViewFilter<T, TView> filter;
26+
27+ private readonly Func<T, TView> transform;
28+ private readonly Func<T, TKey> identitySelector;
29+ private readonly Dictionary<TKey, (T value, TView view)> map; // required when removing.
30+ private readonly List<(TKey id, T value, TView view)> list;
31+
32+ private readonly bool disposeElement;
33+ private bool disposedValue;
34+
35+ public SortableSynchronizedCoupleView(
36+ IObservableCollection<T> source,
37+ Func<T, TKey> identitySelector, Func<T, TView> transform,
38+ bool disposeElement)
39+ : this(source, identitySelector, transform, new DefaultComparer(), disposeElement)
40+ {
41+ }
42+
43+ public SortableSynchronizedCoupleView(
44+ IObservableCollection<T> source,
45+ Func<T, TKey> identitySelector, Func<T, TView> transform, IComparer<(TKey, T, TView)> comparer,
46+ bool disposeElement)
47+ {
48+ this.source = source;
49+
50+ this.identitySelector = identitySelector;
51+ this.transform = transform;
52+ this.comparer = comparer;
53+ this.disposeElement = disposeElement;
54+
55+ this.filter = SynchronizedViewFilter<T, TView>.Null;
56+
57+ this.list = new List<(TKey, T, TView)>();
58+ this.map = new Dictionary<TKey, (T, TView)>();
59+
60+ ConnectSource(source);
61+ }
62+
63+ public int Count
64+ {
65+ get
66+ {
67+ lock (this.SyncRoot)
68+ {
69+ return this.list.Count;
70+ }
71+ }
72+ }
73+
74+ public void AttachFilter(ISynchronizedViewFilter<T, TView> filter)
75+ {
76+ lock (this.SyncRoot)
77+ {
78+ this.filter = filter;
79+ foreach (var (_, value, view) in this.list)
80+ {
81+ this.filter.InvokeOnAttach(value, view);
82+ }
83+ }
84+ }
85+
86+ public void ResetFilter(Action<T, TView>? resetAction)
87+ {
88+ lock (this.SyncRoot)
89+ {
90+ this.filter = SynchronizedViewFilter<T, TView>.Null;
91+ if (resetAction != null)
92+ {
93+ foreach (var (_, value, view) in this.list)
94+ {
95+ resetAction(value, view);
96+ }
97+ }
98+ }
99+ }
100+
101+ public IEnumerator<(T, TView)> GetEnumerator()
102+ {
103+ lock (this.SyncRoot)
104+ {
105+ foreach (var (_, value, view) in this.list)
106+ {
107+ if (this.filter.IsMatch(value, view))
108+ {
109+ yield return (value, view);
110+ }
111+ }
112+ }
113+ }
114+
115+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
116+
117+ private void Dispose(bool disposing)
118+ {
119+ if (!this.disposedValue)
120+ {
121+ if (disposing)
122+ {
123+#if DEBUG
124+ logger.Trace("{0} disposing SortableSynchronizedCoupleView...", GetType().FullName);
125+#endif
126+
127+ DisconnectSource();
128+
129+#if DEBUG
130+ logger.Trace("{0} SortableSynchronizedCoupleView disposed.", GetType().FullName);
131+#endif
132+ }
133+
134+ this.disposedValue = true;
135+ }
136+ }
137+
138+ public void Dispose()
139+ {
140+ Dispose(disposing: true);
141+ GC.SuppressFinalize(this);
142+ }
143+
144+ public void ConnectSource(IObservableCollection<T> source)
145+ {
146+ ConnectSource(source, this.comparer);
147+ }
148+
149+ public void ConnectSource(IObservableCollection<T> source, IComparer<(TKey, T, TView)> comparer)
150+ {
151+ if (this.source != null)
152+ {
153+ DisconnectSource();
154+ }
155+
156+ lock (source.SyncRoot)
157+ {
158+ foreach (var value in source)
159+ {
160+ var view = transform(value);
161+ var id = identitySelector(value);
162+ this.list.Add((id, value, view));
163+ this.map.Add(id, (value, view));
164+ }
165+
166+ this.list.Sort(comparer);
167+
168+ this.source = source;
169+ this.comparer = comparer;
170+
171+ source.CollectionChanged += SourceCollectionChanged;
172+ }
173+ }
174+
175+ public IObservableCollection<T>? DisconnectSource()
176+ {
177+ IObservableCollection<T>? source = this.source;
178+
179+ if (source == null)
180+ {
181+ return null;
182+ }
183+
184+ lock (this.SyncRoot)
185+ {
186+#if DEBUG
187+ logger.Trace("{0} disconnect from source.", GetType().FullName);
188+#endif
189+
190+ source.CollectionChanged -= SourceCollectionChanged;
191+
192+ if (this.disposeElement)
193+ {
194+#if DEBUG
195+ logger.Trace("{0} (T, TView) elements disposing...", GetType().FullName);
196+#endif
197+
198+ foreach (var (_, _, view) in this.list)
199+ {
200+ if (view is IDisposable disposable)
201+ {
202+ disposable.Dispose();
203+ }
204+ }
205+
206+#if DEBUG
207+ logger.Trace("{0} (T, TView) elements disposed.", GetType().FullName);
208+#endif
209+ }
210+
211+ this.list.Clear();
212+ this.map.Clear();
213+
214+ this.source = null;
215+
216+ RoutingCollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<(T, TView)>.Reset());
217+
218+ return source;
219+ }
220+ }
221+
222+ private void SourceCollectionChanged(in NotifyCollectionChangedEventArgs<T> e)
223+ {
224+ lock (this.SyncRoot)
225+ {
226+ switch (e.Action)
227+ {
228+ case NotifyCollectionChangedAction.Add:
229+ {
230+ // Add, Insert
231+ if (e.IsSingleItem)
232+ {
233+ AddNewItem(e, e.NewItem);
234+ }
235+ else
236+ {
237+ foreach (var newItem in e.NewItems)
238+ {
239+ AddNewItem(e, newItem);
240+ }
241+ }
242+ }
243+ break;
244+ case NotifyCollectionChangedAction.Remove:
245+ {
246+ if (e.IsSingleItem)
247+ {
248+ RemoveOldItem(e, e.OldItem);
249+ }
250+ else
251+ {
252+ foreach (var oldItem in e.OldItems)
253+ {
254+ RemoveOldItem(e, oldItem);
255+ }
256+ }
257+ }
258+ break;
259+ case NotifyCollectionChangedAction.Replace:
260+ // Replace is remove old item and insert new item.
261+ {
262+ RemoveOldItem(e, e.OldItem);
263+ AddNewItem(e, e.NewItem);
264+ }
265+ break;
266+ case NotifyCollectionChangedAction.Move:
267+ // Move(index change) does not affect sorted this.list.
268+ MoveItem(e, e.OldItem);
269+ break;
270+ case NotifyCollectionChangedAction.Reset:
271+ if (!filter.IsNullFilter())
272+ {
273+ foreach (var (_, value, view) in this.list)
274+ {
275+ this.filter.InvokeOnRemove((value, view), e);
276+ }
277+ }
278+ this.list.Clear();
279+ this.map.Clear();
280+ RoutingCollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<(T, TView)>.Reset());
281+ break;
282+ default:
283+ break;
284+ }
285+
286+ CollectionStateChanged?.Invoke(e.Action);
287+ }
288+ }
289+
290+ private void AddNewItem(NotifyCollectionChangedEventArgs<T> e, T? newItem)
291+ {
292+ if (newItem == null)
293+ {
294+ throw new ArgumentNullException(nameof(newItem));
295+ }
296+
297+ var id = identitySelector(newItem);
298+ var view = transform(newItem);
299+
300+ var key = (id, newItem, view);
301+ int newStartingIndex = this.list.BinarySearch(key, this.comparer);
302+
303+ newStartingIndex = newStartingIndex < 0 ? ~newStartingIndex : newStartingIndex + 1;
304+
305+ var value = (newItem, view);
306+
307+ this.filter.InvokeOnAdd(value, e);
308+
309+ this.list.Insert(newStartingIndex, key);
310+ this.map.Add(id, (newItem, view));
311+
312+ RoutingCollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<(T, TView)>.Add(value, newStartingIndex));
313+ }
314+
315+ private void RemoveOldItem(NotifyCollectionChangedEventArgs<T> e, T? oldItem)
316+ {
317+ if (oldItem == null)
318+ {
319+ throw new ArgumentNullException(nameof(oldItem));
320+ }
321+
322+ var id = identitySelector(oldItem);
323+
324+ if (!map.Remove(id, out (T value, TView view) couple))
325+ {
326+ throw new ArgumentException($"old item id [{id}] not found.");
327+ }
328+
329+ var key = (id, couple.value, couple.view);
330+
331+ int oldStartingIndex = this.list.BinarySearch(key, this.comparer);
332+
333+ if (oldStartingIndex < 0)
334+ {
335+ throw new ArgumentException($"old item [{oldItem}] not found.");
336+ }
337+
338+ this.filter.InvokeOnRemove(couple, e);
339+
340+ if (this.disposeElement && couple.view is IDisposable disposable)
341+ {
342+ disposable.Dispose();
343+ }
344+
345+ this.list.RemoveAt(oldStartingIndex);
346+
347+ RoutingCollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<(T, TView)>.Remove(couple, oldStartingIndex));
348+ }
349+
350+ private void MoveItem(NotifyCollectionChangedEventArgs<T> e, T? oldItem)
351+ {
352+ if (oldItem == null)
353+ {
354+ throw new ArgumentNullException(nameof(oldItem));
355+ }
356+
357+ var id = identitySelector(oldItem);
358+
359+ if (!map.Remove(id, out (T value, TView view) couple))
360+ {
361+ throw new ArgumentException($"old item id [{id}] not found.");
362+ }
363+
364+ var key = (id, couple.value, couple.view);
365+
366+ int oldStartingIndex = this.list.BinarySearch(key, this.comparer);
367+
368+ if (oldStartingIndex < 0)
369+ {
370+ throw new ArgumentException($"old item [{oldItem}] not found.");
371+ }
372+
373+ // Move(index change) does not affect sorted this.list.
374+
375+ this.filter.InvokeOnMove(couple, e);
376+
377+ RoutingCollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<(T, TView)>.Move(couple, oldStartingIndex, oldStartingIndex));
378+ }
379+ }
380+}
--- /dev/null
+++ b/Internal/SynchronizedCoupleView.cs
@@ -0,0 +1,362 @@
1+#if DEBUG
2+using NLog;
3+#endif
4+using ObservableCollections;
5+using System.Collections;
6+using System.Collections.Specialized;
7+using System.Runtime.InteropServices;
8+
9+namespace CleanAuLait.ObservableCollectionsMod.Internal
10+{
11+ internal sealed class SynchronizedCoupleView<T, TView> : ISynchronizedCoupleView<T, TView>
12+ {
13+#if DEBUG
14+ private static readonly ILogger logger = LogManager.GetCurrentClassLogger();
15+#endif
16+
17+ public event NotifyCollectionChangedEventHandler<(T, TView)>? RoutingCollectionChanged;
18+ public event Action<NotifyCollectionChangedAction>? CollectionStateChanged;
19+
20+ public object SyncRoot { get; } = new object();
21+
22+ private IObservableCollection<T>? source;
23+ private ISynchronizedViewFilter<T, TView> filter;
24+
25+ private readonly Func<T, TView> transform;
26+ private readonly bool reverse;
27+ private readonly List<(T, TView)> list;
28+
29+ private readonly bool disposeElement;
30+ private bool disposedValue;
31+
32+ public SynchronizedCoupleView(IObservableCollection<T> source, Func<T, TView> transform, bool reverse, bool disposeElement)
33+ {
34+ this.source = source;
35+
36+ this.transform = transform;
37+ this.reverse = reverse;
38+
39+ this.disposeElement = disposeElement;
40+
41+ this.list = new List<(T, TView)>();
42+ this.filter = SynchronizedViewFilter<T, TView>.Null;
43+
44+ ConnectSource(source);
45+ }
46+
47+ public int Count
48+ {
49+ get
50+ {
51+ lock (this.SyncRoot)
52+ {
53+ return this.list.Count;
54+ }
55+ }
56+ }
57+
58+ public void AttachFilter(ISynchronizedViewFilter<T, TView> filter)
59+ {
60+ lock (this.SyncRoot)
61+ {
62+ this.filter = filter;
63+ foreach (var (value, view) in this.list)
64+ {
65+ this.filter.InvokeOnAttach(value, view);
66+ }
67+ }
68+ }
69+
70+ public void ResetFilter(Action<T, TView>? resetAction)
71+ {
72+ lock (this.SyncRoot)
73+ {
74+ this.filter = SynchronizedViewFilter<T, TView>.Null;
75+ if (resetAction != null)
76+ {
77+ foreach (var (item, view) in this.list)
78+ {
79+ resetAction(item, view);
80+ }
81+ }
82+ }
83+ }
84+
85+ public IEnumerator<(T, TView)> GetEnumerator()
86+ {
87+ lock (this.SyncRoot)
88+ {
89+ if (!reverse)
90+ {
91+ foreach (var item in this.list)
92+ {
93+ if (this.filter.IsMatch(item.Item1, item.Item2))
94+ {
95+ yield return item;
96+ }
97+ }
98+ }
99+ else
100+ {
101+ foreach (var item in this.list.AsEnumerable().Reverse())
102+ {
103+ if (this.filter.IsMatch(item.Item1, item.Item2))
104+ {
105+ yield return item;
106+ }
107+ }
108+ }
109+ }
110+ }
111+
112+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
113+
114+ private void Dispose(bool disposing)
115+ {
116+ if (!this.disposedValue)
117+ {
118+ if (disposing)
119+ {
120+#if DEBUG
121+ logger.Trace("{0} disposing CoupleView...", GetType().FullName);
122+#endif
123+
124+ DisconnectSource();
125+
126+#if DEBUG
127+ logger.Trace("{0} CoupleView disposed.", GetType().FullName);
128+#endif
129+ }
130+
131+ this.disposedValue = true;
132+ }
133+ }
134+
135+ public void Dispose()
136+ {
137+ Dispose(disposing: true);
138+ GC.SuppressFinalize(this);
139+ }
140+
141+ public void ConnectSource(IObservableCollection<T> source)
142+ {
143+ if (this.source != null)
144+ {
145+ DisconnectSource();
146+ }
147+
148+ lock (this.SyncRoot)
149+ {
150+ lock (source.SyncRoot)
151+ {
152+ this.list.AddRange(source.Select(x => (x, transform(x))));
153+ this.source = source;
154+
155+ source.CollectionChanged += SourceCollectionChanged;
156+ }
157+ }
158+ }
159+
160+ public IObservableCollection<T>? DisconnectSource()
161+ {
162+ IObservableCollection<T>? source = this.source;
163+
164+ if (source == null)
165+ {
166+ return null;
167+ }
168+
169+ lock (this.SyncRoot)
170+ {
171+#if DEBUG
172+ logger.Trace("{0} disconnect from source.", GetType().FullName);
173+#endif
174+
175+ source.CollectionChanged -= SourceCollectionChanged;
176+
177+ if (this.disposeElement)
178+ {
179+#if DEBUG
180+ logger.Trace("{0} (T, TView) elements disposing...", this.GetType().FullName);
181+#endif
182+
183+ foreach (var item in this.list)
184+ {
185+ if (item.Item2 is IDisposable disposable)
186+ {
187+ disposable.Dispose();
188+ }
189+ }
190+#if DEBUG
191+ logger.Trace("{0} (T, TView) elements disposed.", this.GetType().FullName);
192+#endif
193+ }
194+
195+ this.list.Clear();
196+
197+ this.source = null;
198+
199+ RoutingCollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<(T, TView)>.Reset());
200+
201+ return source;
202+ }
203+ }
204+
205+ private void SourceCollectionChanged(in NotifyCollectionChangedEventArgs<T> e)
206+ {
207+ lock (this.SyncRoot)
208+ {
209+ switch (e.Action)
210+ {
211+ case NotifyCollectionChangedAction.Add:
212+ // Add
213+ if (e.NewStartingIndex == this.list.Count)
214+ {
215+ if (e.IsSingleItem)
216+ {
217+ var v = (e.NewItem, transform(e.NewItem));
218+ this.filter.InvokeOnAdd(v, e);
219+ this.list.Add(v);
220+ RoutingCollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<(T, TView)>.Add(v, e.NewStartingIndex));
221+ }
222+ else
223+ {
224+ // inefficient copy, need refactoring
225+ var newArray = new (T, TView)[e.NewItems.Length];
226+ var span = e.NewItems;
227+ for (int i = 0; i < span.Length; i++)
228+ {
229+ var v = (span[i], transform(span[i]));
230+ newArray[i] = v;
231+ this.filter.InvokeOnAdd(v, e);
232+ }
233+ this.list.AddRange(newArray);
234+ RoutingCollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<(T, TView)>.Add(newArray, e.NewStartingIndex));
235+ }
236+ }
237+ // Insert
238+ else
239+ {
240+ if (e.IsSingleItem)
241+ {
242+ var v = (e.NewItem, transform(e.NewItem));
243+ this.filter.InvokeOnAdd(v, e);
244+ this.list.Insert(e.NewStartingIndex, v);
245+ RoutingCollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<(T, TView)>.Add(v, e.NewStartingIndex));
246+ }
247+ else
248+ {
249+ // inefficient copy, need refactoring
250+ var newArray = new (T, TView)[e.NewItems.Length];
251+ var span = e.NewItems;
252+ for (int i = 0; i < span.Length; i++)
253+ {
254+ var v = (span[i], transform(span[i]));
255+ newArray[i] = v;
256+ this.filter.InvokeOnAdd(v, e);
257+ }
258+ this.list.InsertRange(e.NewStartingIndex, newArray);
259+ RoutingCollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<(T, TView)>.Add(newArray, e.NewStartingIndex));
260+ }
261+ }
262+ break;
263+ case NotifyCollectionChangedAction.Remove:
264+ if (e.IsSingleItem)
265+ {
266+ var v = this.list[e.OldStartingIndex];
267+
268+ this.filter.InvokeOnRemove(v, e);
269+
270+ if (this.disposeElement && v.Item2 is IDisposable disposable)
271+ {
272+ disposable.Dispose();
273+ }
274+
275+ this.list.RemoveAt(e.OldStartingIndex);
276+
277+ RoutingCollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<(T, TView)>.Remove(v, e.OldStartingIndex));
278+ }
279+ else
280+ {
281+ var len = e.OldStartingIndex + e.OldItems.Length;
282+ for (int i = e.OldStartingIndex; i < len; i++)
283+ {
284+ var v = this.list[i];
285+ this.filter.InvokeOnRemove(v, e);
286+ }
287+
288+#if NET5_0_OR_GREATER
289+ var range = CollectionsMarshal.AsSpan(this.list).Slice(e.OldStartingIndex, e.OldItems.Length);
290+#else
291+ var range = this.list.GetRange(e.OldStartingIndex, e.OldItems.Length);
292+#endif
293+ using (var xs = new CloneCollection<(T, TView)>(range))
294+ {
295+ for (int i = e.OldStartingIndex; i < len; i++)
296+ {
297+ var v = this.list[i];
298+ if (this.disposeElement && v.Item2 is IDisposable disposable)
299+ {
300+ disposable.Dispose();
301+ }
302+ }
303+
304+ this.list.RemoveRange(e.OldStartingIndex, e.OldItems.Length);
305+
306+ RoutingCollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<(T, TView)>.Remove(xs.Span, e.OldStartingIndex));
307+ }
308+ }
309+ break;
310+ case NotifyCollectionChangedAction.Replace:
311+ // ObservableList does not support replace range
312+ {
313+ var v = (e.NewItem, transform(e.NewItem));
314+
315+ var oldItem = this.list[e.NewStartingIndex];
316+
317+ this.filter.InvokeOnRemove(oldItem, e);
318+ this.filter.InvokeOnAdd(v, e);
319+
320+ list[e.NewStartingIndex] = v;
321+
322+ if (this.disposeElement && oldItem.Item2 is IDisposable disposable)
323+ {
324+ disposable.Dispose();
325+ }
326+
327+ RoutingCollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<(T, TView)>.Replace(v, oldItem, e.NewStartingIndex));
328+ break;
329+ }
330+ case NotifyCollectionChangedAction.Move:
331+ {
332+ var removeItem = this.list[e.OldStartingIndex];
333+
334+ this.filter.InvokeOnMove(removeItem, e);
335+
336+ this.list.RemoveAt(e.OldStartingIndex);
337+ this.list.Insert(e.NewStartingIndex, removeItem);
338+
339+ RoutingCollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<(T, TView)>.Move(removeItem, e.NewStartingIndex, e.OldStartingIndex));
340+ }
341+ break;
342+ case NotifyCollectionChangedAction.Reset:
343+ if (!filter.IsNullFilter())
344+ {
345+ foreach (var item in this.list)
346+ {
347+ this.filter.InvokeOnRemove(item, e);
348+ }
349+ }
350+ this.list.Clear();
351+ RoutingCollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<(T, TView)>.Reset());
352+ break;
353+ default:
354+ break;
355+ }
356+
357+ CollectionStateChanged?.Invoke(e.Action);
358+ }
359+ }
360+
361+ }
362+}
\ No newline at end of file
--- /dev/null
+++ b/Internal/SynchronizedSingleView.NCCListView.cs
@@ -0,0 +1,205 @@
1+#if DEBUG
2+using NLog;
3+#endif
4+using ObservableCollections;
5+using System.Collections;
6+using System.Collections.Specialized;
7+using System.ComponentModel;
8+
9+namespace CleanAuLait.ObservableCollectionsMod.Internal
10+{
11+ internal sealed partial class SynchronizedSingleView<T, TView> : ISynchronizedSingleView<T, TView>
12+ {
13+ public INotifyCollectionChangedListSynchronizedSingleView<T, TView> WithINotifyCollectionChangedList()
14+ {
15+ return new NCCListView(this);
16+ }
17+
18+ class NCCListView : INotifyCollectionChangedListSynchronizedSingleView<T, TView>
19+ {
20+#if DEBUG
21+ private static readonly ILogger logger = LogManager.GetCurrentClassLogger();
22+#endif
23+
24+ private static readonly PropertyChangedEventArgs CountPropertyChangedEventArgs = new(nameof(Count));
25+
26+ private readonly ISynchronizedSingleView<T, TView> parent;
27+
28+ public NCCListView(ISynchronizedSingleView<T, TView> parent)
29+ {
30+ this.parent = parent;
31+ this.parent.RoutingCollectionChanged += Parent_RoutingCollectionChanged;
32+ }
33+
34+ private void Parent_RoutingCollectionChanged(in NotifyCollectionChangedEventArgs<TView> e)
35+ {
36+ switch (e.Action)
37+ {
38+ case NotifyCollectionChangedAction.Add:
39+ if (e.IsSingleItem)
40+ {
41+ CollectionChanged?.Invoke(this, e.ToStandardEventArgs());
42+ }
43+ else
44+ {
45+ var newItems = e.NewItems.ToArray();
46+ for (int i = 0; i < newItems.Length; i++)
47+ {
48+ CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(e.Action, newItems[i], e.NewStartingIndex + i));
49+ }
50+ }
51+ break;
52+ case NotifyCollectionChangedAction.Remove:
53+ if (e.IsSingleItem)
54+ {
55+ CollectionChanged?.Invoke(this, e.ToStandardEventArgs());
56+ }
57+ else
58+ {
59+ var oldItems = e.OldItems.ToArray();
60+ for (int i = oldItems.Length - 1; i >= 0; i++)
61+ {
62+ CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(e.Action, oldItems[i], e.OldStartingIndex + i));
63+ }
64+ }
65+ break;
66+ default:
67+ CollectionChanged?.Invoke(this, e.ToStandardEventArgs());
68+ break;
69+ }
70+
71+ switch (e.Action)
72+ {
73+ // add, remove, reset will change the count.
74+ case NotifyCollectionChangedAction.Add:
75+ case NotifyCollectionChangedAction.Remove:
76+ case NotifyCollectionChangedAction.Reset:
77+ PropertyChanged?.Invoke(this, CountPropertyChangedEventArgs);
78+ break;
79+ case NotifyCollectionChangedAction.Replace:
80+ case NotifyCollectionChangedAction.Move:
81+ default:
82+ break;
83+ }
84+ }
85+
86+ public object SyncRoot => parent.SyncRoot;
87+
88+ public int Count => parent.Count;
89+
90+ public event NotifyCollectionChangedEventHandler? CollectionChanged;
91+ public event PropertyChangedEventHandler? PropertyChanged;
92+
93+ public event Action<NotifyCollectionChangedAction>? CollectionStateChanged
94+ {
95+ add { parent.CollectionStateChanged += value; }
96+ remove { parent.CollectionStateChanged -= value; }
97+ }
98+
99+ public event NotifyCollectionChangedEventHandler<TView>? RoutingCollectionChanged
100+ {
101+ add { parent.RoutingCollectionChanged += value; }
102+ remove { parent.RoutingCollectionChanged -= value; }
103+ }
104+
105+ object? IList.this[int index]
106+ {
107+ get
108+ {
109+ lock (this.SyncRoot)
110+ {
111+ return parent is SynchronizedSingleView<T, TView> view ? view.list[index] : throw new NotSupportedException();
112+ }
113+ }
114+ set => throw new NotSupportedException();
115+ }
116+
117+ bool IList.IsFixedSize => true;
118+ bool IList.IsReadOnly => true;
119+ bool ICollection.IsSynchronized => true;
120+
121+ public void AttachFilter(ISynchronizedViewFilter<T, TView> filter) => parent.AttachFilter(filter);
122+ public void ResetFilter(Action<T, TView>? resetAction) => parent.ResetFilter(resetAction);
123+
124+ public INotifyCollectionChangedListSynchronizedSingleView<T, TView> WithINotifyCollectionChangedList() => this;
125+
126+ public void Dispose()
127+ {
128+#if DEBUG
129+ logger.Trace("{0} disposing NCCListSingleView...", this.GetType().FullName);
130+#endif
131+ this.parent.RoutingCollectionChanged -= Parent_RoutingCollectionChanged;
132+
133+#if DEBUG
134+ logger.Trace("{0} parent disposing...", this.GetType().FullName);
135+#endif
136+
137+ parent.Dispose();
138+
139+#if DEBUG
140+ logger.Trace("{0} parent and NCCListSingleView disposed.", this.GetType().FullName);
141+#endif
142+ }
143+
144+ public IEnumerator<TView> GetEnumerator() => parent.GetEnumerator();
145+ IEnumerator IEnumerable.GetEnumerator() => parent.GetEnumerator();
146+
147+ int IList.Add(object? value)
148+ {
149+ throw new NotSupportedException();
150+ }
151+
152+ void IList.Clear()
153+ {
154+ throw new NotSupportedException();
155+ }
156+
157+ bool IList.Contains(object? value)
158+ {
159+ lock (this.SyncRoot)
160+ {
161+ return parent is SynchronizedSingleView<T, TView> view ? ((IList)view.list).Contains(value) : throw new NotSupportedException();
162+ }
163+ }
164+
165+ int IList.IndexOf(object? value)
166+ {
167+ lock (this.SyncRoot)
168+ {
169+ return parent is SynchronizedSingleView<T, TView> view ? ((IList)view.list).IndexOf(value) : throw new NotSupportedException();
170+ }
171+ }
172+
173+ void IList.Insert(int index, object? value)
174+ {
175+ throw new NotSupportedException();
176+ }
177+
178+ void IList.Remove(object? value)
179+ {
180+ throw new NotSupportedException();
181+ }
182+
183+ void IList.RemoveAt(int index)
184+ {
185+ throw new NotSupportedException();
186+ }
187+
188+ void ICollection.CopyTo(Array array, int index)
189+ {
190+ lock (this.SyncRoot)
191+ {
192+ if (parent is SynchronizedSingleView<T, TView> view)
193+ {
194+ ((ICollection)view.list).CopyTo(array, index);
195+ }
196+ else
197+ {
198+ throw new NotSupportedException();
199+ }
200+ }
201+ }
202+
203+ }
204+ }
205+}
--- /dev/null
+++ b/Internal/SynchronizedSingleView.cs
@@ -0,0 +1,207 @@
1+#if DEBUG
2+using NLog;
3+#endif
4+using ObservableCollections;
5+using System.Collections;
6+using System.Collections.Specialized;
7+using System.Runtime.InteropServices;
8+
9+namespace CleanAuLait.ObservableCollectionsMod.Internal
10+{
11+ internal sealed partial class SynchronizedSingleView<T, TView> : ISynchronizedSingleView<T, TView>
12+ {
13+#if DEBUG
14+ private static readonly ILogger logger = LogManager.GetCurrentClassLogger();
15+#endif
16+
17+ public event NotifyCollectionChangedEventHandler<TView>? RoutingCollectionChanged;
18+
19+ public object SyncRoot { get; } = new object();
20+
21+ private readonly ISynchronizedCoupleView<T, TView> parent;
22+ private readonly List<TView> list;
23+
24+ private readonly bool disposeParent;
25+
26+ public SynchronizedSingleView(ISynchronizedCoupleView<T, TView> parent, bool disposeParent)
27+ {
28+ this.parent = parent;
29+ this.disposeParent = disposeParent;
30+
31+ lock (parent.SyncRoot)
32+ {
33+ this.list = parent.Select(x => x.Item2).ToList();
34+ this.parent.RoutingCollectionChanged += Parent_RoutingCollectionChanged;
35+ }
36+ }
37+
38+ public int Count
39+ {
40+ get
41+ {
42+ lock (this.SyncRoot)
43+ {
44+ return this.list.Count;
45+ }
46+ }
47+ }
48+
49+ public event Action<NotifyCollectionChangedAction>? CollectionStateChanged
50+ {
51+ add { parent.CollectionStateChanged += value; }
52+ remove { parent.CollectionStateChanged -= value; }
53+ }
54+
55+ public void AttachFilter(ISynchronizedViewFilter<T, TView> filter) => parent.AttachFilter(filter);
56+ public void ResetFilter(Action<T, TView>? resetAction) => parent.ResetFilter(resetAction);
57+
58+ public IEnumerator<TView> GetEnumerator()
59+ {
60+ lock (this.SyncRoot)
61+ {
62+ foreach (var item in list)
63+ {
64+ yield return item;
65+ }
66+ }
67+ }
68+
69+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
70+
71+ public void Dispose()
72+ {
73+#if DEBUG
74+ logger.Trace("{0} disposing SingleView...", this.GetType().FullName);
75+#endif
76+
77+ this.parent.RoutingCollectionChanged -= Parent_RoutingCollectionChanged;
78+
79+ if (this.disposeParent)
80+ {
81+#if DEBUG
82+ logger.Trace("{0} parent disposing...", this.GetType().FullName);
83+#endif
84+
85+ this.parent.Dispose();
86+
87+#if DEBUG
88+ logger.Trace("{0} parent disposed.", this.GetType().FullName);
89+#endif
90+ }
91+
92+#if DEBUG
93+ logger.Trace("{0} SingleView disposed.", this.GetType().FullName);
94+#endif
95+ }
96+
97+ private void Parent_RoutingCollectionChanged(in NotifyCollectionChangedEventArgs<(T, TView)> e)
98+ {
99+ lock (this.SyncRoot)
100+ {
101+ switch (e.Action)
102+ {
103+ case NotifyCollectionChangedAction.Add:
104+ // Add
105+ if (e.NewStartingIndex == this.list.Count)
106+ {
107+ if (e.IsSingleItem)
108+ {
109+ var v = e.NewItem.Item2;
110+ this.list.Add(v);
111+ RoutingCollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<TView>.Add(v, e.NewStartingIndex));
112+ }
113+ else
114+ {
115+ // inefficient copy, need refactoring
116+ var newArray = new TView[e.NewItems.Length];
117+ var span = e.NewItems;
118+ for (int i = 0; i < span.Length; i++)
119+ {
120+ var v = span[i].Item2;
121+ newArray[i] = v;
122+ }
123+ this.list.AddRange(newArray);
124+ RoutingCollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<TView>.Add(newArray, e.NewStartingIndex));
125+ }
126+ }
127+ // Insert
128+ else
129+ {
130+ if (e.IsSingleItem)
131+ {
132+ var v = e.NewItem.Item2;
133+ this.list.Insert(e.NewStartingIndex, v);
134+ RoutingCollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<TView>.Add(v, e.NewStartingIndex));
135+ }
136+ else
137+ {
138+ // inefficient copy, need refactoring
139+ var newArray = new TView[e.NewItems.Length];
140+ var span = e.NewItems;
141+ for (int i = 0; i < span.Length; i++)
142+ {
143+ var v = span[i].Item2;
144+ newArray[i] = v;
145+ }
146+ this.list.InsertRange(e.NewStartingIndex, newArray);
147+ RoutingCollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<TView>.Add(newArray, e.NewStartingIndex));
148+ }
149+ }
150+ break;
151+ case NotifyCollectionChangedAction.Remove:
152+ if (e.IsSingleItem)
153+ {
154+ var v = list[e.OldStartingIndex];
155+ this.list.RemoveAt(e.OldStartingIndex);
156+ RoutingCollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<TView>.Remove(v, e.OldStartingIndex));
157+ }
158+ else
159+ {
160+#if NET5_0_OR_GREATER
161+ var range = CollectionsMarshal.AsSpan(list).Slice(e.OldStartingIndex, e.OldItems.Length);
162+#else
163+ var range = this.list.GetRange(e.OldStartingIndex, e.OldItems.Length);
164+#endif
165+ using (var xs = new CloneCollection<TView>(range))
166+ {
167+ this.list.RemoveRange(e.OldStartingIndex, e.OldItems.Length);
168+ RoutingCollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<TView>.Remove(xs.Span, e.OldStartingIndex));
169+ }
170+ }
171+ break;
172+ case NotifyCollectionChangedAction.Replace:
173+ // ObservableList does not support replace range
174+ {
175+ var v = e.NewItem.Item2;
176+
177+ var oldItem = list[e.NewStartingIndex];
178+ list[e.NewStartingIndex] = v;
179+
180+ RoutingCollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<TView>.Replace(v, oldItem, e.NewStartingIndex));
181+ break;
182+ }
183+ case NotifyCollectionChangedAction.Move:
184+ {
185+ var removeItem = list[e.OldStartingIndex];
186+ this.list.RemoveAt(e.OldStartingIndex);
187+ this.list.Insert(e.NewStartingIndex, removeItem);
188+
189+ RoutingCollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<TView>.Move(removeItem, e.NewStartingIndex, e.OldStartingIndex));
190+ }
191+ break;
192+ case NotifyCollectionChangedAction.Reset:
193+ {
194+ this.list.Clear();
195+ this.list.AddRange(parent.Select(x => x.Item2));
196+ RoutingCollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<TView>.Reset());
197+ }
198+ break;
199+ default:
200+ break;
201+ }
202+
203+ }
204+ }
205+
206+ }
207+}
\ No newline at end of file
--- /dev/null
+++ b/Internal/SynchronizedViewFilterExtensions.cs
@@ -0,0 +1,58 @@
1+using ObservableCollections;
2+
3+namespace CleanAuLait.ObservableCollectionsMod.Internal
4+{
5+ // copy from ObservableCollections.SynchronizedViewFilterExtensions
6+ internal static class SynchronizedViewFilterExtensions
7+ {
8+ internal static void InvokeOnAdd<T, TView>(this ISynchronizedViewFilter<T, TView> filter, (T value, TView view) value, in NotifyCollectionChangedEventArgs<T> eventArgs)
9+ {
10+ filter.InvokeOnAdd(value.value, value.view, eventArgs);
11+ }
12+
13+ internal static void InvokeOnAdd<T, TView>(this ISynchronizedViewFilter<T, TView> filter, T value, TView view, in NotifyCollectionChangedEventArgs<T> eventArgs)
14+ {
15+ if (filter.IsMatch(value, view))
16+ {
17+ filter.WhenTrue(value, view);
18+ }
19+ else
20+ {
21+ filter.WhenFalse(value, view);
22+ }
23+ filter.OnCollectionChanged(ChangedKind.Add, value, view, eventArgs);
24+ }
25+
26+ internal static void InvokeOnRemove<T, TView>(this ISynchronizedViewFilter<T, TView> filter, (T value, TView view) value, in NotifyCollectionChangedEventArgs<T> eventArgs)
27+ {
28+ filter.InvokeOnRemove(value.value, value.view, eventArgs);
29+ }
30+
31+ internal static void InvokeOnRemove<T, TView>(this ISynchronizedViewFilter<T, TView> filter, T value, TView view, in NotifyCollectionChangedEventArgs<T> eventArgs)
32+ {
33+ filter.OnCollectionChanged(ChangedKind.Remove, value, view, eventArgs);
34+ }
35+
36+ internal static void InvokeOnMove<T, TView>(this ISynchronizedViewFilter<T, TView> filter, (T value, TView view) value, in NotifyCollectionChangedEventArgs<T> eventArgs)
37+ {
38+ filter.InvokeOnMove(value.value, value.view, eventArgs);
39+ }
40+
41+ internal static void InvokeOnMove<T, TView>(this ISynchronizedViewFilter<T, TView> filter, T value, TView view, in NotifyCollectionChangedEventArgs<T> eventArgs)
42+ {
43+ filter.OnCollectionChanged(ChangedKind.Move, value, view, eventArgs);
44+ }
45+
46+ internal static void InvokeOnAttach<T, TView>(this ISynchronizedViewFilter<T, TView> filter, T value, TView view)
47+ {
48+ if (filter.IsMatch(value, view))
49+ {
50+ filter.WhenTrue(value, view);
51+ }
52+ else
53+ {
54+ filter.WhenFalse(value, view);
55+ }
56+ }
57+ }
58+}
--- /dev/null
+++ b/ObservableCollectionsModExtensions.cs
@@ -0,0 +1,54 @@
1+using CleanAuLait.ObservableCollectionsMod.Internal;
2+using ObservableCollections;
3+
4+namespace CleanAuLait.ObservableCollectionsMod
5+{
6+ public static class ObservableCollectionsModExtensions
7+ {
8+ public static ISynchronizedCoupleView<T, TView> ToSynchronizedCoupleView<T, TView>(
9+ this IObservableCollection<T> source, Func<T, TView> transform, bool reverse = false, bool disposeElement = true)
10+ {
11+ return new SynchronizedCoupleView<T, TView>(source, transform, reverse, disposeElement);
12+ }
13+
14+ public static ISortableSynchronizedCoupleView<T, TKey, TView> ToSortableSynchronizedCoupleView<T, TKey, TView>(
15+ this IObservableCollection<T> source,
16+ Func<T, TKey> identitySelector, Func<T, TView> transform, IComparer<(TKey, T, TView)> comparer,
17+ bool disposeElement = true)
18+ where TKey : notnull
19+ {
20+ return new SortableSynchronizedCoupleView<T, TKey, TView>(source, identitySelector, transform, comparer, disposeElement);
21+ }
22+
23+ public static ISynchronizedSingleView<T, TView> ToSynchronizedSingleView<T, TView>(
24+ this ISynchronizedCoupleView<T, TView> parent, bool disposeParent = true)
25+ {
26+ return new SynchronizedSingleView<T, TView>(parent, disposeParent);
27+ }
28+
29+
30+ public static IComparer<(TKey, T, TView)> CreateValueComparer<T, TKey, TView, TValueColumn>(
31+ this ISortableSynchronizedCoupleView<T, TKey, TView> view, Func<T, TValueColumn> selector, bool ascending = true) where TKey: notnull
32+ {
33+ return SortableSynchronizedCoupleView<T, TKey, TView>.CreateValueComparer(selector, ascending);
34+ }
35+
36+ public static IComparer<(TKey, T, TView)> CreateViewComparer<TKey, T, TView, TViewColumn>(
37+ this ISortableSynchronizedCoupleView<T, TKey, TView> view, Func<TView, TViewColumn> selector, bool ascending = true) where TKey : notnull
38+ {
39+ return SortableSynchronizedCoupleView<T, TKey, TView>.CreateViewComparer(selector, ascending);
40+ }
41+
42+ public static IComparer<(TKey, T, TView)> CreateMultiValueComparer<TKey, T, TView, TValueColumn>(
43+ this ISortableSynchronizedCoupleView<T, TKey, TView> view, IEnumerable<Func<T, TValueColumn>> selectors, bool ascending = true) where TKey : notnull
44+ {
45+ return SortableSynchronizedCoupleView<T, TKey, TView>.CreateMultiValueComparer(selectors, ascending);
46+ }
47+
48+ public static IComparer<(TKey, T, TView)> CreateMultiViewComparer<TKey, T, TView, TViewColumn>(
49+ this ISortableSynchronizedCoupleView<T, TKey, TView> view, IEnumerable<Func<TView, TViewColumn>> selectors, bool ascending = true) where TKey : notnull
50+ {
51+ return SortableSynchronizedCoupleView<T, TKey, TView>.CreateMultiViewComparer(selectors, ascending);
52+ }
53+ }
54+}
--- /dev/null
+++ b/SortableSynchronizedCoupleViewComparer.cs
@@ -0,0 +1,105 @@
1+namespace CleanAuLait.ObservableCollectionsMod
2+{
3+ public class SortableSynchronizedCoupleViewValueComparer<TKey, T, TView, TValueColumn> : IComparer<(TKey, T, TView)>
4+ {
5+ private readonly Func<T, TValueColumn> selector;
6+ private readonly int f;
7+
8+ public SortableSynchronizedCoupleViewValueComparer(Func<T, TValueColumn> selector, bool ascending = true)
9+ {
10+ this.selector = selector;
11+ this.f = ascending ? 1 : -1;
12+ }
13+
14+ public int Compare((TKey, T, TView) x, (TKey, T, TView) y)
15+ {
16+ int c = Comparer<TValueColumn>.Default.Compare(selector(x.Item2), selector(y.Item2)) * f;
17+
18+ if (c == 0)
19+ {
20+ c = Comparer<TKey>.Default.Compare(x.Item1, y.Item1) * f;
21+ }
22+
23+ return c;
24+ }
25+ }
26+
27+ public class SortableSynchronizedCoupleViewMultiValueComparer<TKey, T, TView, TValueColumn> : IComparer<(TKey, T, TView)>
28+ {
29+ private readonly IEnumerable<Func<T, TValueColumn>> selectors;
30+ private readonly int f;
31+
32+ public SortableSynchronizedCoupleViewMultiValueComparer(IEnumerable<Func<T, TValueColumn>> selectors, bool ascending = true)
33+ {
34+ this.selectors = selectors;
35+ this.f = ascending ? 1 : -1;
36+ }
37+
38+ public int Compare((TKey, T, TView) x, (TKey, T, TView) y)
39+ {
40+ foreach (var selector in selectors)
41+ {
42+ int c = Comparer<TValueColumn>.Default.Compare(selector(x.Item2), selector(y.Item2)) * f;
43+
44+ if (c != 0)
45+ {
46+ return c;
47+ }
48+ }
49+
50+ return Comparer<TKey>.Default.Compare(x.Item1, y.Item1) * f;
51+ }
52+ }
53+
54+ public class SortableSynchronizedCoupleViewViewComparer<TKey, T, TView, TViewColumn> : IComparer<(TKey, T, TView)>
55+ {
56+ private readonly Func<TView, TViewColumn> selector;
57+ private readonly int f;
58+
59+ public SortableSynchronizedCoupleViewViewComparer(Func<TView, TViewColumn> selector, bool ascending = true)
60+ {
61+ this.selector = selector;
62+ this.f = ascending ? 1 : -1;
63+ }
64+
65+ public int Compare((TKey, T, TView) x, (TKey, T, TView) y)
66+ {
67+ int c = Comparer<TViewColumn>.Default.Compare(selector(x.Item3), selector(y.Item3)) * f;
68+
69+ if (c == 0)
70+ {
71+ c = Comparer<TKey>.Default.Compare(x.Item1, y.Item1) * f;
72+ }
73+
74+ return c;
75+ }
76+ }
77+
78+ public class SortableSynchronizedCoupleViewMultiViewComparer<TKey, T, TView, TViewColumn> : IComparer<(TKey, T, TView)>
79+ {
80+ private readonly IEnumerable<Func<TView, TViewColumn>> selectors;
81+ private readonly int f;
82+
83+ public SortableSynchronizedCoupleViewMultiViewComparer(IEnumerable<Func<TView, TViewColumn>> selectors, bool ascending = true)
84+ {
85+ this.selectors = selectors;
86+ this.f = ascending ? 1 : -1;
87+ }
88+
89+ public int Compare((TKey, T, TView) x, (TKey, T, TView) y)
90+ {
91+ foreach (var selector in selectors)
92+ {
93+ int c = Comparer<TViewColumn>.Default.Compare(selector(x.Item3), selector(y.Item3)) * f;
94+
95+ if (c != 0)
96+ {
97+ return c;
98+ }
99+ }
100+
101+ return Comparer<TKey>.Default.Compare(x.Item1, y.Item1) * f;
102+ }
103+ }
104+
105+}