diff --git a/src/libraries/System.ObjectModel/src/System/Collections/ObjectModel/ObservableCollection.cs b/src/libraries/System.ObjectModel/src/System/Collections/ObjectModel/ObservableCollection.cs index 48109cfa49e194..95c802eaac121e 100644 --- a/src/libraries/System.ObjectModel/src/System/Collections/ObjectModel/ObservableCollection.cs +++ b/src/libraries/System.ObjectModel/src/System/Collections/ObjectModel/ObservableCollection.cs @@ -26,6 +26,15 @@ public class ObservableCollection : Collection, INotifyCollectionChanged, [NonSerialized] private int _blockReentrancyCount; + [NonSerialized] + private bool _skipRaisingEvents; + + /// + /// true to opt into raising with list + /// of items when a range is inserted, removed or replaced. Instead of resets + /// + private static bool RaiseBatchCollectionChangedEvents => false; + /// /// Initializes a new instance of ObservableCollection that is empty and has default initial capacity. /// @@ -107,9 +116,116 @@ protected override void RemoveItem(int index) base.RemoveItem(index); - OnCountPropertyChanged(); - OnIndexerPropertyChanged(); - OnCollectionChanged(NotifyCollectionChangedAction.Remove, removedItem, index); + if (!_skipRaisingEvents) + { + OnCountPropertyChanged(); + OnIndexerPropertyChanged(); + OnCollectionChanged(NotifyCollectionChangedAction.Remove, removedItem, index); + } + } + + /// + /// Called by base class Collection<T> when a count of items is removed from the list; + /// raises a CollectionChanged event to any listeners. + /// + protected override void RemoveItemsRange(int index, int count) + { + CheckReentrancy(); + + NotifyCollectionChangedEventArgs collectionChangedEventArgs = EventArgsCache.ResetCollectionChanged; + bool skipEvents = _skipRaisingEvents; + if (!skipEvents) + { + _skipRaisingEvents = true; + + if (RaiseBatchCollectionChangedEvents && count > 0 && CollectionChanged is not null) + { + T[] removedItems = new T[count]; + for (int i = 0; i < count; i++) + { + removedItems[i] = this[index + i]; + } + + collectionChangedEventArgs = new(NotifyCollectionChangedAction.Remove, removedItems, index); + } + } + + try + { + base.RemoveItemsRange(index, count); + } + finally + { + if (!skipEvents) + { + _skipRaisingEvents = false; + } + } + + if (count > 0 && !_skipRaisingEvents) + { + OnCountPropertyChanged(); + OnIndexerPropertyChanged(); + OnCollectionChanged(collectionChangedEventArgs); + } + } + + /// + /// Called by base class Collection<T> when a collection of items is added to list; + /// raises a CollectionChanged event to any listeners. + /// + protected override void ReplaceItemsRange(int index, int count, IEnumerable collection) + { + CheckReentrancy(); + + int countBefore = default; + bool skipEvents = _skipRaisingEvents; + if (!skipEvents) + { + _skipRaisingEvents = true; + countBefore = Count; + } + + NotifyCollectionChangedEventArgs collectionChangedEventArgs = EventArgsCache.ResetCollectionChanged; + if (!skipEvents && RaiseBatchCollectionChangedEvents && CollectionChanged is not null) + { + T[] itemsToReplace = new T[count]; + for (int i = 0; i < count; i++) + { + itemsToReplace[i] = this[i + index]; + } + + IList newItems = collection as IList ?? new List(collection); + collectionChangedEventArgs = new(NotifyCollectionChangedAction.Replace, newItems, itemsToReplace, index); + } + + try + { + base.ReplaceItemsRange(index, count, collection); + } + finally + { + if (!skipEvents) + { + _skipRaisingEvents = false; + } + } + + if (!skipEvents) + { + if (countBefore != Count) + { + OnCountPropertyChanged(); + OnIndexerPropertyChanged(); + OnCollectionChanged(collectionChangedEventArgs); + } + else if (count > 0) + { + // We replaced positive number of items with the same number of items, only the contents changed + OnIndexerPropertyChanged(); + OnCollectionChanged(collectionChangedEventArgs); + } + } } /// @@ -121,9 +237,58 @@ protected override void InsertItem(int index, T item) CheckReentrancy(); base.InsertItem(index, item); - OnCountPropertyChanged(); - OnIndexerPropertyChanged(); - OnCollectionChanged(NotifyCollectionChangedAction.Add, item, index); + if (!_skipRaisingEvents) + { + OnCountPropertyChanged(); + OnIndexerPropertyChanged(); + OnCollectionChanged(NotifyCollectionChangedAction.Add, item, index); + } + } + + /// + /// Called by base class Collection<T> when a collection of items is added to list; + /// raises a CollectionChanged event to any listeners. + /// + protected override void InsertItemsRange(int index, IEnumerable collection) + { + CheckReentrancy(); + + int countBefore = default; + bool skipEvents = _skipRaisingEvents; + if (!skipEvents) + { + _skipRaisingEvents = true; + countBefore = Count; + } + + try + { + base.InsertItemsRange(index, collection); + } + finally + { + if (!skipEvents) + { + _skipRaisingEvents = false; + } + } + + if (!_skipRaisingEvents) + { + NotifyCollectionChangedEventArgs collectionChangedEventArgs = EventArgsCache.ResetCollectionChanged; + if (RaiseBatchCollectionChangedEvents && CollectionChanged is not null) + { + IList newItems = collection as IList ?? new List(collection); + collectionChangedEventArgs = new(NotifyCollectionChangedAction.Add, newItems, index); + } + + if (countBefore != Count) + { + OnCountPropertyChanged(); + OnIndexerPropertyChanged(); + OnCollectionChanged(collectionChangedEventArgs); + } + } } /// diff --git a/src/libraries/System.ObjectModel/tests/ObservableCollection/ObservableCollection_RangeMethodTests.cs b/src/libraries/System.ObjectModel/tests/ObservableCollection/ObservableCollection_RangeMethodTests.cs new file mode 100644 index 00000000000000..0e48394a6f6e9c --- /dev/null +++ b/src/libraries/System.ObjectModel/tests/ObservableCollection/ObservableCollection_RangeMethodTests.cs @@ -0,0 +1,360 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Specialized; +using System.Linq; +using Xunit; + +namespace System.Collections.ObjectModel.Tests +{ + /// + /// Tests the public Range modification methods in ObservableCollection as well as verifies + /// that the CollectionChanged events and eventargs are fired and populated + /// properly. + /// + public static class ObservableCollection_RangeMethodTests + { + [Fact] + public static void InsertRange_NotifyCollectionChanged_Beginning_Test() + { + int[] dataToInsert = new int[] { 1, 2, 3, 4, 5 }; + int[] initialData = new int[] { 10, 11, 12, 13 }; + int eventCounter = 0; + ObservableCollection collection = new ObservableCollection(initialData); + collection.CollectionChanged += (o, e) => eventCounter++; + + collection.InsertRange(0, dataToInsert); + + Assert.Equal(dataToInsert.Length + initialData.Length, collection.Count); + Assert.Equal(1, eventCounter); + + int[] collectionAssertion = collection.ToArray(); + Assert.Equal(dataToInsert, collectionAssertion.AsSpan(0, 5).ToArray()); + Assert.Equal(initialData, collectionAssertion.AsSpan(5).ToArray()); + } + + [Fact] + public static void InsertRange_NotifyCollectionChanged_Middle_Test() + { + int[] dataToInsert = new int[] { 1, 2, 3, 4, 5 }; + int[] initialData = new int[] { 10, 11, 12, 13 }; + int eventCounter = 0; + ObservableCollection collection = new ObservableCollection(initialData); + collection.CollectionChanged += (o, e) => eventCounter++; + + collection.InsertRange(2, dataToInsert); + + Assert.Equal(dataToInsert.Length + initialData.Length, collection.Count); + Assert.Equal(1, eventCounter); + + int[] collectionAssertion = collection.ToArray(); + Assert.Equal(initialData.AsSpan(0, 2).ToArray(), collectionAssertion.AsSpan(0, 2).ToArray()); + Assert.Equal(dataToInsert, collectionAssertion.AsSpan(2, 5).ToArray()); + Assert.Equal(initialData.AsSpan(2, 2).ToArray(), collectionAssertion.AsSpan(7, 2).ToArray()); + } + + [Fact] + public static void InsertRange_NotifyCollectionChanged_End_Test() + { + int[] dataToInsert = new int[] { 1, 2, 3, 4, 5 }; + int[] initialData = new int[] { 10, 11, 12, 13 }; + int eventCounter = 0; + ObservableCollection collection = new ObservableCollection(initialData); + collection.CollectionChanged += (o, e) => eventCounter++; + + collection.InsertRange(4, dataToInsert); + + Assert.Equal(dataToInsert.Length + initialData.Length, collection.Count); + Assert.Equal(1, eventCounter); + + int[] collectionAssertion = collection.ToArray(); + Assert.Equal(initialData, collectionAssertion.AsSpan(0, 4).ToArray()); + Assert.Equal(dataToInsert, collectionAssertion.AsSpan(4).ToArray()); + } + + [Fact] + public static void AddRange_NotifyCollectionChanged_Test() + { + int[] dataToInsert = new int[] { 1, 2, 3, 4, 5 }; + int[] initialData = new int[] { 10, 11, 12, 13 }; + int eventCounter = 0; + ObservableCollection collection = new ObservableCollection(initialData); + collection.CollectionChanged += (o, e) => eventCounter++; + + collection.AddRange(dataToInsert); + + Assert.Equal(dataToInsert.Length + initialData.Length, collection.Count); + Assert.Equal(1, eventCounter); + + int[] collectionAssertion = collection.ToArray(); + Assert.Equal(initialData, collectionAssertion.AsSpan(0, 4).ToArray()); + Assert.Equal(dataToInsert, collectionAssertion.AsSpan(4).ToArray()); + } + + [Theory] + [InlineData(true, Skip = "Reenable when AppContext switch is added to opt into multiple items in NotifyCollectionChangedEventArgs")] + [InlineData(false)] + public static void AddRange_NotifyCollectionChanged_EventArgs_Test(bool batchCollectionChanged) + { + int[] dataToAdd = new int[] { 1, 2, 3, 4, 5, 6, 7, 8 }; + ObservableCollection collection = new(); + NotifyCollectionChangedEventArgs? args = null; + + collection.CollectionChanged += (o, e) => args = e; + collection.AddRange(dataToAdd); + + Assert.NotNull(args); + if (batchCollectionChanged) + { + Assert.Equal(NotifyCollectionChangedAction.Add, args.Action); + Assert.Equal(0, args.NewStartingIndex); + Assert.Equal(dataToAdd, args.NewItems.Cast().ToArray()); + } + else + { + Assert.Equal(NotifyCollectionChangedAction.Reset, args.Action); + } + } + + [Theory] + [InlineData(true, Skip = "Reenable when AppContext switch is added to opt into multiple items in NotifyCollectionChangedEventArgs")] + [InlineData(false)] + public static void InsertRange_NotifyCollectionChanged_EventArgs_Test(bool batchCollectionChanged) + { + int[] dataToAdd = new int[] { 1, 2, 3, 4, 5, 6, 7, 8 }; + ObservableCollection collection = new(); + NotifyCollectionChangedEventArgs? args = null; + + collection.CollectionChanged += (o, e) => args = e; + collection.InsertRange(0, dataToAdd); + + Assert.NotNull(args); + if (batchCollectionChanged) + { + Assert.Equal(NotifyCollectionChangedAction.Add, args.Action); + Assert.Equal(0, args.NewStartingIndex); + Assert.Equal(dataToAdd, args.NewItems.Cast().ToArray()); + } + else + { + Assert.Equal(NotifyCollectionChangedAction.Reset, args.Action); + } + } + + [Theory] + [InlineData(true, Skip = "Reenable when AppContext switch is added to opt into multiple items in NotifyCollectionChangedEventArgs")] + [InlineData(false)] + public static void InsertRange_NotifyCollectionChanged_EventArgs_Middle_Test(bool batchCollectionChanged) + { + int[] dataToAdd = new int[] { 1, 2, 3, 4, 5, 6, 7, 8 }; + ObservableCollection collection = new ObservableCollection(); + NotifyCollectionChangedEventArgs? args = null; + + for (int i = 0; i < 4; i++) + { + collection.Add(i); + } + + collection.CollectionChanged += (o, e) => args = e; + collection.InsertRange(2, dataToAdd); + + Assert.NotNull(args); + if (batchCollectionChanged) + { + Assert.Equal(NotifyCollectionChangedAction.Add, args.Action); + Assert.Equal(2, args.NewStartingIndex); + Assert.Equal(dataToAdd, args.NewItems.Cast().ToArray()); + } + else + { + Assert.Equal(NotifyCollectionChangedAction.Reset, args.Action); + } + } + + [Theory] + [InlineData(true, Skip = "Reenable when AppContext switch is added to opt into multiple items in NotifyCollectionChangedEventArgs")] + [InlineData(false)] + public static void InsertRange_NotifyCollectionChanged_EventArgs_End_Test(bool batchCollectionChanged) + { + int[] dataToAdd = new int[] { 1, 2, 3, 4, 5, 6, 7, 8 }; + ObservableCollection collection = new ObservableCollection(); + NotifyCollectionChangedEventArgs? args = null; + + for (int i = 0; i < 4; i++) + { + collection.Add(i); + } + + collection.CollectionChanged += (o, e) => args = e; + collection.InsertRange(4, dataToAdd); + + Assert.NotNull(args); + if (batchCollectionChanged) + { + Assert.Equal(NotifyCollectionChangedAction.Add, args.Action); + Assert.Equal(4, args.NewStartingIndex); + Assert.Equal(dataToAdd, args.NewItems.Cast().ToArray()); + } + else + { + Assert.Equal(NotifyCollectionChangedAction.Reset, args.Action); + } + } + + [Fact] + public static void RemoveRange_NotifyCollectionChanged_FirstTwo_Test() + { + int[] initialData = new int[] { 10, 11, 12, 13 }; + int itemsToRemove = 2; + int eventCounter = 0; + ObservableCollection collection = new ObservableCollection(initialData); + collection.CollectionChanged += (o, e) => eventCounter++; + + collection.RemoveRange(0, itemsToRemove); + + Assert.Equal(initialData.Length - itemsToRemove, collection.Count); + Assert.Equal(1, eventCounter); + Assert.Equal(initialData.AsSpan(2).ToArray(), collection.ToArray()); + } + + [Fact] + public static void RemoveRange_NotifyCollectionChanged_MiddleTwo_Test() + { + int[] initialData = new int[] { 10, 11, 12, 13 }; + int itemsToRemove = 2; + int eventCounter = 0; + ObservableCollection collection = new ObservableCollection(initialData); + collection.CollectionChanged += (o, e) => eventCounter++; + + collection.RemoveRange(1, itemsToRemove); + + Assert.Equal(initialData.Length - itemsToRemove, collection.Count); + Assert.Equal(1, eventCounter); + Assert.Equal(initialData[0], collection[0]); + Assert.Equal(initialData[3], collection[1]); + } + + [Fact] + public static void RemoveRange_NotifyCollectionChanged_LastTwo_Test() + { + int[] initialData = new int[] { 10, 11, 12, 13 }; + int itemsToRemove = 2; + int eventCounter = 0; + ObservableCollection collection = new ObservableCollection(initialData); + collection.CollectionChanged += (o, e) => eventCounter++; + + collection.RemoveRange(2, itemsToRemove); + + Assert.Equal(initialData.Length - itemsToRemove, collection.Count); + Assert.Equal(1, eventCounter); + Assert.Equal(initialData.AsSpan(0, 2).ToArray(), collection.ToArray()); + } + + [Fact] + public static void RemoveRange_NotifyCollectionChanged_IntMaxValueOverflow_Test() + { + int count = 500; + ObservableCollection collection = new ObservableCollection(); + for (int i = 0; i < count; i++) + { + collection.Add(i); + } + + Assert.Throws(() => collection.RemoveRange(collection.Count - 2, int.MaxValue)); + } + + [Theory] + [InlineData(true, Skip = "Reenable when AppContext switch is added to opt into multiple items in NotifyCollectionChangedEventArgs")] + [InlineData(false)] + public static void RemoveRange_NotifyCollectionChanged_EventArgs_IndexOfZero_Test(bool batchCollectionChanged) + { + int[] initialData = new int[] { 1, 2, 3, 4, 5, 6, 7, 8 }; + int numberOfItemsToRemove = 4; + ObservableCollection collection = new(initialData); + NotifyCollectionChangedEventArgs? args = null; + + collection.CollectionChanged += (o, e) => args = e; + collection.RemoveRange(0, numberOfItemsToRemove); + + Assert.Equal(initialData.Length - numberOfItemsToRemove, collection.Count); + + Assert.NotNull(args); + if (batchCollectionChanged) + { + Assert.Equal(NotifyCollectionChangedAction.Remove, args.Action); + Assert.Equal(0, args.OldStartingIndex); + Assert.Equal(initialData.AsSpan(0, numberOfItemsToRemove).ToArray(), args.OldItems.Cast().ToArray()); + } + else + { + Assert.Equal(NotifyCollectionChangedAction.Reset, args.Action); + } + } + + [Theory] + [InlineData(true, Skip = "Reenable when AppContext switch is added to opt into multiple items in NotifyCollectionChangedEventArgs")] + [InlineData(false)] + public static void RemoveRange_NotifyCollectionChanged_EventArgs_IndexMiddle_Test(bool batchCollectionChanged) + { + int[] initialData = new int[] { 1, 2, 3, 4, 5, 6, 7, 8 }; + int numberOfItemsToRemove = 4; + int startIndex = 3; + + ObservableCollection collection = new(initialData); + NotifyCollectionChangedEventArgs? args = null; + + collection.CollectionChanged += (o, e) => args = e; + collection.RemoveRange(startIndex, numberOfItemsToRemove); + + Assert.Equal(initialData.Length - numberOfItemsToRemove, collection.Count); + + Assert.NotNull(args); + if (batchCollectionChanged) + { + Assert.Equal(NotifyCollectionChangedAction.Remove, args.Action); + Assert.Equal(startIndex, args.OldStartingIndex); + Assert.Equal(initialData.AsSpan(startIndex, numberOfItemsToRemove).ToArray(), args.OldItems.Cast().ToArray()); + } + else + { + Assert.Equal(NotifyCollectionChangedAction.Reset, args.Action); + } + } + + [Theory] + [InlineData(true, Skip = "Reenable when AppContext switch is added to opt into multiple items in NotifyCollectionChangedEventArgs")] + [InlineData(false)] + public static void ReplaceRange_NotifyCollectionChanged_Test(bool batchCollectionChanged) + { + int[] initialData = new int[] { 10, 11, 12, 13 }; + int[] dataToReplace = new int[] { 3, 8 }; + int[] expectedResult = new int[] { 10, 3, 8 ,13 }; + int startIndex = 1; + int count = 2; + + ObservableCollection collection = new ObservableCollection(initialData); + NotifyCollectionChangedEventArgs? args = null; + + collection.CollectionChanged += (o, e) => { Assert.Null(args); args = e; }; + + collection.ReplaceRange(startIndex, count, dataToReplace); + + Assert.Equal(expectedResult, collection.ToArray()); + + Assert.NotNull(args); + if (batchCollectionChanged) + { + Assert.Equal(NotifyCollectionChangedAction.Replace, args.Action); + Assert.Equal(startIndex, args.OldStartingIndex); + Assert.Equal(startIndex, args.NewStartingIndex); + Assert.Equal(initialData.AsSpan(startIndex, count).ToArray(), args.OldItems.Cast().ToArray()); + Assert.Equal(dataToReplace, args.NewItems.Cast().ToArray()); + } + else + { + Assert.Equal(NotifyCollectionChangedAction.Reset, args.Action); + } + } + } +} diff --git a/src/libraries/System.ObjectModel/tests/ObservableCollection/ObservableCollection_SkipCollectionChangedTests.cs b/src/libraries/System.ObjectModel/tests/ObservableCollection/ObservableCollection_SkipCollectionChangedTests.cs new file mode 100644 index 00000000000000..9a9b1c6f2120e6 --- /dev/null +++ b/src/libraries/System.ObjectModel/tests/ObservableCollection/ObservableCollection_SkipCollectionChangedTests.cs @@ -0,0 +1,126 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using Xunit; + +namespace System.ObjectModel.Tests.ObservableCollection +{ + public class ObservableCollection_SkipCollectionChangedTests + { + [Fact] + public void SkipCollectionChanged_AddRange_Test() + { + int collectionChangedCounter = 0; + NonNullObservableCollection collection = new NonNullObservableCollection(); + collection.Add("1"); + collection.Add("2"); + collection.Add("3"); + collection.CollectionChanged += (s, e) => collectionChangedCounter++; + + Assert.Throws(() => collection.AddRange(new string[1])); + Assert.Equal(0, collectionChangedCounter); + + collection.Add("4"); + Assert.Equal(1, collectionChangedCounter); + } + + [Fact] + public void SkipCollectionChanged_InsertRange_Test() + { + int collectionChangedCounter = 0; + NonNullObservableCollection collection = new NonNullObservableCollection(); + collection.Add("1"); + collection.Add("2"); + collection.Add("3"); + collection.CollectionChanged += (s, e) => collectionChangedCounter++; + + Assert.Throws(() => collection.InsertRange(0, new string[1])); + Assert.Equal(0, collectionChangedCounter); + + collection.Add("4"); + Assert.Equal(1, collectionChangedCounter); + } + + [Fact] + public void SkipCollectionChanged_RemoveRange_Test() + { + int collectionChangedCounter = 0; + NonNullObservableCollection collection = new NonNullObservableCollection(); + collection.Add("1"); + collection.Add("2"); + collection.Add("3"); + collection.CollectionChanged += (s, e) => collectionChangedCounter++; + + collection.RemoveRange(0, 2); + Assert.Equal(1, collectionChangedCounter); + + collection.Add("1"); + Assert.Equal(2, collectionChangedCounter); + } + + [Fact] + public void SkipCollectionChanged_RemoveRange_NoEventsRaised_Test() + { + int collectionChangedCounter = 0; + NonNullObservableCollection collection = new NonNullObservableCollection(); + collection.Add("1"); + collection.Add("2"); + collection.Add("3"); + collection.CollectionChanged += (s, e) => collectionChangedCounter++; + + collection.RemoveRange(0, 0); + + Assert.Equal(0, collectionChangedCounter); + } + + [Fact] + public void SkipCollectionChanged_ReplaceRange_Test() + { + int collectionChangedCounter = 0; + NonNullObservableCollection collection = new NonNullObservableCollection(); + collection.Add("1"); + collection.Add("2"); + collection.Add("3"); + collection.CollectionChanged += (s, e) => collectionChangedCounter++; + + Assert.Throws(() => collection.ReplaceRange(0, 2, new string[1])); + Assert.Equal(0, collectionChangedCounter); + + collection.Add("1"); + Assert.Equal(1, collectionChangedCounter); + } + + [Fact] + public void SkipCollectionChanged_ReplaceRange_Empty_Test() + { + int collectionChangedCounter = 0; + NonNullObservableCollection collection = new NonNullObservableCollection(); + collection.Add("1"); + collection.Add("2"); + collection.Add("3"); + collection.CollectionChanged += (s, e) => collectionChangedCounter++; + + collection.ReplaceRange(0, 0, new string[0]); + Assert.Equal(0, collectionChangedCounter); + + collection.Add("1"); + Assert.Equal(1, collectionChangedCounter); + } + + public class NonNullObservableCollection : ObservableCollection + { + + public NonNullObservableCollection() : base() { } + public NonNullObservableCollection(List list) : base(list) { } + + protected override void InsertItem(int index, T item) + { + if (item == null) + { + throw new ArgumentNullException(); + } + + base.InsertItem(index, item); + } + } + } +} diff --git a/src/libraries/System.ObjectModel/tests/System.ObjectModel.Tests.csproj b/src/libraries/System.ObjectModel/tests/System.ObjectModel.Tests.csproj index f015015de4d3ed..8d42c79bc5de7d 100644 --- a/src/libraries/System.ObjectModel/tests/System.ObjectModel.Tests.csproj +++ b/src/libraries/System.ObjectModel/tests/System.ObjectModel.Tests.csproj @@ -17,8 +17,10 @@ Link="Common\System\CollectionsIDictionaryTest.cs" /> + + diff --git a/src/libraries/System.Private.CoreLib/src/System/Collections/ObjectModel/Collection.cs b/src/libraries/System.Private.CoreLib/src/System/Collections/ObjectModel/Collection.cs index 4c6aa24b0a4a63..4e5f761fa51499 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Collections/ObjectModel/Collection.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Collections/ObjectModel/Collection.cs @@ -63,6 +63,8 @@ public void Add(T item) InsertItem(index, item); } + public void AddRange(IEnumerable collection) => InsertItemsRange(items.Count, collection); + public void Clear() { if (items.IsReadOnly) @@ -108,6 +110,26 @@ public void Insert(int index, T item) InsertItem(index, item); } + public void InsertRange(int index, IEnumerable collection) + { + if (items.IsReadOnly) + { + ThrowHelper.ThrowNotSupportedException(ExceptionResource.NotSupported_ReadOnlyCollection); + } + + if (collection == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.collection); + } + + if ((uint)index > (uint)items.Count) + { + ThrowHelper.ThrowArgumentOutOfRange_IndexMustBeLessException(); + } + + InsertItemsRange(index, collection!); // TODO-NULLABLE: Remove ! when [DoesNotReturn] respected + } + public bool Remove(T item) { if (items.IsReadOnly) @@ -121,6 +143,61 @@ public bool Remove(T item) return true; } + public void RemoveRange(int index, int count) + { + if (items.IsReadOnly) + { + ThrowHelper.ThrowNotSupportedException(ExceptionResource.NotSupported_ReadOnlyCollection); + } + + if ((uint)index > (uint)items.Count) + { + ThrowHelper.ThrowArgumentOutOfRange_IndexMustBeLessException(); + } + + if (count < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.count, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum); + } + + if (index > items.Count - count) + { + ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_InvalidOffLen); + } + + RemoveItemsRange(index, count); + } + + public void ReplaceRange(int index, int count, IEnumerable collection) + { + if (items.IsReadOnly) + { + ThrowHelper.ThrowNotSupportedException(ExceptionResource.NotSupported_ReadOnlyCollection); + } + + if ((uint)index > (uint)items.Count) + { + ThrowHelper.ThrowArgumentOutOfRange_IndexMustBeLessException(); + } + + if (count < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.count, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum); + } + + if (index > items.Count - count) + { + ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_InvalidOffLen); + } + + if (collection == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.collection); + } + + ReplaceItemsRange(index, count, collection); + } + public void RemoveAt(int index) { if (items.IsReadOnly) @@ -156,6 +233,42 @@ protected virtual void SetItem(int index, T item) items[index] = item; } + protected virtual void InsertItemsRange(int index, IEnumerable collection) + { + if (GetType() == typeof(Collection) && items is List list) + { + list.InsertRange(index, collection); + } + else + { + foreach (T item in collection) + { + InsertItem(index++, item); + } + } + } + + protected virtual void RemoveItemsRange(int index, int count) + { + if (GetType() == typeof(Collection) && items is List list) + { + list.RemoveRange(index, count); + } + else + { + for (int i = 0; i < count; i++) + { + RemoveItem(index); + } + } + } + + protected virtual void ReplaceItemsRange(int index, int count, IEnumerable collection) + { + RemoveItemsRange(index, count); + InsertItemsRange(index, collection); + } + bool ICollection.IsReadOnly => items.IsReadOnly; IEnumerator IEnumerable.GetEnumerator() diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs index e7e61105315e09..c34130e3b56a9f 100644 --- a/src/libraries/System.Runtime/ref/System.Runtime.cs +++ b/src/libraries/System.Runtime/ref/System.Runtime.cs @@ -8332,6 +8332,7 @@ public Collection(System.Collections.Generic.IList list) { } bool System.Collections.IList.IsReadOnly { get { throw null; } } object? System.Collections.IList.this[int index] { get { throw null; } set { } } public void Add(T item) { } + public void AddRange(System.Collections.Generic.IEnumerable collection) { } public void Clear() { } protected virtual void ClearItems() { } public bool Contains(T item) { throw null; } @@ -8340,9 +8341,15 @@ public void CopyTo(T[] array, int index) { } public int IndexOf(T item) { throw null; } public void Insert(int index, T item) { } protected virtual void InsertItem(int index, T item) { } + protected virtual void InsertItemsRange(int index, System.Collections.Generic.IEnumerable collection) { } + public void InsertRange(int index, System.Collections.Generic.IEnumerable collection) { } public bool Remove(T item) { throw null; } public void RemoveAt(int index) { } protected virtual void RemoveItem(int index) { } + protected virtual void RemoveItemsRange(int index, int count) { } + public void RemoveRange(int index, int count) { } + protected virtual void ReplaceItemsRange(int index, int count, System.Collections.Generic.IEnumerable collection) { } + public void ReplaceRange(int index, int count, System.Collections.Generic.IEnumerable collection) { } protected virtual void SetItem(int index, T item) { } void System.Collections.ICollection.CopyTo(System.Array array, int index) { } System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; } diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System.Runtime.Tests.csproj b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System.Runtime.Tests.csproj index 1c9fa97e35c6da..94992d98003c11 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System.Runtime.Tests.csproj +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System.Runtime.Tests.csproj @@ -187,6 +187,7 @@ + diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Collections/ObjectModel/CollectionTests.RangeMethods.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Collections/ObjectModel/CollectionTests.RangeMethods.cs new file mode 100644 index 00000000000000..6cc14aa3014ba9 --- /dev/null +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Collections/ObjectModel/CollectionTests.RangeMethods.cs @@ -0,0 +1,407 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace System.Collections.ObjectModel.Tests +{ + /// + /// Since is just a wrapper base class around an , + /// we just verify that the underlying list is what we expect, validate that the calls which + /// we expect are forwarded to the underlying list, and verify that the exceptions we expect + /// are thrown. + /// + public partial class CollectionTests : CollectionTestBase + { + [Fact] + public void Collection_AddRange_ToEmpty_Test() + { + int[] expected = new[] { 1, 2, 3, 4 }; + Collection collection = new Collection(); + + collection.AddRange(expected); + + Assert.Equal(expected.Length, collection.Count); + Assert.Equal(expected, collection.ToArray()); + } + + [Fact] + public void Collection_AddRange_ToExisting_Test() + { + int[] initial = new int[] { 1, 2, 3, 4 }; + int[] dataToInsert = new int[] { 5, 6, 7, 8 }; + Collection collection = new Collection(initial); + + collection.AddRange(dataToInsert); + + Assert.Equal(initial.Length + dataToInsert.Length, collection.Count); + + int[] collectionAssertion = collection.ToArray(); + Assert.Equal(initial, collectionAssertion.AsSpan(0, 4).ToArray()); + Assert.Equal(dataToInsert, collectionAssertion.AsSpan(4, 4).ToArray()); + } + + [Fact] + public void Collection_AddRange_Empty_Test() + { + int[] expected = new int[0]; + Collection collection = new Collection(); + + collection.AddRange(expected); + + Assert.Equal(expected.Length, collection.Count); + } + + [Fact] + public void Collection_AddRange_Null_Test() + { + Collection collection = new Collection(); + Exception ex = Assert.Throws(() => collection.AddRange(null)); + } + + [Fact] + public void Collection_AddRange_ReadOnly_Test() + { + int[] expected = new int[] { 1, 2, 3, 4 }; + int[] baseCollection = new int[] { 5, 6, 7, 8 }; + Collection collection = new Collection(baseCollection); + + Exception ex = Assert.Throws(() => collection.AddRange(expected)); + } + + [Fact] + public void Collection_InsertRange_Beginning_Test() + { + int[] expected = new int[] { 1, 2, 3, 4, 5 }; + int[] originalCollection = new int[] { 10, 11, 12, 13 }; + List baseCollection = new List(originalCollection); + + int expectedLength = expected.Length + originalCollection.Length; + Collection collection = new Collection(baseCollection); + + collection.InsertRange(0, expected); + + Assert.Equal(expectedLength, collection.Count); + + int[] collectionAssertion = collection.ToArray(); + Assert.Equal(expected, collectionAssertion.AsSpan(0, 5).ToArray()); + Assert.Equal(originalCollection, collectionAssertion.AsSpan(5, 4).ToArray()); + } + + [Fact] + public void Collection_InsertRange_End_Test() + { + int[] expected = new int[] { 1, 2, 3, 4, 5 }; + int[] originalCollection = new int[] { 10, 11, 12, 13 }; + List baseCollection = new List(originalCollection); + + int expectedLength = expected.Length + originalCollection.Length; + Collection collection = new Collection(baseCollection); + + collection.InsertRange(expected.Length - 1, expected); + + Assert.Equal(expectedLength, collection.Count); + + int[] collectionAssertion = collection.ToArray(); + Assert.Equal(originalCollection, collectionAssertion.AsSpan(0, 4).ToArray()); + Assert.Equal(expected, collectionAssertion.AsSpan(4, 5).ToArray()); + } + + [Fact] + public void Collection_InsertRange_Middle_Test() + { + int[] expected = new int[] { 1, 2, 3, 4, 5 }; + int[] originalCollection = new int[] { 10, 11, 12, 13 }; + List baseCollection = new List(originalCollection); + + int expectedLength = expected.Length + originalCollection.Length; + Collection collection = new Collection(baseCollection); + + collection.InsertRange(2, expected); + + Assert.Equal(expectedLength, collection.Count); + + int[] collectionAssertion = collection.ToArray(); + Assert.Equal(originalCollection.AsSpan(0, 2).ToArray(), collectionAssertion.AsSpan(0, 2).ToArray()); + Assert.Equal(expected, collectionAssertion.AsSpan(2, 5).ToArray()); + Assert.Equal(originalCollection.AsSpan(2, 2).ToArray(), collectionAssertion.AsSpan(7, 2).ToArray()); + } + + [Fact] + public void Collection_InsertRange_Empty_Test() + { + List baseCollection = new List(new[] { 10, 11, 12, 13 }); + Collection collection = new Collection(baseCollection); + + collection.InsertRange(0, new int[0]); + + Assert.Equal(baseCollection.Count, collection.Count); + Assert.Equal(baseCollection, collection.ToArray()); + } + + [Fact] + public void Collection_InsertRange_Null_Test() + { + Collection collection = new Collection(); + Exception ex = Assert.Throws(() => collection.InsertRange(0, null)); + } + + [Fact] + public void Collection_InsertRange_ReadOnly_Test() + { + int[] expected = new int[] { 1, 2, 3, 4 }; + int[] baseCollection = new int[] { 5, 6, 7, 8 }; + Collection collection = new Collection(baseCollection); + + Exception ex = Assert.Throws(() => collection.InsertRange(0, expected)); + } + + [Fact] + public void Collection_InsertRange_IndexLessThan0_Test() + { + int[] expected = new int[] { 1, 2, 3, 4 }; + Collection collection = new Collection(); + Exception ex = Assert.Throws(() => collection.InsertRange(-1, expected)); + } + + [Fact] + public void Collection_InsertRange_IndexGreaterThanCount_Test() + { + int[] expected = new int[] { 1, 2, 3, 4 }; + Collection collection = new Collection(); + Exception ex = Assert.Throws(() => collection.InsertRange(10, expected)); + } + + [Fact] + public void Collection_RemoveRange_Overflow_Test() + { + Collection collection = new Collection(); + collection.Add(1); + collection.Add(2); + collection.Add(3); + + Assert.Throws(() => collection.RemoveRange(0, 4)); + } + + [Fact] + public void Collection_RemoveRange_IntMaxValueOverflow_Test() + { + var count = 500; + Collection collection = new Collection(); + for (int i = 0; i < count; i++) + { + collection.Add(i); + } + + Assert.Throws(() => collection.RemoveRange(collection.Count - 2, int.MaxValue)); + } + + [Fact] + public void Collection_RemoveRange_CountIsZero_Test() + { + int[] expected = new int[] { 1, 2, 3, 4, 5 }; + Collection collection = new Collection(); + for (int i = 0; i < expected.Length; i++) + { + collection.Add(expected[i]); + } + + collection.RemoveRange(0, 0); + + Assert.Equal(expected.Length, collection.Count); + Assert.Equal(expected, collection.ToArray()); + } + + [Fact] + public void Collection_RemoveRange_CountIsLessThanZero_Test() + { + Collection collection = new Collection(); + collection.Add(1); + collection.Add(2); + + Assert.Throws(() => collection.RemoveRange(0, -1)); + } + + [Fact] + public void Collection_RemoveRange_All_Test() + { + Collection collection = new Collection(); + collection.Add(1); + collection.Add(2); + collection.Add(3); + + collection.RemoveRange(0, 3); + + Assert.Equal(0, collection.Count); + } + + [Fact] + public void Collection_RemoveRange_FirstTwoItems_Test() + { + Collection collection = new Collection(); + collection.Add(1); + collection.Add(2); + collection.Add(3); + + collection.RemoveRange(0, 2); + + Assert.Equal(1, collection.Count); + Assert.Equal(3, collection[0]); + } + + [Fact] + public void Collection_RemoveRange_LastTwoItems_Test() + { + Collection collection = new Collection(); + collection.Add(1); + collection.Add(2); + collection.Add(3); + + collection.RemoveRange(1, 2); + + Assert.Equal(1, collection.Count); + Assert.Equal(1, collection[0]); + } + + [Fact] + public void Collection_RemoveRange_ZeroItems_Test() + { + Collection collection = new Collection(); + collection.Add(1); + collection.Add(2); + collection.Add(3); + + collection.RemoveRange(0, 0); + + Assert.Equal(3, collection.Count); + Assert.Equal(1, collection[0]); + Assert.Equal(2, collection[1]); + Assert.Equal(3, collection[2]); + } + + [Fact] + public void Collection_RemoveRange_IndexLessThanZero_Test() + { + Collection collection = new Collection(); + collection.Add(1); + collection.Add(2); + collection.Add(3); + + AssertExtensions.Throws("index", () => collection.RemoveRange(-1, 3)); + } + + [Fact] + public void Collection_RemoveRange_IndexGreaterThanCollection_Test() + { + Collection collection = new Collection(); + collection.Add(1); + collection.Add(2); + collection.Add(3); + + AssertExtensions.Throws("index", () => collection.RemoveRange(4, 3)); + } + + [Fact] + public void Collection_RemoveRange_ReadOnly_Test() + { + Collection collection = new Collection(new int[] { 1, 2, 3 }); + + Assert.Throws(() => collection.RemoveRange(0, 2)); + } + + [Fact] + public void Collection_ReplaceRange_FirstTwo_Test() + { + int[] initial = new int[] { 1, 2, 3, 4 }; + int[] replace = new int[] { 5, 6, 7, 8 }; + Collection collection = new Collection(); + foreach (var item in initial) + collection.Add(item); + + collection.ReplaceRange(0, 2, replace); + + Assert.Equal(initial.Length + 2, collection.Count); + + int[] collectionAssertion = collection.ToArray(); + Assert.Equal(replace, collectionAssertion.AsSpan(0, 4).ToArray()); + Assert.Equal(initial.AsSpan(2, 2).ToArray(), collectionAssertion.AsSpan(4, 2).ToArray()); + } + + [Fact] + public void Collection_ReplaceRange_LastTwo_Test() + { + int[] initial = new int[] { 1, 2, 3, 4 }; + int[] replace = new int[] { 5, 6, 7, 8 }; + Collection collection = new Collection(); + foreach (var item in initial) + collection.Add(item); + + collection.ReplaceRange(2, 2, replace); + + Assert.Equal(initial.Length + 2, collection.Count); + + int[] collectionAssertion = collection.ToArray(); + Assert.Equal(initial.AsSpan(0, 2).ToArray(), collectionAssertion.AsSpan(0, 2).ToArray()); + Assert.Equal(replace.AsSpan(0, 4).ToArray(), collectionAssertion.AsSpan(2, 4).ToArray()); + } + + [Fact] + public void Collection_ReplaceRange_MiddleTwo_Test() + { + int[] initial = new int[] { 1, 2, 3, 4 }; + int[] replace = new int[] { 5, 6, 7, 8 }; + Collection collection = new Collection(); + foreach (var item in initial) + collection.Add(item); + + collection.ReplaceRange(1, 2, replace); + + Assert.Equal(initial.Length + 2, collection.Count); + + Assert.Equal(initial[0], collection[0]); + Assert.Equal(replace, collection.ToArray().AsSpan(1, 4).ToArray()); + Assert.Equal(initial[3], collection[5]); + } + + [Fact] + public void Collection_ReplaceRange_NullCollection_Test() + { + Collection collection = new Collection(); + collection.Add(1); + collection.Add(2); + + Assert.Throws(() => collection.ReplaceRange(0, 2, null)); + } + + [Fact] + public void Collection_ReplaceRange_ReadOnly_Test() + { + Collection collection = new Collection(new int[] { 1, 2, 3 }); + Assert.Throws(() => collection.ReplaceRange(0, 2, new int[] { 4, 5 })); + } + + [Fact] + public void Collection_ReplaceRange_IndexLessThanZero_Test() + { + Collection collection = new Collection(); + collection.Add(1); + collection.Add(2); + + AssertExtensions.Throws("index", () => collection.ReplaceRange(-2, 2, new int[] { 1, 2 })); + } + + [Fact] + public void Collection_ReplaceRange_IndexGreaterThanCount_Test() + { + Collection collection = new Collection(); + collection.Add(1); + collection.Add(2); + + AssertExtensions.Throws("index", () => collection.ReplaceRange(4, 2, new int[] { 1, 2 })); + } + } +} diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Collections/ObjectModel/CollectionTests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Collections/ObjectModel/CollectionTests.cs index 2818edafd238a9..723b2acda03133 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Collections/ObjectModel/CollectionTests.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Collections/ObjectModel/CollectionTests.cs @@ -15,7 +15,7 @@ namespace System.Collections.ObjectModel.Tests /// we expect are forwarded to the underlying list, and verify that the exceptions we expect /// are thrown. /// - public class CollectionTests : CollectionTestBase + public partial class CollectionTests : CollectionTestBase { private static readonly Collection s_empty = new Collection();