在递归函数中使用 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;
【讨论】:
对于BackgroundWorker
和Task.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的主要内容,如果未能解决你的问题,请参考以下文章