by Zoran Horvat
Typical object diagram in modern applications contains many-to-one relationships between objects: companies have departments, departments have employees, machines have parts, user interfaces contain controls, to-do lists have items, books have pages... Possibilities are endless. In such a setting, one object contains a collection of child objects, while each child object is associated with a single parent object. So if we speak about a company that has one or more departments, then we also speak of a department that belongs to a single company. Figure below depicts a corresponding object-role model describing this case.
In terms of program code, we rather want to have simple navigation properties: one that leads from parent object to all of its child objects, and another one on the child object that returns its corresponding parent object. In terms of a company-department example, that would mean that company object exposes a collection of departments and department object exposes a reference to its company.
But this design raises a couple of issues. Caller must not be able to destroy the collection, or to make abrupt changes to its content without notifying the parent and child objects involved. The two navigation properties must be synchronized in terms that if department appears in one company’s collection then its Company navigation property must reference that particular company rather than any other company in the system. Conversely, if one department references a parent company, then it must appear in that company’s collection of departments. Additionally, it is often required to disallow adding equal child objects to the same parent (e.g. company cannot have two departments with the same name).
In this article we are going to present a pattern which provides collection property functionality on parent objects and navigation to parent property on child objects. Solution will keep track of unique child objects in order to prevent any attempt to add child object which equals an existing child object within the same parent.
Solution that we are going to present will provide these features:
It will soon become clear that this pattern is not of the simplest kind. It might also be a signal that a more general solution should be devised which encapsulates this fairly complex set of features. But that would be beyond the scope of this article.
Below is the complete source code of a console application which demonstrates use of navigation properties. All consistency checks are applied both to client and the server. Following the Company and Department classes, there is a rather detailed Main function which demonstrates behavior of these classes in several characteristic situations.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
namespace CollectionProp
{
/// <summary>
/// Wraps information about one company, including a collection of its departments.
/// </summary>
public class Company
{
/// <summary>
/// Default cnostructor.
/// </summary>
public Company()
{
_uniqueDepartments = new HashSet<Department>();
_departments = new ObservableCollection<Department>();
// Subscribe to NotifyCollectionChanged event so that Company object knows when other entities
// are modifying its collection of departments
_departments.CollectionChanged += new NotifyCollectionChangedEventHandler(DepartmentsChanged);
}
/// <summary>
/// Handles NotifyCollectionChanged event raised by the observable collection which contains
/// company's departments.
/// </summary>
/// <param name="sender">Object which has raised the event.</param>
/// <param name="e">Event arguments.</param>
void DepartmentsChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Reset)
{ // Dramatic change has been made to the collection of departments.
// Parent object will clear the collection of contained objects and then
// add those that may have survived in the observable collection.
foreach (Department d in _uniqueDepartments)
d.SetCompany(null);
_uniqueDepartments.Clear();
// Now add the survivors, although they should not exist
// since Reset action on observable collection occurs as a result
// of calling its Clear method.
foreach (Department d in _departments)
OnDepartmentAdded(d);
}
else
{ // Otherwise traverse through collections of old and new items and
// refresh collections accordingly
if (e.OldItems != null)
foreach (Department d in e.OldItems)
OnDepartmentRemoved(d);
if (e.NewItems != null)
foreach (Department d in e.NewItems)
OnDepartmentAdded(d);
}
}
/// <summary>
/// Handles case when specified department has been added to the observable collection.
/// </summary>
/// <param name="d">Department which was added to the observable collection.</param>
/// <exception cref="System.ArgumentException">Specified department already exists
/// in the collection of departments in this company object.</exception>
private void OnDepartmentAdded(Department d)
{
if (d == null)
{ // Attempted to add null department; simply remove it from observable collection
SilentlyRemoveDepartment(d);
}
else if (object.ReferenceEquals(d.Company, this) || _uniqueDepartments.Contains(d))
{ // Attempted to add duplicate department - remove it and throw exception
SilentlyRemoveDepartment(d);
throw new System.ArgumentException("Duplicate department.");
}
else
{ // Otherwise try to add department to collection of unique departments
// and set its parent reference to this object
try
{
d.SetCompany(this); // Throws ArgumentException if d already has a parent
_uniqueDepartments.Add(d);
}
catch
{
SilentlyRemoveDepartment(d);
throw;
}
}
}
/// <summary>
/// Handles case when specified department has been removed from the observable collection.
/// </summary>
/// <param name="d">Department which was removed from the observable collection.</param>
private void OnDepartmentRemoved(Department d)
{
_uniqueDepartments.Remove(d);
d.SetCompany(null);
}
/// <summary>
/// Removes specified department from observable collection, preivously removing
/// NotifyCollectionChanged event handler. This prevents event handler from
/// executing. Method is intended to be called from inside the event handler,
/// so to avoid recursive events.
/// </summary>
/// <param name="d">Department which should be removed from the observable collection.</param>
private void SilentlyRemoveDepartment(Department d)
{
_departments.CollectionChanged -= new NotifyCollectionChangedEventHandler(DepartmentsChanged);
// Remove item by reference equality, not by semantic equality!
int i = 0;
foreach (Department dept in _departments)
if (object.ReferenceEquals(dept, d))
{
_departments.RemoveAt(i);
break;
}
else
{
i++;
}
_departments.CollectionChanged += new NotifyCollectionChangedEventHandler(DepartmentsChanged);
}
/// <summary>
/// Gets or sets company name.
/// </summary>
public string Name { get; set; }
/// <summary>
/// Gets collection of unique departments of this company.
/// </summary>
public ICollection<Department> Departments
{
get
{
return _departments;
}
}
/// <summary>
/// Collection of departments added to this company object.
/// </summary>
private ObservableCollection<Department> _departments;
/// <summary>
/// Collection which ensures uniqueness of departments
/// added to the company object.
/// </summary>
private HashSet<Department> _uniqueDepartments;
}
/// <summary>
/// Wraps information about one department of a company.
/// Department is uniquely identified by its name and parent company.
/// </summary>
public class Department
{
/// <summary>
/// Default constructor.
/// </summary>
public Department() { }
/// <summary>
/// Constructor which initializes department name.
/// </summary>
/// <param name="name">Name of the company department.</param>
public Department(string name) { Name = name; }
/// <summary>
/// Gets or sets the department name. Attempting to change name of the department
/// which is already part of a company may cause name collision within the company,
/// in which case this operation fails and name remains the same.
/// </summary>
/// <exception cref="System.ArgumentException">Changing the name to desired value
/// would cause name collision within the company.</exception>
public string Name
{
get
{
return _name;
}
set
{
bool failed = false;
if (_company != null && value != _name)
{ // Must remove this department from the company and then
// add it again after setting the new name.
// That will reorganize company object's internal structures.
string prevName = _name;
Company prevCompany = _company;
_company.Departments.Remove(this);
try
{
_name = value;
prevCompany.Departments.Add(this); // Will throw exception if same name is in use
}
catch
{ // Name collision
failed = true;
_name = prevName;
prevCompany.Departments.Add(this); // Return into the company with previous name
}
}
else
{ // Department is not part of the company - there is no fear of name collision
_name = value;
}
if (failed) // Throw exception only after all content has been returned to original state
throw new System.ArgumentException("Error changing department name.");
}
}
/// <summary>
/// Gets hash code which is determined by content of this object.
/// </summary>
/// <returns>Value determined by the name of the department.</returns>
public override int GetHashCode()
{
return (Name ?? string.Empty).GetHashCode();
}
/// <summary>
/// Tests whether this instance is equal to other object.
/// </summary>
/// <param name="obj">Object against which this instance is compared for equality.</param>
/// <returns>true if <paramref name="obj"/> is a department with same name as this
/// department.</returns>
public override bool Equals(object obj)
{
return obj != null && obj is Department && (obj as Department).Name == this.Name;
}
/// <summary>
/// Gets company to which this department belongs.
/// </summary>
public Company Company
{
get
{
return _company;
}
}
/// <summary>
/// Non-public method which allows setting the company to which this department belongs.
/// </summary>
/// <param name="comp">Company to which this department has been added.</param>
/// <exception cref="System.ArgumentException">This department is already part
/// of another company. One department object cannot appear in more than one company.</exception>
internal void SetCompany(Company comp)
{
if (_company != null && comp != null && _company != comp)
throw new System.ArgumentException("Department already belongs to another company.");
_company = comp;
}
/// <summary>
/// Company to which this department object has been added.
/// </summary>
private Company _company;
/// <summary>
/// Name of the company department.
/// </summary>
private string _name;
}
public class Program
{
static void PrintDepartments(Company comp)
{
Console.Write("Departments of {0}:", comp.Name);
foreach (Department d in comp.Departments)
Console.Write(" {0}", d.Name);
Console.WriteLine();
}
static void Main(string[] args)
{
Console.WriteLine("Departments.Add test:");
Company comp1 = new Company();
comp1.Name = "First Corp.";
comp1.Departments.Add(new Department("Retail"));
comp1.Departments.Add(new Department("Planning"));
Department dept = new Department("Cleaning");
comp1.Departments.Add(dept);
comp1.Departments.Add(null);
PrintDepartments(comp1);
Console.WriteLine();
Console.WriteLine("Adding another company's department - failure test:");
Company comp2 = new Company();
comp2.Name = "Second Corp.";
comp2.Departments.Add(new Department("Retail")); // Not the same Retail as in First Corp.
comp2.Departments.Add(new Department("Planning"));
PrintDepartments(comp2);
try
{
comp2.Departments.Add(dept); // Same department as in First Corp.
}
catch (System.ArgumentException ex)
{
// Expected exception: added another company's department
Console.WriteLine("ERROR: {0}", ex.Message);
}
PrintDepartments(comp2);
Console.WriteLine();
Console.WriteLine("Renaming test - name collision failure:");
dept = new Department("Cleaning");
comp2.Departments.Add(dept);
PrintDepartments(comp2);
try
{
dept.Name = "Planning"; // Name collision - Planning already exists in comp2
}
catch (System.ArgumentException ex)
{
Console.WriteLine("ERROR: {0}", ex.Message);
}
PrintDepartments(comp2);
Console.WriteLine();
Console.WriteLine("Renaming test - no collision:");
PrintDepartments(comp2);
dept.Name = "Catering"; // Successful name change
PrintDepartments(comp2);
Console.WriteLine();
Console.WriteLine("Adding equal department test - failure:");
comp1.Departments.Remove(new Department("Retail")); // Finds department that is equal to supplied object
comp1.Departments.Remove(dept);
PrintDepartments(comp1);
try
{
comp1.Departments.Add(new Department("Planning")); // Duplicate department
}
catch (System.ArgumentException ex)
{
// Expected exception: attempted to add another department with same name
Console.WriteLine("ERROR: {0}", ex.Message);
}
PrintDepartments(comp1);
Console.WriteLine();
Console.WriteLine("Clear test:");
PrintDepartments(comp1);
comp1.Departments.Clear();
PrintDepartments(comp1);
comp1.Departments.Add(new Department("Planning")); // Doesn't fail after emptying the collection
PrintDepartments(comp1);
Console.WriteLine();
Console.WriteLine("Department.Company navigation test:");
dept = new Department("Retail");
comp1.Departments.Add(dept);
PrintDepartments(dept.Company); // Print company to which this department belongs
Console.WriteLine();
Console.Write("Press ENTER to continue... ");
Console.ReadLine();
}
}
}
When this code is run, it produces the following output:
Departments.Add test:
Departments of First Corp.: Retail Planning Cleaning
Adding another company's department - failure test:
Departments of Second Corp.: Retail Planning
ERROR: Department already belongs to another company.
Departments of Second Corp.: Retail Planning
Renaming test - name collision failure:
Departments of Second Corp.: Retail Planning Cleaning
ERROR: Error changing department name.
Departments of Second Corp.: Retail Planning Cleaning
Renaming test - no collision:
Departments of Second Corp.: Retail Planning Cleaning
Departments of Second Corp.: Retail Planning Catering
Adding equal department test - failure:
Departments of First Corp.: Planning Cleaning
ERROR: Duplicate department.
Departments of First Corp.: Planning Cleaning
Clear test:
Departments of First Corp.: Planning Cleaning
Departments of First Corp.:
Departments of First Corp.: Planning
Department.Company navigation test:
Departments of First Corp.: Planning Retail
Press ENTER to continue...
In this article we have demonstrated means by which two classes can be tied in a many-to-one relationship. Number of special cases have been covered in order to support all conceivable scenarios that might occur when using these classes. Goal of the exercise was to reach a design in which caller is free to add, change and remove child objects at will, at the same time resting assured that all moving parts are automatically kept in their proper places.
Design demonstrated above can be a sound base on which a fully general solution could be built.
If you wish to learn more, please watch my latest video courses
In this course, you will learn the basic principles of object-oriented programming, and then learn how to apply those principles to construct an operational and correct code using the C# programming language and .NET.
As the course progresses, you will learn such programming concepts as objects, method resolution, polymorphism, object composition, class inheritance, object substitution, etc., but also the basic principles of object-oriented design and even project management, such as abstraction, dependency injection, open-closed principle, tell don't ask principle, the principles of agile software development and many more.
More...
In this course, you will learn how design patterns can be applied to make code better: flexible, short, readable.
You will learn how to decide when and which pattern to apply by formally analyzing the need to flex around specific axis.
More...
This course begins with examination of a realistic application, which is poorly factored and doesn't incorporate design patterns. It is nearly impossible to maintain and develop this application further, due to its poor structure and design.
As demonstration after demonstration will unfold, we will refactor this entire application, fitting many design patterns into place almost without effort. By the end of the course, you will know how code refactoring and design patterns can operate together, and help each other create great design.
More...
In four and a half hours of this course, you will learn how to control design of classes, design of complex algorithms, and how to recognize and implement data structures.
After completing this course, you will know how to develop a large and complex domain model, which you will be able to maintain and extend further. And, not to forget, the model you develop in this way will be correct and free of bugs.
More...
Zoran Horvat is the Principal Consultant at Coding Helmet, speaker and author of 100+ articles, and independent trainer on .NET technology stack. He can often be found speaking at conferences and user groups, promoting object-oriented and functional development style and clean coding practices and techniques that improve longevity of complex business applications.