How to turn Silverlight DataGrid to TreeGrid in 15 minutes.

Eugene Akinshin

by Eugene Akinshin, Ph.D., Founder of Perpetuum Software LLC

I am one of those guys who constantly try to convince people around them that Silverlight is a wonderful thing and how it is easy to design user-friendly user interface with it. And as expected my enthusiasm rarely understood by colleagues especially by experienced developers whose favorite argument is “All this can be perfectly done in Windows Forms (MFC, VCL, OWL, HTML, Ajax, you name it), so we don’t need this disgusting Silverlight”. And it is hard to argue against it as nothing absolutely new can be created – as musicians blamed for plagiarism say “There are only seven notes”. Power of Silverlight UI Framework is not in the ability to design incredible pile of effects and animation, but in the simplicity and lightning speed of implementation of sophisticated scenarios.

And today I have a vivid sample. In practice, there often appears a necessity to use a hybrid control joining functionality of the TreeView and DataGrid components, i.e. of the tree representing information by each item in several columns with all abilities characteristic to table view: changing columns by a single mouse movement, sorting upon a click on the column title, editing cell content etc. For example, such control is an essential part of the interface of any mailer: Outlook Express, Windows Mail, Mozilla Thunderbird.

And I needed such a control when decided to create an on-line message board to represent message chains in the thread.

If you are a Windows Forms developer and face the requirement to use this control – this is a bad luck. Most likely you will have to buy special third party library – standard toolbox doesn’t include such control, and independent development will take several hundreds of men hours.

But if you use Silverlight your life will be much easier. Combination of mechanisms of templates and data binding makes it possible to turn an ordinary DataGrid to such control in minutes.

To do it, we need to create a data source making hierarchical structure be flat list.

Such data source creates a wrapper around each element and its Level and IsExpanded properties serve to display correct shift and state of each element in the original hierarchy.

public sealed class GridTreeDataSource
{
public GridTreeDataSource(IList originalSource, Func<object, IList> getChildren)
{
OriginalSource = originalSource;
GetChildren = getChildren;
Source = new ObservableCollection<GridTreeDataItem>();
FillWithGridTreeItems(Source, OriginalSource, 0);
}
public IList OriginalSource { get; private set; }
private Func<object, IList> GetChildren { get; set; }
public ObservableCollection<GridTreeDataItem> Source { get; private set; }
private void FillWithGridTreeItems(IList<GridTreeDataItem> destinationCollection, IList sourceCollection, int level)
{
foreach (object item in sourceCollection)
{
GridTreeDataItem treeItem = new GridTreeDataItem(item, this, level);
treeItem.IsVisible = (level == 0);
destinationCollection.Add(treeItem);
IList list = GetChildren(item);
if (list != null)
FillWithGridTreeItems(treeItem.Children, list, level + 1);
}
}
internal void NotifyIsExpandedChanged(GridTreeDataItem item)
{
if (item.IsVisible)
{
if (!item.IsExpanded)
{
RemoveChildren(item);
}
else
{
int index = Source.IndexOf(item);
AddChildren(item, ref index);
}
}
}
private void AddChildren(GridTreeDataItem element, ref int index)
{
foreach (var item in element.Children)
{
Source.Insert(++index, item);
item.IsVisible = true;
if (item.IsExpanded)
{
AddChildren(item, ref index);
}
}
}
private void RemoveChildren(GridTreeDataItem element)
{
foreach (var item in element.Children)
{
Source.Remove(item);
item.IsVisible = false;
RemoveChildren(item);
}
}
}
public class GridTreeDataItem : INotifyPropertyChanged
{
internal GridTreeDataItem(object item, GridTreeDataSource owner, int level)
{
Item = item;
Owner = owner;
Level = level;
}
public GridTreeDataSource Owner { get; private set; }
public object Item { get; private set; }
public int Level { get; private set; }
public bool IsVisible { get; internal set; }
private bool isExpanded = false;
public bool IsExpanded
{
get
{
return isExpanded;
}
set
{
if (value != isExpanded)
{
isExpanded = value;
RaisePropertyChanged(“IsExpanded”);
Owner.NotifyIsExpandedChanged(this);
}
}
}
private List<GridTreeDataItem> children = new List<GridTreeDataItem>();
public List<GridTreeDataItem> Children
{
get
{
return children;
}
}
public bool HasChildren
{
get
{
return Children.Count > 0;
}
}
#region INotifyPropertyChanged Members
protected void RaisePropertyChanged(string name)
{
OnPropertyChanged(new PropertyChangedEventArgs(name));
}
protected virtual void OnPropertyChanged(PropertyChangedEventArgs args)
{
PropertyChangedEventHandler temp = PropertyChanged;
if (temp != null)
{
temp(this, args);
}
}
public event PropertyChangedEventHandler PropertyChanged;
#endregion
}

As you see, here are only 150 lines of trivial code. (And there could be even less if I wasn’t a follower of the formatting style when each curly bracket takes a separate line :) ).

Then substitute template for the first table column using the designed structure as a data source:

<data:DataGrid x:Name=”dataGrid” AutoGenerateColumns=”False” IsReadOnly=”True” CanUserSortColumns=”False”>
<data:DataGrid.Columns>
<data:DataGridTemplateColumn Header=”Ex”>
<data:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<StackPanel Orientation=”Horizontal” Margin=”{Binding Level, Converter={StaticResource mc}}>
<ToggleButton x:Name=”ExpanderButton” IsChecked=”{Binding IsExpanded, Mode=TwoWay}
Visibility=”{Binding HasChildren, Converter={StaticResource vc}}
VerticalAlignment=”Center” IsTabStop=”False” TabNavigation=”Once” Margin=”2″ Template=”{StaticResource btnTemplate}/>
<TextBlock Text=”{Binding Item.Name}VerticalAlignment=”Center”/>
</StackPanel>
</DataTemplate>
</data:DataGridTemplateColumn.CellTemplate>
</data:DataGridTemplateColumn>
<data:DataGridTextColumn Binding=”{Binding Item.Num}Header=”Num”/>
</data:DataGrid.Columns>
</data:DataGrid>

And this is it! TreeGrid can be used! The result looks like that.

To make it understandable and more obvious, I simplified this implementation a little; the sample is not ideal as it comes to performance and it doesn’t include functions for manipulations over the tree, such as ExpandAll etc. But the main point is that this code is taken from the real application. Prototype of this project is being tested at the moment and you can make sure that it really works.

You can download source code of this sample and use it for examination and in your applications.

If you have any questions or comments, please feel free to contact me directly at eugene@perpetuumsoft.com

kick it on DotNetKicks.com

March 12th, 2010

Comments

  1. Great piece of code, thank you so much!

  2. How to expand all items at the time of initial load?

  3. Eugene Akinshin:

    Regretably, I’m not an expert in WPF so I don’t know what is the difference between WPF and Silverlight Togglebutton behaviours.

    In Silverlight and WinRT, in similar circumstances, when I need to have some non-standard binding to existing control, I usually consider to create AttachedProperty with required behaviour.

  4. Thanks Eugene Akinshin. this one is just brilliant. i have been searching in so many ways and so many control to achieve these kind of structure. but ur post gave me an idea that i can achieve the same with observable collection and datagird. Thanks a ton. :)

Leave a Comment