You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

跨线程操作TreeView控件报错且UI阻塞问题求助

问题

在WinForms主窗体中通过线程调用TreeView类的PopulateDirectories方法时,出现以下异常:

System.InvalidOperationException: 'Action being performed on this control is being called from the wrong thread. Marshal to the correct thread'

同时存在两个核心问题:

  • TreeNode没有Invoke方法,不知如何处理跨线程操作异常
  • 虽使用多线程,但UI仍被完全阻塞,无法交互

主窗体线程调用代码

var PopulateTreeThread = new Thread(() =>
{
    response = treeViewObj.PopulateDirectories(rootNode, $"ftp://{hostIPAddress}:{port}",
    hostUserName, hostPassword,
    request, response);
    Thread.Sleep(25);
});
PopulateTreeThread.IsBackground = true;
PopulateTreeThread.Start();

TreeView类方法代码

using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Net;
using System.Windows.Forms;
using static System.Windows.Forms.VisualStyles.VisualStyleElement;

namespace FileManagerClient
{
    public class TreeView
    {
        public FtpWebResponse PopulateDirectories(TreeNode parentNode,
            string ftpRootPath,
            string hostUsername,
            string hostPassword,
            FtpWebRequest request,
            FtpWebResponse response
            )
        {
            try
            {
                // ftp request for each directory list
                request = (FtpWebRequest)WebRequest.Create(ftpRootPath);
                request.Credentials = new NetworkCredential(hostUsername, hostPassword);
                request.Method = WebRequestMethods.Ftp.ListDirectory;
                request.KeepAlive = true;

                response = (FtpWebResponse)request.GetResponse();
                Stream responseStream = response.GetResponseStream();
                StreamReader reader = new StreamReader(responseStream);

                string directoryContents = reader.ReadToEnd();
                string[] directories = directoryContents.Split(new char[] { '\r', '\n' },StringSplitOptions.RemoveEmptyEntries);

                List<string> modifiedDirectories = new List<string>();

                foreach (string directory in directories)
                {
                    // splitting directory name for refined folder
                    string[] parts = directory.Split('/');

                    // getting last string after '/' as that will be present directory "AWGSS/DG" will give DG
                    string modifiedDirectory = parts[parts.Length - 1];

                    // refined directories name to remove confusion
                    modifiedDirectories.Add(modifiedDirectory);
                }

                // converting list to string array before passing
                string[] finalDirectories = modifiedDirectories.ToArray();

                foreach (string directory in finalDirectories)
                {
                    // creating node for each directory
                    Console.WriteLine($"directory looks like this: {directory}");

                    TreeNode node = new TreeNode(directory);
                    parentNode.Nodes.Add(node);

                    Console.WriteLine($"new stream looks like this: {ftpRootPath}/{Path.GetFileName(directory)}");
                    // condition to differentiate between file and directory
                    bool isDirectory = Path.GetFileName(directory).Contains(".");

                    if (isDirectory)
                    {
                        continue;
                    }
                    else
                    {
                        // when directory found then run this method recursively
                        PopulateDirectories(node, $"{ftpRootPath}/{Path.GetFileName(directory)}", hostUsername, hostPassword, request, response);
                    }
                }
                reader.Close();

            }
            catch (WebException ex)
            {
                // Handle exception
                // MessageBox.Show($"Error: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);

                Console.WriteLine($"Error: {ex.Message}");
            }
            return response;
        }
    }
}
解决方案

1. 修复跨线程UI操作异常

WinForms控件仅允许创建它的UI线程进行操作,TreeNode本身无Invoke方法,但它所属的TreeView控件拥有Invoke能力。通过parentNode.TreeView获取到对应的TreeView控件,将添加节点的操作委托给UI线程执行即可。

2. 解决UI阻塞问题

原代码中递归调用PopulateDirectories时,FTP请求为同步阻塞操作,且文件/目录判断逻辑错误,导致递归逻辑异常。同时建议使用Task.Run替代手动创建Thread,利用线程池管理后台任务,更高效且避免不必要的线程资源浪费。

修改后的TreeViewHelper类(改名避免与WinForms内置TreeView冲突)

using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Windows.Forms;

namespace FileManagerClient
{
    public class TreeViewHelper
    {
        // 定义委托用于跨线程添加节点
        private delegate void AddNodeDelegate(TreeNode parentNode, TreeNode newNode);

        public FtpWebResponse PopulateDirectories(TreeNode parentNode,
            string ftpRootPath,
            string hostUsername,
            string hostPassword,
            FtpWebRequest request,
            FtpWebResponse response
            )
        {
            try
            {
                // FTP请求操作(后台线程执行,无UI交互)
                request = (FtpWebRequest)WebRequest.Create(ftpRootPath);
                request.Credentials = new NetworkCredential(hostUsername, hostPassword);
                request.Method = WebRequestMethods.Ftp.ListDirectory;
                request.KeepAlive = true;

                response = (FtpWebResponse)request.GetResponse();
                // 使用using自动释放资源
                using Stream responseStream = response.GetResponseStream();
                using StreamReader reader = new StreamReader(responseStream);

                string directoryContents = reader.ReadToEnd();
                string[] directories = directoryContents.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);

                List<string> modifiedDirectories = new List<string>();

                foreach (string directory in directories)
                {
                    string[] parts = directory.Split('/');
                    string modifiedDirectory = parts[parts.Length - 1];
                    modifiedDirectories.Add(modifiedDirectory);
                }

                string[] finalDirectories = modifiedDirectories.ToArray();

                foreach (string directory in finalDirectories)
                {
                    Console.WriteLine($"directory looks like this: {directory}");
                    TreeNode node = new TreeNode(directory);

                    // 跨线程添加节点:判断是否需要Invoke
                    if (parentNode.TreeView.InvokeRequired)
                    {
                        parentNode.TreeView.Invoke(new AddNodeDelegate((parent, newNode) => parent.Nodes.Add(newNode)), parentNode, node);
                    }
                    else
                    {
                        parentNode.Nodes.Add(node);
                    }

                    Console.WriteLine($"new stream looks like this: {ftpRootPath}/{Path.GetFileName(directory)}");
                    // 修正文件/目录判断逻辑:不包含.的为目录
                    bool isDirectory = !Path.GetFileName(directory).Contains(".");
                    if (isDirectory)
                    {
                        // 递归处理子目录
                        PopulateDirectories(node, $"{ftpRootPath}/{Path.GetFileName(directory)}", hostUsername, hostPassword, request, response);
                    }
                }
            }
            catch (WebException ex)
            {
                Console.WriteLine($"Error: {ex.Message}");
            }
            return response;
        }
    }
}

修改后主窗体调用代码

// 使用Task.Run替代手动创建Thread,简化后台任务管理
Task.Run(() =>
{
    response = treeViewHelperObj.PopulateDirectories(rootNode, $"ftp://{hostIPAddress}:{port}",
    hostUserName, hostPassword,
    request, response);
});

关键说明

  • 跨线程操作:通过parentNode.TreeView.Invoke将节点添加操作切换到UI线程执行,避免异常
  • 资源释放:使用using语句自动释放Stream和StreamReader资源,避免内存泄漏
  • 逻辑修正:调整文件/目录判断条件,确保递归处理子目录的逻辑正确
  • UI非阻塞:后台线程仅处理FTP数据获取,UI操作完全委托给UI线程,同时使用Task.Run优化线程资源管理

内容的提问来源于stack exchange,提问作者P0tat0627

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.25 18:54:54