Automatic hierarchies using Linq and IValueConverter

by Tom 9. November 2008 16:54

The big news in the last week or so has been the release of the Silverlight Toolkit, packed with some nice controls. At my work we were particularly hanging out for the TreeView and Expander controls, but the Charting stuff looks really nice as well, so I'm sure I'll find myself using most of the toolkit before long.

One of the first things I needed to do with the new control was rig up a TreeView for some data. Sounds pretty easy, and it is - if you data happen to be in the right structure for the data binding. Reading the docos I discovered that in order to display hierarchical data to the nth level, I need objects that contain collections of their child objects. Damn! For a moment I thought this would mean either changes to the business objects (unlikely) or some sort of conversion layer to covert my business objects into hierarchical objects.

Before we get any further, this is what I am making:

03-01

(Click on the screenshot to view a demo)

To explain what I mean, this is how my business objects were structured:

public class Person
{
    public Person() { PersonID = 0; }
    public int PersonID { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string JobTitle { get; set; }
    public Person Manager { get; set; }
}

So, in my objects, I carry an instance of the parent, not a collection of the children. This is fairly standard stuff, so I figured there must be a neat way to make a collection of objects like this bind to a tree view.

 

Convert flat collection to hierarchy using LINQ

 

In my reading I came across a brilliant blog post at scip.be by a guy name Stefan Cruysberghs (LINQ AsHierarchy) which demonstrates how to use a Linq Extension method to dynamically convert flat objects into hierarchical ones. What a great idea! I highly recommend you read his post.

I needed to make a minor customization to the source provided by Stefan: I threw in an extra method that allows you to specify the parent ids that root nodes should have, but other than that, everything is the same. Here is my altered code:

using System;
using System.Linq;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
 
/// <summary>
/// Hierarchy node class which contains a nested collection of hierarchy nodes
/// </summary>
/// <typeparam name="T">Entity</typeparam>
public class HierarchyNode<T> where T : class
{
    public T Entity { get; set; }
    public IEnumerable<HierarchyNode<T>> ChildNodes { get; set; }
    public int Depth { get; set; }
    public T Parent { get; set; }
}
 
// Stefan Cruysberghs, July 2008, http://www.scip.be
/// <summary>
/// AsHierarchy extension methods for LINQ to Objects IEnumerable
/// </summary>
public static class LinqToObjectsExtensionMethods
{
    private static IEnumerable<HierarchyNode<TEntity>>
      CreateHierarchy<TEntity, TProperty>(
        IEnumerable<TEntity> allItems,
        TEntity parentItem,
        Func<TEntity, TProperty> idProperty,
        Func<TEntity, TProperty> parentIdProperty,
        object rootItemId,
        object rootParentId,
        int maxDepth,
        int depth) where TEntity : class
    {
        IEnumerable<TEntity> childs;
 
        if (rootItemId != null)
        {
            childs = allItems.Where(i => idProperty(i).Equals(rootItemId));
        }
        else if (rootParentId != null)
        {
            childs = allItems.Where(i => parentIdProperty(i).Equals(rootParentId));
        }
        else
        {
            if (parentItem == null)
            {
                childs = allItems.Where(i => parentIdProperty(i).Equals(default(TEntity)));
            }
            else
            {
                childs = allItems.Where(i => parentIdProperty(i).Equals(idProperty(parentItem)));
            }
        }
 
        if (childs.Count() > 0)
        {
            depth++;
 
            if ((depth <= maxDepth) || (maxDepth == 0))
            {
                foreach (var item in childs)
                    yield return
                      new HierarchyNode<TEntity>()
                      {
                          Entity = item,
                          ChildNodes =
                            CreateHierarchy(allItems.AsEnumerable(), item, idProperty, parentIdProperty, null, null, maxDepth, depth),
                          Depth = depth,
                          Parent = parentItem
                      };
            }
        }
    }
 
    /// <summary>
    /// LINQ to Objects (IEnumerable) AsHierachy() extension method
    /// </summary>
    /// <typeparam name="TEntity">Entity class</typeparam>
    /// <typeparam name="TProperty">Property of entity class</typeparam>
    /// <param name="allItems">Flat collection of entities</param>
    /// <param name="idProperty">Func delegete to Id/Key of entity</param>
    /// <param name="parentIdProperty">Func delegete to parent Id/Key</param>
    /// <returns>Hierarchical structure of entities</returns>
    public static IEnumerable<HierarchyNode<TEntity>> AsHierarchy<TEntity, TProperty>(
      this IEnumerable<TEntity> allItems,
      Func<TEntity, TProperty> idProperty,
      Func<TEntity, TProperty> parentIdProperty) where TEntity : class
    {
        return CreateHierarchy(allItems, default(TEntity), idProperty, parentIdProperty, null, null, 0, 0);
    }
 
    /// <summary>
    /// LINQ to Objects (IEnumerable) AsHierachy() extension method
    /// </summary>
    /// <typeparam name="TEntity">Entity class</typeparam>
    /// <typeparam name="TProperty">Property of entity class</typeparam>
    /// <param name="allItems">Flat collection of entities</param>
    /// <param name="idProperty">Func delegete to Id/Key of entity</param>
    /// <param name="parentIdProperty">Func delegete to parent Id/Key</param>
    /// <param name="rootItemId">Value of root item Id/Key</param>
    /// <returns>Hierarchical structure of entities</returns>
    public static IEnumerable<HierarchyNode<TEntity>> AsHierarchy<TEntity, TProperty>(
      this IEnumerable<TEntity> allItems,
      Func<TEntity, TProperty> idProperty,
      Func<TEntity, TProperty> parentIdProperty,
      object rootItemId) where TEntity : class
    {
        return CreateHierarchy(allItems, default(TEntity), idProperty, parentIdProperty, rootItemId, null, 0, 0);
    }
 
    /// <summary>
    /// LINQ to Objects (IEnumerable) AsHierachy() extension method
    /// </summary>
    /// <typeparam name="TEntity">Entity class</typeparam>
    /// <typeparam name="TProperty">Property of entity class</typeparam>
    /// <param name="allItems">Flat collection of entities</param>
    /// <param name="idProperty">Func delegete to Id/Key of entity</param>
    /// <param name="parentIdProperty">Func delegete to parent Id/Key</param>
    /// <param name="rootItemId">Value of root item Id/Key</param>
    /// <returns>Hierarchical structure of entities</returns>
    public static IEnumerable<HierarchyNode<TEntity>> AsHierarchy<TEntity, TProperty>(
      this IEnumerable<TEntity> allItems,
      Func<TEntity, TProperty> idProperty,
      Func<TEntity, TProperty> parentIdProperty,
      object rootItemId,
      object rootParentId) where TEntity : class
    {
        return CreateHierarchy(allItems, default(TEntity), idProperty, parentIdProperty, rootItemId, rootParentId, 0, 0);
    }
 
    /// <summary>
    /// LINQ to Objects (IEnumerable) AsHierachy() extension method
    /// </summary>
    /// <typeparam name="TEntity">Entity class</typeparam>
    /// <typeparam name="TProperty">Property of entity class</typeparam>
    /// <param name="allItems">Flat collection of entities</param>
    /// <param name="idProperty">Func delegete to Id/Key of entity</param>
    /// <param name="parentIdProperty">Func delegete to parent Id/Key</param>
    /// <param name="rootItemId">Value of root item Id/Key</param>
    /// <param name="maxDepth">Maximum depth of tree</param>
    /// <returns>Hierarchical structure of entities</returns>
    public static IEnumerable<HierarchyNode<TEntity>> AsHierarchy<TEntity, TProperty>(
      this IEnumerable<TEntity> allItems,
      Func<TEntity, TProperty> idProperty,
      Func<TEntity, TProperty> parentIdProperty,
      object rootItemId,
      object rootParentId,
      int maxDepth) where TEntity : class
    {
        return CreateHierarchy(allItems, default(TEntity), idProperty, parentIdProperty, rootItemId, rootParentId, maxDepth, 0);
    }
}

Using this technique, I quickly converted my flat collection in a hierarchical one and bound it to my treeview. Great!

 

Implementing the conversion via an IValueConverter

 

The solution still seemed a bit messy, so I hooked it up to an IValueConverter to make the conversion automatic and seamless. The less code the better, right? The IValueConverter interface is incredibly useful (I've been using it all week to display different ItemTemplate content in an ItemsControl depending what the data is). Basically, it grabs data during data binding and lets you examine / alter / replace data while it moves from the data source to the destination. In my case, I want to bind the ItemsSource of a TreeView to a flat collection of objects, but I wanted the TreeView to receive a hierarchical collection of objects. Here's my converter:

public class HierarchyConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        ObservableCollection<Person> p = (ObservableCollection<Person>)value;
        var tree = p.AsHierarchy(e => e.PersonID, e => e.Manager.PersonID, null, 0);
        return tree;
    }
    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

 

Wrap Up

 

Using this technique, I took  a flat collection of business objects and dynamically converted it to a hierarchical collection of node objects at run-time. Once again this is an instance where I expected something to be painful and involve writing a lot of boring code, but Silverlight managed to give me a really nice, elegant solution that I can re-use throughout my app.

I have done  some experimentation, though, and if you want to get two-way data binding to work for you, then you'll have to drop the IValueConverter part. This is becuase the Convert() method is called when the initial binding takes place to the TreeView's ItemsSource, but at no time will the ConvertBack() method be called - even if you specify fields in your Hierarchical ItemTemplate to have two-way binding. I still use the AsHierarchy extension method for a quick conversion when I am using two-way data binding however.

Here's the Page.xaml:

<UserControl 
    x:Class="LinqTree.Page"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:controls="clr-namespace:Microsoft.Windows.Controls;assembly=Microsoft.Windows.Controls"
    xmlns:local="clr-namespace:LinqTree"
    >
    <Grid x:Name="LayoutRoot" Background="White" Width="400" Height="470">
        <Grid.Resources>
            <local:HierarchyConverter x:Key="HierarchyConverter" />
            <Style x:Key="NameStyle" TargetType="TextBlock">
                <Setter Property="FontSize" Value="14" />
            </Style>
            <Style x:Key="JobTitleStyle" TargetType="TextBlock">
                <Setter Property="FontSize" Value="12" />
                <Setter Property="Foreground" Value="LightGray" />
                <Setter Property="FontStyle" Value="Italic" />
            </Style>
            <Style x:Key="MainBorder" TargetType="Border">
                <Setter Property="Background" Value="WhiteSmoke" />
                <Setter Property="BorderThickness" Value="1" />
                <Setter Property="CornerRadius" Value="8" />
                <Setter Property="BorderBrush" Value="LightGray" />
                <Setter Property="Padding" Value="12" />
            </Style>
        </Grid.Resources>
        <Border Style="{StaticResource MainBorder}">
            <controls:TreeView 
            x:Name="treeview"
            ItemsSource="{Binding Converter={StaticResource HierarchyConverter}}">
                <controls:TreeView.ItemTemplate>
                    <controls:HierarchicalDataTemplate ItemsSource="{Binding ChildNodes}">
                        <StackPanel Orientation="Vertical">
                            <StackPanel Orientation="Horizontal">
                                <TextBlock Style="{StaticResource NameStyle}" Text="{Binding Entity.FirstName}" />
                                <TextBlock Text=" " />
                                <TextBlock Style="{StaticResource NameStyle}" Text="{Binding Entity.LastName}" />
                            </StackPanel>
                            <StackPanel Orientation="Horizontal">
                                <TextBlock Style="{StaticResource JobTitleStyle}" Text="{Binding Entity.JobTitle}" />
                            </StackPanel>
                        </StackPanel>
                    </controls:HierarchicalDataTemplate>
                </controls:TreeView.ItemTemplate>
            </controls:TreeView>
        </Border>
    </Grid>
</UserControl>

And here's  the code behind (Page.xaml.cs):

using System;
using System.Collections.ObjectModel;
using System.Windows.Controls;
 
namespace LinqTree
{
    public partial class Page : UserControl
    {
        public Page()
        {
            InitializeComponent();
            ObservableCollection<Person> Persons = PersonCollection.PersonList();
            treeview.DataContext = Persons;
        }
    }
}

As you can see, a nice clean solution to a common problem. Thanks to Stefan Cruysberghs for pointing me in the right direction!

 

Source

Source is here.

Categories: Work

Comments

cash loans
cash loans United States on 10/17/2009 12:32:00 PM

Interesting post

fast cash advance
fast cash advance United States on 10/22/2009 4:24:13 AM

Hmmm interesting stuff

fast personal loans
fast personal loans United States on 11/4/2009 12:22:44 PM

Yea nice Work !Laughing

faxless payday loans
faxless payday loans United States on 11/7/2009 10:46:03 AM

Searching for this for some time now - i guess luck is more advanced than search engines Smile

Multivariate Testing
Multivariate Testing United States on 11/16/2009 10:46:31 AM

Thanks for the information regarding automatic hierarchical data.

payday loans
payday loans United States on 12/20/2009 7:30:17 PM

Interesting post

SEO Nepal
SEO Nepal United States on 12/22/2009 11:09:50 PM

Well maybe it will get good going with the blog.

payday loans
payday loans United States on 1/20/2010 2:41:49 AM

Whatever is worth doing at all is worth doing well.

Loans in SC
Loans in SC United States on 1/21/2010 1:30:50 PM

Master books, but do not let them master you. Read to live, not live to read.

payday loans
payday loans United States on 1/23/2010 12:21:10 PM

Minds are like parachutes - they only function when open.

online payday advance
online payday advance United States on 1/30/2010 3:55:16 AM

We all start with all there is. It's how we use it that makes things possible.

online payday loans
online payday loans United States on 2/5/2010 9:21:16 PM

The major reason for setting a goal is for what it makes of you to accomplish it. What it makes of you will always be the far greater value than what you get.

lose weight
lose weight United States on 2/15/2010 11:49:15 PM

Plunge boldly into the thick of life, and seize it where you will, it is always interesting.

reverse cell phone number look up
reverse cell phone number look up United States on 3/4/2010 4:50:05 PM

one more comment from me to appreciate your work

Add comment


(Will show your Gravatar icon)

  Country flag

biuquote
  • Comment
  • Preview
Loading