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
- What should happen when the Name or Parent of an instance of P changes.
- 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:
- No internal functions. We will only use advertised interfaces and/or public functions. Further, we will not use special functions that could be misused.
- 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.
- When an unnamed instance p is added to c.Properties, an exception should be thrown.
- 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.
- 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.
- 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
- 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.
- 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:
- Notify the parent when an item is added or removed or when the collection is cleared.
- Listen to Name change events raised by the child instances and update the associated entry for that item.
- 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.
- 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<TKey>"/> 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<TKey>"/> 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