JSEDLAK » Blog Archive » FGF: A Collection Class with Events

FGF: A Collection Class with Events

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.

?View Code CSHARP
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.

?View Code CSHARP
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.

?View Code CSHARP
1
2
3
4
5
public interface ICollection
{
    event CollectionChangedHandler ItemAdded;
    event CollectionChangedHandler ItemRemoved;
}

When combined with the .NET Framework’s List class, a powerful collection class is created.

?View Code CSHARP
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.

?View Code CSHARP
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 class has implemented them.

2 Responses to “FGF: A Collection Class with Events”

  1. Markus Ewald says:

    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: :P
    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

  2. John Sedlak says:

    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.

Leave a Reply