|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Services
Chapters
Feature Zones
|
ContentsIntroductionMost business applications need to display hierarchal data, and of course a So naturally when starting to learn WPF, the first thing on the list was how to get a 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 End ResultThe 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. ImplementationFor 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 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 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 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 // 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 // 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 // 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 Further ImprovementsNow, 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 One scenario would have the The other scenario would be to databind directly to an 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 FeedbackThis 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
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||