From ddac43c18ef8c939ca7e7479fa65e78d72ff7a9d Mon Sep 17 00:00:00 2001 From: John Stewien Date: Fri, 29 Sep 2023 13:56:55 +0930 Subject: [PATCH] #29 Created new ConcurrentObservableDictionaryLite, fixed some issues in ConcurrentObservableHashSet --- ...ConcurrentObservableDictionaryLiteTests.cs | 255 +++++++++++++++++ ...onaryLite_INotifyCollectionChangedTests.cs | 266 ++++++++++++++++++ .../ConcurrentObservableBase.cs | 7 +- .../ConcurrentObservableDictionaryLite.cs | 233 +++++++++++++++ .../ConcurrentObservableHashSet.cs | 55 ++-- 5 files changed, 779 insertions(+), 37 deletions(-) create mode 100644 ExamplesAndTests/Swordfish.NET.UnitTestV3/ConcurrentObservableDictionaryLiteTests.cs create mode 100644 ExamplesAndTests/Swordfish.NET.UnitTestV3/ConcurrentObservableDictionaryLite_INotifyCollectionChangedTests.cs create mode 100644 Swordfish.NET.CollectionsV3/ConcurrentObservableDictionaryLite.cs diff --git a/ExamplesAndTests/Swordfish.NET.UnitTestV3/ConcurrentObservableDictionaryLiteTests.cs b/ExamplesAndTests/Swordfish.NET.UnitTestV3/ConcurrentObservableDictionaryLiteTests.cs new file mode 100644 index 0000000..270271d --- /dev/null +++ b/ExamplesAndTests/Swordfish.NET.UnitTestV3/ConcurrentObservableDictionaryLiteTests.cs @@ -0,0 +1,255 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Swordfish.NET.Collections; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace Swordfish.NET.UnitTestV3 +{ + [TestClass] + public class ConcurrentObservableDictionaryListTests + { + [TestMethod] + public void AddRangeTest() + { + // There was an issue with ConcurrentObservableDictionaryLite.AddMany throwing an + // exception when passed an IEnumerable. + + IEnumerable> GetIEnumerable() + { + for(int i=0; i<10; ++i) + { + yield return new KeyValuePair(i.ToString(), i.ToString()); + } + } + + var itemsToAdd = GetIEnumerable(); + var dictionary1 = new ConcurrentObservableDictionaryLite(); + dictionary1.AddRange(itemsToAdd); + + Assert.IsTrue(dictionary1.Count == itemsToAdd.Count(), "Right number of items"); + + var sourceDictionary = itemsToAdd.ToDictionary(a => a.Key, b => b.Value); + var dictionary2 = new ConcurrentObservableDictionaryLite(); + dictionary2.AddRange(sourceDictionary); + + Assert.IsTrue(dictionary2.Count == sourceDictionary.Count, "Right number of items"); + } + + [TestMethod] + public void AddTest() + { + int testCollectionCount = 10; + int itemsPerCollection = 200_000; + var sourceCollections = new List>(); + + // Use a fixed seed for consistency in results + Random random = new Random(1); + + // Create 10 test sets + for (int collection = 0; collection < testCollectionCount; ++collection) + { + List sourceCollection = new List(); + for (int item = 0; item < itemsPerCollection; ++item) + { + // Ensure we have some duplicates by picking a random number + // less than half the number of items. + sourceCollection.Add(random.Next(itemsPerCollection / 2)); + } + sourceCollections.Add(sourceCollection); + } + + var testCollection = new ConcurrentObservableDictionaryLite(); + + // Create test subject + // Populate test subject + sourceCollections.AsParallel().ForAll(collection => + { + foreach (var item in collection) + { + testCollection[item] = item; + } + }); + + bool keyMatchesValue = testCollection + .All(kv => kv.Key == kv.Value); + + Assert.IsTrue(keyMatchesValue, "Keys match values"); + + var sorted = + sourceCollections + .SelectMany(x => x) + .Distinct() + .OrderBy(x => x) + .ToList(); + + var sortedFromTest = + testCollection + .OrderBy(x => x.Key) + .ToList(); + + Assert.IsTrue(sorted.Count == sortedFromTest.Count, "Right number of items"); + + var allItemsPresent = + sorted + .Zip(sortedFromTest, (a, b) => a == b.Key) + .All(a => a); + + Assert.IsTrue(allItemsPresent, "All items present"); + } + + [TestMethod] + public void TestTryRemove() + { + var testCollection = new ConcurrentObservableDictionaryLite(); + for (int i = 0; i < 10; i++) + { + testCollection.Add($"Key{i}", $"Value{i}"); + } + Assert.AreEqual(10, testCollection.Count); + Assert.IsFalse(testCollection.TryRemove("Key10", out var value)); + for (int i = 9; i >= 0; i--) + { + string key = $"Key{i}"; + string expectedValue = $"Value{i}"; + Assert.IsTrue(testCollection.TryRemove(key, out var item)); + Assert.AreEqual(expectedValue, item); + Assert.AreEqual(i, testCollection.Count); + } + } + + [TestMethod] + public void TestManyOperations() + { + // Create some random, but unique items + // Use a fixed seed for consistency in results + Random random = new Random(1); + HashSet baseItemsSet = new HashSet(); + while (baseItemsSet.Count < 1_100_000) + { + baseItemsSet.Add(random.Next()); + } + + // Create 2 collections, 1 to test, and 1 to compare against + var testCollection = new ConcurrentObservableDictionaryLite(); + var list = new List>(); + + // Create 1,000,000 items to add and insert + var itemsToAdd = + baseItemsSet + .Take(1_000_000) + .Select(x => Swordfish.NET.Collections.KeyValuePair.Create($"Key {x}", $"Value {x}")) + .ToList(); + + // Create 100,000 items to insert + var itemsToInsert = + baseItemsSet + .Skip(1_000_000) + .Take(100_000) + .Select(x => Swordfish.NET.Collections.KeyValuePair.Create($"Insert Key {x}", $"Insert Value {x}")) + .ToList(); + + // Create items to remove + var itemsToRemove = + itemsToInsert + .Take(1000) + .ToList(); + + foreach (var item in itemsToAdd) + { + testCollection.Add(item.Key, item.Value); + list.Add(item); + } + + // Check items are equal count + Assert.IsTrue(list.Count == testCollection.Count, "Added Items correct count"); + + // Test removing items + + foreach (var item in itemsToRemove) + { + testCollection.Remove(item.Key); + list.Remove(item); + } + + // Check items are equal count + Assert.IsTrue(list.Count == testCollection.Count, "Items correct count after removing"); + + // Test contains + + var containsAll = list + .All(kv => testCollection.Contains(kv)); + + Assert.IsTrue(containsAll, "Contains all the items is true"); + + var containsNone = itemsToRemove + .Any(kv => testCollection.ContainsKey(kv.Key)); + + Assert.IsFalse(containsNone, "Contains any of the removed items is false"); + } + + [TestMethod] + public void TestIndexOfInViews() + { + var initial = Enumerable.Range(0, 100).Select(x => new KeyValuePair(x, x.ToString())).ToList(); + var other = Enumerable.Range(100, 100).Select(x => new KeyValuePair(x, x.ToString())).ToList(); + var testCollection = new ConcurrentObservableDictionaryLite(); + testCollection.AddRange(initial); + var collectionView = testCollection.CollectionView; + var keysView = testCollection.Keys; + var valuesView = testCollection.Values; + + // Test the IList implementation because it had a bug + + IList collectionList = (IList)testCollection.CollectionView; + IList keysList = (IList)testCollection.Keys; + IList valuesList = (IList)testCollection.Values; + + foreach (var item in initial) + { + Assert.IsTrue(testCollection.Contains(item)); + + Assert.IsTrue(collectionView.Contains(item)); + Assert.IsTrue(keysView.Contains(item.Key)); + Assert.IsTrue(valuesView.Contains(item.Value)); + + Assert.IsTrue(collectionList.Contains(item)); + Assert.IsTrue(keysList.Contains(item.Key)); + Assert.IsTrue(valuesList.Contains(item.Value)); + } + + foreach (var item in other) + { + Assert.IsFalse(testCollection.Contains(item)); + + Assert.IsFalse(collectionView.Contains(item)); + Assert.IsFalse(keysView.Contains(item.Key)); + Assert.IsFalse(valuesView.Contains(item.Value)); + + Assert.IsFalse(collectionList.Contains(item)); + Assert.IsFalse(keysList.Contains(item.Key)); + Assert.IsFalse(valuesList.Contains(item.Value)); + } + + for (int i=0; i + /// Tests that the following methods fire the correct event in ConcurrentObservableDictionaryLite: + /// + /// - AddRange + /// - RemoveRange + /// - Clear + /// + /// Test the following collection classes: + /// + /// - ConcurrentObservableCollection - done (other class) + /// - ConcurrentObservableDictionaryLite - done (this class) + /// - ConcurrentObservableHashSet + /// - ConcurrentObservableSortedCollection + /// - ConcurrentObservableSortedDictionary + /// - ConcurrentObservableSortedSet + /// + [TestClass] + public class ConcurrentObservableDictionaryLite_INotifyCollectionChangedTests + { + [TestMethod] + public void Test_ConcurrentObservableDictionaryLite_AddRange_IEnumerable() + { + var toAdd = Enumerable.Range(0, 100).Select(x => new KeyValuePair(x, x)); + var collection = new ConcurrentObservableDictionaryLite(); + + // Record all the collection changed events + List<(object, NotifyCollectionChangedEventArgs)> returnedList = new List<(object, NotifyCollectionChangedEventArgs)>(); + collection.CollectionChanged += (s, e) => returnedList.Add((s, e)); + + collection.AddRange(toAdd); + + // Check just one collection changed event was fired + Assert.AreEqual(1, returnedList.Count); + (var returnedObject, var returnedArgs) = returnedList[0]; + + Assert.AreEqual(returnedObject, collection); + Assert.AreEqual(returnedArgs.Action, NotifyCollectionChangedAction.Add); + Assert.IsNotNull(returnedArgs.NewItems); + Assert.IsNull(returnedArgs.OldItems); + Assert.AreEqual(toAdd.Count(), returnedArgs.NewItems.Count); + Assert.IsTrue(CollectionsAreEqual(toAdd, returnedArgs.NewItems)); + } + + [TestMethod] + public void Test_ConcurrentObservableDictionaryLite_AddRange_List() + { + var toAdd = Enumerable.Range(0, 100).Select(x => new KeyValuePair(x, x)).ToList(); + var collection = new ConcurrentObservableDictionaryLite(); + + // Record all the collection changed events + List<(object, NotifyCollectionChangedEventArgs)> returnedList = new List<(object, NotifyCollectionChangedEventArgs)>(); + collection.CollectionChanged += (s, e) => returnedList.Add((s, e)); + + collection.AddRange(toAdd); + + // Check just one collection changed event was fired + Assert.AreEqual(1, returnedList.Count); + (var returnedObject, var returnedArgs) = returnedList[0]; + + Assert.AreEqual(returnedObject, collection); + Assert.AreEqual(returnedArgs.Action, NotifyCollectionChangedAction.Add); + Assert.IsNotNull(returnedArgs.NewItems); + Assert.IsNull(returnedArgs.OldItems); + Assert.AreEqual(toAdd.Count(), returnedArgs.NewItems.Count); + Assert.IsTrue(CollectionsAreEqual(toAdd, returnedArgs.NewItems)); + } + + [TestMethod] + public void Test_ConcurrentObservableDictionaryLite_AddRange_Dictionary() + { + var toAdd = Enumerable.Range(0, 100).Select(x => new KeyValuePair(x, x)).ToDictionary(x => x.Key, x => x.Value); + var collection = new ConcurrentObservableDictionaryLite(); + + // Record all the collection changed events + List<(object, NotifyCollectionChangedEventArgs)> returnedList = new List<(object, NotifyCollectionChangedEventArgs)>(); + collection.CollectionChanged += (s, e) => returnedList.Add((s, e)); + + collection.AddRange(toAdd); + + // Check just one collection changed event was fired + Assert.AreEqual(1, returnedList.Count); + (var returnedObject, var returnedArgs) = returnedList[0]; + + Assert.AreEqual(returnedObject, collection); + Assert.AreEqual(returnedArgs.Action, NotifyCollectionChangedAction.Add); + Assert.IsNotNull(returnedArgs.NewItems); + Assert.IsNull(returnedArgs.OldItems); + Assert.AreEqual(toAdd.Count(), returnedArgs.NewItems.Count); + Assert.IsTrue(CollectionsAreEqual(toAdd, returnedArgs.NewItems)); + } + + [TestMethod] + public void Test_ConcurrentObservableCollection_RemoveRange_ByItems_IList() + { + var initial = Enumerable.Range(0, 100).Select(x => new KeyValuePair(x, x)); + var toRemove = Enumerable.Range(1, 40).Select(x => x * 2).ToList(); + var collection = new ConcurrentObservableDictionaryLite(); + collection.AddRange(initial); + Assert.AreEqual(100, collection.Count); + + // Record all the collection changed events + List<(object, NotifyCollectionChangedEventArgs)> returnedList = new List<(object, NotifyCollectionChangedEventArgs)>(); + collection.CollectionChanged += (s, e) => returnedList.Add((s, e)); + + collection.RemoveRange(toRemove); + + // Check just one collection changed event was fired + Assert.AreEqual(1, returnedList.Count); + (var returnedObject, var returnedArgs) = returnedList[0]; + + Assert.IsNotNull(returnedObject); + Assert.IsNotNull(returnedArgs); + + Assert.AreEqual(returnedObject, collection); + Assert.AreEqual(NotifyCollectionChangedAction.Remove, returnedArgs.Action); + Assert.AreEqual(-1, returnedArgs.OldStartingIndex); + Assert.IsNull(returnedArgs.NewItems); + Assert.IsNotNull(returnedArgs.OldItems); + Assert.AreEqual(toRemove.Count, returnedArgs.OldItems.Count); + Assert.IsTrue(CollectionsAreEqual(toRemove, returnedArgs.OldItems)); + } + + [TestMethod] + public void Test_ConcurrentObservableCollection_RemoveRange_ByItems_IEnumerable() + { + var initial = Enumerable.Range(0, 100).Select(x => new KeyValuePair(x, x)); + var toRemove = Enumerable.Range(1, 40).Select(x => x * 2); + var collection = new ConcurrentObservableDictionaryLite(); + collection.AddRange(initial); + Assert.AreEqual(100, collection.Count); + + // Record all the collection changed events + List<(object, NotifyCollectionChangedEventArgs)> returnedList = new List<(object, NotifyCollectionChangedEventArgs)>(); + collection.CollectionChanged += (s, e) => returnedList.Add((s, e)); + + collection.RemoveRange(toRemove); + + // Check just one collection changed event was fired + Assert.AreEqual(1, returnedList.Count); + (var returnedObject, var returnedArgs) = returnedList[0]; + + Assert.IsNotNull(returnedObject); + Assert.IsNotNull(returnedArgs); + + Assert.AreEqual(returnedObject, collection); + Assert.AreEqual(NotifyCollectionChangedAction.Remove, returnedArgs.Action); + Assert.AreEqual(-1, returnedArgs.OldStartingIndex); + Assert.IsNull(returnedArgs.NewItems); + Assert.IsNotNull(returnedArgs.OldItems); + Assert.AreEqual(toRemove.Count(), returnedArgs.OldItems.Count); + Assert.IsTrue(CollectionsAreEqual(toRemove, returnedArgs.OldItems)); + } + + + [TestMethod] + public void Test_ConcurrentObservableDictionaryLite_Clear() + { + var initial = Enumerable.Range(0, 100).Select(x => new KeyValuePair(x, x)); + var collection = new ConcurrentObservableDictionaryLite(); + collection.AddRange(initial); + Assert.AreEqual(100, collection.Count); + + // Record all the collection changed events + List<(object, NotifyCollectionChangedEventArgs)> returnedList = new List<(object, NotifyCollectionChangedEventArgs)>(); + collection.CollectionChanged += (s, e) => returnedList.Add((s, e)); + + collection.Clear(); + + // Check just one collection changed event was fired + Assert.AreEqual(1, returnedList.Count); + (var returnedObject, var returnedArgs) = returnedList[0]; + + Assert.AreEqual(0, collection.Count); + + Assert.AreEqual(returnedObject, collection); + Assert.AreEqual(NotifyCollectionChangedAction.Remove, returnedArgs.Action); + + Assert.IsNull(returnedArgs.NewItems); + + Assert.IsNotNull(returnedArgs.OldItems); + Assert.AreEqual(initial.Count(), returnedArgs.OldItems.Count); + Assert.IsTrue(initial.Zip(returnedArgs.OldItems.OfType>(), (a, b) => a.Key == b.Key && a.Value == b.Value).All(c => c)); + } + + bool CollectionsAreEqual(IEnumerable> collectionA, IList collectionB) => + collectionA.Zip(collectionB.OfType>(), (a, b) => a.Key == b.Key && a.Value == b.Value).All(c => c); + + bool CollectionsAreEqual(IEnumerable collectionA, IList collectionB) => + collectionA.Zip(collectionB.OfType(), (a, b) => a == b).All(c => c); + + [TestMethod] + public void Test_ConcurrentObservableDictionaryLite_With_EqualityComparer() + { + BoxEqualityComparer boxEqC = new BoxEqualityComparer(); + + var boxes = new Dictionary(boxEqC); + + var redBox = new Box(4, 3, 4); + boxes.Add(redBox, "red"); + + var blueBox = new Box(4, 3, 4); + Assert.ThrowsException( + action: () => boxes.Add(blueBox, "blue"), + message: "An item with the same key has already been added. Key: (4, 3, 4)"); + + var greenBox = new Box(3, 4, 3); + boxes.Add(greenBox, "green"); + Console.WriteLine(); + + Assert.AreEqual(2, boxes.Count); + } + + public class Box + { + public Box(int h, int l, int w) + { + this.Height = h; + this.Length = l; + this.Width = w; + } + + public int Height { get; set; } + public int Length { get; set; } + public int Width { get; set; } + + public override String ToString() + { + return String.Format("({0}, {1}, {2})", Height, Length, Width); + } + } + + class BoxEqualityComparer : IEqualityComparer + { + public bool Equals(Box b1, Box b2) + { + if (b2 == null && b1 == null) + return true; + else if (b1 == null || b2 == null) + return false; + else if (b1.Height == b2.Height && b1.Length == b2.Length + && b1.Width == b2.Width) + return true; + else + return false; + } + + public int GetHashCode(Box bx) + { + int hCode = bx.Height ^ bx.Length ^ bx.Width; + return hCode.GetHashCode(); + } + } + } +} diff --git a/Swordfish.NET.CollectionsV3/ConcurrentObservableBase.cs b/Swordfish.NET.CollectionsV3/ConcurrentObservableBase.cs index ae8a900..5c8ffe3 100644 --- a/Swordfish.NET.CollectionsV3/ConcurrentObservableBase.cs +++ b/Swordfish.NET.CollectionsV3/ConcurrentObservableBase.cs @@ -92,10 +92,9 @@ public IDisposable FreezeUpdates() }); } - protected void DoWriteNotify(Func write, Func change) { - DoReadWriteNotify(() => 0, n => write(), n => change()); + DoReadWriteNotify(() => 0, _ => write(), _ => change()); } private TRead BodyReadWriteNotify(TRead readValue, Func write, params Func[] changes) @@ -140,12 +139,10 @@ protected bool DoTestReadWriteNotify(Func test, Func read, F return testResult; } - protected bool DoTestReadWriteNotify( Func test, Func readTrue, Func writeTrue, Func changeTrue, - Func readFalse, Func writeFalse, Func changeFalse - ) + Func readFalse, Func writeFalse, Func changeFalse) { _lock?.EnterUpgradeableReadLock(); diff --git a/Swordfish.NET.CollectionsV3/ConcurrentObservableDictionaryLite.cs b/Swordfish.NET.CollectionsV3/ConcurrentObservableDictionaryLite.cs new file mode 100644 index 0000000..7bfcc0d --- /dev/null +++ b/Swordfish.NET.CollectionsV3/ConcurrentObservableDictionaryLite.cs @@ -0,0 +1,233 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Collections.Specialized; +using System.Linq; +using System.Runtime.Serialization; + +namespace Swordfish.NET.Collections +{ + /// + /// A collection that can be updated from multiple threads, and can be bound to an items control in the user interface. + /// Has the advantage over ObservableCollection in that it doesn't have to be updated from the Dispatcher thread. + /// When using this in your view model you should bind to the CollectionView property in your view model. If you + /// bind directly this this class it will throw an exception. + /// + /// + [Serializable] + public class ConcurrentObservableDictionaryLite : + ConcurrentObservableBase, ImmutableDictionary>, + ICollection>, + IDictionary, + ICollection, + ISerializable + { + public ConcurrentObservableDictionaryLite() : this(true) + { + } + + public ConcurrentObservableDictionaryLite(IEqualityComparer keyComparer) : base(true, ImmutableDictionary.Empty.WithComparers(keyComparer)) + { + } + + /// + /// Constructructor. Takes an optional isMultithreaded argument where when true allows you to update the collection + /// from multiple threads. In testing there didn't seem to be any performance hit from turning this on, so I made + /// it the default. + /// + /// + public ConcurrentObservableDictionaryLite(bool isMultithreaded) : base(isMultithreaded, ImmutableDictionary.Empty) + { + } + + public void Add(TKey key, TValue value) + { + DoWriteNotify( + () =>_internalCollection.Add(key, value), + () => new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, new KeyValuePair(key, value)) + ); + } + + public void Add(KeyValuePair pair) + { + Add(pair.Key, pair.Value); + } + + /// + /// Adds a range of items to the end of the collection. Quicker than adding them individually, + /// but the view doesn't update until the last item has been added. + /// + public void AddRange(IEnumerable> values) + { + // Convert to a list off the bat, as this is used multiple times and is required to be + // an IList for NotifyCollectionChangedEventArgs + var valuesList = values as IList> ?? values.ToList(); + + DoWriteNotify( + () => _internalCollection.AddRange(valuesList), + () => new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, (IList)valuesList) + ); + } + + public bool Remove(TKey value) + { + bool wasRemoved = false; + DoWriteNotify( + () => + { + var newCollection = _internalCollection.Remove(value); + wasRemoved = newCollection != _internalCollection; + return newCollection; + }, + () => wasRemoved ? new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, value) : null + ); + return wasRemoved; + } + + public void RemoveRange(IEnumerable keys) + { + // Convert to a list off the bat, as this is used multiple times and is required to be + // an IList for NotifyCollectionChangedEventArgs + var keysList = keys as IList ?? keys.ToList(); + + DoWriteNotify( + () => _internalCollection.RemoveRange(keysList), + () => new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, (IList)keysList) + ); + } + + public bool TryRemove(TKey key, out TValue item) + { + TValue tempItem = default(TValue); + var removed = DoReadWriteNotify( + // Get the list of keys and values from the internal list + () => _internalCollection.TryGetValue(key, out tempItem), + // remove the keys from the dictionary, remove the range from the list + (found) => found ? _internalCollection.Remove(key) : _internalCollection, + // Notify which items were removed + (found) => found ? new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, key) : null + ); + + item = tempItem; + return removed; + } + + /// + /// This is the view of the colleciton that you should be binding to with your ListView/GridView control. + /// + public override IList> CollectionView => _internalCollection.ToList(); + + public override int Count => _internalCollection.Count; + + public bool IsReadOnly => false; + + public override string ToString() + { + return $"{{Items : {Count}}}"; + } + + public void Clear() + { + DoReadWriteNotify( + // Get the list of keys and values from the internal list + () => _internalCollection.ToList(), + // remove the keys from the dictionary, remove the range from the list + (items) => ImmutableDictionary.Empty, + // Notify which items were removed + (items) => new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, (IList)items, 0) + ); + } + + public bool Contains(KeyValuePair item) => _internalCollection.Contains(item); + + public bool ContainsKey(TKey key) => _internalCollection.ContainsKey(key); + + public void CopyTo(KeyValuePair[] array, int arrayIndex) => ((ICollection>)_internalCollection).CopyTo(array, arrayIndex); + + void ICollection.CopyTo(Array array, int arrayIndex) => ((ICollection)_internalCollection).CopyTo(array, arrayIndex); + + object ICollection.SyncRoot => ((ICollection)_internalCollection).SyncRoot; + + bool ICollection.IsSynchronized => ((ICollection)_internalCollection).IsSynchronized; + + public ICollection Keys => _internalCollection.Keys.ToList(); + + public ICollection Values => _internalCollection.Values.ToList(); + + bool ICollection>.IsReadOnly => throw new NotImplementedException(); + + public TValue this[TKey key] + { + get => _internalCollection[key]; + set + { + var pair = KeyValuePair.Create(key, value); + DoTestReadWriteNotify( + // Test if adding or replacing + () => !_internalCollection.ContainsKey(key), + // Same as Add + () => (KeyValuePair?)null, + (_) => _internalCollection.Add(key, value), + (_) => new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, pair), + // Do replace + () => new KeyValuePair(key, _internalCollection[key]), + (oldPair) => _internalCollection.SetItem(key, value), + (oldPair) => new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, pair, oldPair)); + } + } + + public bool TryGetValue(TKey key, out TValue value) + { + return _internalCollection.TryGetValue(key, out value); + } + + public bool Remove(KeyValuePair item) + { + if (_internalCollection.TryGetValue(item.Key, out var value)) + { + // Check that value matches + if (value.Equals(item.Value)) + { + return Remove(item.Key); + } + } + return false; + } + + // ************************************************************************ + // IEnumerable Implementation + // ************************************************************************ + #region IEnumerable> Implementation + + public IEnumerator> GetEnumerator() + { + return _internalCollection.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return _internalCollection.GetEnumerator(); + } + + #endregion IEnumerable> Implementation + + // ************************************************************************ + // ISerializable Implementation + // ************************************************************************ + #region ISerializable Implementation + protected override void GetObjectData(SerializationInfo info, StreamingContext context) + { + base.GetObjectData(info, context); + var children = _internalCollection.ToArray(); + info.AddValue("children", children); + } + + protected ConcurrentObservableDictionaryLite(SerializationInfo information, StreamingContext context) : base(information, context) + { + var children = (KeyValuePair[])information.GetValue("children", typeof(KeyValuePair[])); + _internalCollection = ImmutableDictionary.Empty.AddRange(children); + } + #endregion + } +} diff --git a/Swordfish.NET.CollectionsV3/ConcurrentObservableHashSet.cs b/Swordfish.NET.CollectionsV3/ConcurrentObservableHashSet.cs index d165051..d35f96c 100644 --- a/Swordfish.NET.CollectionsV3/ConcurrentObservableHashSet.cs +++ b/Swordfish.NET.CollectionsV3/ConcurrentObservableHashSet.cs @@ -17,7 +17,7 @@ namespace Swordfish.NET.Collections /// [Serializable] public class ConcurrentObservableHashSet : - ConcurrentObservableBase>, + ConcurrentObservableBase>, ICollection, ISet, ICollection, @@ -44,15 +44,14 @@ public ConcurrentObservableHashSet(bool isMultithreaded) : base(isMultithreaded, public bool Add(T value) { bool wasAdded = false; - DoReadWriteNotify( - () => _internalCollection.Count, - (index) => + DoWriteNotify( + () => { - var newCollection = ((ImmutableHashSet)_internalCollection).Add(value); + var newCollection = _internalCollection.Add(value); wasAdded = newCollection != _internalCollection; return newCollection; }, - (index) => wasAdded ? new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, value, index) : null + () => wasAdded ? new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, value) : null ); return wasAdded; } @@ -65,30 +64,25 @@ public void AddRange(IEnumerable values) { // Convert to a list off the bat, as this is used multiple times and is required to be // an IList for NotifyCollectionChangedEventArgs - if (!(values is IList valuesList)) - { - valuesList = values.ToList(); - } + var valuesList = values as IList ?? values.ToList(); - DoReadWriteNotify( - () => _internalCollection.Count, - (index) => ((ImmutableHashSet)_internalCollection).AddRange(valuesList), - (index) => new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, (IList)valuesList, index) + DoWriteNotify( + () => _internalCollection.AddRange(valuesList), + () => new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, (IList)valuesList) ); } public bool Remove(T value) { bool wasRemoved = false; - DoReadWriteNotify( - () => _internalCollection.Count, - (index) => + DoWriteNotify( + () => { - var newCollection = ((ImmutableHashSet)_internalCollection).Remove(value); + var newCollection = _internalCollection.Remove(value); wasRemoved = newCollection != _internalCollection; return newCollection; }, - (index) => wasRemoved ? new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, value, index) : null + () => wasRemoved ? new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, value) : null ); return wasRemoved; } @@ -98,13 +92,10 @@ public void RemoveRange(IEnumerable values) { // Convert to a list off the bat, as this is used multiple times and is required to be // an IList for NotifyCollectionChangedEventArgs - if (!(values is IList valuesList)) - { - valuesList = values.ToList(); - } + var valuesList = values as IList ?? values.ToList(); DoWriteNotify( - () => ((ImmutableHashSet)_internalCollection).RemoveRange(valuesList), + () => _internalCollection.RemoveRange(valuesList), () => new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, (IList)valuesList) ); } @@ -112,7 +103,7 @@ public void RemoveRange(IEnumerable values) /// /// This is the view of the colleciton that you should be binding to with your ListView/GridView control. /// - public override IList CollectionView => ((IEnumerable)_internalCollection).ToList(); + public override IList CollectionView => _internalCollection.ToList(); public override int Count => _internalCollection.Count; @@ -159,7 +150,7 @@ public bool Contains(T item) public void CopyTo(T[] array, int arrayIndex) { - ((ICollection)_internalCollection).CopyTo(array, arrayIndex); + ((ICollection)_internalCollection).CopyTo(array, arrayIndex); } void ICollection.CopyTo(Array array, int arrayIndex) => ((ICollection)_internalCollection).CopyTo(array, arrayIndex); @@ -178,17 +169,17 @@ public void CopyTo(T[] array, int arrayIndex) void ISet.SymmetricExceptWith(IEnumerable other) => ((ISet)_internalCollection).SymmetricExceptWith(other); - bool ISet.IsSubsetOf(IEnumerable other) => ((ISet)_internalCollection).IsSubsetOf(other); + bool ISet.IsSubsetOf(IEnumerable other) => _internalCollection.IsSubsetOf(other); - bool ISet.IsSupersetOf(IEnumerable other) => ((ISet)_internalCollection).IsSupersetOf(other); + bool ISet.IsSupersetOf(IEnumerable other) => _internalCollection.IsSupersetOf(other); - bool ISet.IsProperSupersetOf(IEnumerable other) => ((ISet)_internalCollection).IsProperSupersetOf(other); + bool ISet.IsProperSupersetOf(IEnumerable other) => _internalCollection.IsProperSupersetOf(other); - bool ISet.IsProperSubsetOf(IEnumerable other) => ((ISet)_internalCollection).IsProperSubsetOf(other); + bool ISet.IsProperSubsetOf(IEnumerable other) => _internalCollection.IsProperSubsetOf(other); - bool ISet.Overlaps(IEnumerable other) => ((ISet)_internalCollection).Overlaps(other); + bool ISet.Overlaps(IEnumerable other) => _internalCollection.Overlaps(other); - bool ISet.SetEquals(IEnumerable other) => ((ISet)_internalCollection).SetEquals(other); + bool ISet.SetEquals(IEnumerable other) => _internalCollection.SetEquals(other); // ************************************************************************ // ISerializable Implementation