Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Enforcing parent-child relationship in C# and .Net

Tags:

c#

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

like image 571
Thanatos Avatar asked Jun 11 '09 18:06

Thanatos


2 Answers

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; }
}
like image 97
AgileJon Avatar answered Oct 21 '22 03:10

AgileJon


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;
        }
    }
}
like image 21
Daniel Brückner Avatar answered Oct 21 '22 03:10

Daniel Brückner