多线程 WPF TreeView 浏览器






4.83/5 (34投票s)
多线程 WPF TreeView 浏览器。
目录
引言
大多数业务应用程序都需要显示分层数据,当然,在我记忆中 TreeView
一直是首选。然而,许多情况下加载树中的数据可能需要很长时间。而这正是本文试图解决的问题。此 TreeView
中的所有项目加载都以多线程方式进行。
因此,很自然地,在开始学习 WPF 时,列表中的第一件事是如何启动并运行一个 TreeView
。
我偶然发现了一篇很棒的文章,这是我工作的基础,作者是 Sacha Barber,标题为 "一个简单的 WPF Explorer Tree"。我强烈建议您阅读这篇文章,以及 Sacha 关于 WPF 的精彩入门系列。Josh Smith 也在他的博客 Reaction to: A Simple WPF Explorer Tree 上对 Sacha 的工作进行了补充。这两位作者都为我们在 TreeView
中显示带有图像的项目提供了一个很好的起点。
最终结果
本文的最终结果是这样一个树,它在后台线程中加载子节点,并即时向用户显示节点插入的情况。UI 永远不会因为等待长时间运行的 I/O 操作而锁定。

您可能还注意到,可以同时加载多个节点,并且还可以取消加载。
既然您已经看到了最终结果,那么让我们开始实现。
实现
在我的示例实现中,我继续了 Sacha 显示本地文件系统的 C++ 方面的工作。当然,其他类型的数据也可以轻松地以线程方式显示和加载。
我的实现的不同之处在于,当一个节点被单击时,它的子项将在后台线程上获取。我试图使实现保持通用性,但同时又不至于过于复杂和抽象。
这里的概念非常简单。我们从一个在表单加载时创建的根节点开始。
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);
}
上面看到的绝大部分都很清楚。header
属性是实际的显示文本。添加虚拟节点是为了使根节点上的加号指示符可见。OnRoot_Expanded
处理实际加载,稍后会再讨论。
现在您可能会问 TreeViewItemProps.SetItemImageName
是什么?这是一个我定义的附加属性,还有其他几个,都在 static
类 TreeViewItemProps
中。这些属性在 XAML 中通过数据绑定到 TreeViewItem
的 DataTemplate
,以 控制 进度条、取消和重新加载按钮的显示设置。Sacha 在他的文章 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);
}
}
当根项展开时,会触发 OnRoot_Expanded
处理程序。它首先要做的是检查 IsLoaded
附加属性是否设置为 false
。如果是,则触发加载逻辑。
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);
}
现在有趣的事情发生在 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);
}
SetCancelState
函数只是将查找字典中的一个条目设置为 false
。
这在加载例程中用于检查是否按下了取消按钮。
请注意,我选择不在此处使用依赖属性,因为加载例程是在后台线程上工作的。如果它是一个依赖属性,那么每次加载例程想检查是否已启动取消操作,它都必须将检查分派到 UI 线程。这似乎更直接一些。
所有的取消状态函数都在下面
// 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);
}
}
您可能还注意到,我将一些 delegate
s 传递给了 BeginInvoke
,实际上我们主要处理三个。
// 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);
现在让我们看一下 LoadSubItems
例程,它是上面 BeginInvoke
的目标,在后台线程上运行,特别注意传入的两个 delegate
s。actGetItems delegate
返回我们想要加载的 IEnumerable
,这在后台线程上运行。actAddSubItem delegate
创建一个 TreeViewItem
并将其添加到 TreeView
,这在UI 线程上运行。
// 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)));
}
}
现在对于文件夹项,它只是另外两个委托来获取子文件夹和添加 TreeViewItem
。我强烈建议您尝试使用代码来感受其中简单的逻辑。这真的只是在 UI 和后台线程之间来回切换的问题。
进一步改进
现在,显然这段代码不像人们希望的那样干净地分离。我真的尽量不变得过于抽象。
但对于后续版本,我想提供一种基于接口的方法来加载 TreeViewItems
。并且能够支持分层集合的数据绑定。
一种场景是让 TreeViewItem
实现一个特定的接口,称之为 IThreadTreeItem
。这个接口会暴露一个 GetItems
方法,用于获取一个 IEnumerable
,以及其他几个方法,可能用于 UI 反馈 - 比如在数据加载失败时显示警报之类的事情。
另一种场景是直接绑定到 ObservableCollection
,但是,这可能需要一些仔细的包装才能正确分派更改。
另一个改进领域是队列化插入,而不是分派每个插入。我想公开一个属性来指定在实际分派之前要队列化的插入数量。
任何反馈
这是我在 The Code Project 上的第一篇文章。所以请告诉我您的想法。
如果这篇文章在某种程度上帮助了您,请告诉我。如果我的方法完全错误,也请随时告诉我。
我非常想听听社区的看法。
历史
DATE | 版本 | 描述 |
2008 年 3 月 9 日 | 1.0.0.0 | 简单的演示 |