views:

214

answers:

3

Let's say I have a simple class Cat in C#, with a Name property of type string. Now I need a collection class for my cats, so I decide to wrap a Dictionary<string, Cat> in a custom collection class. Basically, this class holds a private dictionary variable and adds or removes collection members as necessary, as well as indexing cats by their name:

class Cats
{
    private Dictionary<string, Cat> m_dic = new Dictionary<string,Cat>();

    public void Add(Cat c)
    {
        m_dic.Add(c.Name, c);
    }

    public void Remove(string name)
    {
        m_dic.Remove(name);
    }

    public Cat this[string name]
    {
        get
        {
            return m_dic[name];
        }
    }
}

Now I can create a collection and cats to it, like this:

Cats cs = new Cats();
cs.Add(new Cat("Valentina"));
cs.Add(new Cat("Sophie"));
cs.Add(new Cat("Tomboy"));

And I can retrieve a cat from its name:

Cat c1 = cs["Sophie"];

All this is very good. The problem is, when I change the name of a cat, like this:

c1.Name = "Sofia";

...the collection key for the object referred to by c1 is not updated, evidently. So, if I try to retrieve the same item using its new name, I get an exception:

Cat c2 = cs["Sofia"]; //KeyNotFoundException is thrown here.

This is correct and obvious behavior by the runtime. My question is: can you suggest an elegant and reliable method to alter the collection keys whenever the name property of an element changes?

My objective is to be able to retrieve, at any time, an item from its name, as you can imagine. I have approached the problem by having the setter of the Name property raise an event so that any collection holding that object can update the corresponding key. This method is quite cumbersome and not very efficient, though.

Can you think of anything better? Thank you.

+4  A: 

How large is your collection going to be, and how important is being able to retrieve an item through an index?

If it's going to be relatively small (several hundred, as opposed to thousands), you might be better off using a List<Cat>, and access them using the new LINQ extension methods, like:

public Cat this[string name]{
    get{
        //Will return the first Cat in the list (or null if none is found)
        return m_List.Where(c => c.Name == name).FirstOrDefault();
    }
}

Adding (and deleting) is also trivial:

public void Add(Cat c){
    m_List.Add(c);
}

Let me know if this will not work for you. Hope this helps!

Pwninstein
Hey, thanks. I understand that List<> is indexed by int only, so using a Where like that should be quite inefficient, right? I don't expect to have very large collections, so that may work.
CesarGon
"indexed by int" should be "indexed by position" ;p
CesarGon
It's not a terrible solution, in my opinion. Like I mentioned in my answer, the size of the collection will affect the speed of object access. Also, the frequency of access might affect performance negatively if the List is very large and you're searching for an element many times in quick succession. I'll try to think of another solution comparable to your original example!
Pwninstein
Well, thank you!
CesarGon
A: 

you can also put a callback on Cat so when its Name property changes your collection gets notified

Keith Nicholas
I explained in my question that I tried having Cat raising an event when Name is changed, and the collection responding to that event by updating the key. Is that what you mean? :-)
CesarGon
+4  A: 

...by having the setter of the Name property raise an event...

Do you mean something like this?

c1.Name = "Sofia";
NameChangedEventHandler handler = NameChangedEvent;
if (handler != null)
{
    handler(c1, new NameChangedEventArgs("Sophie", "Sophia"));
}

Is this what you mean by having the setter raise an event? If so, then I would suggest moving this to the Name property setter of the Cat class. There's no reason to require the setters to raise the event like this. It should be done implicitly when the name of the Cat changes through the public property.

To me, this is an elegant solution; it just doesn't conform to the way Dictionary collections work. I don't know that that's a problem per se, but it does tightly couple the Cats collection to the Cat class.

Keep in mind that you'll probably want to implement many of the same interfaces that the generic Dictionary class does. Otherwise, the Cats collection will resemble a Dictionary in some ways, but not fully.

EDIT: This is in response to your comment. I hope that I can more clearly convey my thoughts. My intent is to improve your design.

I agree that, in general, events do provide a looser level of coupling. However, in this case, the Cats collection is still tightly coupled with the Cat class because the collection is registering with a specific type of event exposed by a specific type of class.

So how can this be improved?

A straightforward way to improve this is to have the Cat class implement an event that is defined in an interface. .NET provides such an interface for this express purpose - the INotifyPropertyChanged interface in the System.ComponentModel namespace. By implementing this interface in the Cat class, this would allow the Cats collection wrapper to be defined like this:

class Cats
{
    private Dictionary<string, INotifyPropertyChanged> m_dic =
        new Dictionary<string, INotifyPropertyChanged>();
    public void Add(INotifyPropertyChanged obj)
    {
        m_dic.Add(obj.Name, obj);
    }
    public void Remove(string name)
    {
        m_dic.Remove(name);
    }
    public INotifyPropertyChanged this[string name]
    {
        get { return m_dic[name]; }
    }
}

See the improvement? The collection is more flexible now. It can hold any type that implements the INotifyPropertyChanged interface. In other words, it is not tied to the Cat class anymore.

However, it still has the requirement that whatever value is stored in the dictionary implement a Name property (see the Add() method), so there is still some work to be done.

Ultimately, you are wanting the collection to hold objects that provide a string property to be used as the key value. The solution is to define this as an interface as well.

public interface INotificationKey : INotifyPropertyChanged
{
    string Key { get; set; }
}

Notice that the INotificationKey interface inherits from the INotifyPropertyChanged interface, which allows the collection wrapper to be defined like this:

class NotificationDictionary
{
    private Dictionary<string, INotificationKey> m_dic =
        new Dictionary<string, INotificationKey>();
    public void Add(INotificationKey obj)
    {
        m_dic.Add(obj.Key, obj);
    }
    public void Remove(string key)
    {
        m_dic.Remove(key);
    }
    public INotificationKey this[string key]
    {
        get { return m_dic[key]; }
    }
}

This is a substantially more flexible solution. But it still falls short because it does not fully act like a Dictionary should. For example, as defined, the NotificationDictionary class cannot be used in a foreach iteration since it does not implement the IEnumerable<> interface.

To qualify as a truly elegant solution, the collection should behave like a Dictionary. This will require a little more effort on the front end, but on the back end, you'd have a solution that would be flexible enough to adapt to a variety of situations.

Matt Davis
What I did (and what I tried to explain in my question) was precisely what you suggest: raising the event from the set block in the public Name property of Cat. :-)Coupling is okay, because events, after all, don't couple classes that much. Collections are free to subscribe to events or not. This is good (low coupling) and bad too (because there is no way to force a collection to keep its keys up to date). This reason is why I don't like the approach too much; it's relatively easy for somebody to forget subscribing to the NameChanged event and consequently update the collection keys.
CesarGon
Thanks Matt. Your edit is most appreciated. The whole point of the Cats collection, though, is to have a strongly-typed container for Cat. Not only I don't mind that Cats is tightly coupled to Cat; it is so by design. Where I want them to be loosely coupled is with regard to key updates, i.e. an instance of Cat doesn't "know" what collections hold references to it, so an event-based approach (which is inherently loose-coupled) sounds good to me, in principle. Still, thanks for the idea about INotifyPropertyChanged, I didn't know about it. :-)
CesarGon