For most purposes, the builtin collection classes supplied by the .NET Framework are terrific. They are simple and straight forward implementations that do exactly as you expect. But for our purposes, a collection class that fires events when the collection is changed is simply better and in some cases, necessary. For this class, we make the jump over to the FocusedGames.Collections namespace, maintained in the FocusedGames library.
Before writing the collection class itself, we need a delegate that can define how our events will work. Enter the CollectionChangedHandler delegate.
1 | public delegate void CollectionChangedHandler(object sender, CollectionChangedEventArgs args); |
As you can see, we need to define the CollectionChangedEventArgs class itself. This is a simple class that has two properties. The two properties help listeners of the events to understand how the collection was changed.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | public class CollectionChangedEventArgs : EventArgs { public CollectionChangedEventArgs() { } public CollectionChangedEventArgs(object item) { Item = item; } public CollectionChangedEventArgs(object item, int index) : this(item) { Index = index; } public object Item { get; set; } public int Index { get; set; } } |
In the future, I hope that I can implement the INotifyCollectionChanged interface from the .NET Framework. Unfortunately I refuse to do so because the interface is placed in a DLL that is not supported on some frameworks that I need to support with the FG Framework. Instead, I have written my own ICollection interface that basically takes the place of the .NET Framework’s interface.
1 2 3 4 5 | public interface ICollection { event CollectionChangedHandler ItemAdded; event CollectionChangedHandler ItemRemoved; } |
When combined with the .NET Framework’s List
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | public class Collection<T> : ICollection, IList<T>, ICollection<T>, IEnumerable<T>, IList, System.Collections.ICollection, IEnumerable { public event CollectionChangedHandler ItemAdded; public event CollectionChangedHandler ItemRemoved; private List<T> internalList = new List<T>(); private bool IsViableType(object item) { if(item is T && item != null) return true; return false; } protected virtual void OnItemAdded(object sender, CollectionChangedEventArgs args) { if (ItemAdded != null) ItemAdded.Invoke(sender, args); } protected virtual void OnItemRemoved(object sender, CollectionChangedEventArgs args) { if (ItemRemoved != null) ItemRemoved.Invoke(sender, args); } |
But it is more complicated than that. We have to implement every single interface. I am not going to bother explaining each one because they are all simple pass through methods.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 | #region Sorting public void Sort() { internalList.Sort(); } public void Sort(IComparer<T> comparer) { internalList.Sort(comparer); } #endregion public void AddRange(IEnumerable<T> items) { foreach (T item in items) Add(item); } #region IList<T> Members public int IndexOf(T item) { return internalList.IndexOf(item); } public void Insert(int index, T item) { internalList.Insert(index, item); OnItemAdded(this, new CollectionChangedEventArgs(item, index)); } public void RemoveAt(int index) { T item = this[index]; internalList.RemoveAt(index); OnItemRemoved(this, new CollectionChangedEventArgs(item, index)); } public T this[int index] { get { return internalList[index]; } set { internalList[index] = value; } } #endregion #region ICollection<T> Members public void Add(T item) { internalList.Add(item); OnItemAdded(this, new CollectionChangedEventArgs(item, Count - 1)); } public void Clear() { // TODO: How do throw the event here? for (int i = Count - 1; i > 0; i--) RemoveAt(i); internalList.Clear(); } public bool Contains(T item) { return internalList.Contains(item); } public void CopyTo(T[] array, int arrayIndex) { internalList.CopyTo(array, arrayIndex); } public int Count { get { return internalList.Count; } } bool ICollection<T>.IsReadOnly { get { return false; } } public bool Remove(T item) { int index = internalList.IndexOf(item); bool returnValue = internalList.Remove(item); OnItemRemoved(this, new CollectionChangedEventArgs(item, index)); return returnValue; } #endregion #region IEnumerable<T> Members public IEnumerator<T> GetEnumerator() { return internalList.GetEnumerator(); } #endregion #region IEnumerable Members IEnumerator IEnumerable.GetEnumerator() { return internalList.GetEnumerator(); } #endregion #region IList Members int IList.Add(object value) { if (IsViableType(value)) { Add((T)value); return internalList.IndexOf((T)value); } return -1; } bool IList.Contains(object value) { if (IsViableType(value)) { return Contains((T)value); } return false; } int IList.IndexOf(object value) { if(IsViableType(value)) return internalList.IndexOf((T)value); return -1; } void IList.Insert(int index, object value) { if (IsViableType(value)) Insert(index, (T)value); } bool IList.IsFixedSize { get { return false; } } void IList.Remove(object value) { if (IsViableType(value)) Remove((T)value); } object IList.this[int index] { get { return internalList[index]; } set { if (value is T && value != null) internalList[index] = (T)value; } } bool IList.IsReadOnly { get { return false; } } #endregion #region ICollection Members void System.Collections.ICollection.CopyTo(Array array, int index) { throw new NotImplementedException(); } bool System.Collections.ICollection.IsSynchronized { get { return false; } } object System.Collections.ICollection.SyncRoot { get { return null; } } #endregion } |
And there you have it, a collection that fires events when an item is added or removed. In the future, I plan to clean this class up. I decided to include the non-generic interfaces only because the .NET Framework’s List
Having taken that route already, allow me to make some suggestions
Is there any reason why you avoid generics in that code? I would hate to have to downcast in the event handler. Especially since with this design, if I later changed the collection’s type, those downcasts would still happily compile and only fail a runtime.
You also don’t seem to derive from System.Collections.ObjectModel.Collection (which is made for this purpose and would implement IList, ICollection, IEnumerable, IList, ICollection and IEnumerable for you)
Supplying an index *and* the object seems redundant. Such a talkative interface limits the concept to indexable collections. I have had a use for observable dictionaries in the past
Here’s my hammer for the same nail:
https://devel.nuclex.org/framework/browser/Nuclex.Support/trunk/Source/Collections/ItemEventArgs.cs
https://devel.nuclex.org/framework/browser/Nuclex.Support/trunk/Source/Collections/ObservableCollection.cs
https://devel.nuclex.org/framework/browser/Nuclex.Support/trunk/Source/Collections/ObservableDictionary.cs
I am assuming that you are speaking about the event portion of the code when you talk about avoiding generics? The reason for this is that a single event handler couldn’t listen to multiple lists. It is a very small and insignificant reason, I know, but it also allows for complete coverage of use cases.
As for deriving from System…Collection – I honestly didn’t know it existed. I will have to double check to see if this object is available on the 360 and Zune HD because Reflector seems to be having issues reflecting on them.
The reason for the index and the object is because originally this interface was modeled after the INotifyCollectionChanged interface, which supplies both the index and the object as well as another parameter telling how the collection was changed. I am hoping that with the release of .NET 4.0, this interface will be better placed in the framework’s libraries such that I can use it. This will give me interoperability with other .NET software.
Again, the fact that the index exists does not limit the use to index based collections but does supply complete coverage which is an overriding goal for FGF. The point here is that if it is needed or can be used, the index parameter is there. If it weren’t there than the use would be limited to non-indexable collections.
Having said all that, your implementation seems perfectly reasonable and is a good way of approaching the situation.