Generic BidirectionalAssociationCollection

By Fons Sonnemans, posted on 25-Mar-2008
1050 Views

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 {

    publicstring Name { get; set; }
    publicdecimal Salary { get; set; }

    private Company _employer;

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

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

publicclass Company {

    publicstring Name { get; set; }

    privatereadonly List<Employee> _employees = new List<Employee>();

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

    publicvoid 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 {

    publicstring Name { get; set; }

    publicreadonly EmployeeList Employees;

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

}

publicclass EmployeeList : Collection<Employee> {

    private Company _owner;

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

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

    protectedoverridevoid 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 {

    publicstring Name { get; set; }
    publicdecimal Salary { get; set; }

    private Company _employer;

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

publicclass Company {

    publicstring Name { get; set; }

    publicreadonly EmployeeList Employees;

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

publicabstractclass BidirectionalAssociationCollection<TChild, TParent> : Collection<TChild> {

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

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

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

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

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

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

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

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

    protectedoverridevoid 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;
    }

    protectedabstractvoid SyncChild(TChild child, TParent parent);
}

publicclass EmployeeList : BidirectionalAssociationCollection<Employee, Company> {

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

    protectedoverridevoid 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

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.

Leave a comment

Blog comments

0 responses