Thanatos
Thanatos

Reputation: 44256

Enforcing parent-child relationship in C# and .Net

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.

Upvotes: 9

Views: 8548

Answers (5)

Fabian
Fabian

Reputation: 1190

I was also looking into it recently, and thought about really enforcing this relationship as error proof as possible. Additionally, I tried to keep it as general and type safe as possible. It may be just overengineered, but still I would like to share it.

public class ChildCollection<TChild> : IEnumerable<TChild> 
    where TChild : ChildCollection<TChild>.Child
{
    private readonly List<TChild> childCollection = new List<TChild>();

    private void Add(TChild child) => this.childCollection.Add(child);

    public IEnumerator<TChild> GetEnumerator() => this.childCollection.GetEnumerator();

    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

    public abstract class Child
    {
        private readonly ChildCollection<TChild> childCollection;

        protected Child(ChildCollection<TChild> childCollection)
        {
            this.childCollection = childCollection;
            childCollection.Add((TChild)this);
        }
    }
}

Here comes the Example:

public class Parent
{
    public ChildCollection<Child> ChildCollection { get; }
    public Parent()
    {
        ChildCollection = new ChildCollection<Child>();
    }
}

public class Child : ChildCollection<Child>.Child
{
   public Child(ChildCollection<Child> childCollection) : base(childCollection)
   {
   }
}

And adding a child to the parent would look like:

var parent = new Parent();
var child1 = new Child(parent.ChildCollection);

The final implementation also has Ids for the children and allows removing of children. But the latter destroys the strong enforcement of the parent child relationship.

Upvotes: 0

Daniel Br&#252;ckner
Daniel Br&#252;ckner

Reputation: 59645

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 Itemclass.

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;
        }
    }
}

Upvotes: 2

Colin Burnett
Colin Burnett

Reputation: 11518

Could this sequence work for you?

  • call CollectionOfChild.Add(Child c)
  • add the child to the internal collection
  • CollectionOfChild.Add invokes Child.UpdateParent(this)
  • Child.UpdateParent(CollectionOfChild newParent) calls newParent.Contains(this) to make sure the child is in that collection then change the backing of Child.Parent accordingly. Also have to call CollectionOfChild.Remove(this) to remove itself from the old parent's collection.
  • CollectionOfChild.Remove(Child) would check Child.Parent to make sure it's not the child's collection anymore before it would remove the child from the collection.

Putting some code down:

public class CollectionOfChild
{
    public void Add(Child c)
    {
        this._Collection.Add(c);
        try
        {
            c.UpdateParent(this);
        }
        catch
        {
            // Failed to update parent
            this._Collection.Remove(c);
        }
    }

    public void Remove(Child c)
    {
        this._Collection.Remove(c);
        c.RemoveParent(this);
    }
}

public class Child
{
    public void UpdateParent(CollectionOfChild col)
    {
        if (col.Contains(this))
        {
            this._Parent = col;
        }
        else
        {
            throw new Exception("Only collection can invoke this");
        }
    }

    public void RemoveParent(CollectionOfChild col)
    {
        if (this.Parent != col)
        {
            throw new Exception("Removing parent that isn't the parent");
        }
        this._Parent = null;
    }
}

Not sure if that works but the idea should. It effectively creates and internal method by using Contains as the child's way to check the "authenticity" of the parent.

Keep in mind you can blow all this away with reflection so you really only need to make it slightly hard to get around to deter people. Thomas' use of explicit interfaces is another way to deter, though I think this is a bit harder.

Upvotes: 0

Thomas Levesque
Thomas Levesque

Reputation: 292405

I recently implemented a solution similar to AgileJon's, in the form of a generic collection and an interface to be implemented by child items :

ChildItemCollection<P,T> :

/// <summary>
/// Collection of child items. This collection automatically set the
/// Parent property of the child items when they are added or removed
/// </summary>
/// <typeparam name="P">Type of the parent object</typeparam>
/// <typeparam name="T">Type of the child items</typeparam>
public class ChildItemCollection<P, T> : IList<T>
    where P : class
    where T : IChildItem<P>
{
    private P _parent;
    private IList<T> _collection;

    public ChildItemCollection(P parent)
    {
        this._parent = parent;
        this._collection = new List<T>();
    }

    public ChildItemCollection(P parent, IList<T> collection)
    {
        this._parent = parent;
        this._collection = collection;
    }

    #region IList<T> Members

    public int IndexOf(T item)
    {
        return _collection.IndexOf(item);
    }

    public void Insert(int index, T item)
    {
        if (item != null)
            item.Parent = _parent;
        _collection.Insert(index, item);
    }

    public void RemoveAt(int index)
    {
        T oldItem = _collection[index];
        _collection.RemoveAt(index);
        if (oldItem != null)
            oldItem.Parent = null;
    }

    public T this[int index]
    {
        get
        {
            return _collection[index];
        }
        set
        {
            T oldItem = _collection[index];
            if (value != null)
                value.Parent = _parent;
            _collection[index] = value;
            if (oldItem != null)
                oldItem.Parent = null;
        }
    }

    #endregion

    #region ICollection<T> Members

    public void Add(T item)
    {
        if (item != null)
            item.Parent = _parent;
        _collection.Add(item);
    }

    public void Clear()
    {
        foreach (T item in _collection)
        {
            if (item != null)
                item.Parent = null;
        }
        _collection.Clear();
    }

    public bool Contains(T item)
    {
        return _collection.Contains(item);
    }

    public void CopyTo(T[] array, int arrayIndex)
    {
        _collection.CopyTo(array, arrayIndex);
    }

    public int Count
    {
        get { return _collection.Count; }
    }

    public bool IsReadOnly
    {
        get { return _collection.IsReadOnly; }
    }

    public bool Remove(T item)
    {
        bool b = _collection.Remove(item);
        if (item != null)
            item.Parent = null;
        return b;
    }

    #endregion

    #region IEnumerable<T> Members

    public IEnumerator<T> GetEnumerator()
    {
        return _collection.GetEnumerator();
    }

    #endregion

    #region IEnumerable Members

    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        return (_collection as System.Collections.IEnumerable).GetEnumerator();
    }

    #endregion
}

IChildItem<T> :

public interface IChildItem<P> where P : class
{
    P Parent { get; set; }
}

The only drawback of using an interface is that it's not possible to put an internal modifier on the set accessor... but anyway, in a typical implementation, this member would be "hidden" behind an explicit implementation :

public class Employee : IChildItem<Company>
{
    [XmlIgnore]
    public Company Company { get; private set; }

    #region IChildItem<Company> explicit implementation

    Company IChildItem<Company>.Parent
    {
        get
        {
            return this.Company;
        }
        set
        {
            this.Company = value;
        }
    }

    #endregion

}

public class Company
{
    public Company()
    {
        this.Employees = new ChildItemCollection<Company, Employee>(this);
    }

    public ChildItemCollection<Company, Employee> Employees { get; private set; }
}

This is particularly useful when you want to serialize this kind of object in XML : you can't serialize the Parent property because it would cause cyclic references, but you want to keep the parent/child relation.

Upvotes: 1

AgileJon
AgileJon

Reputation: 53586

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; }
}

Upvotes: 7

Related Questions