One WPF control that we haven't taken a look at here 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 GamingTextures 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. 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 GTImage
s: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:
Comments
Post a Comment