首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> C# >> 關於C#

WinForms TreeView 非同步載入節點「正在展開...」效果完整實作

     編輯:關於C#

需求場景

TreeView 節點資料來自資料庫、Web API 或檔案系統,展開時需耗時查詢。若在 UI 執行緒同步載入,介面會凍結;若直接啟動背景執行緒更新 UI,會觸發跨執行緒例外。解法:先顯示暫時節點「正在載入...」,背景執行緒查詢完成後,透過 Invoke 替換為真實子節點。

核心實作模式

using System;
using System.Threading;
using System.Windows.Forms;

public partial class AsyncTreeViewForm : Form
{
    public AsyncTreeViewForm()
    {
        InitializeComponent();
        treeView1.AfterSelect += TreeView1_AfterSelect;
        // 初始根節點
        treeView1.Nodes.Add(new TreeNode("根節點") { Tag = "" });
    }

    private void TreeView1_AfterSelect(object sender, TreeViewEventArgs e)
    {
        // 已載入過(Tag != null/空)或非葉節點不重複載入
        if (e.Node.Tag != null && e.Node.Tag.ToString() != "") return;

        // 1. 標記正在載入,防止重複觸發
        e.Node.Tag = "Loading";

        // 2. 先加入暫時節點,給使用者即時回饋
        var loadingNode = new TreeNode("正在載入...") { ForeColor = Color.Gray };
        e.Node.Nodes.Add(loadingNode);
        e.Node.Expand(); // 展開顯示暫時節點

        // 3. 背景執行緒取得資料
        ThreadPool.QueueUserWorkItem(_ =>
        {
            // 模擬耗時查詢(實際換成 DB/Web API 呼叫)
            Thread.Sleep(1500);
            var children = new[] { "子節點 A", "子節點 B", "子節點 C", "子節點 D" };

            // 4. 必須在 UI 執行緒更新 TreeView
            treeView1.Invoke(new Action(() =>
            {
                // 移除暫時節點
                e.Node.Nodes.Clear();

                // 加入真實子節點
                foreach (var name in children)
                {
                    e.Node.Nodes.Add(new TreeNode(name) { Tag = "" }); // Tag="" 表示可再展開
                }

                // 標記已載入完成
                e.Node.Tag = "Loaded";
            }));
        });
    }
}

進階:支援多層級遞迴載入

private void LoadChildren(TreeNode parent, Func dataLoader, object param)
{
    if (parent.Tag?.ToString() == "Loaded") return; // 已載入
    parent.Tag = "Loading";
    parent.Nodes.Clear();
    parent.Nodes.Add(new TreeNode("正在載入...") { ForeColor = Color.Gray });
    parent.Expand();

    ThreadPool.QueueUserWorkItem(_ =>
    {
        string[] result = dataLoader(param);
        treeView1.Invoke(new Action(() =>
        {
            parent.Nodes.Clear();
            foreach (var name in result)
            {
                parent.Nodes.Add(new TreeNode(name) { Tag = "" });
            }
            parent.Tag = "Loaded";
        }));
    });
}

// 使用範例
private void TreeView1_BeforeExpand(object sender, TreeViewCancelEventArgs e)
{
    LoadChildren(e.Node, GetFileSystemChildren, e.Node.FullPath);
}

private string[] GetFileSystemChildren(string path)
{
    try { return Directory.GetDirectories(path).Select(Path.GetFileName).ToArray(); }
    catch { return Array.Empty(); }
}

關鍵點總結

技巧說明
Tag 狀態機""=未載入, "Loading"=載入中, "Loaded"=完成,防重複請求
先加暫時節點使用者立即看到回饋,體驗優於空白等待
Invoke/BeginInvoke跨執行緒安全更新 UI,Invoke 同步阻塞,BeginInvoke 非同步
ThreadPool.QueueUserWorkItem輕量背景執行緒,避免手動 new Thread 產生過多執行緒
BeforeExpand 事件比 AfterSelect 更適合「展開時載入」語意

效能優化建議

  • 虛擬模式:節點極多(>1000)時,考慮 TreeView.VirtualMode(需自訂繪製),或分頁載入。
  • 快取:已載入節點的子節點集合可快取於 Tag 物件中,避免重複查詢。
  • 取消機制:長時間查詢需支援取消,可用 CancellationToken 傳入背景任務。
  • 異步/等待:.NET 4.5+ 建議改用 async/await + Task.Run,程式碼更線性易讀。

async/await 現代化改寫

private async void TreeView1_BeforeExpand(object sender, TreeViewCancelEventArgs e)
{
    if (e.Node.Tag?.ToString() == "Loaded") return;

    e.Node.Tag = "Loading";
    e.Node.Nodes.Clear();
    e.Node.Nodes.Add(new TreeNode("正在載入...") { ForeColor = Color.Gray });
    e.Node.Expand();

    string[] children = await Task.Run(() => GetFileSystemChildren(e.Node.FullPath));

    e.Node.Nodes.Clear();
    foreach (var name in children)
        e.Node.Nodes.Add(new TreeNode(name) { Tag = "" });
    e.Node.Tag = "Loaded";
}

總結

TreeView 非同步載入核心在於:狀態標記防重入 + 暫時節點給回饋 + 背景查詢 + UI 執行緒安全替換。本文提供傳統 Invoke 與現代 async/await 兩種寫法,可直接套用於資料庫樹狀結構、檔案總管、組織架構圖等場景,大幅提升介面流暢度與使用者體驗。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved