Reputation: 15335
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.
Upvotes: 2
Views: 819
Reputation: 46044
...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.
Upvotes: 4
Reputation: 44298
you can also put a callback on Cat so when its Name property changes your collection gets notified
Upvotes: 1
Reputation: 17775
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!
Upvotes: 4