Neatly tucked away in the System.Collections.ObjectModel namespace is the KeyedCollection<TKey, TItem> class. This is a very cool abstract class which gives you both built-in indexing and dictionary access. All you have to do is override the relevant methods and you have your own custom collection.
The key thing (pardon the pun) to understand with this class is that the key must be a member of its corresponding item. I will clarify that with an example. In this example, I am going to create a class called IncrementAspxCollection which inherits from the KeyedCollection abstract class. The basic idea for this class will be that it will track users’ movements when they visit a web site.
First, I will create a class which will represent a page-visit by a user:
public class PageVisit { private string page; private DateTime timeOfVisit; public string Page { get { return page; } set { page = value; } } public DateTime TimeOfVisit { get { return timeOfVisit; } set { timeOfVisit = value; } } public PageVisit(string page, DateTime timeOfVisit) { this.page = page; this.timeOfVisit = timeOfVisit; } public override string ToString() { return timeOfVisit.Hour.ToString() ; } }
Now, my IncrementAspxCollection class will contain objects of the PageVisit type. So, the obvious question is, if the key must be a member of its corresponding item, how will the collection know which member of that item to use as the key? This is easily done by overriding the KeyedCollection’s GetKeyForItem method:
protected override string GetKeyForItem(PageVisit item) { return item.Page; }
There, I made the Page property of the PageVisit class the key for my new custom collection. I am now free to override the protected ClearItems, InsertItem, RemoveItem, and SetItem methods of the abstract KeyedCollection class.
For my limited purposes, I only overrode the InsertItem method. In doing so, I wanted to create a method which would add pages, to the collection as users load them. I also wanted to add numbering to the pages, so that if a user went back to a page during the same session, its number would increment. For example, if the user loaded default.aspx, the first entry in the collection would be default1.aspx. If that page was loaded again in the same session, it’s key would be default2.aspx etc. Implementing this was quite a lot of fun, in that it gave me an opportunity to use regular expressions; a very powerful string parsing technique.
protected override void InsertItem(int index, PageVisit item) { int count = 1; Match m2 = Regex.Match(item.Page, @"(?<first>\D+)(?<second>\d*)\.aspx"); string pageNameSansExt = m2.Groups["first"].ToString(); // Check to see if this is not the first item to be inserted. if (base.Dictionary != null) { IDictionary collDict = base.Dictionary; IEnumerator enumTor = collDict.Keys.GetEnumerator(); while (enumTor.MoveNext()) { Match m1 = Regex.Match(enumTor.Current, @"(?<first>\w+)(?<second>\d+)\.aspx"); string origPageStr = m1.Groups["first"].ToString(); if (origPageStr.CompareTo(pageNameSansExt) == 0) { count++; } } } // If a page has already been inserted into the collection, increment the number // appended to its name. if (count > 1) { item.Page = string.Concat(pageNameSansExt, string.Concat(count.ToString(), ".aspx")); base.InsertItem(index, item); } // If this is the first instance of the page, append '1' to its name. else { item.Page = string.Concat(pageNameSansExt, "1.aspx"); base.InsertItem(index, item); } }
I recently tested this out on a Web application that I was developing and the log file looked like this:
itwjaf45l35era55oqrfxt55, default1.aspx, 27/08/2009 12:10:12 AM itwjaf45l35era55oqrfxt55, contact1.aspx, 27/08/2009 12:10:14 AM itwjaf45l35era55oqrfxt55, contact2.aspx, 27/08/2009 12:10:15 AM itwjaf45l35era55oqrfxt55, contact3.aspx, 27/08/2009 12:10:15 AM itwjaf45l35era55oqrfxt55, default2.aspx, 27/08/2009 12:10:16 AM itwjaf45l35era55oqrfxt55, default3.aspx, 27/08/2009 12:10:16 AM itwjaf45l35era55oqrfxt55, default4.aspx, 27/08/2009 12:10:17 AM itwjaf45l35era55oqrfxt55, default5.aspx, 27/08/2009 12:10:17 AM
The long, random string at the start of each line was the session ID for the user loading the pages.
So there we have a groovy, handy custom collection that fit my needs perfectly. And it was easily put together using the existing functionality (plus a little custom finesse) of a very cool abstract class. Feel free to download and play with my IncrementAspxCollection class.