2

I want to declare a generic collection of objects and be able to access them through the indexer either by a key string value or by index. How do I do this? Is there is an out of the box .Net class that doesn't require sub-classing?

class Program
{

    static void Main(string[] args)
    {

        System.Collections.Generic.WhatKindOfCollection<PageTab> myPageTabs 
            = new System.Collections.Generic.WhatKindOfCollection<PageTab>();

        PageTab pageTab1 = new PageTab();
        pageTab1.ID = "tab1";
        myPageTabs.Add(pageTab1);

        myPageTabs.Add(new PageTab("tab2"));

        myPageTabs[0].label = "First Tab";
        myPageTabs["tab2"].label = "Second Tab";

    }

    public class PageTab
    {
        public PageTab(string id)
        {
            this.ID = id;
        }

        public PageTab() { }


        //Can I define ID to get the key property by default?
        public string ID { get; set; }

        public string label { get; set; }
        public bool visible { get; set; }
    }
}
Walter
  • 105
  • 1
  • 1
  • 5
  • What you are looking for is a cross between a property dictionary and a list. A collection like that is not available out of the box. Building it is not rocket science, but it's not a matter of five..ten lines of code. – Sergey Kalinichenko Aug 17 '12 at 01:19

3 Answers3

3

It looks like you're looking for something derived from System.Collections.ObjectModel.KeyedCollections.

I don't think that the specific class you're looking for exists in the .NET framework, so you'll probably have to subclass it yourself.

KeyedCollection is a base class for objects where the key is part of the object. This means that when you access it with an integer index, you'll get back the original object instead of a KeyValueCollection.

It's been a while since I've used it, but I don't remember it being too difficult.

Edit: Another code option for you. It was easier than I remember:

public class MyKeyedCollection<TKey, TItem> : KeyedCollection<TKey, TItem>
{
    public MyKeyedCollection(Func<TItem, TKey> keyFunction)
    {
        _keyFunction = keyFunction;
    }

    private Func<TItem, TKey> _keyFunction;

    protected override TKey GetKeyForItem(TItem item)
    {
        return _keyFunction(item);
    }
}

To use:

var myPageTabs = new MyKeyedCollection<String, PageTab>(i => i.ID);

Or pre-LINQ:

public class MyKeyedCollection<TKey, TItem> : KeyedCollection<TKey, TItem>
{
    public MyKeyedCollection(String keyProperty)
    {
        _keyProperty = keyProperty;
    }

    private String _keyProperty;

    protected override TKey GetKeyForItem(TItem item)
    {
        return (TKey)item.GetType().GetProperty(_keyProperty).GetValue(item, null);
    }
}

and

MyKeyedCollection<String, PageTab> myPageTabs = new MyKeyedCollection<String, PageTab>("ID");
TheEvilPenguin
  • 5,634
  • 1
  • 26
  • 47
  • I'm getting the compile error 'The type or namespace name 'Func' could not be found (are you missing a using directive or an assembly reference?)' – Walter Aug 17 '12 at 02:04
  • If you're using .NET 3.5 up, it should be in System. If not, I'll have to edit it slightly to use reflection instead of predicate syntax. – TheEvilPenguin Aug 17 '12 at 02:06
  • I'm using 3.5 but was building off an old project that targeted 2.0. I changed the target framework 3.5. – Walter Aug 17 '12 at 02:10
  • Make sure you're referencing System.Core in that case. It may have other prerequisites, but when you add System.Core it'll tell you about them if you don't already have them. – TheEvilPenguin Aug 17 '12 at 02:13
  • I wish there was something out of the box and I didn't have to create the derived class but this works exactly the way I want it to. Thank you! I have to admit I don't quite understand everything that's going on here. – Walter Aug 17 '12 at 02:26
  • The Func part stores a function which takes one argument of type TItem, and returns an object of type TKey. This function is defined by i => i.ID, where 'i' names the one argument and i.ID is the actual function code, returning TKey (String, in this case). Whenever something is added to a KeyedCollection, it calls this GetKeyForItem method to get the key, and handles the rest internally. If this helped, would you mind marking it as answered? – TheEvilPenguin Aug 17 '12 at 02:32
  • This is a very elegant well done solution. Thanks for the response. – Walter Aug 17 '12 at 19:58
2

This is effectively the OrderedDictionary class. However, it is, unfortunately, not a generic class, so you'd have to include casts, or wrap it in your own collection type.

There is no generic equivelent in the base class libraries, though KeyedCollection<T,U> provides the base class infrastructure to implement your own version.

The simplest alternative is just to maintain two collections - a Dictionary<string, PageTab> and a List<PageTab>. When you create your items, you can add it to both collections, and access via the appropriate one. Since PageTab is a class, the extra overhead is minimal (since you're just storing object references). This could also be easily wrapped into a class:

public class IndexedDictionary<T, U>
{
    private Dictionary<T,U> dictionary = new Dictionary<T,U>();
    private List<U> list = new List<U>();

    public void Add(T key, U value)
    {
        list.Add(value);
        dictionary.Add(key, value);
    }

    public U this[int index]
    {
       get { return list[index]; }
    }
    public U this[T key]
    {
       get { return dictionary[key]; }
    }
}

Granted, you'd potentially want to implement some appropriate interfaces as well (such as IEnumerable<U>), but the above would accomplish your goals as listed in the question.

Community
  • 1
  • 1
Reed Copsey
  • 554,122
  • 78
  • 1,158
  • 1,373
  • This sounds like a waste of RAM. I would just use a 2D array and a loop to find it by key. But if it is small enough, this is fine IMO – Cole Tobin Aug 17 '12 at 01:26
  • 2
    @ColeJohnson It depends - if lookups are frequent, this gives you O(1) lookups by either method, so it could be **very** beneficial with large collections. As long as `U` is a reference type, it's not too bad. – Reed Copsey Aug 17 '12 at 01:27
  • @ColeJohnson Not sure how a 2D array will give you string-based lookups, though ;) – Reed Copsey Aug 17 '12 at 01:27
  • @ColeJohnson FYI - my class above is very similar to how `OrderedDictionary` (in the framework) works - though it uses `ArrayList` and `HashTable` as it pre-dates generics. – Reed Copsey Aug 17 '12 at 01:30
  • Like this `object[length, 2]` and use it like this: `arr[int, 0] = string` and `arr[int, 1] = val`. Then just enumerate through it looking to see if the second dimension index 0 is the key. – Cole Tobin Aug 17 '12 at 04:12
  • @ColeJohnson With the boxing involved, that's less efficient than my option ;) – Reed Copsey Aug 17 '12 at 16:08
  • @ReedCospey you could always have two arrays. One of type string for your keys and the other of type T for your values – Cole Tobin Aug 17 '12 at 19:37
0

Why don't you use a dictionary?

Dictionary<string, PageTab> myDictionary = new Dictionary<string, PageTab>();
myDictionary.Add("tab1", new PageTab("tab1"));
PageTab myPageTab = myDictionary["tab1"];

EDIT To avoid typing the key twice (once as dictionary key and once in the constructor) you could extend the dictionary. Create the following class in the toplevel of your namespace:

public static class Extensions
{
    public static void AddPageTab(this Dictionary<string, PageTab> mydict, PageTab pt)
    {
        mydict.Add(pt.ID, pt);
    }
}

And you call simple add the PageTab like this:

myDictionary.AddPageTab(new PageTab("tab1"));
Jan
  • 2,168
  • 2
  • 19
  • 28
  • Is there a way for the collection to know to use ID, so you don't need to specify the key when you add it to the collection? – Walter Aug 17 '12 at 01:30