From c0c9cd48d7b4efbe4201016ac5f5a6ed4192be35 Mon Sep 17 00:00:00 2001 From: neuecc Date: Mon, 2 Sep 2024 19:31:29 +0900 Subject: [PATCH] replace SynchronizedViewList backed uses AlternateIndexList --- .../AlternateIndexList.cs | 222 ++++++ .../Internal/AlternateIndexList.cs | 211 ----- .../Internal/RemoveAllMatcher.cs | 30 - src/ObservableCollections/ObservableList.cs | 13 +- .../SynchronizedViewList.cs | 738 +++++++++--------- .../AlternateIndexListTest.cs | 13 + 6 files changed, 602 insertions(+), 625 deletions(-) create mode 100644 src/ObservableCollections/AlternateIndexList.cs delete mode 100644 src/ObservableCollections/Internal/AlternateIndexList.cs delete mode 100644 src/ObservableCollections/Internal/RemoveAllMatcher.cs diff --git a/src/ObservableCollections/AlternateIndexList.cs b/src/ObservableCollections/AlternateIndexList.cs new file mode 100644 index 0000000..964905b --- /dev/null +++ b/src/ObservableCollections/AlternateIndexList.cs @@ -0,0 +1,222 @@ +#pragma warning disable CS0436 + +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Runtime.InteropServices; + +namespace ObservableCollections; + +public class AlternateIndexList : IEnumerable +{ + List list; // alternate index is ordered + + public AlternateIndexList() + { + this.list = new(); + } + + public AlternateIndexList(IEnumerable<(int OrderedAlternateIndex, T Value)> values) + { + this.list = values.Select(x => new IndexedValue(x.OrderedAlternateIndex, x.Value)).ToList(); + } + + void UpdateAlternateIndex(int startIndex, int incr) + { + var span = CollectionsMarshal.AsSpan(list); + for (int i = startIndex; i < span.Length; i++) + { + span[i].AlternateIndex += incr; + } + } + + public T this[int index] + { + get => list[index].Value; + } + + public int Count => list.Count; + + public void Insert(int alternateIndex, T value) + { + var index = list.BinarySearch(alternateIndex); + if (index < 0) + { + index = ~index; + } + list.Insert(index, new(alternateIndex, value)); + UpdateAlternateIndex(index + 1, 1); + } + + public void InsertRange(int startingAlternateIndex, IEnumerable values) + { + var index = list.BinarySearch(startingAlternateIndex); + if (index < 0) + { + index = ~index; + } + + using var iter = new InsertIterator(startingAlternateIndex, values); + list.InsertRange(index, iter); + UpdateAlternateIndex(index + iter.ConsumedCount, iter.ConsumedCount); + } + + public void Remove(T value) + { + var index = list.FindIndex(x => EqualityComparer.Default.Equals(x.Value, value)); + if (index != -1) + { + list.RemoveAt(index); + UpdateAlternateIndex(index, -1); + } + } + + public void RemoveAt(int alternateIndex) + { + var index = list.BinarySearch(alternateIndex); + if (index != -1) + { + list.RemoveAt(index); + UpdateAlternateIndex(index, -1); + } + } + + public void RemoveRange(int alternateIndex, int count) + { + var index = list.BinarySearch(alternateIndex); + if (index < 0) + { + index = ~index; + } + + list.RemoveRange(index, count); + UpdateAlternateIndex(index, -count); + } + + public bool TryGetAtAlternateIndex(int alternateIndex, [MaybeNullWhen(true)] out T value) + { + var index = list.BinarySearch(alternateIndex); + if (index < 0) + { + value = default!; + return false; + } + value = list[index].Value!; + return true; + } + + public bool TrySetAtAlternateIndex(int alternateIndex, T value) + { + var index = list.BinarySearch(alternateIndex); + if (index < 0) + { + return false; + } + CollectionsMarshal.AsSpan(list)[index].Value = value; + return true; + } + + public bool TryReplaceByValue(T searchValue, T replaceValue) + { + var index = list.FindIndex(x => EqualityComparer.Default.Equals(x.Value, searchValue)); + if (index != -1) + { + CollectionsMarshal.AsSpan(list)[index].Value = replaceValue; + return true; + } + return false; + } + + public void Clear() + { + list.Clear(); + } + + public void Clear(IEnumerable<(int OrderedAlternateIndex, T Value)> values) + { + list.Clear(); + list.AddRange(values.Select(x => new IndexedValue(x.OrderedAlternateIndex, x.Value))); + } + + public IEnumerator GetEnumerator() + { + foreach (var item in list) + { + yield return item.Value; + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public IEnumerable<(int AlternateIndex, T Value)> GetIndexedValues() + { + foreach (var item in list) + { + yield return (item.AlternateIndex, item.Value); + } + } + + class InsertIterator(int startingIndex, IEnumerable values) : IEnumerable, IEnumerator + { + IEnumerator iter = values.GetEnumerator(); + IndexedValue current; + + public int ConsumedCount { get; private set; } + + public IndexedValue Current => current; + + object IEnumerator.Current => Current; + + public void Dispose() => iter.Reset(); + + public bool MoveNext() + { + if (iter.MoveNext()) + { + ConsumedCount++; + current = new(startingIndex++, iter.Current); + return true; + } + return false; + } + + public void Reset() => iter.Reset(); + + public IEnumerator GetEnumerator() => this; + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } + + struct IndexedValue : IComparable + { + public int AlternateIndex; // mutable + public T Value; // mutable + + public IndexedValue(int alternateIndex, T value) + { + this.AlternateIndex = alternateIndex; + this.Value = value; + } + + public static implicit operator IndexedValue(int alternateIndex) // for query + { + return new IndexedValue(alternateIndex, default!); + } + + public int CompareTo(IndexedValue other) + { + return AlternateIndex.CompareTo(other.AlternateIndex); + } + + public override string ToString() + { + return (AlternateIndex, Value).ToString(); + } + } +} diff --git a/src/ObservableCollections/Internal/AlternateIndexList.cs b/src/ObservableCollections/Internal/AlternateIndexList.cs deleted file mode 100644 index 6c276fa..0000000 --- a/src/ObservableCollections/Internal/AlternateIndexList.cs +++ /dev/null @@ -1,211 +0,0 @@ -#pragma warning disable CS0436 - -using System; -using System.Collections; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Runtime.InteropServices; - -namespace ObservableCollections.Internal -{ - public class AlternateIndexList : IEnumerable - { - List list; // alternate index is ordered - - public AlternateIndexList() - { - this.list = new(); - } - - public AlternateIndexList(IEnumerable<(int OrderedAlternateIndex, T Value)> values) - { - this.list = values.Select(x => new IndexedValue(x.OrderedAlternateIndex, x.Value)).ToList(); - } - - void UpdateAlternateIndex(int startIndex, int incr) - { - var span = CollectionsMarshal.AsSpan(list); - for (int i = startIndex; i < span.Length; i++) - { - span[i].AlternateIndex += incr; - } - } - - public T this[int index] - { - get => list[index].Value; - } - - public int Count => list.Count; - - public void Insert(int alternateIndex, T value) - { - var index = list.BinarySearch(alternateIndex); - if (index < 0) - { - index = ~index; - } - list.Insert(index, new(alternateIndex, value)); - UpdateAlternateIndex(index + 1, 1); - } - - public void InsertRange(int startingAlternateIndex, IEnumerable values) - { - var index = list.BinarySearch(startingAlternateIndex); - if (index < 0) - { - index = ~index; - } - - using var iter = new InsertIterator(startingAlternateIndex, values); - list.InsertRange(index, iter); - UpdateAlternateIndex(index + iter.ConsumedCount, iter.ConsumedCount); - } - - public void Remove(T value) - { - var index = list.FindIndex(x => EqualityComparer.Default.Equals(x.Value, value)); - if (index != -1) - { - list.RemoveAt(index); - UpdateAlternateIndex(index, -1); - } - } - - public void RemoveAt(int alternateIndex) - { - var index = list.BinarySearch(alternateIndex); - if (index != -1) - { - list.RemoveAt(index); - UpdateAlternateIndex(index, -1); - } - } - - public void RemoveRange(int alternateIndex, int count) - { - var index = list.BinarySearch(alternateIndex); - if (index < 0) - { - index = ~index; - } - - list.RemoveRange(index, count); - UpdateAlternateIndex(index, -count); - } - - public bool TryGetAtAlternateIndex(int alternateIndex, [MaybeNullWhen(true)] out T value) - { - var index = list.BinarySearch(alternateIndex); - if (index < 0) - { - value = default!; - return false; - } - value = list[index].Value!; - return true; - } - - public bool TrySetAtAlternateIndex(int alternateIndex, T value) - { - var index = list.BinarySearch(alternateIndex); - if (index < 0) - { - return false; - } - CollectionsMarshal.AsSpan(list)[index].Value = value; - return true; - } - - public void Clear() - { - list.Clear(); - } - - // Can't implement add, reverse and sort because alternate index is unknown - // void Add(); - // void AddRange(); - // void Reverse(); - // void Sort(); - - public IEnumerator GetEnumerator() - { - foreach (var item in list) - { - yield return item.Value; - } - } - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - - public IEnumerable<(int AlternateIndex, T Value)> GetIndexedValues() - { - foreach (var item in list) - { - yield return (item.AlternateIndex, item.Value); - } - } - - class InsertIterator(int startingIndex, IEnumerable values) : IEnumerable, IEnumerator - { - IEnumerator iter = values.GetEnumerator(); - IndexedValue current; - - public int ConsumedCount { get; private set; } - - public IndexedValue Current => current; - - object IEnumerator.Current => Current; - - public void Dispose() => iter.Reset(); - - public bool MoveNext() - { - if (iter.MoveNext()) - { - ConsumedCount++; - current = new(startingIndex++, iter.Current); - return true; - } - return false; - } - - public void Reset() => iter.Reset(); - - public IEnumerator GetEnumerator() => this; - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - } - - struct IndexedValue : IComparable - { - public int AlternateIndex; // mutable - public T Value; // mutable - - public IndexedValue(int alternateIndex, T value) - { - this.AlternateIndex = alternateIndex; - this.Value = value; - } - - public static implicit operator IndexedValue(int alternateIndex) // for query - { - return new IndexedValue(alternateIndex, default!); - } - - public int CompareTo(IndexedValue other) - { - return AlternateIndex.CompareTo(other.AlternateIndex); - } - - public override string ToString() - { - return (AlternateIndex, Value).ToString(); - } - } - } -} diff --git a/src/ObservableCollections/Internal/RemoveAllMatcher.cs b/src/ObservableCollections/Internal/RemoveAllMatcher.cs deleted file mode 100644 index ecd81a7..0000000 --- a/src/ObservableCollections/Internal/RemoveAllMatcher.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace ObservableCollections.Internal -{ - internal class RemoveAllMatcher - { - readonly HashSet hashSet; - - public RemoveAllMatcher(ReadOnlySpan source) - { -#if !NETSTANDARD2_0 - var set = new HashSet(capacity: source.Length); -#else - var set = new HashSet(); -#endif - foreach (var item in source) - { - set.Add(item); - } - - this.hashSet = set; - } - - public bool Predicate(T value) - { - return hashSet.Contains(value); - } - } -} \ No newline at end of file diff --git a/src/ObservableCollections/ObservableList.cs b/src/ObservableCollections/ObservableList.cs index bf9e950..a7d325c 100644 --- a/src/ObservableCollections/ObservableList.cs +++ b/src/ObservableCollections/ObservableList.cs @@ -100,11 +100,16 @@ namespace ObservableCollections { lock (SyncRoot) { - var index = list.Count; + var index = list.Count; // starting index + +#if NET8_0_OR_GREATER + list.AddRange(items); +#else foreach (var item in items) { list.Add(item); } +#endif CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs.Add(items, index)); } @@ -204,11 +209,16 @@ namespace ObservableCollections { lock (SyncRoot) { +#if NET8_0_OR_GREATER + list.InsertRange(index, items); + CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs.Add(items, index)); +#else using (var xs = new CloneCollection(items)) { list.InsertRange(index, xs.AsEnumerable()); CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs.Add(xs.Span, index)); } +#endif } } @@ -304,7 +314,6 @@ namespace ObservableCollections } } - public void Reverse(int index, int count) { lock (SyncRoot) diff --git a/src/ObservableCollections/SynchronizedViewList.cs b/src/ObservableCollections/SynchronizedViewList.cs index 5e4fd7e..ab1b445 100644 --- a/src/ObservableCollections/SynchronizedViewList.cs +++ b/src/ObservableCollections/SynchronizedViewList.cs @@ -7,442 +7,416 @@ using System.ComponentModel; using System.Linq; using System.Runtime.InteropServices; -namespace ObservableCollections +namespace ObservableCollections; + +internal class SynchronizedViewList : ISynchronizedViewList { + readonly ISynchronizedView parent; + protected readonly AlternateIndexList listView; + protected readonly object gate = new object(); - internal class SynchronizedViewList : ISynchronizedViewList + public SynchronizedViewList(ISynchronizedView parent) { - readonly ISynchronizedView parent; - protected readonly List listView; - protected readonly object gate = new object(); - - public SynchronizedViewList(ISynchronizedView parent) + this.parent = parent; + lock (parent.SyncRoot) { - this.parent = parent; - lock (parent.SyncRoot) + listView = new AlternateIndexList(IterateFilteredIndexedViewsOfParent()); + parent.ViewChanged += Parent_ViewChanged; + } + } + + IEnumerable<(int, TView)> IterateFilteredIndexedViewsOfParent() + { + var filter = parent.Filter; + var index = 0; + if (filter.IsNullFilter()) + { + foreach (var item in parent.Unfiltered) // use Unfiltered { - listView = parent.ToList(); - parent.ViewChanged += Parent_ViewChanged; + yield return (index, item.View); + index++; } } - - private void Parent_ViewChanged(in SynchronizedViewChangedEventArgs e) + else { - lock (gate) + foreach (var item in parent.Unfiltered) // use Unfiltered { - switch (e.Action) + if (filter.IsMatch(item.Value)) { - case NotifyCollectionChangedAction.Add: // Add or Insert - if (e.IsSingleItem) - { - if (e.NewStartingIndex == -1) - { - listView.Add(e.NewItem.View); - } - else - { - listView.Insert(e.NewStartingIndex, e.NewItem.View); - } - } - else - { - if (e.NewStartingIndex == -1) - { - listView.AddRange(e.NewViews); - } - else - { - // fnew SortedList().sort + yield return (index, item.View); + } + index++; + } + } + } - listView.InsertRange(e.NewStartingIndex, e.NewViews); - } - } - break; - case NotifyCollectionChangedAction.Remove: // Remove - if (e.IsSingleItem) + private void Parent_ViewChanged(in SynchronizedViewChangedEventArgs e) + { + // event is called inside parent lock + lock (gate) + { + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: // Add or Insert + if (e.IsSingleItem) + { + listView.Insert(e.NewStartingIndex, e.NewItem.View); + } + else + { + using var array = new CloneCollection(e.NewViews); + listView.InsertRange(e.NewStartingIndex, array.AsEnumerable()); + } + break; + case NotifyCollectionChangedAction.Remove: // Remove + if (e.IsSingleItem) + { + if (e.OldStartingIndex == -1) // can't gurantee correct remove if index is not provided { - if (e.OldStartingIndex == -1) // can't gurantee correct remove if index is not provided - { - listView.Remove(e.OldItem.View); - } - else - { - listView.RemoveAt(e.OldStartingIndex); - } - } - else - { - if (e.OldStartingIndex == -1) - { - var matcher = new RemoveAllMatcher(e.OldViews); - listView.RemoveAll(matcher.Predicate); - } - else - { - listView.RemoveRange(e.OldStartingIndex, e.OldViews.Length); - } - } - - - break; - case NotifyCollectionChangedAction.Replace: // Indexer - if (e.NewStartingIndex == -1) - { - var index = listView.IndexOf(e.OldItem.View); - listView[index] = e.NewItem.View; - } - else - { - listView[e.NewStartingIndex] = e.NewItem.View; - } - - break; - case NotifyCollectionChangedAction.Move: //Remove and Insert - if (e.NewStartingIndex == -1) - { - // do nothing + listView.Remove(e.OldItem.View); } else { listView.RemoveAt(e.OldStartingIndex); - listView.Insert(e.NewStartingIndex, e.NewItem.View); } - break; - case NotifyCollectionChangedAction.Reset: // Clear or drastic changes - if (e.SortOperation.IsNull) + } + else + { + if (e.OldStartingIndex == -1) { - listView.Clear(); - foreach (var item in parent) // refresh + foreach (var view in e.OldViews) // index is unknown, can't do batching { - listView.Add(item); + listView.Remove(view); } } - else if (e.SortOperation.IsReverse) - { - listView.Reverse(); - } else { -#if NET6_0_OR_GREATER -#pragma warning disable CS0436 - if (parent is ObservableList.View observableListView) - { - var comparer = new ObservableList.View.IgnoreViewComparer(e.SortOperation.Comparer ?? Comparer.Default); - var viewSpan = CollectionsMarshal.AsSpan(listView).Slice(e.SortOperation.Index, e.SortOperation.Count); - var sourceSpan = CollectionsMarshal.AsSpan(observableListView.list).Slice(e.SortOperation.Index, e.SortOperation.Count); - sourceSpan.Sort(viewSpan, comparer); // span.Sort is NET6 or greater - } - else -#pragma warning restore CS0436 -#endif - { - // can not get source Span, do Clear and Refresh - listView.Clear(); - foreach (var item in parent) - { - listView.Add(item); - } - } + listView.RemoveRange(e.OldStartingIndex, e.OldViews.Length); } - break; - default: - break; - } + } + break; + case NotifyCollectionChangedAction.Replace: // Indexer + if (e.NewStartingIndex == -1) + { + listView.TryReplaceByValue(e.OldItem.View, e.NewItem.View); + } + else + { + listView.TrySetAtAlternateIndex(e.NewStartingIndex, e.NewItem.View); + } - OnCollectionChanged(e); + break; + case NotifyCollectionChangedAction.Move: //Remove and Insert + if (e.NewStartingIndex == -1) + { + // do nothing + } + else + { + listView.RemoveAt(e.OldStartingIndex); + listView.Insert(e.NewStartingIndex, e.NewItem.View); + } + break; + case NotifyCollectionChangedAction.Reset: // Clear or drastic changes + listView.Clear(IterateFilteredIndexedViewsOfParent()); // clear and fill refresh + break; + default: + break; } - } - protected virtual void OnCollectionChanged(in SynchronizedViewChangedEventArgs args) - { - } - - public TView this[int index] - { - get - { - lock (gate) - { - return listView[index]; - } - } - } - - public int Count - { - get - { - lock (gate) - { - return listView.Count; - } - } - } - - public IEnumerator GetEnumerator() - { - lock (gate) - { - foreach (var item in listView) - { - yield return item; - } - } - } - - IEnumerator IEnumerable.GetEnumerator() - { - return listView.GetEnumerator(); - } - - public void Dispose() - { - parent.ViewChanged -= Parent_ViewChanged; + OnCollectionChanged(e); } } - internal class NotifyCollectionChangedSynchronizedView : - SynchronizedViewList, - INotifyCollectionChangedSynchronizedView, - IList, IList + protected virtual void OnCollectionChanged(in SynchronizedViewChangedEventArgs args) { - static readonly PropertyChangedEventArgs CountPropertyChangedEventArgs = new("Count"); - static readonly Action raiseChangedEventInvoke = RaiseChangedEvent; + } - readonly ICollectionEventDispatcher eventDispatcher; - - public event NotifyCollectionChangedEventHandler? CollectionChanged; - public event PropertyChangedEventHandler? PropertyChanged; - - public NotifyCollectionChangedSynchronizedView(ISynchronizedView parent, ICollectionEventDispatcher? eventDispatcher) - : base(parent) + public TView this[int index] + { + get { - this.eventDispatcher = eventDispatcher ?? InlineCollectionEventDispatcher.Instance; - } - - protected override void OnCollectionChanged(in SynchronizedViewChangedEventArgs args) - { - if (CollectionChanged == null && PropertyChanged == null) return; - - switch (args.Action) + lock (gate) { - case NotifyCollectionChangedAction.Add: - if (args.IsSingleItem) - { - eventDispatcher.Post(new CollectionEventDispatcherEventArgs(NotifyCollectionChangedAction.Add, args.NewItem.View, args.NewStartingIndex) - { - Collection = this, - Invoker = raiseChangedEventInvoke, - IsInvokeCollectionChanged = true, - IsInvokePropertyChanged = true - }); - } - else - { - eventDispatcher.Post(new CollectionEventDispatcherEventArgs(NotifyCollectionChangedAction.Add, args.NewViews.ToArray(), args.NewStartingIndex) - { - Collection = this, - Invoker = raiseChangedEventInvoke, - IsInvokeCollectionChanged = true, - IsInvokePropertyChanged = true - }); - } - break; - case NotifyCollectionChangedAction.Remove: - if (args.IsSingleItem) - { - eventDispatcher.Post(new CollectionEventDispatcherEventArgs(NotifyCollectionChangedAction.Remove, args.OldItem.View, args.OldStartingIndex) - { - Collection = this, - Invoker = raiseChangedEventInvoke, - IsInvokeCollectionChanged = true, - IsInvokePropertyChanged = true - }); - } - else - { - eventDispatcher.Post(new CollectionEventDispatcherEventArgs(NotifyCollectionChangedAction.Remove, args.OldViews.ToArray(), args.OldStartingIndex) - { - Collection = this, - Invoker = raiseChangedEventInvoke, - IsInvokeCollectionChanged = true, - IsInvokePropertyChanged = true - }); - } - break; - case NotifyCollectionChangedAction.Reset: - eventDispatcher.Post(new CollectionEventDispatcherEventArgs(NotifyCollectionChangedAction.Reset) + return listView[index]; + } + } + } + + public int Count + { + get + { + lock (gate) + { + return listView.Count; + } + } + } + + public IEnumerator GetEnumerator() + { + lock (gate) + { + foreach (var item in listView) + { + yield return item; + } + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return listView.GetEnumerator(); + } + + public void Dispose() + { + parent.ViewChanged -= Parent_ViewChanged; + } +} + +internal class NotifyCollectionChangedSynchronizedView : + SynchronizedViewList, + INotifyCollectionChangedSynchronizedView, + IList, IList +{ + static readonly PropertyChangedEventArgs CountPropertyChangedEventArgs = new("Count"); + static readonly Action raiseChangedEventInvoke = RaiseChangedEvent; + + readonly ICollectionEventDispatcher eventDispatcher; + + public event NotifyCollectionChangedEventHandler? CollectionChanged; + public event PropertyChangedEventHandler? PropertyChanged; + + public NotifyCollectionChangedSynchronizedView(ISynchronizedView parent, ICollectionEventDispatcher? eventDispatcher) + : base(parent) + { + this.eventDispatcher = eventDispatcher ?? InlineCollectionEventDispatcher.Instance; + } + + protected override void OnCollectionChanged(in SynchronizedViewChangedEventArgs args) + { + if (CollectionChanged == null && PropertyChanged == null) return; + + switch (args.Action) + { + case NotifyCollectionChangedAction.Add: + if (args.IsSingleItem) + { + eventDispatcher.Post(new CollectionEventDispatcherEventArgs(NotifyCollectionChangedAction.Add, args.NewItem.View, args.NewStartingIndex) { Collection = this, Invoker = raiseChangedEventInvoke, IsInvokeCollectionChanged = true, IsInvokePropertyChanged = true }); - break; - case NotifyCollectionChangedAction.Replace: - eventDispatcher.Post(new CollectionEventDispatcherEventArgs(NotifyCollectionChangedAction.Replace, args.NewItem.View, args.OldItem.View, args.NewStartingIndex) + } + else + { + eventDispatcher.Post(new CollectionEventDispatcherEventArgs(NotifyCollectionChangedAction.Add, args.NewViews.ToArray(), args.NewStartingIndex) { Collection = this, Invoker = raiseChangedEventInvoke, IsInvokeCollectionChanged = true, - IsInvokePropertyChanged = false + IsInvokePropertyChanged = true }); - break; - case NotifyCollectionChangedAction.Move: - eventDispatcher.Post(new CollectionEventDispatcherEventArgs(NotifyCollectionChangedAction.Move, args.NewItem.View, args.NewStartingIndex, args.OldStartingIndex) + } + break; + case NotifyCollectionChangedAction.Remove: + if (args.IsSingleItem) + { + eventDispatcher.Post(new CollectionEventDispatcherEventArgs(NotifyCollectionChangedAction.Remove, args.OldItem.View, args.OldStartingIndex) { Collection = this, Invoker = raiseChangedEventInvoke, IsInvokeCollectionChanged = true, - IsInvokePropertyChanged = false + IsInvokePropertyChanged = true }); - break; - } - } - - static void RaiseChangedEvent(NotifyCollectionChangedEventArgs e) - { - var e2 = (CollectionEventDispatcherEventArgs)e; - var self = (NotifyCollectionChangedSynchronizedView)e2.Collection; - - if (e2.IsInvokeCollectionChanged) - { - self.CollectionChanged?.Invoke(self, e); - } - if (e2.IsInvokePropertyChanged) - { - self.PropertyChanged?.Invoke(self, CountPropertyChangedEventArgs); - } - } - - // IList, IList implementation - - TView IList.this[int index] - { - get => ((IReadOnlyList)this)[index]; - set => throw new NotSupportedException(); - } - - object? IList.this[int index] - { - get - { - return this[index]; - } - set => throw new NotSupportedException(); - } - - static bool IsCompatibleObject(object? value) - { - return value is T || value == null && default(T) == null; - } - - public bool IsReadOnly => true; - - public bool IsFixedSize => false; - - public bool IsSynchronized => true; - - public object SyncRoot => gate; - - public void Add(TView item) - { - throw new NotSupportedException(); - } - - public int Add(object? value) - { - throw new NotImplementedException(); - } - - public void Clear() - { - throw new NotSupportedException(); - } - - public bool Contains(TView item) - { - lock (gate) - { - foreach (var listItem in listView) - { - if (EqualityComparer.Default.Equals(listItem, item)) - { - return true; - } } - } - return false; - } - - public bool Contains(object? value) - { - if (IsCompatibleObject(value)) - { - return Contains((TView)value!); - } - return false; - } - - public void CopyTo(TView[] array, int arrayIndex) - { - throw new NotSupportedException(); - } - - public void CopyTo(Array array, int index) - { - throw new NotImplementedException(); - } - - public int IndexOf(TView item) - { - lock (gate) - { - var index = 0; - foreach (var listItem in listView) + else { - if (EqualityComparer.Default.Equals(listItem, item)) + eventDispatcher.Post(new CollectionEventDispatcherEventArgs(NotifyCollectionChangedAction.Remove, args.OldViews.ToArray(), args.OldStartingIndex) { - return index; - } - index++; + Collection = this, + Invoker = raiseChangedEventInvoke, + IsInvokeCollectionChanged = true, + IsInvokePropertyChanged = true + }); } - } - return -1; - } - - public int IndexOf(object? item) - { - if (IsCompatibleObject(item)) - { - return IndexOf((TView)item!); - } - return -1; - } - - public void Insert(int index, TView item) - { - throw new NotSupportedException(); - } - - public void Insert(int index, object? value) - { - throw new NotImplementedException(); - } - - public bool Remove(TView item) - { - throw new NotSupportedException(); - } - - public void Remove(object? value) - { - throw new NotImplementedException(); - } - - public void RemoveAt(int index) - { - throw new NotSupportedException(); + break; + case NotifyCollectionChangedAction.Reset: + eventDispatcher.Post(new CollectionEventDispatcherEventArgs(NotifyCollectionChangedAction.Reset) + { + Collection = this, + Invoker = raiseChangedEventInvoke, + IsInvokeCollectionChanged = true, + IsInvokePropertyChanged = true + }); + break; + case NotifyCollectionChangedAction.Replace: + eventDispatcher.Post(new CollectionEventDispatcherEventArgs(NotifyCollectionChangedAction.Replace, args.NewItem.View, args.OldItem.View, args.NewStartingIndex) + { + Collection = this, + Invoker = raiseChangedEventInvoke, + IsInvokeCollectionChanged = true, + IsInvokePropertyChanged = false + }); + break; + case NotifyCollectionChangedAction.Move: + eventDispatcher.Post(new CollectionEventDispatcherEventArgs(NotifyCollectionChangedAction.Move, args.NewItem.View, args.NewStartingIndex, args.OldStartingIndex) + { + Collection = this, + Invoker = raiseChangedEventInvoke, + IsInvokeCollectionChanged = true, + IsInvokePropertyChanged = false + }); + break; } } + + static void RaiseChangedEvent(NotifyCollectionChangedEventArgs e) + { + var e2 = (CollectionEventDispatcherEventArgs)e; + var self = (NotifyCollectionChangedSynchronizedView)e2.Collection; + + if (e2.IsInvokeCollectionChanged) + { + self.CollectionChanged?.Invoke(self, e); + } + if (e2.IsInvokePropertyChanged) + { + self.PropertyChanged?.Invoke(self, CountPropertyChangedEventArgs); + } + } + + // IList, IList implementation + + TView IList.this[int index] + { + get => ((IReadOnlyList)this)[index]; + set => throw new NotSupportedException(); + } + + object? IList.this[int index] + { + get + { + return this[index]; + } + set => throw new NotSupportedException(); + } + + static bool IsCompatibleObject(object? value) + { + return value is T || value == null && default(T) == null; + } + + public bool IsReadOnly => true; + + public bool IsFixedSize => false; + + public bool IsSynchronized => true; + + public object SyncRoot => gate; + + public void Add(TView item) + { + throw new NotSupportedException(); + } + + public int Add(object? value) + { + throw new NotImplementedException(); + } + + public void Clear() + { + throw new NotSupportedException(); + } + + public bool Contains(TView item) + { + lock (gate) + { + foreach (var listItem in listView) + { + if (EqualityComparer.Default.Equals(listItem, item)) + { + return true; + } + } + } + return false; + } + + public bool Contains(object? value) + { + if (IsCompatibleObject(value)) + { + return Contains((TView)value!); + } + return false; + } + + public void CopyTo(TView[] array, int arrayIndex) + { + throw new NotSupportedException(); + } + + public void CopyTo(Array array, int index) + { + throw new NotImplementedException(); + } + + public int IndexOf(TView item) + { + lock (gate) + { + var index = 0; + foreach (var listItem in listView) + { + if (EqualityComparer.Default.Equals(listItem, item)) + { + return index; + } + index++; + } + } + return -1; + } + + public int IndexOf(object? item) + { + if (IsCompatibleObject(item)) + { + return IndexOf((TView)item!); + } + return -1; + } + + public void Insert(int index, TView item) + { + throw new NotSupportedException(); + } + + public void Insert(int index, object? value) + { + throw new NotImplementedException(); + } + + public bool Remove(TView item) + { + throw new NotSupportedException(); + } + + public void Remove(object? value) + { + throw new NotImplementedException(); + } + + public void RemoveAt(int index) + { + throw new NotSupportedException(); + } } \ No newline at end of file diff --git a/tests/ObservableCollections.Tests/AlternateIndexListTest.cs b/tests/ObservableCollections.Tests/AlternateIndexListTest.cs index 28eda2f..52b00c4 100644 --- a/tests/ObservableCollections.Tests/AlternateIndexListTest.cs +++ b/tests/ObservableCollections.Tests/AlternateIndexListTest.cs @@ -97,4 +97,17 @@ public class AlternateIndexListTest list.TryGetAtAlternateIndex(4, out var baz).Should().BeTrue(); baz.Should().Be("new-baz"); } + + [Fact] + public void TryReplaceByValue() + { + var list = new AlternateIndexList(); + + list.Insert(0, "foo"); + list.Insert(2, "bar"); + list.Insert(4, "baz"); + + list.TryReplaceByValue("bar", "new-bar"); + list.GetIndexedValues().Should().Equal((0, "foo"), (2, "new-bar"), (4, "baz")); + } }