diff --git a/mRemoteNGTests/Tools/FullyObservableCollectionTests.cs b/mRemoteNGTests/Tools/FullyObservableCollectionTests.cs new file mode 100644 index 000000000..690d57690 --- /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 15e749b60..bbc85e5e1 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 0970c1764..20661941b 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 5f877a0da..000000000 --- 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 716509d04..a6d38aefb 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 000000000..e561ef623 --- /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 000000000..7c6989421 --- /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 000000000..34d897359 --- /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 000000000..34ded2220 --- /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 ea98fd2dc..78037068c 100644 --- a/mRemoteV1/mRemoteV1.csproj +++ b/mRemoteV1/mRemoteV1.csproj @@ -212,6 +212,8 @@ + + @@ -261,10 +263,12 @@ + +