Share via


The Parent-Container-Child Pattern

This is an interesting problem that I come across often. There are three types involved in this pattern:

1.  C – The type of the parent object.

2.  O<P> – A collection of instances of P. This is a hybrid collection-dictionary that allows end users to retrieve instances by index or by a key. The implementation extends KeyedCollection<K,V>.

3.  P – The type of the instances in O<P> . Instances of P have to maintain a Parent relationship with the instance of C that “contains” them. They also have a Name property which assigns each instance of P a string name that is unique in its parent container (an instance of O<P> ).

While the rest of the behavior is up to the user, we will focus on

  1. What should happen when the Name or Parent of an instance of P changes.
  2. What should happen when an instance of P is added or removed from the O<P> container in an instance of C.

In this particular specification and implementation, only one instance of an instance of P exists in an instance of O<P> and further, instances are accessed in O(1) time by using the Name property as a key.

Nor surprisingly, because of the entities involved, I call it the Parent-Container-Child pattern. Of course, I have seen similar patterns, for example in LINQ but I haven’t seen this pattern expressed in a generic form anywhere. Please feel free to post links or resources about similar or the same pattern that is discussed better or implemented better in the comments. While it is possible to implement this with internal functions, state variables etc, here are some restrictions that we will impose that makes the problem less of a hack and more applicable generally:

  1. No internal functions. We will only use advertised interfaces and/or public functions. Further, we will not use special functions that could be misused.
  2. No state variables. We do not want to do hacky things such as keeping a state variable to communicate between event handlers etc.

Interestingly, imposing these restrictions leads to a cleaner design than not imposing them. In fact, my first approach did involve using hacky public functions (that could be misused easily) and state variables to signal that some event handler should not handle a particular event because it was indirectly raised due to an action made by another event handler. This state variable was required to ensure that there was no circular chain of events.

Now, let’s come up with a free format, simple to read technical specification for the above problem.


C

Contains Properties collection of instances of P.

P

Parent Property

When Parent is changed

a. If old parent and new parent are same instance, no effect.

b. Signal that parent is going to be changed.

c. Signal to the new parent that it is becoming the parent of a new child.

d. Set new parent.

e. Signal that parent has been changed.

f. Signal to the new parent that it has become the parent of a new child.

Name Property

When Name is changed

a. If new value of Name is invalid, throw exception.

b. If new value of Name and current Name are the same, no effect.

c. Signal that the name is going to be changed.

d. Set new name.

e. Signal that the name has been changed.

O<P>

General Operations

a. When an object is added to the collection:

1. If its name is incorrect, throw an exception.

2. If its name already exists, thrown an exception.

3. Signal that an object is going to be added.

4. Add the object.

5. Signal that an object has been added.

6. Subscribe to name change events on the object.

b. When an object is removed from the collection:

1. If its name is incorrect, throw an exception.

2. If its name already exists, throw an exception.

3. Signal that an object is going to be removed.

4. Unsubscribe from name change events on the object.

5. Remove the object.

6. Signal that an object has been removed.

c. When the collection is cleared:

1. Signal that the collection is clearing.

2. Unsubscribe from name changes for each object.

3. Signal that the collection has been cleared.

Name Changes

OnNameChanging

1. Check that the new name does not already exist.

OnNameChanged

1. Change the name of the object.

Expectations

No circular event handling leading to death by a stack overflow.

Parent is Changed

oldParent.Properties.Contains(p) = false

newParent.Properties.Contains(p) = true

p.Parent = newParent

oldParent does not receive events from p.

newParent receives events from p.

Name is Changed

parent.Properties.Contains(oldName) = false

parent.Properties.Contains(newName) = true

parent.Properties[newName] = p


Based on these specifications, we can figure out the scenarios we will need to test:

In the following,

  • c, c1 and c2 are instances of C.
  • p is an instance of P.
  1. When an unnamed instance p is added to c.Properties, an exception should be thrown.
  2. When a named but un-parented instance p is added to c.Properties, then
    • The p should acquire c as its Parent.
    • c.Properties must have an entry for p.Name.
    • c.Properties[p.Name] should be equal to p.
  3. For an existing named instance p in c.Properties, when p.Name is changed, then
    • p.Parent must still be c.
    • c.Properties must not have an entry for p’ old name.
    • c.Properties must have an entry for p.Name.
    • c.Properties[p.Name] must be p.
  4. When an existing named instance p in c.Properties is removed, then
    • p.Parent must be null.
    • c.Properties must not have an entry for p.Name
  5. For an existing named instance p in c1.Properties, when p.Parent is changed to c2, then
    • p.Parent must be c2.
    • c1.Properties must not contain p.
    • c2.Properties must container p.
  6. For an existing instance p in c1.Properties, when p is added to c2.Properties, then
    • p.Parent must be c2.
    • c1.Properties should not contain p.
    • c2.Properties should contain p. 

The complete source code that demonstrates an implementation of the above pattern is provided in this file. (You will need VSTT 2008 to run the tests). The solution divides the responsibilities of maintaining the relationships correctly amongst C, P and O<P>. Further, for the solution to be correct according to the tests we have outlined, we the order in which actions are performed and the time at which they are performed are important.

P

Implementation of the type P is rather simple.

When its Name property is changed and the new value is valid and different from its old value, then an event NameChanging is fired before the change is fired and an event NameChanged is fired after the change is persisted.

When its Parent property is changed and the new value is different from its old value, then an event ParentChanging is fired before the change is persisted and an event ParentChanged is fired after the change is persisted. The following code fragment shows how this is implemented:

   79         /// <summary>

   80         /// Gets the parent of this instance.

   81         /// </summary>

   82         /// <value>The parent.</value>

   83         public string Name

   84         {

   85             get

   86             {

   87                 return this.name;

   88             }

   89             set

   90             {

   91                 value = (value ?? String.Empty).Trim();

   92 

   93                 if (String.IsNullOrEmpty(value))

   94                 {

   95                     throw new ArgumentException("Cannot set the name to an empty or null value.");

   96                 }

   97 

   98                 if (0 != StringComparer.Ordinal.Compare(this.name, value))

   99                 {

  100                     string oldName = this.name;

  101                     this.OnNameChanging(value, oldName);

  102                     this.name = value;

  103                     this.OnNameChanged(value, oldName);

  104                 }

  105             }

  106         }

  107 

  108         /// <summary>

  109         /// Gets the parent of this instance.

  110         /// </summary>

  111         /// <value>The parent.</value>

  112         public Entity Parent

  113         {

  114             get

  115             {

  116                 return this.parent;

  117             }

  118             set

  119             {

  120                 if (!Object.ReferenceEquals(this.parent, value))

  121                 {

  122                     Entity oldParent = this.parent;

  123                     this.OnParentChanging(value, oldParent);

  124                     this.parent = value;

  125                     this.OnParentChanged(value, oldParent);

  126                 }

  127             }

  128         }

  129 

  130         /// <summary>

  131         /// Called after a change to the name of this entity is persisted.

  132         /// </summary>

  133         /// <param name="newName">The new name.</param>

  134         /// <param name="oldName">The old name.</param>

  135         protected virtual void OnNameChanged(string newName, string oldName)

  136         {

  137             this.NameChanged(this, new NameChangeEventArgs<string>(oldName, newName));

  138         }

  139 

  140         /// <summary>

  141         /// Called before a change to the name of this entity is persisted.

  142         /// </summary>

  143         /// <param name="newName">The new name.</param>

  144         /// <param name="oldName">The old name.</param>

  145         protected virtual void OnNameChanging(string newName, string oldName)

  146         {

  147             this.NameChanging(this, new NameChangeEventArgs<string>(oldName, newName));

  148         }

  149 

  150         /// <summary>

  151         /// Called after a change to the parent of this entity is persisted.

  152         /// </summary>

  153         /// <param name="newParent">The new parent.</param>

  154         /// <param name="oldParent">The old parent.</param>

  155         protected virtual void OnParentChanged(Entity newParent, Entity oldParent)

  156         {

  157             this.ParentChanged(this, new ParentChangeEventArgs<Entity>(oldParent, newParent));

  158         }

  159 

  160         /// <summary>

  161         /// Called before a change to the parent of this entity is persisted.

  162         /// </summary>

  163         /// <param name="newParent">The new parent.</param>

  164         /// <param name="oldParent">The old parent.</param>

  165         protected virtual void OnParentChanging(Entity newParent, Entity oldParent)

  166         {

  167             this.ParentChanging(this, new ParentChangeEventArgs<Entity>(oldParent, newParent));

  168         }

The responsibility of the type P ends here. Thus, its task in the PCC pattern is to ensure that the appropriate checks are carried out and that the appropriate events are fired to notify the parent and the container that some aspect of it is changing.

O<P>

Implementing O<P> is fairly simple. In the sample implementation, O<P> is named ObservableKeyedCollection<TKey, TValue> and extends the KeyedCollection<K,V> type. The responsibilities of the O<P> type in the PCC pattern include:

  1. Notify the parent when an item is added or removed or when the collection is cleared.
  2. Listen to Name change events raised by the child instances and update the associated entry for that item.
  3. When an item is added to the collection: fire the ItemAdding event before adding the item and fire the ItemAdded event after adding the item; subscribe to name change events on the item.
  4. When an item is removed from the collection, fire the ItemRemoving event before removing the item and fire the ItemRemoved event after removing the item; unsubscribe from name change events on the item.

All of these are simple to implement and the following code fragments show how these are implemented in the sample:

 

  128         protected override void InsertItem(int index, TValue item)

  129         {

  130             this.OnItemAdding(item);

  131             base.InsertItem(index, item);

  132             this.Subscribe(item);

  133             this.OnItemAdded(item);

  134         }

  192 protected override void RemoveItem(int index)

  193         {

  194             TValue item = base[index];

  195             this.OnItemRemoving(item);

  196             base.RemoveItem(index);

  197             this.Unsubscribe(item);

  198             this.OnItemRemoved(item);

  199         }

   89 protected override void ClearItems()

   90         {

   91             this.OnClearing();

   92             base.ClearItems();

   93             this.OnCleared();

   94         }

   73 /// <summary>

   74         /// Changes the key of an item to a new value.

   75         /// </summary>

   76         /// <param name="oldKey">The old key.</param>

   77         /// <param name="newKey">The new key.</param>

   78         protected void ChangeKey(TKey oldKey, TKey newKey)

   79         {

   80             TValue value = this[oldKey];

   81 

   82             this.Dictionary.Remove(oldKey);

   83             this.Dictionary.Add(newKey, value);

   84         }

  256 /// <summary>

  257         /// Handles the NameChanged event of the value control.

  258         /// </summary>

  259         /// <param name="sender">The source of the event.</param>

  260         /// <param name="e">The <see cref="Microsoft.Test.Environment.Collections.NameChangeEventArgs&lt;TKey&gt;"/> instance containing the event data.</param>

  261         private void value_NameChanged(object sender, NameChangeEventArgs<TKey> e)

  262         {

  263             this.ChangeKey(e.OldName, e.NewName);

  264         }

  265 

  266         /// <summary>

  267         /// Handles the NameChanging event of the value control.

  268         /// </summary>

  269         /// <param name="sender">The source of the event.</param>

  270         /// <param name="e">The <see cref="Microsoft.Test.Environment.Collections.NameChangeEventArgs&lt;TKey&gt;"/> instance containing the event data.</param>

  271         private void value_NameChanging(object sender, NameChangeEventArgs<TKey> e)

  272         {

  273             TKey newKey = e.NewName;

  274 

  275             if (this.Contains(newKey))

  276             {

  277                 throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Cannot rename {0} to {1}. An instance with name {1} already exists.", sender, newKey));

  278             }

  279         }

C

The parent type C has the responsibility for maintaining the Parent relationship with instances of type P. The following code fragments illustrates how this is implemented in the sample:

 

  196         /// <summary>

  197         /// Handles the ItemRemoving event of the children control.

  198         /// </summary>

  199         /// <param name="sender">The source of the event.</param>

  200         /// <param name="e">The <see cref="Microsoft.Test.Environment.Collections.CollectionChangeEventArgs{T}"/> instance containing the event data.</param>

  201         private void children_ItemRemoving(object sender, CollectionChangeEventArgs<Entity> e)

  202         {

  203             this.Unsubscribe(e.Value);

  204             e.Value.Parent = null;

  205         }

  206 

  207         /// <summary>

  208         /// Handles the Clearing event of the children control.

  209         /// </summary>

  210         /// <param name="sender">The source of the event.</param>

  211         /// <param name="e">The <see cref="Microsoft.Test.Environment.Collections.CollectionChangeEventArgs{T}"/> instance containing the event data.</param>

  212         private void children_Clearing(object sender, CollectionChangeEventArgs<Entity> e)

  213         {

  214             foreach (Entity child in this.children)

  215             {

  216                 this.Unsubscribe(child);

  217                 child.Parent = null;

  218             }

  219         }

  220 

  221         /// <summary>

  222         /// Handles the ItemAdded event of the children control.

  223         /// </summary>

  224         /// <param name="sender">The source of the event.</param>

  225         /// <param name="e">The <see cref="Microsoft.Test.Environment.Collections.CollectionChangeEventArgs{T}"/> instance containing the event data.</param>

  226         private void children_ItemAdded(object sender, CollectionChangeEventArgs<Entity> e)

  227         {

  228             e.Value.Parent = this;

  229             this.Subscribe(e.Value);

  230         }

  252         /// <summary>

  253         /// Handles the ParentChanging event of the child control.

  254         /// </summary>

  255         /// <param name="sender">The source of the event.</param>

  256         /// <param name="e">The <see cref="Microsoft.Test.Environment.Collections.ParentChangeEventArgs{T}"/> instance containing the event data.</param>

  257         private void child_ParentChanging(object sender, ParentChangeEventArgs<Entity> e)

  258         {

  259             Entity child = (Entity)sender;

  260             this.children.Remove(child);

  261 

  262             if (!(e.NewParent.Children.Contains(child.Name) && Object.Equals(e.NewParent.Children[child.Name], child)))

  263             {

  264                 e.NewParent.Children.Add(child);

  265             }

  266         }

 

 

LiveJournal Tags: design,pattern,relationship,parent,container,child,maintenance,automatic

AutoParentChild_Final.zip