Tuesday, February 8, 2011

Hirerachy Data Source - Using Fluent API convert flat data structure to hirerachy structure

It is often required to represent data in hirerachy structure. Over period of time, for several projects I had this requirement. In process of building this structure I observed a following common pattern/trend:

- Often times, to represent hirerachy structure, a custom class is created to hold original data.
- We will have recursive functions to select nodes at various levels and build tree structure.

Lets take a example of Employee. Employees are managed by Manager, who themselves are employees and also managed by some other employee.

Code:
class Employee
{
public int ID { get; set; }
public string Name { get; set; }
public int ManagerID { get; set; }
}

And we have following class to represent Hirerachy structure
Code:

public interface INode
{
List ChildNodes { get; set; }
Object PayLoad { get; set; }
bool IsSelected { get; set; }
}
public class Node:INode
{
public List ChildNodes { get; set; }
public Object PayLoad { get; set; }
public Node()
{
ChildNodes = new List();
}
}

Before going further lets think of nature of conditions which we will be applying to Employee class to arrange it in hierarchy structure:

Employees who doesnt have any managerId are super employees like CEO who wont report to any other employee.
Condition for root employees: employee.ManagerID == default(int)
Employee child nodes will be those employees whose manager id is current employee's Id.
Condition which needs to be applied recursively is : parent.ID == child.ManagerID && child.ManagerID != default(int)

Using HirerachyData Structure you can compose your nodes as below:

List nodes = HirerachyDataSource
.ComposeHirerachyUsingStructure(persons) //Pass list of employees classes
.RootNodesFilter((p) => p.ManagerID == default(int)) //How root nodes to be selected.
.ChildNodeFilter((parent, child) => parent.ID == child.ManagerID && child.ManagerID != default(int)) //What condition to check recursively to build hirerachy
.UsingObjectBuilder((d) => new Node() { PayLoad = d }) //And finally how to build node.
.BuildHeirarchy();


Using this approach , you can compose how to select root, how to select nodes recursively, and even you can specify how to build final nodes (could be any type), due to which you can get the power to build any type of hirerachy structures with any properties (NOTE: code listing below is coupled to INode, but you can safely avoid that as Object builder will take care of instancing out of hirerachydatasource).

HirerachyDataSource code listing is below:
Code:

public class HirerachyDataSource
{
public static CompositionStructure ComposeHirerachyUsingStructure(List NodesList)
{
CompositionStructure structure = new CompositionStructure(NodesList);
return structure;
}



public class CompositionStructure
{
private List FlatStructure = new List();
Predicate rootNodeSelector;
Func ObjectBuilder;
Func childNodeSelector;
public CompositionStructure UsingObjectBuilder(Func nodeBuilder)
{
ObjectBuilder = nodeBuilder;
return this;
}
public ObservableCollection BuildHeirarchy()
{
return BuildNode();
}
internal CompositionStructure()
{

}
internal CompositionStructure(List nodes)
{
this.FlatStructure = nodes;
}

public CompositionStructure RootNodesFilter(Predicate rootNodesFilter)
{
rootNodeSelector = rootNodesFilter;
return this;
}
public CompositionStructure ChildNodeFilter(Func childNodeFilter)
{
childNodeSelector = childNodeFilter;
return this;
}
protected ObservableCollection BuildNode()
{
//INode node = new Node();
List nodes = (from a in FlatStructure
where rootNodeSelector(a)
select ObjectBuilder(a)).ToList();
if (nodes != null)
{
foreach (var item in nodes)
{
BuildNode(item);
}
}
return new ObservableCollection(nodes);
}

protected void BuildNode(INode node)
{
if (node != null)
{

List items= (from a in FlatStructure
where childNodeSelector((T)node.PayLoad, a)
select ObjectBuilder(a)).ToList();
node.ChildNodes = new ObservableCollection(items);
if (node.ChildNodes != null)
{
foreach (var item in node.ChildNodes)
{
BuildNode(item);
}
}
}
}
}
}