From a4000c106a14c5506f4c71a59f0061a8cf53d161 Mon Sep 17 00:00:00 2001 From: Markus Ewald Date: Wed, 24 Aug 2011 11:33:22 +0000 Subject: [PATCH] Added an ObservableList class which is like the ObservableCollection, but allows indexed access git-svn-id: file:///srv/devel/repo-conversion/nusu@220 d2e56fa2-650e-0410-a79f-9358c0239efd --- Nuclex.Support (net-4.0).csproj | 4 + Nuclex.Support (xna-4.0-phone7).csproj | 4 + Nuclex.Support (xna-4.0-xbox360).csproj | 4 + .../Collections/ObservableCollection.Test.cs | 4 +- Source/Collections/ObservableCollection.cs | 11 +- Source/Collections/ObservableDictionary.cs | 3 + Source/Collections/ObservableList.Test.cs | 165 ++++++++ Source/Collections/ObservableList.cs | 374 ++++++++++++++++++ 8 files changed, 561 insertions(+), 8 deletions(-) create mode 100644 Source/Collections/ObservableList.Test.cs create mode 100644 Source/Collections/ObservableList.cs diff --git a/Nuclex.Support (net-4.0).csproj b/Nuclex.Support (net-4.0).csproj index c4b4e6d..6cb17a6 100644 --- a/Nuclex.Support (net-4.0).csproj +++ b/Nuclex.Support (net-4.0).csproj @@ -101,6 +101,10 @@ ObservableDictionary.cs + + + ObservableList.cs + PairPriorityQueue.cs diff --git a/Nuclex.Support (xna-4.0-phone7).csproj b/Nuclex.Support (xna-4.0-phone7).csproj index ebcd888..b813099 100644 --- a/Nuclex.Support (xna-4.0-phone7).csproj +++ b/Nuclex.Support (xna-4.0-phone7).csproj @@ -132,6 +132,10 @@ ObservableDictionary.cs + + + ObservableList.cs + PairPriorityQueue.cs diff --git a/Nuclex.Support (xna-4.0-xbox360).csproj b/Nuclex.Support (xna-4.0-xbox360).csproj index 39e934d..c7be8eb 100644 --- a/Nuclex.Support (xna-4.0-xbox360).csproj +++ b/Nuclex.Support (xna-4.0-xbox360).csproj @@ -143,6 +143,10 @@ ObservableDictionary.cs + + + ObservableList.cs + PairPriorityQueue.cs diff --git a/Source/Collections/ObservableCollection.Test.cs b/Source/Collections/ObservableCollection.Test.cs index cc1f98d..41aa18b 100644 --- a/Source/Collections/ObservableCollection.Test.cs +++ b/Source/Collections/ObservableCollection.Test.cs @@ -141,12 +141,12 @@ namespace Nuclex.Support.Collections { this.mockery.VerifyAllExpectationsHaveBeenMet(); } - /// Tests whether the ItemRemoved event is fired + /// Tests whether a the list constructor is working [Test] public void TestListConstructor() { int[] integers = new int[] { 12, 34, 56, 78 }; - ObservableCollection testCollection = new ObservableCollection(integers); + var testCollection = new ObservableCollection(integers); CollectionAssert.AreEqual(integers, testCollection); } diff --git a/Source/Collections/ObservableCollection.cs b/Source/Collections/ObservableCollection.cs index 0c6a42a..89f5fc1 100644 --- a/Source/Collections/ObservableCollection.cs +++ b/Source/Collections/ObservableCollection.cs @@ -86,7 +86,7 @@ namespace Nuclex.Support.Collections { protected virtual void OnCollectionChanged( NotifyCollectionChangedAction action, TItem item, int index ) { - if (CollectionChanged != null) { + if(CollectionChanged != null) { CollectionChanged( this, new NotifyCollectionChangedEventArgs(action, item, index) ); @@ -141,30 +141,29 @@ namespace Nuclex.Support.Collections { /// Fires the 'ItemAdded' event /// Item that has been added to the collection protected virtual void OnAdded(TItem item) { - if (ItemAdded != null) + if(ItemAdded != null) ItemAdded(this, new ItemEventArgs(item)); } /// Fires the 'ItemRemoved' event /// Item that has been removed from the collection protected virtual void OnRemoved(TItem item) { - if (ItemRemoved != null) + if(ItemRemoved != null) ItemRemoved(this, new ItemEventArgs(item)); } /// Fires the 'Clearing' event protected virtual void OnClearing() { - if (Clearing != null) + if(Clearing != null) Clearing(this, EventArgs.Empty); } /// Fires the 'Cleared' event protected virtual void OnCleared() { - if (Cleared != null) + if(Cleared != null) Cleared(this, EventArgs.Empty); } - } } // namespace Nuclex.Support.Collections diff --git a/Source/Collections/ObservableDictionary.cs b/Source/Collections/ObservableDictionary.cs index c8fcc50..e2890d7 100644 --- a/Source/Collections/ObservableDictionary.cs +++ b/Source/Collections/ObservableDictionary.cs @@ -242,6 +242,7 @@ namespace Nuclex.Support.Collections { protected virtual void OnAdded(KeyValuePair item) { if(ItemAdded != null) ItemAdded(this, new ItemEventArgs>(item)); + #if !NO_SPECIALIZED_COLLECTIONS if(CollectionChanged != null) CollectionChanged( @@ -257,6 +258,7 @@ namespace Nuclex.Support.Collections { protected virtual void OnRemoved(KeyValuePair item) { if(ItemRemoved != null) ItemRemoved(this, new ItemEventArgs>(item)); + #if !NO_SPECIALIZED_COLLECTIONS if(CollectionChanged != null) CollectionChanged( @@ -277,6 +279,7 @@ namespace Nuclex.Support.Collections { protected virtual void OnCleared() { if(Cleared != null) Cleared(this, EventArgs.Empty); + #if !NO_SPECIALIZED_COLLECTIONS if(CollectionChanged != null) CollectionChanged(this, CollectionResetEventArgs); diff --git a/Source/Collections/ObservableList.Test.cs b/Source/Collections/ObservableList.Test.cs new file mode 100644 index 0000000..181e54f --- /dev/null +++ b/Source/Collections/ObservableList.Test.cs @@ -0,0 +1,165 @@ +#region CPL License +/* +Nuclex Framework +Copyright (C) 2002-2010 Nuclex Development Labs + +This library is free software; you can redistribute it and/or +modify it under the terms of the IBM Common Public License as +published by the IBM Corporation; either version 1.0 of the +License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +IBM Common Public License for more details. + +You should have received a copy of the IBM Common Public +License along with this library +*/ +#endregion + +using System; +using System.Collections.Generic; + +#if UNITTEST + +using NUnit.Framework; +using NMock; + +namespace Nuclex.Support.Collections { + + /// Unit Test for the observable list class + [TestFixture] + public class ObservableListTest { + + #region interface IObservableCollectionSubscriber + + /// Interface used to test the observable collection + public interface IObservableCollectionSubscriber { + + /// Called when the collection is about to clear its contents + /// Collection that is clearing its contents + /// Not used + void Clearing(object sender, EventArgs arguments); + + /// Called when the collection has been cleared of its contents + /// Collection that was cleared of its contents + /// Not used + void Cleared(object sender, EventArgs arguments); + + /// Called when an item is added to the collection + /// Collection to which an item is being added + /// Contains the item that is being added + void ItemAdded(object sender, ItemEventArgs arguments); + + /// Called when an item is removed from the collection + /// Collection from which an item is being removed + /// Contains the item that is being removed + void ItemRemoved(object sender, ItemEventArgs arguments); + + } + + #endregion // interface IObservableCollectionSubscriber + + /// Initialization routine executed before each test is run + [SetUp] + public void Setup() { + this.mockery = new MockFactory(); + + this.mockedSubscriber = this.mockery.CreateMock(); + + this.observedList = new ObservableList(); + this.observedList.Clearing += new EventHandler( + this.mockedSubscriber.MockObject.Clearing + ); + this.observedList.Cleared += new EventHandler( + this.mockedSubscriber.MockObject.Cleared + ); + this.observedList.ItemAdded += new EventHandler>( + this.mockedSubscriber.MockObject.ItemAdded + ); + this.observedList.ItemRemoved += new EventHandler>( + this.mockedSubscriber.MockObject.ItemRemoved + ); + } + + /// Tests whether the Clearing event is fired + [Test] + public void TestClearingEvent() { + this.mockedSubscriber.Expects.One.Method(m => m.Clearing(null, null)).WithAnyArguments(); + this.mockedSubscriber.Expects.One.Method(m => m.Cleared(null, null)).WithAnyArguments(); + this.observedList.Clear(); + + this.mockery.VerifyAllExpectationsHaveBeenMet(); + } + + /// Tests whether the ItemAdded event is fired + [Test] + public void TestItemAddedEvent() { + this.mockedSubscriber.Expects.One.Method(m => m.ItemAdded(null, null)).WithAnyArguments(); + + this.observedList.Add(123); + + this.mockery.VerifyAllExpectationsHaveBeenMet(); + } + + /// Tests whether the ItemRemoved event is fired + [Test] + public void TestItemRemovedEvent() { + this.mockedSubscriber.Expects.One.Method(m => m.ItemAdded(null, null)).WithAnyArguments(); + + this.observedList.Add(123); + + this.mockedSubscriber.Expects.One.Method(m => m.ItemRemoved(null, null)).WithAnyArguments(); + + this.observedList.Remove(123); + + this.mockery.VerifyAllExpectationsHaveBeenMet(); + } + + /// Tests whether items in the collection can be replaced + [Test] + public void TestItemReplacement() { + this.mockedSubscriber.Expects.Exactly(3).Method( + m => m.ItemAdded(null, null) + ).WithAnyArguments(); + + this.observedList.Add(1); + this.observedList.Add(2); + this.observedList.Add(3); + + this.mockedSubscriber.Expects.One.Method(m => m.ItemRemoved(null, null)).WithAnyArguments(); + this.mockedSubscriber.Expects.One.Method(m => m.ItemAdded(null, null)).WithAnyArguments(); + + // Replace the middle item with something else + this.observedList[1] = 4; + + Assert.AreEqual( + 1, this.observedList.IndexOf(4) + ); + + this.mockery.VerifyAllExpectationsHaveBeenMet(); + } + + /// Tests whether a the list constructor is working + [Test] + public void TestListConstructor() { + int[] integers = new int[] { 12, 34, 56, 78 }; + + var testList = new ObservableList(integers); + + CollectionAssert.AreEqual(integers, testList); + } + + /// Mock object factory + private MockFactory mockery; + /// The mocked observable collection subscriber + private Mock mockedSubscriber; + /// An observable collection to which a mock will be subscribed + private ObservableList observedList; + + } + +} // namespace Nuclex.Support.Collections + +#endif // UNITTEST diff --git a/Source/Collections/ObservableList.cs b/Source/Collections/ObservableList.cs new file mode 100644 index 0000000..f6ce57c --- /dev/null +++ b/Source/Collections/ObservableList.cs @@ -0,0 +1,374 @@ +#region CPL License +/* +Nuclex Framework +Copyright (C) 2002-2010 Nuclex Development Labs + +This library is free software; you can redistribute it and/or +modify it under the terms of the IBM Common Public License as +published by the IBM Corporation; either version 1.0 of the +License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +IBM Common Public License for more details. + +You should have received a copy of the IBM Common Public +License along with this library +*/ +#endregion + +using System; +using System.Collections; +using System.Collections.Generic; + +#if !NO_SPECIALIZED_COLLECTIONS +using System.Collections.Specialized; +#endif + +namespace Nuclex.Support.Collections { + + /// List which fires events when items are added or removed + /// Type of items the collection manages + public class ObservableList : IList, IList, ICollection, +#if !NO_SPECIALIZED_COLLECTIONS + INotifyCollectionChanged, +#endif + IObservableCollection { + + /// Raised when an item has been added to the collection + public event EventHandler> ItemAdded; + /// Raised when an item is removed from the collection + public event EventHandler> ItemRemoved; + /// Raised when the collection is about to be cleared + /// + /// This could be covered by calling ItemRemoved for each item currently + /// contained in the collection, but it is often simpler and more efficient + /// to process the clearing of the entire collection as a special operation. + /// + public event EventHandler Clearing; + /// Raised when the collection has been cleared + public event EventHandler Cleared; + +#if !NO_SPECIALIZED_COLLECTIONS + /// Called when the collection has changed + public event NotifyCollectionChangedEventHandler CollectionChanged; +#endif + + /// + /// Initializes a new instance of the ObservableList class that is empty. + /// + public ObservableList() : this(new List()) { } + + /// + /// Initializes a new instance of the ObservableList class as a wrapper + /// for the specified list. + /// + /// The list that is wrapped by the new collection. + /// List is null + public ObservableList(IList list) { + this.typedList = list; + this.objectList = list as IList; // Gah! + } + + /// Determines the index of the specified item in the list + /// Item whose index will be determined + /// The index of the item in the list or -1 if not found + public int IndexOf(TItem item) { + return this.typedList.IndexOf(item); + } + + /// Inserts an item into the list at the specified index + /// Index the item will be insertted at + /// Item that will be inserted into the list + public void Insert(int index, TItem item) { + this.typedList.Insert(index, item); + OnAdded(item); +#if !NO_SPECIALIZED_COLLECTIONS + OnCollectionChanged(NotifyCollectionChangedAction.Add, item, index); +#endif + } + + /// Removes the item at the specified index from the list + /// Index at which the item will be removed + public void RemoveAt(int index) { + TItem item = this.typedList[index]; + this.typedList.RemoveAt(index); + OnRemoved(item); +#if !NO_SPECIALIZED_COLLECTIONS + OnCollectionChanged(NotifyCollectionChangedAction.Remove, item, index); +#endif + } + + /// Accesses the item at the specified index in the list + /// Index of the item that will be accessed + /// The item at the specified index + public TItem this[int index] { + get { return this.typedList[index]; } + set { + TItem oldItem = this.typedList[index]; + this.typedList[index] = value; + OnRemoved(oldItem); + OnAdded(value); +#if !NO_SPECIALIZED_COLLECTIONS + if(CollectionChanged != null) { + CollectionChanged( + this, new NotifyCollectionChangedEventArgs( + NotifyCollectionChangedAction.Replace, value, oldItem, index + ) + ); + } +#endif + + } + } + + /// Adds an item to the end of the list + /// Item that will be added to the list + public void Add(TItem item) { + this.typedList.Add(item); + OnAdded(item); + } + + /// Removes all items from the list + public void Clear() { + OnClearing(); + this.typedList.Clear(); + OnCleared(); +#if !NO_SPECIALIZED_COLLECTIONS + if(CollectionChanged != null) { + CollectionChanged(this, CollectionResetEventArgs); + } +#endif + } + + /// Checks whether the list contains the specified item + /// Item the list will be checked for + /// True if the list contains the specified items + public bool Contains(TItem item) { + return this.typedList.Contains(item); + } + + /// Copies the contents of the list into an array + /// Array the list will be copied into + /// + /// Index in the target array where the first item will be copied to + /// + public void CopyTo(TItem[] array, int arrayIndex) { + this.typedList.CopyTo(array, arrayIndex); + } + + /// Total number of items in the list + public int Count { + get { return this.typedList.Count; } + } + + /// Whether the list is a read-only list + public bool IsReadOnly { + get { return this.typedList.IsReadOnly; } + } + + /// Removes the specified item from the list + /// Item that will be removed from the list + /// + /// True if the item was found and removed from the list, false otherwise + /// + public bool Remove(TItem item) { + int index = this.typedList.IndexOf(item); + if(index == -1) { + return false; + } + + TItem removedItem = this.typedList[index]; + this.typedList.RemoveAt(index); + OnRemoved(removedItem); +#if !NO_SPECIALIZED_COLLECTIONS + OnCollectionChanged(NotifyCollectionChangedAction.Remove, item, index); +#endif + return true; + } + + /// Returns an enumerator for the items in the list + /// An enumerator for the list's items + public IEnumerator GetEnumerator() { + return this.typedList.GetEnumerator(); + } + + #region IEnumerable implementation + + /// Returns an enumerator for the items in the list + /// An enumerator for the list's items + IEnumerator IEnumerable.GetEnumerator() { + return this.objectList.GetEnumerator(); + } + + #endregion // IEnumerable implementation + + #region ICollection implementation + + /// Copies the contents of the list into an array + /// Array the list will be copied into + /// + /// Index in the target array where the first item will be copied to + /// + void ICollection.CopyTo(Array array, int arrayIndex) { + this.objectList.CopyTo(array, arrayIndex); + } + + /// Whether this list performs thread synchronization + bool ICollection.IsSynchronized { + get { return this.objectList.IsSynchronized; } + } + + /// Synchronization root used by the list to synchronize threads + object ICollection.SyncRoot { + get { return this.objectList.SyncRoot; } + } + + #endregion // ICollection implementation + + #region IList implementation + + /// Adds an item to the list + /// Item that will be added to the list + /// + /// The position at which the item has been inserted or -1 if the item was not inserted + /// + int IList.Add(object value) { + int index = this.objectList.Add(value); + TItem addedItem = this.typedList[index]; + OnAdded(addedItem); +#if !NO_SPECIALIZED_COLLECTIONS + OnCollectionChanged(NotifyCollectionChangedAction.Add, addedItem, index); +#endif + return index; + } + + /// Checks whether the list contains the specified item + /// Item the list will be checked for + /// True if the list contains the specified items + bool IList.Contains(object item) { + return this.objectList.Contains(item); + } + + /// Determines the index of the specified item in the list + /// Item whose index will be determined + /// The index of the item in the list or -1 if not found + int IList.IndexOf(object item) { + return this.objectList.IndexOf(item); + } + + /// Inserts an item into the list at the specified index + /// Index the item will be insertted at + /// Item that will be inserted into the list + void IList.Insert(int index, object item) { + this.objectList.Insert(index, item); + TItem addedItem = this.typedList[index]; + OnAdded(addedItem); +#if !NO_SPECIALIZED_COLLECTIONS + OnCollectionChanged(NotifyCollectionChangedAction.Add, addedItem, index); +#endif + } + + /// Whether the list is of a fixed size + bool IList.IsFixedSize { + get { return this.objectList.IsFixedSize; } + } + + /// Removes the specified item from the list + /// Item that will be removed from the list + void IList.Remove(object item) { + int index = this.objectList.IndexOf(item); + if(index == -1) { + return; + } + + TItem removedItem = this.typedList[index]; + this.objectList.RemoveAt(index); + OnRemoved(removedItem); +#if !NO_SPECIALIZED_COLLECTIONS + OnCollectionChanged(NotifyCollectionChangedAction.Remove, removedItem, index); +#endif + } + + /// Accesses the item at the specified index in the list + /// Index of the item that will be accessed + /// The item at the specified index + object IList.this[int index] { + get { return this.objectList[index]; } + set { + TItem oldItem = this.typedList[index]; + this.objectList[index] = value; + TItem newItem = this.typedList[index]; + OnRemoved(oldItem); + OnAdded(newItem); +#if !NO_SPECIALIZED_COLLECTIONS + if(CollectionChanged != null) { + CollectionChanged( + this, new NotifyCollectionChangedEventArgs( + NotifyCollectionChangedAction.Replace, newItem, oldItem, index + ) + ); + } +#endif + } + } + + #endregion // IList implementation + +#if !NO_SPECIALIZED_COLLECTIONS + /// Fires the CollectionChanged event + /// Type of change that has occured + /// The item that has been added, removed or replaced + /// Index of the changed item + protected virtual void OnCollectionChanged( + NotifyCollectionChangedAction action, TItem item, int index + ) { + if(CollectionChanged != null) { + CollectionChanged( + this, new NotifyCollectionChangedEventArgs(action, item, index) + ); + } + } +#endif + + /// Fires the 'ItemAdded' event + /// Item that has been added to the collection + protected virtual void OnAdded(TItem item) { + if(ItemAdded != null) + ItemAdded(this, new ItemEventArgs(item)); + } + + /// Fires the 'ItemRemoved' event + /// Item that has been removed from the collection + protected virtual void OnRemoved(TItem item) { + if(ItemRemoved != null) + ItemRemoved(this, new ItemEventArgs(item)); + } + + /// Fires the 'Clearing' event + protected virtual void OnClearing() { + if(Clearing != null) + Clearing(this, EventArgs.Empty); + } + + /// Fires the 'Cleared' event + protected virtual void OnCleared() { + if(Cleared != null) + Cleared(this, EventArgs.Empty); + } + +#if !NO_SPECIALIZED_COLLECTIONS + /// Fixed event args used to notify that the collection has reset + private static readonly NotifyCollectionChangedEventArgs CollectionResetEventArgs = + new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset); +#endif + + /// The wrapped List under its type-safe interface + private IList typedList; + /// The wrapped List under its object interface + private IList objectList; + + } + +} // namespace Nuclex.Support.Collections