Click here to Skip to main content
15,867,453 members
Articles / Programming Languages / C#

Indexed Dictionary

Rate me:
Please Sign up or sign in to vote.
3.79/5 (9 votes)
4 Jul 2009CPOL6 min read 85.4K   668   43   24
An article on a generic collection accessible both as a dictionary and as an indexed list.

Introduction

This code provides a generic collection of key-value pairs where the values can also be accessed by index. The collection also exposes events occurring when items are added or removed.

Background

I came across the need for such a collection developing an infrastructure based on collections of uniquely identified objects (the application contains forms, the forms contains items etc.). The collections had to be accessible and iterateable with and without unique ID's. Searching the web I came across a collection by Brad Vincent which can be found in a post in his blog here. I made some fixes and extensions and the result is presented here.

Using the Code

Using the Dictionary

The IndexedDictionary class is initiated with two types, the first of which is the key type and the second is the value type:

C#
IndexedDictionary<string, int> dic = new IndexedDictionary<string, int>()

Values may be added using the Add function:

C#
    dic.Add("key1", 10);
dic.Add("key2", 20);

Or using the AddAt function which will insert the value at a specific location:

C#
dic.AddAt(1, "key1.5", 15);

The Remove function removes an item by key:

C#
dic.Remove("key1.5");

While the RemoveAt function removes by index:

C#
dic.RemoveAt(0);

The collection may be accessed by key:

C#
string s1 = dic["key2"];

Or by index:

C#
string s2 = dic[0];

The collection can be iterated using foreach loops on the keys collection or on the values collection:

C#
// Iterate by key:
foreach (string key in dic.Keys)
{
    sum += dic[key];
}

// Iterate by value:
foreach (string value in dic.Values)
{
    sum += value;
}

It can also be iterated with a for loop using the index (something you cannot do with a normal dictionary):

C#
// Iterate by index:
for (int i = 0; i < dic.Count; i++)
{
    sum += dic[i];
}

You may also get the key in a specified index:

C#
string key = dic.GetKeyByIndex(1);

You may also use a sutiable predicate to get the index of a given key:

C#
int index = dic.GetIndex(s => s == KEY_STRING + testParam);

The collection cannot be initiated with a key of type int, because there would be no definitive answer to the question how dic[i] should pick it's value — by key or by index (this is probably why you don't get this kind of collection from Microsoft, not clean enough...). Trying to do so will yield an exception.

Using the Events

I found the collections I used heavily lacking events occurring when items are added and removed and for when thy are cleared. This functionality opens a great big world for the programmer, especially when working with dictated collection (dictated by an infrastructure, for instance). The IndexedDictionary class exposes six such events, two for each of the following: Add, Remove and Clear. The events occur before and after each of the actions (and are so named: BeforeAdd, AfterAdd etc.)

To use the events just hook up to them. The functions and the event arguments are generic (using the dictionary's types) so it is best to let the visual studio create the method stub for you:

C#
dic.BeforeAdd += new DictionaryBeforeDelegate<string, int>(dic_BeforeAdd);

static void dic_BeforeAdd(object sender, DictionaryBeforeEventArgs<string, int> e)
{
    throw new Exception("The method or operation is not implemented.");
}

Included in the event argument you would find the key and the value of the item in question (except for the clear events, of course). These can be manipulated if necessary. You can also get the collection itself by casting the sender paremater:

C#
static void dic_BeforeAdd(object sender, DictionaryBeforeEventArgs<string, int> e)
{
    string key = e.Key;
    e.Value = "Cahnged value";
    int i = ((IndexedDictionary<string, int>)sender).Count;
}

When handling a before event (such as BeforeAdd), the EventArgs include a boolean property named Bubble. If this property is set to false the action is cancelled (this is an older style version of the e.Cancel Microsoft is using today).

C#
static void dic_BeforeAdd(object sender, DictionaryBeforeEventArgs<string, int> e)
{
    // Cancel the add operation:
    e.Bubble = false;
}

Other Features

ThrowErrorOnInvalidRemove Property

The IndexedDictionary contains an immutable property (set only in the constructor) named ThrowErrorOnInvalidRemove. This property determines whether or not the collection will throw an exception when a non-existing key is removed.

ReplaceDuplicateKeys

The IndexedDictionary contains an immutable property (set only in the constructor) named ReplaceDuplicateKeys. This property determines whether or not the collection will throw an exception when an item is added with a key already used in the collection.

Points of Interest

The Inner Works

This collection is based on a dictionary and contains an inner list of the dictionary keys (named m_col) which provides it with its list qualities. The Add, Remove and Clear functions are hidden (using the new keyword) by new functions that take care of the list as well as delegating the work to the dictionary and raising the events. This way, all the goodies included in the MS generic dictionary are provided free of charge.

The Inner Classes

The IndexedDictionary class includes the definitions of the delegates and event argument classes used by the events. These definitions are all genreic and require the type to be appeded onto them during construction. This is required since the event args are supposed to carry the strongly typed key and value and they must match the types defined for the specific dictionary.

I find it amusing to write those '+=' and get the function with its generic definitions intact (I sometimes do it just for the fun of it).

The Other Options

The following is free advertisement for the competition (your alternatives):

System.Collections.Specialized.NameObjectCollectionBase is a dictionary base with a string only key and an object value that can be accessed by index. This style (and namespace) of collection was used in .NET 1.0, before generics came along.

It is abstract so to use it you would have to inherit it, and you would have an interesting time figuring out the way to access the values in the hash table hiding inside.

I, for once, think using a non-type-safe collection is unacceptable today.

System.Collection.ObjectModel.KeyedCollection is an abstract generic collection (generic both in key and value) accessible by index, with an interesting twist: It is also abstract and to use is you must provide a function that extracts the key from the item (TKey GetKeyForItem(TValue)).

This collection is in fact a list of items containing both the key and the value of a dictionary entry. This collection also does not support int as an index.

Rune Baess offers here, at CodeProject, an already implemented collection based on the System.Collection.ObjectModel.KeyedCollection, where you supply the function as a delegate during initialization, by implementing an interface on the items class, or by decorating a predesigned property on the items class with an attribute. Check it out.

Loose Ends

  • Add and Remove events should contain the index of the item being added or removed. Implementing that one turned out to be a bit of a pain so I left it out — feel free to take over.
  • The Microsoft dictionary supports adding values by assigning them to a new key:
    C#
    dic[newKey] = newValue;
    

    In the case of our IndexedDictionary, this should have also added the value into the list. For now, it just throws an exception.

  • Using some numeric types as keys might prove to be problematic. Consider the following coed:
    C#
        IndexedDictionary<double, string> dic = new IndexedDictionary<double, string>()
    dic.Add(1, "TEST1");
    dic.Add(0, "TEST0");

    Should dic[0] be "TEST1" or "TEST0"? This can be solved by further limiting the allowed key types (which is a bit crude), or someone else might have a better idea.

History

July 2009 -

  • Method "GetIndex" added at the request of Reimer van Elst.
  • Project converted to VSTO 2008.
  • Test Project added.

Janurary 2009 - Method "GetKeyByIndex" added at the request of bbender.

November 2008 - This is the first version of this article (and my first here). I would like to thank Brad Vincent again for his work, and for allowing me to post it here.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Chief Technology Officer Ziv systems, Israel
Israel Israel
Starting with Apple IIe BASICA, and working my way through Pascal, Power Builder, Visual basic (and the light office VBA) C, C++, I am now a full stack developer and development manager. Mostly with MS technologies on the server side and javascript(typescript) frameworks on the client side.

Comments and Discussions

 
SuggestionToList functionality Pin
Member 134899132-Dec-20 0:23
Member 134899132-Dec-20 0:23 
GeneralApplicationException changes Pin
Dewey5-Jul-09 9:59
Dewey5-Jul-09 9:59 
GeneralRe: ApplicationException changes Pin
Asher Barak5-Jul-09 19:44
professionalAsher Barak5-Jul-09 19:44 
GeneralRe: ApplicationException changes Pin
Dewey8-Jul-09 0:04
Dewey8-Jul-09 0:04 
GeneralRe: ApplicationException changes Pin
Asher Barak8-Jul-09 3:52
professionalAsher Barak8-Jul-09 3:52 
GeneralTest Project Pin
Dewey5-Jul-09 9:39
Dewey5-Jul-09 9:39 
GeneralRe: Test Project Pin
Asher Barak5-Jul-09 19:51
professionalAsher Barak5-Jul-09 19:51 
GeneralGetIndexByKey Pin
Reimer van Elst26-Jun-09 20:48
Reimer van Elst26-Jun-09 20:48 
GeneralRe: GetIndexByKey Pin
Asher Barak30-Jun-09 0:28
professionalAsher Barak30-Jun-09 0:28 
GeneralIsn't this similar to LinkedList class Pin
SyntaxC421-Jan-09 18:47
SyntaxC421-Jan-09 18:47 
AnswerRe: Isn't this similar to LinkedList class Pin
Asher Barak21-Jan-09 20:37
professionalAsher Barak21-Jan-09 20:37 
Generalignore case for string key Pin
Huisheng Chen20-Jan-09 18:49
Huisheng Chen20-Jan-09 18:49 
GeneralRe: ignore case for string key Pin
PIEBALDconsult21-Jan-09 3:30
mvePIEBALDconsult21-Jan-09 3:30 
AnswerRe: ignore case for string key Pin
Asher Barak21-Jan-09 8:28
professionalAsher Barak21-Jan-09 8:28 
GeneralI like it Pin
Huisheng Chen20-Jan-09 18:36
Huisheng Chen20-Jan-09 18:36 
GeneralRe: I like it Pin
Asher Barak21-Jan-09 8:30
professionalAsher Barak21-Jan-09 8:30 
GeneralOh, and Pin
PIEBALDconsult20-Jan-09 13:06
mvePIEBALDconsult20-Jan-09 13:06 
GeneralUh oh Pin
PIEBALDconsult20-Jan-09 12:47
mvePIEBALDconsult20-Jan-09 12:47 
AnswerRe: Uh oh Pin
Asher Barak21-Jan-09 8:54
professionalAsher Barak21-Jan-09 8:54 
GeneralAnother approach Pin
Alexander Turlov11-Nov-08 10:29
Alexander Turlov11-Nov-08 10:29 
GeneralRe: Another approach Pin
Paul B.11-Nov-08 11:06
Paul B.11-Nov-08 11:06 
AnswerRe: Another approach Pin
Asher Barak11-Nov-08 17:56
professionalAsher Barak11-Nov-08 17:56 
GeneralRe: Another approach Pin
mariahayek23-Nov-08 11:31
mariahayek23-Nov-08 11:31 
GeneralRe: Another approach Pin
Bendage15-Jan-09 6:46
Bendage15-Jan-09 6:46 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.