65.9K
CodeProject 正在变化。 阅读更多。
Home

多线程 WPF TreeView 浏览器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.83/5 (34投票s)

2008年3月9日

CPOL

5分钟阅读

viewsIcon

161235

downloadIcon

5475

多线程 WPF TreeView 浏览器。

目录

  1. 引言
  2. 最终结果
  3. 实现
  4. 进一步改进
  5. 任何反馈
  6. 历史

引言

大多数业务应用程序都需要显示分层数据,当然,在我记忆中 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 是什么?这是一个我定义的附加属性,还有其他几个,都在 staticTreeViewItemProps 中。这些属性在 XAML 中通过数据绑定到 TreeViewItemDataTemplate,以 控制 进度条、取消和重新加载按钮的显示设置。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);
    }
}

您可能还注意到,我将一些 delegates 传递给了 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 的目标,在后台线程上运行,特别注意传入的两个 delegates。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 简单的演示
© . All rights reserved.