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.
This commit is contained in:
David Sparer
2017-02-13 13:56:58 -07:00
parent a3b66ec456
commit d003e086bb
10 changed files with 271 additions and 66 deletions

View File

@@ -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<INotifyPropertyChanged>();
Assert.That(list, Is.Empty);
}
[Test]
public void CanCreateWithExistingList()
{
var existingList = new List<INotifyPropertyChanged>
{
Substitute.For<INotifyPropertyChanged>(),
Substitute.For<INotifyPropertyChanged>(),
Substitute.For<INotifyPropertyChanged>()
};
var list = new FullyObservableCollection<INotifyPropertyChanged>(existingList);
Assert.That(list, Has.Count.EqualTo(3));
}
[Test]
public void ItemAdded()
{
var list = new FullyObservableCollection<INotifyPropertyChanged>();
var item = Substitute.For<INotifyPropertyChanged>();
list.Add(item);
Assert.That(list, Has.Member(item));
}
[Test]
public void ItemInserted()
{
var list = new FullyObservableCollection<INotifyPropertyChanged>();
var item = Substitute.For<INotifyPropertyChanged>();
list.Insert(0, item);
Assert.That(list[0], Is.EqualTo(item));
}
[Test]
public void ItemRemoved()
{
var item = Substitute.For<INotifyPropertyChanged>();
var list = new FullyObservableCollection<INotifyPropertyChanged>
{
item
};
list.Remove(item);
Assert.That(list, Does.Not.Contains(item));
}
[Test]
public void ItemRemovedAtIndex()
{
var item = Substitute.For<INotifyPropertyChanged>();
var list = new FullyObservableCollection<INotifyPropertyChanged>
{
item
};
list.RemoveAt(0);
Assert.That(list, Does.Not.Contains(item));
}
[Test]
public void ClearRemovesAllItems()
{
var list = new FullyObservableCollection<INotifyPropertyChanged>
{
Substitute.For<INotifyPropertyChanged>(),
Substitute.For<INotifyPropertyChanged>(),
Substitute.For<INotifyPropertyChanged>()
};
list.Clear();
Assert.That(list, Is.Empty);
}
[Test]
public void ChildItemEventsTriggerListEvents()
{
var wasCalled = false;
var item = Substitute.For<INotifyPropertyChanged>();
var list = new FullyObservableCollection<INotifyPropertyChanged> {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<INotifyPropertyChanged>();
var list = new FullyObservableCollection<INotifyPropertyChanged> { 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<PropertyChangedEventHandler>(item, new PropertyChangedEventArgs("test"));
}
}
}

View File

@@ -148,6 +148,7 @@
<Compile Include="Security\SecureStringExtensionsTests.cs" />
<Compile Include="Security\XmlCryptoProviderBuilderTests.cs" />
<Compile Include="Tools\ExternalToolsArgumentParserTests.cs" />
<Compile Include="Tools\FullyObservableCollectionTests.cs" />
<Compile Include="Tree\ClickHandlers\TreeNodeCompositeClickHandlerTests.cs" />
<Compile Include="Tree\ConnectionTreeDragAndDropHandlerTests.cs" />
<Compile Include="Tree\ConnectionTreeModelTests.cs" />

View File

@@ -2,7 +2,7 @@
namespace mRemoteNG.Credential
{
public class CredentialChangedEventArgs
public class CredentialChangedEventArgs : EventArgs
{
public ICredentialRecord CredentialRecord { get; }
public ICredentialRepository Repository { get; }

View File

@@ -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
}
}

View File

@@ -2,7 +2,7 @@
namespace mRemoteNG.Credential
{
public class CredentialRepositoryChangedArgs
public class CredentialRepositoryChangedArgs : EventArgs
{
public ICredentialRepository Repository { get; }

View File

@@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
namespace mRemoteNG.Tools.CustomCollections
{
public class CollectionUpdatedEventArgs<T> : EventArgs
{
public IEnumerable<T> ChangedItems { get; }
public ActionType Action { get; }
public CollectionUpdatedEventArgs(ActionType action, IEnumerable<T> changedItems)
{
if (changedItems == null)
throw new ArgumentNullException(nameof(changedItems));
Action = action;
ChangedItems = changedItems;
}
}
public enum ActionType
{
Added,
Removed,
Updated
}
}

View File

@@ -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<T> : IFullyNotifiableList<T>
where T : INotifyPropertyChanged
{
private readonly IList<T> _list = new List<T>();
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<T> 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<CollectionUpdatedEventArgs<T>> CollectionUpdated;
private void RaiseCredentialChangedEvent(ActionType action, IEnumerable<T> changedItems)
{
CollectionUpdated?.Invoke(this, new CollectionUpdatedEventArgs<T>(action, changedItems));
}
#region Forwarded method calls
public int IndexOf(T item) => _list.IndexOf(item);
public IEnumerator<T> 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
}
}

View File

@@ -0,0 +1,10 @@
using System.Collections.Generic;
using System.ComponentModel;
namespace mRemoteNG.Tools.CustomCollections
{
public interface IFullyNotifiableList<T> : IList<T>, INotifyCollectionUpdated<T>
where T : INotifyPropertyChanged
{
}
}

View File

@@ -0,0 +1,9 @@
using System;
namespace mRemoteNG.Tools.CustomCollections
{
public interface INotifyCollectionUpdated<T>
{
event EventHandler<CollectionUpdatedEventArgs<T>> CollectionUpdated;
}
}

View File

@@ -212,6 +212,8 @@
<Compile Include="Credential\CredentialChangedEventArgs.cs" />
<Compile Include="Credential\CredentialDeletionMsgBoxConfirmer.cs" />
<Compile Include="Credential\CredentialDomainUserComparer.cs" />
<Compile Include="Tools\CustomCollections\IFullyNotifiableList.cs" />
<Compile Include="Tools\CustomCollections\FullyObservableCollection.cs" />
<Compile Include="Credential\CredentialRepositoryChangedArgs.cs" />
<Compile Include="Credential\CredentialRepositoryList.cs" />
<Compile Include="Credential\CredentialRecord.cs" />
@@ -261,10 +263,12 @@
<Compile Include="Security\CryptographyProviderFactory.cs" />
<Compile Include="Security\XmlCryptoProviderBuilder.cs" />
<Compile Include="Tools\Cmdline\StartupArgumentsInterpreter.cs" />
<Compile Include="Tools\CollectionUpdatedEventArgs.cs" />
<Compile Include="Tools\ExternalToolArgumentParser.cs" />
<Compile Include="Tools\Cmdline\CmdArgumentsInterpreter.cs" />
<Compile Include="Tools\ConnectionsTreeToMenuItemsConverter.cs" />
<Compile Include="Tools\ExternalToolsTypeConverter.cs" />
<Compile Include="Tools\CustomCollections\INotifyCollectionUpdated.cs" />
<Compile Include="Tools\MouseClickSimulator.cs" />
<Compile Include="Tools\NotificationAreaIcon.cs" />
<Compile Include="Tools\ScanHost.cs" />