Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

A Multi-Threaded WPF TreeView Explorer

0.00/5 (No votes)
11 Mar 2008 4  
Multi-Threaded WPF TreeView Explorer

Contents

  1. Introduction
  2. End Result
  3. Implementation
  4. Further Improvements
  5. Any Feedback
  6. History

Introduction

Most business applications need to display hierarchal data, and of course a TreeView has been the way to go for as long as I can remember. However, many situations arise where loading data in a tree can take quite some time. And this is what this article tries to address. All loading of items in this TreeView are done in a multi-threaded manner.

So naturally when starting to learn WPF, the first thing on the list was how to get a TreeView up and running.

I ran into a great article, which is the basis of my work, by Sacha Barber titled "A Simple WPF Explorer Tree". I definitely encourage you to read this article, as well as, Sacha's great beginner's series on WPF. Josh Smith also added to Sacha's work on his blog at Reaction to: A Simple WPF Explorer Tree. Both guys give us a great start to displaying items in a TreeView with images attached.

End Result

The end result of this article is a tree that loads sub-nodes on background threads, with instant feedback to the user as nodes are inserted. The UI never locks up waiting for long running I/O operations.

You may have also noticed that multiple nodes can be loaded simultaneously, and that the loading can also be cancelled.

So now that you've seen the end result, let's get to the implementation.

Implementation

For my sample implementation, I continued on Sacha's work of displaying the local file system. Of course, other types of data could just as easily be displayed and loaded in a threaded manner.

The difference with my implementation is when a node is clicked, its sub-items are fetched on a background thread. I tried to keep the implementation a bit generic, but at the same time not get too complex and abstract.

The concept here is very simple. We start with a root node that is created when the form is loaded.

void DemoWindow_Loaded(object sender, RoutedEventArgs e)
{
    // Create a new TreeViewItem to serve as the root.
    var tviRoot = new TreeViewItem();

    // Set the header to display the text of the item.
    tviRoot.Header = "My Computer";

    // Add a dummy node so the 'plus' indicator
    // shows in the tree
    tviRoot.Items.Add(_dummyNode);

    // Set the item expand handler
    // This is where the deferred loading is handled
    tviRoot.Expanded += OnRoot_Expanded;

    // Set the attached property 'ItemImageName' 
    // to the image we want displayed in the tree
    TreeViewItemProps.SetItemImageName(tviRoot, @"Images/Computer.png");

    // Add the item to the tree folders
    foldersTree.Items.Add(tviRoot);
}

Most of what you see above is pretty clear. The header property is the actual display text. The dummy node is added so that the plus indicator is visible for the root node. The OnRoot_Expanded handles the actual loading, back to this in a minute.

Now you might ask what is TreeViewItemProps.SetItemImageName? This is an attached property that I defined, among a few others, in the static class TreeViewItemProps. These properties are databound in XAML to the TreeViewItem's DataTemplate to control display settings of the progress bar and cancel and reload buttons. Sacha did a great job explaining DependencyProperties in his article WPF: A Beginner's Guide - Part 4 of n (Dependency Properties).

public static class TreeViewItemProps
{
    public static readonly DependencyProperty ItemImageNameProperty;
    public static readonly DependencyProperty IsLoadingProperty;
    public static readonly DependencyProperty IsLoadedProperty;
    public static readonly DependencyProperty IsCanceledProperty;

    static TreeViewItemProps()
    {
        ItemImageNameProperty = DependencyProperty.RegisterAttached
                                ("ItemImageName", typeof(string),
                                    typeof(TreeViewItemProps), 
                                new UIPropertyMetadata(string.Empty));
        IsLoadingProperty = DependencyProperty.RegisterAttached("IsLoading", 
                            typeof(bool), typeof(TreeViewItemProps),
                            new FrameworkPropertyMetadata(false, 
                            FrameworkPropertyMetadataOptions.AffectsRender));

        IsLoadedProperty = DependencyProperty.RegisterAttached("IsLoaded",
                                typeof(bool), typeof(TreeViewItemProps),
                                new FrameworkPropertyMetadata(false));

        IsCanceledProperty = DependencyProperty.RegisterAttached("IsCanceled",
                                 typeof(bool), typeof(TreeViewItemProps),
                                 new FrameworkPropertyMetadata(false));
    }

    public static string GetItemImageName(DependencyObject obj)
    {
        return (string)obj.GetValue(ItemImageNameProperty);
    }

    public static void SetItemImageName(DependencyObject obj, string value)
    {
        obj.SetValue(ItemImageNameProperty, value);
    }

    public static bool GetIsLoading(DependencyObject obj)
    {
        return (bool)obj.GetValue(IsLoadingProperty);
    }

    public static void SetIsLoading(DependencyObject obj, bool value)
    {
        obj.SetValue(IsLoadingProperty, value);
    }

    public static bool GetIsLoaded(DependencyObject obj)
    {
        return (bool)obj.GetValue(IsLoadedProperty);
    }

    public static void SetIsLoaded(DependencyObject obj, bool value)
    {
        obj.SetValue(IsLoadedProperty, value);
    }

    public static bool GetIsCanceled(DependencyObject obj)
    {
        return (bool)obj.GetValue(IsCanceledProperty);
    }

    public static void SetIsCanceled(DependencyObject obj, bool value)
    {
        obj.SetValue(IsCanceledProperty, value);
    }
}

The OnRoot_Expanded handler fires when the root item is expanded. The first thing it does is check to see if the IsLoaded attached property is set to false. If it is, then the loading logic fires.

void OnRoot_Expanded(object sender, RoutedEventArgs e)
{
    var tviSender = e.OriginalSource as TreeViewItem;
    if (IsItemNotLoaded(tviSender))
    {
        StartItemLoading(tviSender, GetDrives, AddDriveItem);
    }
}

bool IsItemNotLoaded(TreeViewItem tviSender)
{
    if (tviSender != null)
    {
        return (TreeViewItemProps.GetIsLoaded(tviSender) == false);
    }
    return (false);
}

Now the interesting stuff happens in StartItemLoading:

void StartItemLoading(TreeViewItem tviSender, 
        DEL_GetItems actGetItems, DEL_AddSubItem actAddSubItem)
{
   // Add a entry in the cancel state dictionary
   SetCancelState(tviSender, false);
    
   // Clear away the dummy node
   tviSender.Items.Clear();

   // Set all attached props to their proper default values
   TreeViewItemProps.SetIsCanceled(tviSender, false);
   TreeViewItemProps.SetIsLoaded(tviSender, true);
   TreeViewItemProps.SetIsLoading(tviSender, true);

    // Store a ref to the main loader logic for cleanup purposes
    // This causes the progress bar and cancel button to appear
   DEL_Loader actLoad = LoadSubItems;

   // Invoke the loader on a background thread.
   actLoad.BeginInvoke(tviSender, tviSender.Tag as string, actGetItems, 
            actAddSubItem, ProcessAsyncCallback, actLoad);
}

The SetCancelState function is just making an entry in a lookup dictionary set to false.

This is used in the load routine to check if the cancel button was pressed.

Notice I opted not to use a dependency property here due to the fact that the load routine is working on a background thread. If it had been a dependency property, then every time the loading routine wanted to check if a cancel had been initiated, it would have to dispatch the check to the UI thread. This just seemed a little more straightforward.

All the cancel state functions are below:

// Keeps a list of all TreeViewItems currently expanding.
// If a cancel request comes in, it causes the bool value to be set to true.
Dictionary<TreeViewItem, bool> m_dic_ItemsExecuting = 
                    new Dictionary<TreeViewItem, bool>();

// Sets the cancel state of specific TreeViewItem
void SetCancelState(TreeViewItem tviSender, bool bState)
{
    lock (m_dic_ItemsExecuting)
    {
        m_dic_ItemsExecuting[tviSender] = bState;
    }
}

// Gets the cancel state of specific TreeViewItem
bool GetCancelState(TreeViewItem tviSender)
{
    lock (m_dic_ItemsExecuting)
    {
        bool bState = false;
        m_dic_ItemsExecuting.TryGetValue(tviSender, out bState);
        return (bState);
    }
}

// Removes the TreeViewItem from the cancel dictionary
void RemoveCancel(TreeViewItem tviSender)
{
    lock (m_dic_ItemsExecuting)
    {
        m_dic_ItemsExecuting.Remove(tviSender);
    }
}

You probably also noticed that I was passing around a few delegates to the BeginInvoke, and actually we are primarily dealing with three.

// The main loader, in this sample app it is always "LoadSubItems"
// RUNS ON: Background Thread
delegate void DEL_Loader(TreeViewItem tviLoad, string strPath, 
    DEL_GetItems actGetItems, DEL_AddSubItem actAddSubItem);

// Adds the actual TreeViewItem, in this sample it's either 
// "AddFolderItem" or "AddDriveItem"
// RUNS ON: UI Thread
delegate void DEL_AddSubItem(TreeViewItem tviParent, string strPath);

// Gets an IEnumerable for the items to load, 
// in this sample it's either "GetFolders" or "GetDrives"
// RUNS ON: Background Thread
delegate IEnumerable<string> DEL_GetItems(string strParent);

Let's now take a look at the LoadSubItems routine which is the target of the BeginInvoke above and runs on a background thread, pay special attention to the two delegates passed in. The actGetItems delegate returns the IEnumerable of what we want to load, this runs on the background thread. The actAddSubItem delegate creates a TreeViewItem and adds it the TreeView, this runs on the UI thread.

// Amount of delay for each item in this demo
static private double sm_dbl_ItemDelayInSeconds = 0.75;

// Runs on background thread.
// Queuing updates can help in rapid loading scenarios,
// I just wanted to illustrate a more granular approach.
void LoadSubItems(TreeViewItem tviParent, string strPath, 
        DEL_GetItems actGetItems, DEL_AddSubItem actAddSubItem)
{
    try
    {
        foreach (string dir in actGetItems(strPath))
        {
            // Be really slow :) for demo purposes
            Thread.Sleep(TimeSpan.FromSeconds(sm_dbl_ItemDelayInSeconds).Milliseconds);

            // Check to see if cancel is requested
            if (GetCancelState(tviParent))
            {
                // If cancel dispatch "ResetTreeItem" for the parent node and
                // get out of here.
                Dispatcher.BeginInvoke(DispatcherPriority.Normal, (Action)(() => 
                    ResetTreeItem(tviParent, false)));
                break;
            }
            else
            {
                // Call "actAddSubItem" on the UI thread to create a TreeViewItem 
                // and add it the control.
                Dispatcher.BeginInvoke(DispatcherPriority.Normal, 
                    actAddSubItem, tviParent, dir);
            }
        }
    }
    catch (Exception ex)
    {
        // Reset the TreeViewItem to unloaded state if an exception occurs
        Dispatcher.BeginInvoke(DispatcherPriority.Normal, 
                (Action)(() => ResetTreeItem(tviParent, true)));

        // Rethrow any exceptions, the EndInvoke handler "ProcessAsyncCallback" 
        // will redispatch on UI thread for further processing and notification.
        throw ex;
    }
    finally
    {
        // Ensure the TreeViewItem is no longer in the cancel state dictionary.
        RemoveCancel(tviParent);

        // Set the "IsLoading" dependency property is set to 'false'
        // this will cause all loading UI (i.e. progress bar, cancel button) 
        // to disappear.
        Dispatcher.BeginInvoke(DispatcherPriority.Normal, (Action)(() => 
                TreeViewItemProps.SetIsLoading(tviParent, false)));
    }
}

Now for the folder items it is just to other delegates that do the fetching of sub-folders and adding of the TreeViewItem. I definitely encourage you to play with the code and get a feel for the simple logic at work. It really is just a matter of popping back and forth from UI to background threads.

Further Improvements

Now, obviously this code is not as cleanly separated as one would hope. I tried really not to get too abstract.

But for a follow-up version, I would like to provide an Interface based approach to loading the TreeViewItems. And one that would support databinding a hierarchal collection.

One scenario would have the TreeViewItems implement a certain interface, call it IThreadTreeItem. This interface would expose a GetItems method that would be used to get an IEnumerable, and few other methods maybe for UI feedback - something along the lines of showing an alert if the data could not be loaded and stuff like that.

The other scenario would be to databind directly to an ObservableCollection, however, this may require some careful wrapping to dispatch changes properly.

The other improvement area would be to queue the inserts, instead of dispatching each one. I would like to expose a property that would specify the number of inserts to queue before actual dispatching.

Any Feedback

This is my first article on The Code Project. So please let me know what you think.

If this helped you in some way, then let me know. If my approach is totally wrong also feel free to let me know.

I definitely want to hear what the community thinks.

History

DATE VERSION DESCRIPTION
9th March, 2008 1.0.0.0 Simple demo

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here