在递归函数中使用 BeginInvoke 将节点添加到 BackgroundWorker 中的 TreeView

Posted

技术标签:

【中文标题】在递归函数中使用 BeginInvoke 将节点添加到 BackgroundWorker 中的 TreeView【英文标题】:Using BeginInvoke in a recursive function adding nodes to a TreeView in a BackgroundWorker 【发布时间】:2021-10-23 00:19:34 【问题描述】:

我的代码具有扫描根目录并在 TreeView 中显示 txt 文件(例如)的功能:

private TreeNode DirectoryToTreeView(TreeNode parentNode, string path,
                                     string extension = ".txt")

    var result = new TreeNode(parentNode == null ? path : Path.GetFileName(path));
    foreach (var dir in Directory.GetDirectories(path))
    
        TreeNode node = DirectoryToTreeView(result, dir);
        if (node.Nodes.Count > 0)
        
            result.Nodes.Add(node);
        
    
    foreach (var file in Directory.GetFiles(path))
    
        if (Path.GetExtension(file).ToLower() == extension.ToLower())
        
            result.Nodes.Add(Path.GetFileName(file));
        
    
    return result;

此函数应通过按钮调用,例如: treeView1.Nodes.Add(DirectoryToTreeView(null, @"C:\Users\Tomer\Desktop\a")); 它显然冻结了用户界面。 我是新手,我在网上搜索过,似乎没有什么与我的问题相关,因为没有人使用递归函数,我不能简单地在整个函数上调用BeginInvoke,因为它没有任何效果。 我应该走什么路?也许将函数更改为使用 while 循环,然后在 if 语句中调用BeginInvoke?在内存中创建一个TreeNode 对象来填充(可能太大)?

【问题讨论】:

假设 WinForms。您可以运行获取节点的任务:缓慢的部分是文件枚举。然后您可以等待[TreeView].SuspendLayout(),等待任务,例如:[TreeView].Nodes.AddRange(await Task.Run(()=> GetTreeNodes([params]))) 并恢复布局:[TreeView].ResumeLayout(false);。现在 TreeView 仅在任务返回其结果且 UI 未冻结时更新。 -- 标记问题,指定在任何情况下使用的 GUI 框架。 小心使用递归方法。如果您使用返回 DirectoryToTreeView() 的结果的中间方法可能会更好(这就是我引入 GetTreeNodes([params]) 方法的原因)。 感谢您的回答,现在提到它的获胜形式。我不明白这个解决方案如何与这个递归函数一起工作,我应该在哪里调用你提到的每一件事。 如前所述,您可以使用一种中间方法(我之前将其命名为GetTreeNodes()),它将返回您的DirectoryToTreeView() 的结果。 -- 您可以从任何异步方法执行第一条注释中描述的代码。它可以是 Button 的 Click 事件的处理程序,但您可以使用 Form 的 Load 事件处理程序或覆盖 OnLoad()) 等。如果调用方法不是事件处理程序,则它必须是方法返回Task(或Task<TResult>,取决于其角色/实现)。不是void 方法。 【参考方案1】:

您可以使用Task.Run 方法将DirectoryToTreeNode 方法转换为asynchronous 方法,并将任何阻塞I/O 操作卸载到ThreadPool

private async Task<TreeNode> DirectoryToTreeNodeAsync(string path,
    TreeNode parentNode = null)

    var node = new TreeNode(parentNode == null ? path : Path.GetFileName(path));
    string[] subdirectories = await Task.Run(() => Directory.GetDirectories(path));
    foreach (string dirPath in subdirectories)
    
        TreeNode childNode = await DirectoryToTreeNodeAsync(dirPath, node);
        node.Nodes.Add(childNode);
    
    string[] files = await Task.Run(() => Directory.GetFiles(path));
    foreach (string filePath in files)
    
        node.Nodes.Add(Path.GetFileName(filePath));
    
    return node;

请注意,在 ThreadPool(在 Task.Run 委托内)上运行时,没有触摸任何 UI 控件。所有 UI 控件都应该被操纵exclusively by the UI thread。

使用示例:

private async void Button1_Click(object sender, EventArgs e)

    Button1.Enabled = false;
    Cursor = Cursors.WaitCursor;
    try
    
        TreeView1.Nodes.Clear();
        TreeView1.Nodes.Add(
            await DirectoryToTreeNodeAsync(@"C:\Users\Tomer\Desktop\a"));
    
    finally
    
        Cursor = Cursors.Default;
        Button1.Enabled = true;
    

【讨论】:

对于BackgroundWorkerTask.Run + async/await 之间的比较,您可以查看this 的答案。 这不起作用。 1)您应该跳过递归方法中的安全和IO异常。在调用者中使用try..catch 不会继续进行。迭代因第一个异常而中断。 2) 你得到的不仅仅是所需的文件类型(.txt 文件)。 3) 你为什么打电话给.GetDirectories.GetFiles? Read. @dr.null 这是一个演示技术的最小示例。我删除了.txt 文件的过滤器和空文件夹的检查,因为冗余的复杂性混淆了核心问题。我相信OP将能够将其添加回来。至于.GetDirectories.GetFiles,这就是OP 在问题中使用的。我认为GetFiles在这种特定情况下优于EnumerateFiles,因为它最大限度地减少了UI线程和ThreadPool之间的跳转次数。 结束了使用这个解决方案,在我的代码中更容易实现并且速度稍快,非常感谢。【参考方案2】:

这是一个使用给定文件类型的目录树填充TreeNode 的异步Task 方法示例。内部的CreateTree(...) 是一个local function,递归调用以遍历目录。

private async Task<TreeNode> CreateTreeAsync(string startDir, string fileExt)

    var di = new DirectoryInfo(startDir);
    var result = new TreeNode(di.Name);
    var searchPattern = $"*.fileExt.TrimStart('.')";

    return await Task.Run(() =>
    
        void CreateTree(DirectoryInfo dirInfo, TreeNode node)
        
            try
            
                foreach (var fileInfo in dirInfo.EnumerateFiles(searchPattern))
                    node.Nodes.Add(fileInfo.Name);

                foreach (var subDir in dirInfo.EnumerateDirectories())
                
                    try
                    
                        // Optional to skip the branches with no files at any level.
                        if (!subDir.EnumerateFiles(searchPattren, 
                            SearchOption.AllDirectories).Any()) continue;

                        var newNode = new TreeNode(subDir.Name);
                        node.Nodes.Add(newNode);
                        CreateTree(subDir, newNode);
                    
                    catch (Exception ex)
                    
                        // Skip exceptions like UnauthorizedAccessException
                        // and continue...
                        Console.WriteLine(ex.Message);
                    
                
            
            catch (Exception ex)
            
                Console.WriteLine(ex.Message);
            
        
        CreateTree(di, result);
        return result;
    );

注意:如果您在 .NET 5+/.NET Core 下,则不需要 try..catch 块来跳过不可访问的目录和文件。使用采用EnumerationOptions 参数的EnumerateXXX 方法重载。

现在你需要像这样的async 调用者:

private async void someButton_Click(object sender, EventArgs e)
        
    // Optional...                
    treeView1.Nodes.Clear();

    var dir = @"...";
    var ext = "txt";
    var node = await CreateTreeAsync(dir, ext);

    if (node.Nodes.Count == 0)
        MessageBox.Show($"No 'ext' files were found.");
    else
    
        treeView1.Nodes.Add(node);
        node.Expand();
    

【讨论】:

以上是关于在递归函数中使用 BeginInvoke 将节点添加到 BackgroundWorker 中的 TreeView的主要内容,如果未能解决你的问题,请参考以下文章

非二叉树递归

向 lambda 函数添​​加附加逻辑以从 python 字典中获取最小值

在递归节点函数中从 Mongoose 检索树数据

从链表中删除节点(递归)

递归函数中的for循环在递归结束后继续

将 async/await 与 Dispatcher.BeginInvoke() 一起使用