Introduction
The classes provided in this article illustrate how to create a custom collection similar to the System.Collections.SortedList
collection, but the collection created here will sort items by value
instead of by key
like the SortedList
.
Background
In one of my major projects, I created a custom field control capable of rendering actual ASP.NET controls based on its fieldtype
property. One of the field-types is dropdown
which renders a combo box that is bound to a SortedList
for its values.
This implementation worked fairly well but it soon became obvious that using a SortedList
was not the best choice since elements displayed in the combo box are sorted by key
instead of by value
.
To solve this problem, I searched through MSDN for a solution and did some Googling. I found out that I was not the only person with this problem!
Nowhere did I find a good solution to my problem, but found a good article written by Marc Clifton which I used as a roadmap for developing the collection shown in this article.
Using the code
The LookupCollection
is used exactly like the SortedList
provided by Microsoft but, as mentioned in the introduction, it will sort items by value
instead of by key
. The main purpose I use this collection for is to serve as a data source for a DropDownList
on an ASP.NET page, as shown in the code snippet below:
LookupCollection collection = new LookupCollection();
collection.Clear();
collection.Add("002", "Hertzogville");
collection.Add("005", "Bloemfontein");
collection.Add("HER", "Herman");
collection.Add("001", "Kimberley");
collection.Add("012", "Bothaville");
collection.Add("HAN", "Hannes");
this.lstResults.DataSource = collection;
this.lstResults.DataValueField = "key";
this.lstResults.DataTextField = "value";
this.lstResults.DataBind();
Unit Test
Perhaps a more thorough illustration on how to use the code can be seen by examining the Unit Test class developed with NUnit.
using System;
using System.Collections;
using NUnit.Framework;
namespace CustomCollection {
[TestFixture]
public class LookupCollectionTest {
private LookupCollection collection;
[SetUp]
public void Init() {
this.collection = new LookupCollection();
}
[Test]
public void TestAdd() {
this.collection.Clear();
this.collection.Add("002", "Hertzogville");
this.collection.Add("005", "Bloemfontein");
this.collection.Add("001", "Kimberley");
this.collection.Add("FRA", "Francisca");
Assert.AreEqual(4, this.collection.Count);
this.collection.Clear();
Assert.AreEqual(0, this.collection.Count);
}
[Test]
public void TestClear() {
this.collection.Add("T1", "Test");
this.collection.Add("TEST", "ATEST");
this.collection.Add("1", "1");
this.collection.Clear();
Assert.AreEqual(0, this.collection.Count);
}
[Test]
public void TestContains() {
this.collection.Clear();
this.collection.Add("HER", "Herman");
this.collection.Add("HAN", "Hannes");
this.collection.Add("ELA", "Elaine");
this.collection.Add("FRA", "Francisca");
this.collection.Add("002", "Hertzogville");
this.collection.Add("005", "Bloemfontein");
Assert.IsTrue(this.collection.Contains("FRA"));
Assert.IsTrue(this.collection.Contains("HAN"));
Assert.IsTrue(this.collection.Contains("002"));
Assert.IsFalse(this.collection.Contains("NON"));
}
[Test]
public void TestCopyTo() {
this.collection.Clear();
this.collection.Add("HER", "Herman");
this.collection.Add("HAN", "Hannes");
this.collection.Add("ELA", "Elaine");
this.collection.Add("FRA", "Francisca");
Lookup[] testArray = new Lookup[4];
Assert.IsNull(testArray[0]);
Assert.IsNull(testArray[3]);
this.collection.CopyTo(testArray, 0);
Assert.IsNotNull(testArray[0]);
Assert.IsNotNull(testArray[3]);
}
[Test]
public void TestGetEnumerator() {
this.collection.Clear();
this.collection.Add("002", "Hertzogville");
this.collection.Add("005", "Bloemfontein");
this.collection.Add("HER", "Herman");
this.collection.Add("001", "Kimberley");
this.collection.Add("012", "Bothaville");
this.collection.Add("HAN", "Hannes");
IDictionaryEnumerator enumurator = this.collection.GetEnumerator();
enumurator.MoveNext(); Assert.AreEqual("Bloemfontein",
((Lookup)enumurator.Current).Value);
enumurator.MoveNext(); Assert.AreEqual("Bothaville",
((Lookup)enumurator.Current).Value);
enumurator.MoveNext(); Assert.AreEqual("Hannes",
((Lookup)enumurator.Current).Value);
enumurator.MoveNext(); Assert.AreEqual("Herman",
((Lookup)enumurator.Current).Value);
enumurator.MoveNext(); Assert.AreEqual("Hertzogville",
((Lookup)enumurator.Current).Value);
enumurator.MoveNext(); Assert.AreEqual("Kimberley",
((Lookup)enumurator.Current).Value);
}
[Test]
public void TestRemove() {
this.collection.Clear();
this.collection.Add("002", "Hertzogville");
this.collection.Add("005", "Bloemfontein");
this.collection.Add("001", "Kimberley");
this.collection.Remove("005");
Assert.AreEqual(2, this.collection.Count);
this.collection.Remove("004");
Assert.AreEqual(2, this.collection.Count);
this.collection.Remove("002");
Assert.AreEqual(1, this.collection.Count);
}
[Test]
public void TestCount() {
this.collection.Clear();
Assert.AreEqual(this.collection.Count, 0);
for (int i = 0; i < 100; i++) {
this.collection.Add(i.ToString(), i + " Item");
}
Assert.AreEqual(this.collection.Count, 100);
}
[Test]
public void TestIndexer() {
this.collection.Clear();
this.collection.Add("002", "Hertzogville");
this.collection.Add("005", "Bloemfontein");
this.collection.Add("001", "Kimberley");
this.collection.Add("012", "Bothaville");
Assert.IsTrue(this.collection.Contains("001"));
Assert.AreEqual("Kimberley", this.collection["001"]);
Assert.IsTrue(this.collection.Contains("002"));
Assert.AreEqual("Hertzogville", this.collection["002"]);
}
[Test]
public void TestKeys() {
this.collection.Clear();
this.collection.Add("002", "Hertzogville");
this.collection.Add("005", "Bloemfontein");
this.collection.Add("HER", "Herman");
this.collection.Add("001", "Kimberley");
this.collection.Add("012", "Bothaville");
this.collection.Add("HAN", "Hannes");
ArrayList keys = (ArrayList)this.collection.Keys;
Assert.AreEqual("005", keys[0]);
Assert.AreEqual("012", keys[1]);
Assert.AreEqual("HAN", keys[2]);
Assert.AreEqual("HER", keys[3]);
Assert.AreEqual("002", keys[4]);
Assert.AreEqual("001", keys[5]);
}
[Test]
public void TestValues() {
this.collection.Clear();
this.collection.Add("002", "Hertzogville");
this.collection.Add("005", "Bloemfontein");
this.collection.Add("HER", "Herman");
this.collection.Add("001", "Kimberley");
this.collection.Add("012", "Bothaville");
this.collection.Add("HAN", "Hannes");
ArrayList values = (ArrayList)this.collection.Values;
Assert.AreEqual("Bloemfontein", values[0]);
Assert.AreEqual("Bothaville", values[1]);
Assert.AreEqual("Hannes", values[2]);
Assert.AreEqual("Herman", values[3]);
Assert.AreEqual("Hertzogville", values[4]);
Assert.AreEqual("Kimberley", values[5]);
}
}
}
Lookup
The Lookup
class illustrated below is used to store a key-value pair in the LookupCollection
, I do however think (hope) that some smart programmer will have something to say about the implementation of ToDictionaryEntry
and CompareTo
, and can't wait for feedback!
using System;
using System.Collections;
namespace CustomCollection {
[Serializable]
public class Lookup : IComparable {
private object mKey;
private object mValue;
public Lookup() : this(null, null) {
}
public Lookup(object key, object value) {
this.Key = key;
this.Value = value;
}
public int CompareTo(object obj) {
int result = 0;
if (obj is Lookup) {
result =
((IComparable)this.Value).CompareTo((IComparable)
(((Lookup)obj).Value));
}
return result;
}
public DictionaryEntry ToDictionaryEntry() {
return new DictionaryEntry(this.Key, this.Value);
}
public object Key {
get {
return this.mKey;
}
set {
if (this.mKey != value) {
this.mKey = value;
}
}
}
public object Value {
get {
return this.mValue;
}
set {
if (this.mValue != value) {
this.mValue = value;
}
}
}
}
}
Enumerator
The enumerator class is used by the LookupCollection
class to create an enumerator that will allow programmers to use the foreach
loop on this collection.
using System;
using System.Collections;
namespace CustomCollection {
public class LookupEnumerator : IDictionaryEnumerator {
private int index = -1;
private ArrayList items;
public LookupEnumerator(ArrayList list) {
this.items = list;
}
public bool MoveNext() {
this.index++;
if (index >= this.items.Count)
return false;
return true;
}
public void Reset() {
this.index = -1;
}
public object Current {
get {
if (this.index < 0 || index >= this.items.Count)
throw new InvalidOperationException();
return this.items[index];
}
}
public DictionaryEntry Entry {
get {
return ((Lookup)this.Current).ToDictionaryEntry();
}
}
public object Key {
get {
return this.Entry.Key;
}
}
public object Value {
get {
return this.Entry.Value;
}
}
}
}
LookupCollection
This is the actual class that implements the collection that this article is all about. As can be seen from the code, dictionary entries added to this collection are stored internally in an ArrayList
of Lookup
items.
using System;
using System.Collections;
namespace CustomCollection {
[Serializable]
public class LookupCollection : ICollection, IDictionary, IEnumerable {
private ArrayList mItems = new ArrayList();
public LookupCollection() {
}
public void Add(object key, object value) {
if (key == null)
throw new ArgumentNullException("key is a null reference");
else if (this.Contains(key))
throw new
ArgumentException("An element with the same key already exists");
Lookup newItem = new Lookup();
newItem.Key = key;
newItem.Value = value;
this.mItems.Add(newItem);
this.mItems.Sort();
}
public void Clear() {
this.mItems.Clear();
}
public bool Contains(object key) {
return (this.GetByKey(key) != null);
}
public void CopyTo(Array array, int index) {
this.mItems.CopyTo(array, index);
}
public IDictionaryEnumerator GetEnumerator() {
return new LookupEnumerator(this.mItems);
}
IEnumerator IEnumerable.GetEnumerator() {
return new LookupEnumerator(this.mItems);
}
public void Remove(object key) {
if (key == null)
throw new ArgumentNullException("key is a null reference");
Lookup deleteItem = this.GetByKey(key);
if (deleteItem != null) {
this.mItems.Remove(deleteItem);
this.mItems.Sort();
}
}
private Lookup GetByKey(object key) {
Lookup result = null;
int keyIndex = -1;
ArrayList keys = (ArrayList)this.Keys;
if (this.mItems.Count > 0) {
keyIndex = keys.IndexOf(key);
if (keyIndex >= 0) {
result = (Lookup)this.mItems[keyIndex];
}
}
return result;
}
public int Count {
get {
return this.mItems.Count;
}
}
public bool IsSynchronized {
get {
return false;
}
}
public object SyncRoot {
get {
return this;
}
}
public bool IsFixedSize {
get {
return false;
}
}
public bool IsReadOnly {
get {
return false;
}
}
public object this[object key] {
get {
if (key == null)
throw new ArgumentNullException("key is a null reference");
object result = null;
Lookup findItem = this.GetByKey(key);
if (findItem != null) {
result = findItem.Value;
}
return result;
}
set {
}
}
public ICollection Keys {
get {
ArrayList result = new ArrayList();
this.mItems.Sort();
foreach (Lookup curItem in this.mItems) {
result.Add(curItem.Key);
}
return result;
}
}
public ICollection Values {
get {
ArrayList result = new ArrayList();
foreach (Lookup curItem in this.mItems) {
result.Add(curItem.Value);
}
return result;
}
}
}
}
Conclusion
Although I am quite sure that my implementation is not the best one, I hope that you find these classes at least somewhat useful, or that they will help to point you in the right direction.