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:
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 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:
{
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:
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:
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.
{
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):
{
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:
{
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:
{
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 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:
<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:
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.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. If you have any questions or comments, leave them below and I'll do my best to answer them.
04/07/2009 - 00:51
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.
04/07/2009 - 12:31
#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:
|
- CollectionContainer
| |
| - List<Category>
| |
| |
| - Category A
| - Catergory B
| ... other child categories
|
- CollectionContainer
|
- List<GTImage>
|
|
- GTImage A
- GTImage B
... other child images
By using a composite collection, the TreeView sees this children collection as just a flat list instead of a nested collection of lists:
Category B
GTImage A
GTImage A
04/08/2009 - 00:38
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
05/29/2009 - 08:34
One of the best on the subject I've seen. Lots of nice tips.
11/02/2010 - 06:50
is it possible that you post sample without that "online" code cause it makes me confused as I never worked with "online" code
08/11/2011 - 13:43
Great tutorial and great site. Thanks for keeping this going!
Add Comment
[language] [/language]
Examples:
[javascript] [/javascript]
[actionscript] [/actionscript]
[csharp] [/csharp]
See here for supported languages.
Javascript must be enabled to submit anonymous comments - or you can login.