Let's take the following two classes:
public class CollectionOfChildren
{
public Child this[int index] { get; }
public void Add(Child c);
}
public class Child
{
public CollectionOfChildren Parent { get; }
}
Child's Parent property should always return the CollectionOfChildren that the Child is in, or null if the child is not in such a collection. Between these two classes, this invariant should be maintained, and should not be breakable (well, easily) by the consumer of the class.
How do you implement such a relationship? CollectionOfChildren cannot set any of the private members of Child, so how is it supposed to inform Child that it was added to the collection? (Throwing an exception is acceptable if the child is already part of a collection.)
The internal
keyword has been mentioned. I am writing a WinForms app at the moment, so everything is in the same assembly, and this is essentially no different than public
.
public class CollectionOfChildren
{
public Child this[int index] { get; }
public void Add(Child c) {
c.Parent = this;
innerCollection.Add(c);
}
}
public class Child
{
public CollectionOfChildren Parent { get; internal set; }
}
My answer contains to solutions - the first one uses nested classes to allow the inner class to access the outer class. Later I realized that there is no need to access private data of the other class, hence no need for nested classe, if the property getters and setters are designed carefully to avoid infinite indirect recursions.
To avoid the problem with internal
fields you can just nest the collection class into the item class and make the field private
. The following code is not exactly what you requestes, but shows how to create a one-to-many relationship and keep it consistent. An Item
may have one parent and many children. If and only if an item has a parent, then it will be in the child collection of the parent. I wrote the code adhoc without testing, but I think there is no way to break this from outsight of the Item
class.
public class Item
{
public Item() { }
public Item(Item parent)
{
// Use Parent property instead of parent field.
this.Parent = parent;
}
public ItemCollection Children
{
get { return this.children; }
}
private readonly ItemCollection children = new ItemCollection(this);
public Item Parent
{
get { return this.parent; }
set
{
if (this.parent != null)
{
this.parent.Children.Remove(this);
}
if (value != null)
{
value.Children.Add(this);
}
}
}
private Item parent = null;
The ItemCollection
class is nested inside the Item
class to gain access to the private field parent
.
public class ItemCollection
{
public ItemCollection(Item parent)
{
this.parent = parent;
}
private readonly Item parent = null;
private readonly List<Item> items = new List<Item>();
public Item this[Int32 index]
{
get { return this.items[index]; }
}
public void Add(Item item)
{
if (!this.items.Contains(item))
{
this.items.Add(item);
item.parent = this.parent;
}
}
public void Remove(Item item)
{
if (this.items.Contains(item))
{
this.items.Remove(item);
item.parent = null;
}
}
}
}
UPDATE
I checked the code now (but only roughly) and I believe it will work without nesting the classes, but I am not yet absolutly sure. It's all about using the Item.Parent
property without causing an infinite loop, but the checks that were already in there and the one I added for efficency protect from this situation - at least I believe it.
public class Item
{
// Constructor for an item without a parent.
public Item() { }
// Constructor for an item with a parent.
public Item(Item parent)
{
// Use Parent property instead of parent field.
this.Parent = parent;
}
public ItemCollection Children
{
get { return this.children; }
}
private readonly ItemCollection children = new ItemCollection(this);
The important part is the Parent
property that will trigger the update of the parent's child collection and prevent from entering a infinte loop.
public Item Parent
{
get { return this.parent; }
set
{
if (this.parent != value)
{
// Update the parent field before modifing the child
// collections to fail the test this.parent != value
// when the child collection accesses this property.
// Keep a copy of the old parent for removing this
// item from its child collection.
Item oldParent = this.parent;
this.parent = value;
if (oldParent != null)
{
oldParent.Children.Remove(this);
}
if (value != null)
{
value.Children.Add(this);
}
}
}
}
private Item parent = null;
}
The the important parts of the ItemCollection
class are the private parent
field that makes the item collection aware of its owner and the Add()
and Remove()
methods that trigger updates of the Parent
property of the added or removed item.
public class ItemCollection
{
public ItemCollection(Item parent)
{
this.parent = parent;
}
private readonly Item parent = null;
private readonly List<Item> items = new List<Item>();
public Item this[Int32 index]
{
get { return this.items[index]; }
}
public void Add(Item item)
{
if (!this.items.Contains(item))
{
this.items.Add(item);
item.Parent = this.parent;
}
}
public void Remove(Item item)
{
if (this.items.Contains(item))
{
this.items.Remove(item);
item.Parent = null;
}
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With