Skip to main content

...

....

C# WPF Tutorial - Dynamic Data and the TreeView [Intermediate]


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:

App Screenshot 1

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

App Screenshot 2

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

App Screenshot 3

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:

App Screenshot 4

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:

Tree View App Error Screenshot

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:

Tree View App Misssing Template

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:

Tree View App Category Template

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:

Only child categories loaded

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:

No template for images

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

Popular posts from this blog

C# Snippet - Shuffling a Dictionary [Beginner]

Randomizing something can be a daunting task, especially with all the algorithms out there. However, sometimes you just need to shuffle things up, in a simple, yet effective manner. Today we are going to take a quick look at an easy and simple way to randomize a dictionary, which is most likely something that you may be using in a complex application. The tricky thing about ordering dictionaries is that...well they are not ordered to begin with. Typically they are a chaotic collection of key/value pairs. There is no first element or last element, just elements. This is why it is a little tricky to randomize them. Before we get started, we need to build a quick dictionary. For this tutorial, we will be doing an extremely simple string/int dictionary, but rest assured the steps we take can be used for any kind of dictionary you can come up with, no matter what object types you use. Dictionary < String , int > origin = new Dictionary < string , int >();

C# WPF Printing Part 2 - Pagination [Intermediate]

About two weeks ago, we had a tutorial here at SOTC on the basics of printing in WPF . It covered the standard stuff, like popping the print dialog, and what you needed to do to print visuals (both created in XAML and on the fly). But really, that's barely scratching the surface - any decent printing system in pretty much any application needs to be able to do a lot more than that. So today, we are going to take one more baby step forward into the world of printing - we are going to take a look at pagination. The main class that we will need to do pagination is the DocumentPaginator . I mentioned this class very briefly in the previous tutorial, but only in the context of the printing methods on PrintDialog , PrintVisual (which we focused on last time) and PrintDocument (which we will be focusing on today). This PrintDocument function takes a DocumentPaginator to print - and this is why we need to create one. Unfortunately, making a DocumentPaginator is not as easy as

C# WPF Tutorial - Implementing IScrollInfo [Advanced]

The ScrollViewer in WPF is pretty handy (and quite flexible) - especially when compared to what you had to work with in WinForms ( ScrollableControl ). 98% of the time, I can make the ScrollViewer do what I need it to for the given situation. Those other 2 percent, though, can get kind of hairy. Fortunately, WPF provides the IScrollInfo interface - which is what we will be talking about today. So what is IScrollInfo ? Well, it is a way to take over the logic behind scrolling, while still maintaining the look and feel of the standard ScrollViewer . Now, first off, why in the world would we want to do that? To answer that question, I'm going to take a an example from a tutorial that is over a year old now - Creating a Custom Panel Control . In that tutorial, we created our own custom WPF panel (that animated!). One of the issues with that panel though (and the WPF WrapPanel in general) is that you have to disable the horizontal scrollbar if you put the panel in a ScrollV