From d003e086bbbb826bfd3ddc2a7ecd6fa187f7f5db Mon Sep 17 00:00:00 2001 From: David Sparer Date: Mon, 13 Feb 2017 13:56:58 -0700 Subject: [PATCH] created a class and event type for handling collections that need to raise events for collection changes and child updates this is for cases where you would like to have INotifyCollectionChanged and INotifyPropertyChanged implemented, but dont need the level of detail that those types provide. --- .../Tools/FullyObservableCollectionTests.cs | 114 ++++++++++++++++++ mRemoteNGTests/mRemoteNGTests.csproj | 1 + .../Credential/CredentialChangedEventArgs.cs | 2 +- mRemoteV1/Credential/CredentialList.cs | 64 ---------- .../CredentialRepositoryChangedArgs.cs | 2 +- .../CollectionUpdatedEventArgs.cs | 27 +++++ .../FullyObservableCollection.cs | 104 ++++++++++++++++ .../CustomCollections/IFullyNotifiableList.cs | 10 ++ .../INotifyCollectionUpdated.cs | 9 ++ mRemoteV1/mRemoteV1.csproj | 4 + 10 files changed, 271 insertions(+), 66 deletions(-) create mode 100644 mRemoteNGTests/Tools/FullyObservableCollectionTests.cs delete mode 100644 mRemoteV1/Credential/CredentialList.cs create mode 100644 mRemoteV1/Tools/CustomCollections/CollectionUpdatedEventArgs.cs create mode 100644 mRemoteV1/Tools/CustomCollections/FullyObservableCollection.cs create mode 100644 mRemoteV1/Tools/CustomCollections/IFullyNotifiableList.cs create mode 100644 mRemoteV1/Tools/CustomCollections/INotifyCollectionUpdated.cs diff --git a/mRemoteNGTests/Tools/FullyObservableCollectionTests.cs b/mRemoteNGTests/Tools/FullyObservableCollectionTests.cs new file mode 100644 index 00000000..690d5769 --- /dev/null +++ b/mRemoteNGTests/Tools/FullyObservableCollectionTests.cs @@ -0,0 +1,114 @@ +using System.Collections.Generic; +using System.ComponentModel; +using mRemoteNG.Tools.CustomCollections; +using NSubstitute; +using NUnit.Framework; + +namespace mRemoteNGTests.Tools +{ + public class FullyObservableCollectionTests + { + [Test] + public void CollectionBeginsEmpty() + { + var list = new FullyObservableCollection(); + Assert.That(list, Is.Empty); + } + + [Test] + public void CanCreateWithExistingList() + { + var existingList = new List + { + Substitute.For(), + Substitute.For(), + Substitute.For() + }; + var list = new FullyObservableCollection(existingList); + Assert.That(list, Has.Count.EqualTo(3)); + } + + [Test] + public void ItemAdded() + { + var list = new FullyObservableCollection(); + var item = Substitute.For(); + list.Add(item); + Assert.That(list, Has.Member(item)); + } + + [Test] + public void ItemInserted() + { + var list = new FullyObservableCollection(); + var item = Substitute.For(); + list.Insert(0, item); + Assert.That(list[0], Is.EqualTo(item)); + } + + [Test] + public void ItemRemoved() + { + var item = Substitute.For(); + var list = new FullyObservableCollection + { + item + }; + list.Remove(item); + Assert.That(list, Does.Not.Contains(item)); + } + + [Test] + public void ItemRemovedAtIndex() + { + var item = Substitute.For(); + var list = new FullyObservableCollection + { + item + }; + list.RemoveAt(0); + Assert.That(list, Does.Not.Contains(item)); + } + + [Test] + public void ClearRemovesAllItems() + { + var list = new FullyObservableCollection + { + Substitute.For(), + Substitute.For(), + Substitute.For() + }; + list.Clear(); + Assert.That(list, Is.Empty); + } + + [Test] + public void ChildItemEventsTriggerListEvents() + { + var wasCalled = false; + var item = Substitute.For(); + var list = new FullyObservableCollection {item}; + list.CollectionUpdated += (sender, args) => wasCalled = true; + RaiseEvent(item); + Assert.That(wasCalled, Is.True); + } + + [Test] + public void ListUnsubscribesFromRemovedItems() + { + var wasCalled = false; + var item = Substitute.For(); + var list = new FullyObservableCollection { item }; + list.Remove(item); + list.CollectionUpdated += (sender, args) => wasCalled = true; + RaiseEvent(item); + Assert.That(wasCalled, Is.False); + } + + private void RaiseEvent(INotifyPropertyChanged item) + { + item.PropertyChanged += Raise.Event(item, new PropertyChangedEventArgs("test")); + } + } +} \ No newline at end of file diff --git a/mRemoteNGTests/mRemoteNGTests.csproj b/mRemoteNGTests/mRemoteNGTests.csproj index 15e749b6..bbc85e5e 100644 --- a/mRemoteNGTests/mRemoteNGTests.csproj +++ b/mRemoteNGTests/mRemoteNGTests.csproj @@ -148,6 +148,7 @@ + diff --git a/mRemoteV1/Credential/CredentialChangedEventArgs.cs b/mRemoteV1/Credential/CredentialChangedEventArgs.cs index 0970c176..20661941 100644 --- a/mRemoteV1/Credential/CredentialChangedEventArgs.cs +++ b/mRemoteV1/Credential/CredentialChangedEventArgs.cs @@ -2,7 +2,7 @@ namespace mRemoteNG.Credential { - public class CredentialChangedEventArgs + public class CredentialChangedEventArgs : EventArgs { public ICredentialRecord CredentialRecord { get; } public ICredentialRepository Repository { get; } diff --git a/mRemoteV1/Credential/CredentialList.cs b/mRemoteV1/Credential/CredentialList.cs deleted file mode 100644 index 5f877a0d..00000000 --- a/mRemoteV1/Credential/CredentialList.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System; -using System.Collections; - - -namespace mRemoteNG.Credential -{ - public class CredentialList : CollectionBase - { - #region Public Properties - public CredentialInfo this[object Index] - { - get - { - var info = Index as CredentialInfo; - if (info != null) - { - return info; - } - else - { - return ((CredentialInfo) (List[Convert.ToInt32(Index)])); - } - } - } - - public new int Count => List.Count; - - #endregion - - #region Public Methods - public CredentialInfo Add(CredentialInfo credentialInfo) - { - List.Add(credentialInfo); - return credentialInfo; - } - - public void AddRange(CredentialInfo[] cInfo) - { - foreach (CredentialInfo cI in cInfo) - { - List.Add(cI); - } - } - - public CredentialList Copy() - { - try - { - return (CredentialList)this.MemberwiseClone(); - } - catch (Exception) - { - } - - return null; - } - - public new void Clear() - { - List.Clear(); - } - #endregion - } -} \ No newline at end of file diff --git a/mRemoteV1/Credential/CredentialRepositoryChangedArgs.cs b/mRemoteV1/Credential/CredentialRepositoryChangedArgs.cs index 716509d0..a6d38aef 100644 --- a/mRemoteV1/Credential/CredentialRepositoryChangedArgs.cs +++ b/mRemoteV1/Credential/CredentialRepositoryChangedArgs.cs @@ -2,7 +2,7 @@ namespace mRemoteNG.Credential { - public class CredentialRepositoryChangedArgs + public class CredentialRepositoryChangedArgs : EventArgs { public ICredentialRepository Repository { get; } diff --git a/mRemoteV1/Tools/CustomCollections/CollectionUpdatedEventArgs.cs b/mRemoteV1/Tools/CustomCollections/CollectionUpdatedEventArgs.cs new file mode 100644 index 00000000..e561ef62 --- /dev/null +++ b/mRemoteV1/Tools/CustomCollections/CollectionUpdatedEventArgs.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; + +namespace mRemoteNG.Tools.CustomCollections +{ + public class CollectionUpdatedEventArgs : EventArgs + { + public IEnumerable ChangedItems { get; } + public ActionType Action { get; } + + public CollectionUpdatedEventArgs(ActionType action, IEnumerable changedItems) + { + if (changedItems == null) + throw new ArgumentNullException(nameof(changedItems)); + + Action = action; + ChangedItems = changedItems; + } + } + + public enum ActionType + { + Added, + Removed, + Updated + } +} \ No newline at end of file diff --git a/mRemoteV1/Tools/CustomCollections/FullyObservableCollection.cs b/mRemoteV1/Tools/CustomCollections/FullyObservableCollection.cs new file mode 100644 index 00000000..7c698942 --- /dev/null +++ b/mRemoteV1/Tools/CustomCollections/FullyObservableCollection.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; + +namespace mRemoteNG.Tools.CustomCollections +{ + public class FullyObservableCollection : IFullyNotifiableList + where T : INotifyPropertyChanged + { + private readonly IList _list = new List(); + + public int Count => _list.Count; + public bool IsReadOnly => _list.IsReadOnly; + + public T this[int index] + { + get { return _list[index]; } + set { _list[index] = value; } + } + + public FullyObservableCollection() + { + } + + public FullyObservableCollection(IEnumerable items) + { + foreach (var item in items) + Add(item); + } + + public void Add(T item) + { + _list.Add(item); + SubscribeToChildEvents(item); + RaiseCredentialChangedEvent(ActionType.Added, new[] {item}); + } + + public void Insert(int index, T item) + { + _list.Insert(index, item); + SubscribeToChildEvents(item); + RaiseCredentialChangedEvent(ActionType.Added, new[] { item }); + } + + public bool Remove(T item) + { + var worked = _list.Remove(item); + if (!worked) return worked; + RaiseCredentialChangedEvent(ActionType.Removed, new[] {item}); + UnsubscribeFromChildEvents(item); + return worked; + } + + public void RemoveAt(int index) + { + var item = _list[index]; + _list.RemoveAt(index); + UnsubscribeFromChildEvents(item); + RaiseCredentialChangedEvent(ActionType.Removed, new[] { item }); + } + + public void Clear() + { + var oldItems = _list.ToArray(); + _list.Clear(); + foreach (var item in oldItems) + UnsubscribeFromChildEvents(item); + RaiseCredentialChangedEvent(ActionType.Removed, oldItems); + } + + private void SubscribeToChildEvents(INotifyPropertyChanged item) + { + item.PropertyChanged += ItemOnPropertyChanged; + } + + private void UnsubscribeFromChildEvents(INotifyPropertyChanged item) + { + item.PropertyChanged -= ItemOnPropertyChanged; + } + + private void ItemOnPropertyChanged(object sender, PropertyChangedEventArgs propertyChangedEventArgs) + { + if (sender is T) + RaiseCredentialChangedEvent(ActionType.Updated, new []{ (T)sender }); + } + + public event EventHandler> CollectionUpdated; + + private void RaiseCredentialChangedEvent(ActionType action, IEnumerable changedItems) + { + CollectionUpdated?.Invoke(this, new CollectionUpdatedEventArgs(action, changedItems)); + } + + #region Forwarded method calls + public int IndexOf(T item) => _list.IndexOf(item); + public IEnumerator GetEnumerator() => _list.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => _list.GetEnumerator(); + public bool Contains(T item) => _list.Contains(item); + public void CopyTo(T[] array, int arrayIndex) => _list.CopyTo(array, arrayIndex); + #endregion + } +} \ No newline at end of file diff --git a/mRemoteV1/Tools/CustomCollections/IFullyNotifiableList.cs b/mRemoteV1/Tools/CustomCollections/IFullyNotifiableList.cs new file mode 100644 index 00000000..34d89735 --- /dev/null +++ b/mRemoteV1/Tools/CustomCollections/IFullyNotifiableList.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using System.ComponentModel; + +namespace mRemoteNG.Tools.CustomCollections +{ + public interface IFullyNotifiableList : IList, INotifyCollectionUpdated + where T : INotifyPropertyChanged + { + } +} \ No newline at end of file diff --git a/mRemoteV1/Tools/CustomCollections/INotifyCollectionUpdated.cs b/mRemoteV1/Tools/CustomCollections/INotifyCollectionUpdated.cs new file mode 100644 index 00000000..34ded222 --- /dev/null +++ b/mRemoteV1/Tools/CustomCollections/INotifyCollectionUpdated.cs @@ -0,0 +1,9 @@ +using System; + +namespace mRemoteNG.Tools.CustomCollections +{ + public interface INotifyCollectionUpdated + { + event EventHandler> CollectionUpdated; + } +} \ No newline at end of file diff --git a/mRemoteV1/mRemoteV1.csproj b/mRemoteV1/mRemoteV1.csproj index ea98fd2d..78037068 100644 --- a/mRemoteV1/mRemoteV1.csproj +++ b/mRemoteV1/mRemoteV1.csproj @@ -212,6 +212,8 @@ + + @@ -261,10 +263,12 @@ + +