One WPF control that we haven't taken a look at here on SOTC is the
TreeView.
Well, no more! Today we are going to rectify that, as we build an
application that not only uses the TreeView, but also dynamically
loads data into it on demand. We are going to cover a couple other new
topics as well, including
HierarchicalDataTemplates
and
CompositeCollections.
So what are we building? A pretty simple app that pulls the tree
hierarchy of categories and images from Gaming
Textures and displays it in a
TreeView. Gaming Textures has a couple of calls that we can make to
get lists of base categories and then the children for each category -
so we will be making a web request on demand to get the children for a
category, parsing the resulting JSON into C# objects, and then adding
those items to the tree view.
For example, we start out with the list of base categories:

When an item is expanded, we send off a request for the children:

And once we have the children, we display them (complete with helpful tooltips!):

Ok, so how do we do this? Well, it is time to find out! Let's start with some simple XAML for the basic window layout:
<Window x:Class="WpfTreeView.TreeViewWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sotc="clr-namespace:WpfTreeView"
Title="Tree View Example" Height="300" Width="300">
<TreeView>
<TreeViewItem Header="Categories" x:Name="_ImageTree"
x:FieldModifier="private">
<TreeViewItem TextBlock.FontStyle="Italic"
Header="Loading..."/>
</TreeViewItem>
</TreeView>
</Window>
This gives up a basic layout that looks like this:

Just by looking at that code snippet, you have probably already figured
out the basics of using a TreeView. You just populate it with
TreeViewItems.
The Header property on TreeViewItem is the content that will appear
for that item, and any children of the TreeViewItem will appear as
children in the tree.
The "Loading..." tree view item is just there as a placeholder - as you might suspect, when the items actually load, we will be replacing that item. So let's take a look at how to load those items:
public partial class TreeViewWindow : Window
{
public const string BaseUrl = "http://www.gamingtextures.com";
public const string QueryURl = BaseUrl + "/Callbacks/query.php";
public TreeViewWindow()
{
InitializeComponent();
var wc = new WebClient();
wc.OpenReadCompleted += BaseCategoryReadCompleted;
wc.OpenReadAsync(new Uri(QueryURl + "?QType=AllBaseCats"));
}
private void BaseCategoryReadCompleted(object sender,
OpenReadCompletedEventArgs e)
{
if (e.Error != null || e.Cancelled)
{
((TreeViewItem)_ImageTree.Items[0]).Header =
"Error Getting Base Categories";
return;
}
_ImageTree.Items.Clear();
_ImageTree.ItemsSource = Category.DeserializeJson(e.Result);
}
}
So when the application starts up, we immediately go off and try and load the list of base categories. Some of this code might look familiar - we did some we requests with JSON deserialization just a few months ago in Silverlight 2 & PHP Tutorial - Transmitting data using JSON. This follows pretty much the same pattern. If there is an error with the web request, we replace the text "Loading..." with the error message:

But what if we do get the data back correctly (which hopefully we do)?
What do we do then? Well, we clear that "Loading..." item out of the
tree view, and then we deserialize the JSON - which means we have to
take a look at the Category class:
public class Category
{
private bool _Loaded = false;
public int IDCategory { get; set; }
public string CatName { get; set; }
public string CatDescription { get; set; }
public CompositeCollection Children { get; set; }
public Category()
{
Children = new CompositeCollection();
Children.Add(new TextBlock() {
Text = "Loading...", FontStyle = FontStyles.Italic });
}
public static List<Category> DeserializeJson(Stream stream)
{
var json = new DataContractJsonSerializer(typeof(List<Category>));
return json.ReadObject(stream) as List<Category>;
}
}
The method DeserializeJson takes a stream and deserializes it as a
List of Category objects. The deserialization process fills in the
fields IDCategory, CatName, and CatDescription. In addition, when
a new Category instance is created, we fill the Children collection
with a "Loading..." TextBlock. We will see how this is used in a
moment.
So now we have a collection of Category objects, but that isn't enough
to display them in the tree view correctly. In fact, if we try to right
now, we will get something that looks like this:

We have to add a data template to the XAML to get the categories to look correct:
<Window x:Class="WpfTreeView.TreeViewWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sotc="clr-namespace:WpfTreeView"
Title="Tree View Example" Height="300" Width="300">
<Window.Resources>
<HierarchicalDataTemplate DataType="{x:Type sotc:Category}"
ItemsSource="{Binding Path=Children}">
<TextBlock Text="{Binding Path=CatName}"
ToolTip="{Binding Path=CatDescription}" />
</HierarchicalDataTemplate>
</Window.Resources>
<TreeView>
<TreeViewItem Header="Categories" x:Name="_ImageTree"
x:FieldModifier="private">
<TreeViewItem TextBlock.FontStyle="Italic"
Header="Loading..."/>
</TreeViewItem>
</TreeView>
</Window>
Here we are using a HierarchicalDataTemplate for the categories. By
setting the DataType property to the type Category, we ensure that
this type of template will be used anytime that a Category instance
appears. The ItemsSource property gets bound to the children of the
category (i.e., the Children property - which at the moment just holds
the text "Loading...". Finally, the content of the template is what will
be used for the header of the tree view item - and here we just make a
TextBlock whose text is the category name and whose tooltip is the
category description.
So now with all that work, you will get an application that looks like this:

Ok, now we want to actually load the category children. The first step
is to get notification that the user actually expanded a category. To do
this, we add a handler on the window for all TreeViewItem Expanded
events:
AddHandler(TreeViewItem.ExpandedEvent,
new RoutedEventHandler(TreeItemExpanded), true);
The Expanded event gets fired when a TreeViewItem is expanded. By
setting up this handler, the method TreeItemExpanded will get called
for any Expanded event for any TreeViewItem in this window.
private void TreeItemExpanded(object sender, RoutedEventArgs e)
{
var item = e.OriginalSource as TreeViewItem;
if (item == null)
{ return; }
var cat = item.DataContext as Category;
if (cat == null)
{ return; }
cat.LoadChildren();
}
So when this method gets called the original source will be the
TreeViewItem being expanded. If the DataContext of that item is a
Category instance, then we need to load the children (and so we call
LoadChildren):
public void LoadChildren()
{
if (_Loaded)
{ return; }
_Loaded = true;
var wc = new WebClient();
wc.OpenReadCompleted += CategoryReadCompleted;
wc.OpenReadAsync(new Uri(TreeViewWindow.QueryURl
+ "?QType=NextCatChildren&IDCat=" + IDCategory));
}
If we have already loaded the children for this category, don't do anything. Otherwise, set that flag to true (we are loading them now!) and send off a new web request. This request will return any child categories for this category:
private void CategoryReadCompleted(object sender, OpenReadCompletedEventArgs e)
{
if (e.Error != null || e.Cancelled)
{
((TextBlock)Children[0]).Text = "Error Getting Category Children";
return;
}
var list = DeserializeJson(e.Result);
_ActualChildrenCount += list.Count;
Children.Insert(0, new CollectionContainer() { Collection = list });
var wc = new WebClient();
wc.OpenReadCompleted += ImageReadCompleted;
wc.OpenReadAsync(new Uri(TreeViewWindow.QueryURl
+ "?QType=NextImgChildren&IDCat=" + IDCategory));
}
So when the web request returns, we do the same type of thing as we did
when loading the base categories. If there was an error, we replace the
"Loading.." text with an error message. Otherwise, we deserialize the
result into a list of category objects. We then add this collection to
the children - and this is where the CompositeCollection starts to
come in handy.
You might be wondering what in the world a CompositeCollection is.
Well, it allows you to have a collection of both items and other
collections of various types - and when it is used as an ItemsSource,
the content is flattened out into a single list for display. For
instance, we now have a collection that contains a TextBlock and a
separate collection of Categories. So at this point, the app looks
something like this:

Ok, but now that we have the category children, it is time to get the
image children. At the end of CategoryReadCompleted, you probably
noticed the new web request being sent off - this is the request for the
image children. When that returns, it will hit this code:
private void ImageReadCompleted(object sender, OpenReadCompletedEventArgs e)
{
if (e.Error != null || e.Cancelled)
{
((TextBlock)Children[1]).Text = "Error Getting Category Children";
return;
}
Children.RemoveAt(1);
var list = GTImage.DeserializeJson(e.Result);
_ActualChildrenCount += list.Count;
Children.Add(new CollectionContainer() { Collection = list });
if (_ActualChildrenCount == 0)
{ Children.Add(new TextBlock() { Text = "No Children" }); }
}
Same type of error cases here as in the other two read completed
handlers. If the read did complete, we remove the "Loading..."
TextBlock from the children, and we deserialize the stream - except
this time we are getting back a collection of GTImages:
public class GTImage
{
public int IDImage { get; set; }
public string { get; set; }
public string { get; set; }
public string IconPath
{
get
{
return TreeViewWindow.BaseUrl
+ "/Images/image.php?IDImage=" + IDImage;
}
}
public string ThumbnailPath
{
get
{
return TreeViewWindow.BaseUrl
+ "/Images/image.php?IDTFS=3&IDImage=" + IDImage;
}
}
public static List<GTImage> DeserializeJson(Stream stream)
{
var json = new DataContractJsonSerializer(typeof(List<GTImage>));
return json.ReadObject(stream) as List<GTImage>;
}
}
The GTImage class is pretty simple - the fields getting set by the
deserializer are IDImage, Name, and Description.
So now our categories are getting both child categories and child
images. But currently our GTImage class is template-less, which means
that the app ends up looking like so:

So it is time to break out that template:
<DataTemplate DataType="{x:Type sotc:GTImage}">
<StackPanel Orientation="Horizontal">
<Image Source="{Binding Path=IconPath}" Width="16"
Height="16" Margin="0 2 2 2" />
<TextBlock Text="{Binding Path=Name}"
VerticalAlignment="Center" />
<StackPanel.ToolTip>
<StackPanel Orientation="Horizontal">
<Image Source="{Binding Path=ThumbnailPath}"
Width="64" Height="64" Margin="0 2 4 0" />
<TextBlock Text="{Binding Path=Description}"
VerticalAlignment="Center" />
</StackPanel>
</StackPanel.ToolTip>
</StackPanel>
</DataTemplate>
Just like with the Category template, we set the DataType property
to make it so that this template is applied for every instance of
GTImage. Past that, it is some pretty standard use of WPF controls. A
StackPanel to lay out the icon image and the name, and another
StackPanel in the ToolTip to lay out the larger image and the
description.
And that is it! Now the app looks like the screenshots at the top of the tutorial. Here is all the code together in a single block:
<Window x:Class="WpfTreeView.TreeViewWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sotc="clr-namespace:WpfTreeView"
Title="Tree View Example" Height="300" Width="300">
<Window.Resources>
<HierarchicalDataTemplate DataType="{x:Type sotc:Category}"
ItemsSource="{Binding Path=Children}">
<TextBlock Text="{Binding Path=CatName}"
ToolTip="{Binding Path=CatDescription}" />
</HierarchicalDataTemplate>
<DataTemplate DataType="{x:Type sotc:GTImage}">
<StackPanel Orientation="Horizontal">
<Image Source="{Binding Path=IconPath}" Width="16"
Height="16" Margin="0 2 2 2" />
<TextBlock Text="{Binding Path=Name}"
VerticalAlignment="Center" />
<StackPanel.ToolTip>
<StackPanel Orientation="Horizontal">
<Image Source="{Binding Path=ThumbnailPath}"
Width="64" Height="64" Margin="0 2 4 0" />
<TextBlock Text="{Binding Path=Description}"
VerticalAlignment="Center" />
</StackPanel>
</StackPanel.ToolTip>
</StackPanel>
</DataTemplate>
</Window.Resources>
<TreeView>
<TreeViewItem Header="Categories" x:Name="_ImageTree"
x:FieldModifier="private">
<TreeViewItem TextBlock.FontStyle="Italic"
Header="Loading..."/>
</TreeViewItem>
</TreeView>
</Window>
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Runtime.Serialization.Json;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
namespace WpfTreeView
{
public partial class TreeViewWindow : Window
{
public const string BaseUrl = "http://www.gamingtextures.com";
public const string QueryURl = BaseUrl + "/Callbacks/query.php";
public TreeViewWindow()
{
InitializeComponent();
AddHandler(TreeViewItem.ExpandedEvent,
new RoutedEventHandler(TreeItemExpanded), true);
var wc = new WebClient();
wc.OpenReadCompleted += BaseCategoryReadCompleted;
wc.OpenReadAsync(new Uri(QueryURl + "?QType=AllBaseCats"));
}
private void BaseCategoryReadCompleted(object sender,
OpenReadCompletedEventArgs e)
{
if (e.Error != null || e.Cancelled)
{
((TreeViewItem)_ImageTree.Items[0]).Header =
"Error Getting Base Categories";
return;
}
_ImageTree.Items.Clear();
_ImageTree.ItemsSource = Category.DeserializeJson(e.Result);
}
private void TreeItemExpanded(object sender, RoutedEventArgs e)
{
var item = e.OriginalSource as TreeViewItem;
if (item == null)
{ return; }
var cat = item.DataContext as Category;
if (cat == null)
{ return; }
cat.LoadChildren();
}
}
public class Category
{
private bool _Loaded = false;
private int _ActualChildrenCount = 0;
public int IDCategory { get; set; }
public string CatName { get; set; }
public string CatDescription { get; set; }
public CompositeCollection Children { get; set; }
public Category()
{
Children = new CompositeCollection();
Children.Add(new TextBlock() {
Text = "Loading...", FontStyle = FontStyles.Italic });
}
public static List<Category> DeserializeJson(Stream stream)
{
var json = new DataContractJsonSerializer(typeof(List<Category>));
return json.ReadObject(stream) as List<Category>;
}
public void LoadChildren()
{
if (_Loaded)
{ return; }
_Loaded = true;
var wc = new WebClient();
wc.OpenReadCompleted += CategoryReadCompleted;
wc.OpenReadAsync(new Uri(TreeViewWindow.QueryURl
+ "?QType=NextCatChildren&IDCat=" + IDCategory));
}
private void CategoryReadCompleted(object sender,
OpenReadCompletedEventArgs e)
{
if (e.Error != null || e.Cancelled)
{
((TextBlock)Children[0]).Text = "Error Getting Category Children";
return;
}
var list = DeserializeJson(e.Result);
_ActualChildrenCount += list.Count;
Children.Insert(0, new CollectionContainer() { Collection = list });
var wc = new WebClient();
wc.OpenReadCompleted += ImageReadCompleted;
wc.OpenReadAsync(new Uri(TreeViewWindow.QueryURl
+ "?QType=NextImgChildren&IDCat=" + IDCategory));
}
private void ImageReadCompleted(object sender,
OpenReadCompletedEventArgs e)
{
if (e.Error != null || e.Cancelled)
{
((TextBlock)Children[1]).Text = "Error Getting Category Children";
return;
}
Children.RemoveAt(1);
var list = GTImage.DeserializeJson(e.Result);
_ActualChildrenCount += list.Count;
Children.Add(new CollectionContainer() { Collection = list });
if (_ActualChildrenCount == 0)
{ Children.Add(new TextBlock() { Text = "No Children" }); }
}
}
public class GTImage
{
public int IDImage { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public string IconPath
{
get
{
return TreeViewWindow.BaseUrl
+ "/Images/image.php?IDImage=" + IDImage;
}
}
public string ThumbnailPath
{
get
{
return TreeViewWindow.BaseUrl
+ "/Images/image.php?IDTFS=3&IDImage=" + IDImage;
}
}
public static List<GTImage> DeserializeJson(Stream stream)
{
var json = new DataContractJsonSerializer(typeof(List<GTImage>));
return json.ReadObject(stream) as List<GTImage>;
}
}
}
Hope this tutorial was an informative introduction to the TreeView and
HierarchicalDataTemplates. As always, you can grab the Visual Studio
solution below if you want to play around with the code.
Source Files:
Hi, Thanks for this great tutorial on treeview.I have been trying to get into WPF for a while and i still feel like I don't know much. I have a few things which are unclear to me: 1. I do not understand your use of compositecollection. Is it Like Every Category contains a List of GT Images
I understand it composite collection can contain Collections too, but still it's making things look very magical.
?2. Also you are loading images and data from the web client asynchronously, But are you also applying the images to the template async? If yes, do you not need to worry about calling invoke? If no, how would you make sure the images are being applied to template async?
?3. Is you expanded a item, and before loading finished, you immediately collapsed it, how could you cancel the web client request.
Thank You, I know these are a lot of questions but WPF is still so mysterious to me.
#2 is pretty easy to answer - the WPF Image control is really robust, and internally supports async. loading, so we don't have to worry about that. Even better, for the larger images in the Tooltips, the image isn't actually requested until the tooltip is displayed.
For #3, you would need to keep around a reference to the
WebClientinstance, and when thecollapsedevent onTreeViewItemfired, you could cancel the currently active request.And #1, let me seen if I can explain it a little better with some diagrams then I did in the article. After all the loading is done, every Category has a Child Composite Collection that looks something like this:
By using a composite collection, the TreeView sees this children collection as just a flat list instead of a nested collection of lists:
Thanks very much for the answers. Seems to be more clear to me now. I had one quick follow up question.
When we apply a ItemTemplate to a ItemsControl, is the template apply to each item async? or is it done sequentially like first apply template to first item and then to second? If it's not async, can we make it async?
Thanks very much
One of the best on the subject I've seen. Lots of nice tips.
is it possible that you post sample without that "online" code cause it makes me confused as I never worked with "online" code
Great tutorial and great site. Thanks for keeping this going!