跨线程操作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
相关产品推荐
相关产品推荐

