Cysharp/ObservableCollections をベースにしたUI表示用の同期ビュー
Revision | 8b29735bcf886a9b4ce5eb5ddbd27736eb67ba7e (tree) |
---|---|
Zeit | 2022-08-25 03:15:30 |
Autor | yoshy <yoshy.org.bitbucket@gz.j...> |
Commiter | yoshy |
initial revision
@@ -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> |
@@ -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 | +} |
@@ -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 | +} |
@@ -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 | +} |
@@ -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 |
@@ -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 |
@@ -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 | +} |
@@ -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 | +} |
@@ -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 |
@@ -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 | +} |
@@ -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 |
@@ -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 | +} |
@@ -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 | +} |
@@ -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 | +} |