Generic BidirectionalAssociationCollection

By Fons Sonnemans, 25-3-2008

Bidirectional associations are easy to design but difficult to program. You must write lot's of synchronization code with the risk of making mistakes. Have a look at the following example in which there is a bidirectional association between Employee (Employer) and Company (Employees).

The synchronization code is implemented in the AddEmployee(), RemoveEmployee() and the (Set)Employer methods.

public class Employee {

    public string Name { get; set; }
    public decimal Salary { get; set; }

    private Company _employer;

    public Company Employer {
        get {
            return _employer;
        }
        set {
            if (Employer != value) {
                
                // Remove from 'old' Employer
                if (Employer != null) {
                    Company old = _employer;
                    this._employer = null;
                    old.RemoveEmployee(this);
                }

                // Set the Employer    
                this._employer = value;
                
                // Add to 'new' Employer
                if (Employer != null) {
                    Employer.AddEmployee(this);
                }
            }
        }
    }
}

public class Company {

    public string Name { get; set; }

    private readonly List<Employee> _employees = new List<Employee>();

    public void AddEmployee(Employee emp) {
        if (!_employees.Contains(emp)) {
            _employees.Add(emp);
            emp.Employer = this;  // Synchronize
        }
    }

    public void RemoveEmployee(Employee emp) {
        if (_employees.Contains(emp)) {
            _employees.Remove(emp);
            emp.Employer = null; // Synchronize
        }
    }
}

In the following example Employee 'fons' is added to Company 'Reflection IT'. The Employer property of Employee 'Jim' is set to Company 'Dummy inc'.

Employee fons = new Employee { Name = "Fons", Salary = 2000 };
Employee jim = new Employee { Name = "Jim", Salary = 3000 };

Company r = new Company { Name = "Reflection IT" };
Company d = new Company { Name = "Dummy Inc" };

r.AddEmployee(fons);

jim.Employer = d;

This solution has some limitations. The employees collection in Company is private and the AddEmployee() and RemoveEmployee() methods are necessary but not business related. I have tried to solve this by creating an extra EmployeeList class which solves these problems.

The Employees property is now a readonly public field of the type EmployeeList. The AddEmployee() and RemoveEmployee() methods are removed. This synchonisation logic is now implemented in the InsertItem() and RemoveItem() methods of the EmployeeList class. These methods override the base class (Collection) implementation and are automatically called when an item is added or removed.

public class Company {

    public string Name { get; set; }

    public readonly EmployeeList Employees;

    public Company() {
        this.Employees = new EmployeeList(this);
    }

}

public class EmployeeList : Collection<Employee> {

    private Company _owner;

    public EmployeeList(Company owner) {
        _owner = owner;
    }

    protected override void InsertItem(int index, Employee item) {
        if (!this.Contains(item)) {
            base.InsertItem(index, item);
            item.Employer = _owner;
        }
    }

    protected override void RemoveItem(int index) {
        Employee emp = this[index];
        base.RemoveItem(index);
        emp.Employer = null;
    }
}

Employees are added to a Company using the Add() method of the Employees property. Or you can set the Employer of an Employee.

Employee fons = new Employee { Name = "Fons", Salary = 2000 };
Employee jim = new Employee { Name = "Jim", Salary = 3000 };

Company r = new Company { Name = "Reflection IT" };
Company d = new Company { Name = "Dummy Inc" };

r.Employees.Add(fons);

jim.Employer = d;

The design is now much better but the implementation still sucks. The risk of making mistakes is still too high. I have created a Generic BidirectionalAssociationCollection class which solves this.

The EmployeeList class now derives from the abstract BidirectionalAssociationCollection class. In it's overriden SyncChild() method the synchronisation is implemented by setting the Employer property of the child to the parent.

The Employee class is now also simplified a lot, reducing the risk of mistakes. The setter of the Employee property is now only 1 line of code. This code uses the static SyncParent() method to synchronize the Employees and the Employer. A Lambda expression is used to retrieve the Employees property of a Company.

Note: I had to introduce a PauseSync field to avoid call stack overflows. This solution is not thread safe (yet).

public class Employee {

    public string Name { get; set; }
    public decimal Salary { get; set; }

    private Company _employer;

    public Company Employer {
        get {
            return _employer;
        }
        set {
            _employer = EmployeeList.SyncParent(this, _employer, value, p => p.Employees);
        }
    }
}

public class Company {

    public string Name { get; set; }

    public readonly EmployeeList Employees;

    public Company() {
        this.Employees = new EmployeeList(this);
    }
}

public abstract class BidirectionalAssociationCollection<TChild, TParent> : Collection<TChild> {

    public static TParent SyncParent(TChild child, TParent oldParent, TParent newParent, 
             Func<TParent, BidirectionalAssociationCollection<TChild, TParent>> getCollection) {
        if (!object.ReferenceEquals(oldParent, newParent)) {

            // Remove from 'old' Employer
            if (oldParent != null) {
                var oldList = getCollection(oldParent);
                try {
                    if (!oldList.PauseSync) {
                        oldList.PauseSync = true;
                        oldList.Remove(child);
                    }
                } finally {
                    oldList.PauseSync = false;
                }
            }

            // Add to 'new' Employer
            if (newParent != null) {
                var newList = getCollection(newParent);
                try {
                    if (!newList.PauseSync) {
                        newList.PauseSync = true;
                        newList.Add(child);
                    }
                } finally {
                    newList.PauseSync = false;
                }
            }
        }
        return newParent;
    }

    private bool PauseSync;
    protected TParent Owner { get; private set; }

    protected BidirectionalAssociationCollection(TParent owner) {
        this.Owner = owner;
    }

    protected override void InsertItem(int index, TChild item) {
        if (!this.Contains(item)) {
            base.InsertItem(index, item);
            if (!PauseSync) {
                SyncChild(item, Owner);
            }
        }
    }

    protected override void RemoveItem(int index) {
        var item = this[index];
        base.RemoveItem(index);
        if (!PauseSync) {
            SyncChild(item, default(TParent));
        }
    }

    protected override void ClearItems() {
        var list = this.ToArray();
        base.ClearItems();
        foreach (var item in list) {
            SyncChild(item, default(TParent));
        }
    }

    protected override void SetItem(int index, TChild item) {
        this.PauseSync = true;
        var old = this[index];
        SyncChild(old, default(TParent));
        this.PauseSync = false;

        base.SetItem(index, item);

        this.PauseSync = true;
        SyncChild(item, Owner);
        this.PauseSync = false;
    }

    protected abstract void SyncChild(TChild child, TParent parent);
}

public class EmployeeList : BidirectionalAssociationCollection<Employee, Company> {

    public EmployeeList(Company parent) : base(parent) {
    }

    protected override void SyncChild(Employee child, Company parent) {
        child.Employer = parent;
    }
}

You can download the final solution written in C# 3.0 and a Visual Studio 2008 Test project here

Tags: CSharp

Leave a Comment

Leave a Comment
Name
Comment
8 + 6 =

0 Comments

All postings/content on this blog are provided "AS IS" with no warranties, and confer no rights. All entries in this blog are my opinion and don't necessarily reflect the opinion of my employer or sponsors. The content on this site is licensed under a Creative Commons Attribution By license.