TreeView绑定前必须调用Nodes.Clear()清空集合,避免重复添加和状态错乱;应一次性查询全部数据并在内存中递归组装,禁用OwnerDraw并预加载图标至ImageList以提升性能。

TreeView 绑定前必须清空 Nodes 集合
直接 Nodes.Add() 而不先调用 Nodes.Clear(),会导致重复添加、节点爆炸、展开状态错乱。尤其在刷新菜单或切换用户权限时,旧节点残留会和新数据叠加,界面看起来“越来越胖”。
常见错误现象:TreeNode 数量翻倍、点击某节点实际触发的是上一轮的旧节点事件、ExpandAll() 后出现大量空白或重复子项。
- 每次重新加载树前,固定加一句:
treeView1.Nodes.Clear(); - 不要依赖“第一次没数据所以不用清”,UI 层应无状态——有就清,再建
- 如果用了
BeginUpdate()/EndUpdate(),Clear()必须放在BeginUpdate()之后、构建逻辑之前
递归查询数据库时避免 N+1 查询
用 EF Core 或 ADO.NET 写递归加载,最常踩的坑是:查出所有父节点后,对每个节点再发一次 SQL 查子节点。100 个父级 = 101 次数据库往返,延迟直接拉垮。
正确做法是一次性查出全部数据,内存中递归组装。适用于菜单、目录、组织架构等层级不深(一般 ≤ 6 层)、总量可控(≤ 5000 条)的场景。
- SQL 端用
WITH RECURSIVE(PostgreSQL / SQL Server 2017+)或自连接一次性拉全;MySQL 8.0+ 也支持 - C# 端别写
foreach (var p in parents) { LoadChildren(p.Id); },改用字典索引:var dict = allItems.ToDictionary(x => x.Id); - 递归函数入参只传
IReadOnlyList和根 ID,不传 DbContext 或 connection
TreeNode.Tag 是唯一靠谱的数据绑定槽位
TreeView 没有 DataSource 属性,也不能像 DataGridView 那样自动绑定对象属性。Text 和 Name 只能存字符串,真正要带业务数据(如 MenuId、Url、PermissionCode),全靠 Tag。
容易被忽略的点:赋值 Tag 后不校验类型,后续取值时直接强转,一崩一个准。
- 存的时候统一用具体类:
node.Tag = new MenuItem { Id = 123, Url = "/admin/user" }; - 取的时候用
as安全转换:var item = treeNode.Tag as MenuItem;,判空再用 - 千万别把
Tag当字符串容器塞 JSON,序列化/反序列化开销大,且调试时看不到结构
WinForms TreeView 性能卡在图标和字体上
节点一多(比如 > 500 个),滚动卡、展开慢,八成不是逻辑问题,而是 UI 渲染拖了后腿。默认启用的 DrawMode = OwnerDrawAll 或自定义图标(尤其是从资源里反复 Image.FromResource)是隐形杀手。
真实影响比递归算法本身还大,但很少被排查到。
- 禁用不必要的绘制:
treeView1.DrawMode = TreeViewDrawMode.Normal;(别碰 OwnerDraw) - 图标统一预加载进
ImageList,绑定到treeView1.ImageList,节点用ImageIndex引用,别每次 new 图片 - 字体保持默认,别给每个节点设
NodeFont—— 每个 font 实例都是 GDI 对象,泄漏风险高
树形结构的复杂点从来不在“怎么连父子关系”,而在于:谁负责清、谁负责查、谁负责画、谁负责扛住点击。漏掉其中一环,表面能跑,上线三天就变卡顿现场。










