Contents
- Introduction
- End Result
- Implementation
- Further Improvements
- Any Feedback
- History
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.
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.
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)
{
var tviRoot = new TreeViewItem();
tviRoot.Header = "My Computer";
tviRoot.Items.Add(_dummyNode);
tviRoot.Expanded += OnRoot_Expanded;
TreeViewItemProps.SetItemImageName(tviRoot, @"Images/Computer.png");
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)
{
SetCancelState(tviSender, false);
tviSender.Items.Clear();
TreeViewItemProps.SetIsCanceled(tviSender, false);
TreeViewItemProps.SetIsLoaded(tviSender, true);
TreeViewItemProps.SetIsLoading(tviSender, true);
DEL_Loader actLoad = LoadSubItems;
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:
Dictionary<TreeViewItem, bool> m_dic_ItemsExecuting =
new Dictionary<TreeViewItem, bool>();
void SetCancelState(TreeViewItem tviSender, bool bState)
{
lock (m_dic_ItemsExecuting)
{
m_dic_ItemsExecuting[tviSender] = bState;
}
}
bool GetCancelState(TreeViewItem tviSender)
{
lock (m_dic_ItemsExecuting)
{
bool bState = false;
m_dic_ItemsExecuting.TryGetValue(tviSender, out bState);
return (bState);
}
}
void RemoveCancel(TreeViewItem tviSender)
{
lock (m_dic_ItemsExecuting)
{
m_dic_ItemsExecuting.Remove(tviSender);
}
}
You probably also noticed that I was passing around a few delegate
s to the BeginInvoke
, and actually we are primarily dealing with three.
delegate void DEL_Loader(TreeViewItem tviLoad, string strPath,
DEL_GetItems actGetItems, DEL_AddSubItem actAddSubItem);
delegate void DEL_AddSubItem(TreeViewItem tviParent, string strPath);
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 delegate
s 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.
static private double sm_dbl_ItemDelayInSeconds = 0.75;
void LoadSubItems(TreeViewItem tviParent, string strPath,
DEL_GetItems actGetItems, DEL_AddSubItem actAddSubItem)
{
try
{
foreach (string dir in actGetItems(strPath))
{
Thread.Sleep(TimeSpan.FromSeconds(sm_dbl_ItemDelayInSeconds).Milliseconds);
if (GetCancelState(tviParent))
{
Dispatcher.BeginInvoke(DispatcherPriority.Normal, (Action)(() =>
ResetTreeItem(tviParent, false)));
break;
}
else
{
Dispatcher.BeginInvoke(DispatcherPriority.Normal,
actAddSubItem, tviParent, dir);
}
}
}
catch (Exception ex)
{
Dispatcher.BeginInvoke(DispatcherPriority.Normal,
(Action)(() => ResetTreeItem(tviParent, true)));
throw ex;
}
finally
{
RemoveCancel(tviParent);
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.
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 TreeViewItem
s 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.
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.
DATE |
VERSION |
DESCRIPTION |
9th March, 2008 |
1.0.0.0 |
Simple demo |