TreeView 虚拟化与懒加载
大约 9 分钟约 2589 字
TreeView 虚拟化与懒加载
简介
TreeView 是展示层级数据的常用控件,如文件目录、组织架构、菜单树等。当数据量大时,一次性加载所有节点会导致性能问题。通过虚拟化(VirtualizingStackPanel)和懒加载(Lazy Loading),可以实现万级节点的流畅展示。
特点
基础数据绑定
层级数据模型
/// <summary>
/// TreeView 数据模型
/// </summary>
public class TreeNode : ObservableObject
{
public string Name { get; set; } = "";
public string Path { get; set; } = "";
public string Icon { get; set; } = "📁";
public bool IsDirectory { get; set; }
[ObservableProperty]
private bool _isExpanded;
[ObservableProperty]
private bool _isSelected;
[ObservableProperty]
private ObservableCollection<TreeNode> _children = new();
[ObservableProperty]
private bool _isLoading;
[ObservableProperty]
private bool _hasDummyChild;
public TreeNode()
{
// 懒加载占位
if (IsDirectory)
{
_children.Add(new TreeNode { Name = "加载中..." });
_hasDummyChild = true;
}
}
public event Func<TreeNode, Task>? LoadChildrenRequested;
partial void OnIsExpandedChanged(bool value)
{
if (value && HasDummyChild)
{
_ = LoadChildrenAsync();
}
}
private async Task LoadChildrenAsync()
{
IsLoading = true;
Children.Clear();
HasDummyChild = false;
try
{
if (LoadChildrenRequested != null)
await LoadChildrenRequested(this);
}
finally
{
IsLoading = false;
}
}
}XAML 模板
<TreeView x:Name="FolderTree"
ItemsSource="{Binding RootNodes}"
VirtualizingStackPanel.IsVirtualizing="True"
VirtualizingStackPanel.VirtualizationMode="Recycling">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<StackPanel Orientation="Horizontal">
<!-- 加载动画 -->
<ProgressBar Width="12" Height="12"
IsIndeterminate="{Binding IsLoading}"
Visibility="{Binding IsLoading, Converter={StaticResource BoolToVisibilityConverter}}"/>
<!-- 图标 -->
<TextBlock Text="{Binding Icon}" Margin="0,0,4,0"
FontSize="14" VerticalAlignment="Center"/>
<!-- 名称 -->
<TextBlock Text="{Binding Name}" VerticalAlignment="Center"/>
<!-- 节点数量 -->
<TextBlock Text="{Binding Children.Count, StringFormat='({0})'}"
Foreground="Gray" FontSize="11"
VerticalAlignment="Center" Margin="4,0,0,0"/>
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
<TreeView.ItemContainerStyle>
<Style TargetType="TreeViewItem">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}"/>
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}"/>
<Setter Property="Padding" Value="2"/>
</Style>
</TreeView.ItemContainerStyle>
</TreeView>懒加载实现
文件目录浏览器
/// <summary>
/// 文件目录懒加载 ViewModel
/// </summary>
public class FileTreeViewModel : ObservableObject
{
public ObservableCollection<TreeNode> RootNodes { get; } = new();
public FileTreeViewModel()
{
LoadDrives();
}
private void LoadDrives()
{
foreach (var drive in DriveInfo.GetDrives().Where(d => d.IsReady))
{
var node = new TreeNode
{
Name = $"{drive.Name} ({drive.VolumeLabel})",
Path = drive.RootDirectory.FullName,
Icon = "💻",
IsDirectory = true
};
node.LoadChildrenRequested = LoadSubDirectoriesAsync;
RootNodes.Add(node);
}
}
private async Task LoadSubDirectoriesAsync(TreeNode parent)
{
await Task.Run(() =>
{
try
{
var dirInfo = new DirectoryInfo(parent.Path);
// 加载子目录
foreach (var dir in dirInfo.EnumerateDirectories()
.Where(d => !d.Attributes.HasFlag(FileAttributes.Hidden)))
{
var child = new TreeNode
{
Name = dir.Name,
Path = dir.FullName,
Icon = "📁",
IsDirectory = true
};
child.LoadChildrenRequested = LoadSubDirectoriesAsync;
Application.Current.Dispatcher.Invoke(() =>
parent.Children.Add(child));
}
// 加载文件(不展开)
foreach (var file in dirInfo.EnumerateFiles()
.Where(f => !f.Attributes.HasFlag(FileAttributes.Hidden))
.OrderBy(f => f.Extension))
{
var fileNode = new TreeNode
{
Name = file.Name,
Path = file.FullName,
Icon = GetFileIcon(file.Extension),
IsDirectory = false
};
Application.Current.Dispatcher.Invoke(() =>
parent.Children.Add(fileNode));
}
}
catch (UnauthorizedAccessException) { }
});
}
private string GetFileIcon(string extension) => extension.ToLower() switch
{
".cs" => "📄",
".xaml" => "📄",
".json" => "📄",
".exe" => "⚙️",
".dll" => "📦",
".png" or ".jpg" => "🖼️",
_ => "📄"
};
}复选框联动
父子节点选择联动
/// <summary>
/// 复选框 TreeView 联动选择
/// </summary>
public class CheckableTreeNode : ObservableObject
{
private bool _isChecked;
public string Name { get; set; } = "";
public ObservableCollection<CheckableTreeNode> Children { get; } = new();
public CheckableTreeNode? Parent { get; set; }
public bool IsChecked
{
get => _isChecked;
set
{
if (SetProperty(ref _isChecked, value))
{
UpdateChildren(value);
Parent?.UpdateParentState();
}
}
}
private void UpdateChildren(bool isChecked)
{
foreach (var child in Children)
{
child.IsChecked = isChecked;
}
}
private void UpdateParentState()
{
if (Children.Count == 0) return;
var allChecked = Children.All(c => c.IsChecked);
var anyChecked = Children.Any(c => c.IsChecked);
var newState = anyChecked && !allChecked ? false : allChecked;
if (_isChecked != newState || anyChecked && !allChecked)
{
_isChecked = newState;
OnPropertyChanged(nameof(IsChecked));
Parent?.UpdateParentState();
}
}
}<!-- 复选框 TreeView -->
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<StackPanel Orientation="Horizontal">
<CheckBox IsChecked="{Binding IsChecked}" Margin="0,0,5,0"
VerticalAlignment="Center"/>
<TextBlock Text="{Binding Name}" VerticalAlignment="Center"/>
</StackPanel>
</HierarchicalDataTemplate>三态复选框
/// <summary>
/// TreeView 三态复选框 — 支持全选/半选/未选
/// </summary>
public enum CheckState
{
Unchecked, // 未选
Checked, // 全选
Indeterminate // 半选(部分子节点选中)
}
public class TriStateTreeNode : ObservableObject
{
public string Name { get; set; } = "";
public string Tag { get; set; } = "";
public ObservableCollection<TriStateTreeNode> Children { get; } = new();
public TriStateTreeNode? Parent { get; set; }
private CheckState _checkState = CheckState.Unchecked;
public CheckState CheckState
{
get => _checkState;
set
{
if (_checkState != value)
{
_checkState = value;
OnPropertyChanged(nameof(CheckState));
OnPropertyChanged(nameof(IsChecked));
OnPropertyChanged(nameof(IsIndeterminate));
}
}
}
// 双向绑定用的 bool(仅用于 UI)
public bool? IsChecked
{
get => CheckState switch
{
CheckState.Checked => true,
CheckState.Unchecked => false,
_ => null
};
set
{
var newState = value switch
{
true => CheckState.Checked,
false => CheckState.Unchecked,
null => CheckState.Indeterminate
};
SetCheckState(newState);
}
}
public bool IsIndeterminate => CheckState == CheckState.Indeterminate;
// 设置选中状态并向下级联
public void SetCheckState(CheckState state)
{
CheckState = state;
// 向下级联到所有子节点
foreach (var child in Children)
child.SetCheckState(state);
// 向上级联更新父节点状态
Parent?.UpdateParentCheckState();
}
// 根据子节点状态更新当前节点
private void UpdateParentCheckState()
{
if (Children.Count == 0) return;
var allChecked = Children.All(c => c.CheckState == CheckState.Checked);
var allUnchecked = Children.All(c => c.CheckState == CheckState.Unchecked);
if (allChecked)
CheckState = CheckState.Checked;
else if (allUnchecked)
CheckState = CheckState.Unchecked;
else
CheckState = CheckState.Indeterminate;
Parent?.UpdateParentCheckState();
}
// 获取所有选中的叶子节点
public List<TriStateTreeNode> GetCheckedLeaves()
{
var result = new List<TriStateTreeNode>();
if (Children.Count == 0 && CheckState == CheckState.Checked)
{
result.Add(this);
}
else
{
foreach (var child in Children)
result.AddRange(child.GetCheckedLeaves());
}
return result;
}
}<!-- 三态复选框 TreeView 模板 -->
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<StackPanel Orientation="Horizontal" Margin="2">
<CheckBox IsThreeState="True"
IsChecked="{Binding IsChecked, Mode=TwoWay}"
Margin="0,0,6,0"
VerticalAlignment="Center"/>
<TextBlock Text="{Binding Name}" VerticalAlignment="Center"
FontSize="13"/>
</StackPanel>
</HierarchicalDataTemplate>拖拽排序
节点拖拽移动
/// <summary>
/// TreeView 拖拽排序服务
/// </summary>
public class TreeViewDragDropService
{
private TreeView? _treeView;
private TriStateTreeNode? _draggedItem;
public void Attach(TreeView treeView)
{
_treeView = treeView;
_treeView.AllowDrop = true;
_treeView.PreviewMouseLeftButtonDown += OnPreviewMouseLeftButtonDown;
_treeView.MouseMove += OnMouseMove;
_treeView.DragOver += OnDragOver;
_treeView.Drop += OnDrop;
_treeView.DragLeave += OnDragLeave;
}
private void OnPreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (e.OriginalSource is not DependencyObject depObj) return;
// 查找被点击的 TreeViewItem
var treeItem = FindAncestor<TreeViewItem>(depObj);
if (treeItem != null)
{
_draggedItem = treeItem.DataContext as TriStateTreeNode;
}
}
private void OnMouseMove(object sender, MouseEventArgs e)
{
if (e.LeftButton != MouseButtonState.Pressed || _draggedItem == null) return;
var data = new DataObject("TreeNode", _draggedItem);
DragDrop.DoDragDrop(_treeView, data, DragDropEffects.Move);
}
private void OnDragOver(object sender, DragEventArgs e)
{
if (!e.Data.GetDataPresent("TreeNode")) return;
var targetItem = GetTargetItem(e);
if (targetItem != null && targetItem != _draggedItem)
{
// 检查不能拖拽到自己的子节点
if (!IsDescendant(_draggedItem, targetItem))
e.Effects = DragDropEffects.Move;
else
e.Effects = DragDropEffects.None;
}
else
{
e.Effects = DragDropEffects.None;
}
e.Handled = true;
}
private void OnDrop(object sender, DragEventArgs e)
{
if (!e.Data.GetDataPresent("TreeNode")) return;
var target = GetTargetItem(e);
if (target == null || _draggedItem == null) return;
// 从原父节点移除
_draggedItem.Parent?.Children.Remove(_draggedItem);
// 添加到目标节点
target.Children.Add(_draggedItem);
_draggedItem.Parent = target;
_draggedItem = null;
}
private void OnDragLeave(object sender, DragEventArgs e)
{
_draggedItem = null;
}
private TriStateTreeNode? GetTargetItem(DragEventArgs e)
{
var depObj = e.OriginalSource as DependencyObject;
var treeItem = FindAncestor<TreeViewItem>(depObj!);
return treeItem?.DataContext as TriStateTreeNode;
}
private bool IsDescendant(TriStateTreeNode? ancestor, TriStateTreeNode? descendant)
{
if (ancestor == null || descendant == null) return false;
var parent = descendant.Parent;
while (parent != null)
{
if (parent == ancestor) return true;
parent = parent.Parent;
}
return false;
}
private static T? FindAncestor<T>(DependencyObject current) where T : DependencyObject
{
while (current != null)
{
if (current is T result) return result;
current = VisualTreeHelper.GetParent(current);
}
return null;
}
}节点右键菜单
上下文菜单
<!-- TreeView 右键菜单 -->
<TreeView x:Name="OrgTree" ItemsSource="{Binding RootNodes}">
<TreeView.ContextMenu>
<ContextMenu>
<MenuItem Header="新增子节点"
Command="{Binding AddChildCommand}"
CommandParameter="{Binding PlacementTarget.SelectedItem, RelativeSource={RelativeSource AncestorType=ContextMenu}}"/>
<MenuItem Header="重命名"
Command="{Binding RenameCommand}"
CommandParameter="{Binding PlacementTarget.SelectedItem, RelativeSource={RelativeSource AncestorType=ContextMenu}}"/>
<Separator/>
<MenuItem Header="删除"
Command="{Binding DeleteCommand}"
CommandParameter="{Binding PlacementTarget.SelectedItem, RelativeSource={RelativeSource AncestorType=ContextMenu}}"/>
<MenuItem Header="复制"
Command="{Binding CopyCommand}"
CommandParameter="{Binding PlacementTarget.SelectedItem, RelativeSource={RelativeSource AncestorType=ContextMenu}}"/>
</ContextMenu>
</TreeView.ContextMenu>
</TreeView>/// <summary>
/// TreeView 操作命令
/// </summary>
public partial class OrgTreeViewModel : ObservableObject
{
public ObservableCollection<TriStateTreeNode> RootNodes { get; } = new();
[RelayCommand]
private void AddChild(TriStateTreeNode? parent)
{
if (parent == null) return;
var newNode = new TriStateTreeNode
{
Name = "新节点",
Parent = parent
};
parent.Children.Add(newNode);
newNode.CheckState = parent.CheckState;
}
[RelayCommand]
private void Delete(TriStateTreeNode? node)
{
if (node == null) return;
var parent = node.Parent;
if (parent != null)
{
parent.Children.Remove(node);
parent.UpdateParentCheckState();
}
else
{
RootNodes.Remove(node);
}
}
[RelayCommand]
private void Copy(TriStateTreeNode? node)
{
if (node == null) return;
var clone = DeepCloneNode(node);
var parent = node.Parent;
if (parent != null)
{
int index = parent.Children.IndexOf(node);
parent.Children.Insert(index + 1, clone);
}
else
{
int index = RootNodes.IndexOf(node);
RootNodes.Insert(index + 1, clone);
}
}
private TriStateTreeNode DeepCloneNode(TriStateTreeNode source)
{
var clone = new TriStateTreeNode
{
Name = source.Name + " (副本)",
Tag = source.Tag,
Parent = source.Parent
};
foreach (var child in source.Children)
{
var childClone = DeepCloneNode(child);
childClone.Parent = clone;
clone.Children.Add(childClone);
}
return clone;
}
}搜索过滤
节点搜索
/// <summary>
/// TreeView 搜索过滤
/// /// </summary>
public async Task SearchNodesAsync(string keyword, IEnumerable<TreeNode> nodes)
{
if (string.IsNullOrEmpty(keyword))
{
// 恢复所有节点
foreach (var node in nodes)
{
node.IsExpanded = false;
}
return;
}
foreach (var node in nodes)
{
var match = node.Name.Contains(keyword, StringComparison.OrdinalIgnoreCase);
var childMatch = await HasMatchingDescendantAsync(keyword, node.Children);
if (match || childMatch)
{
node.IsExpanded = true;
}
}
}
private async Task<bool> HasMatchingDescendantAsync(string keyword, IEnumerable<TreeNode> children)
{
foreach (var child in children)
{
if (child.Name.Contains(keyword, StringComparison.OrdinalIgnoreCase))
return true;
if (await HasMatchingDescendantAsync(keyword, child.Children))
return true;
}
return false;
}优点
缺点
总结
TreeView 大数据优化:启用 VirtualizingStackPanel.IsVirtualizing + Recycling 模式,懒加载用占位子节点 + IsExpanded 事件触发加载。数据模型推荐自引用结构(Children 集合),通过 HierarchicalDataTemplate 递归渲染。搜索场景建议扁平化索引而非遍历树。
关键知识点
- 先分清主题属于界面层、ViewModel 层、线程模型还是设备接入层。
- WPF 文章真正的价值在于把 UI、数据、命令、线程和资源关系讲清楚。
- 上位机场景里,稳定性和异常恢复常常比界面花哨更重要。
- WPF 主题往往要同时理解依赖属性、绑定、可视树和命令系统。
项目落地视角
- 优先明确 DataContext、绑定路径、命令源和线程切换位置。
- 涉及设备时,补齐超时、重连、日志、告警和资源释放策略。
- 复杂控件最好提供最小可运行页面,便于后续复用和排障。
- 优先确认 DataContext、绑定路径、命令触发点和资源引用来源。
常见误区
- 把大量逻辑堆进 code-behind,导致页面难测难维护。
- 在后台线程直接操作 UI,或忽略 Dispatcher 切换。
- 只验证正常路径,不验证设备掉线、权限缺失和资源泄漏。
- 在 code-behind 塞太多状态与业务逻辑。
进阶路线
- 继续向 MVVM 工具链、控件可复用性、性能优化和视觉树诊断深入。
- 把主题放回真实设备流程,思考启动、连接、采集、显示、告警和恢复。
- 沉淀成控件库、通信层和诊断工具,提高整套客户端的复用度。
- 继续补齐控件库、主题系统、诊断工具和启动性能优化。
适用场景
- 当你准备把《TreeView 虚拟化与懒加载》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合桌面业务系统、监控看板、工控上位机和设备交互界面。
- 当系统需要同时处理复杂 UI、后台任务和硬件通信时,这类主题尤为关键。
落地建议
- 优先明确 View、ViewModel、服务层和设备层边界,避免代码隐藏过重。
- 涉及实时刷新时,提前设计 UI 线程切换、节流和资源释放。
- 对硬件通信、日志、告警和异常恢复建立标准流程。
排错清单
- 先检查 DataContext、绑定路径、INotifyPropertyChanged 和命令状态是否正常。
- 排查 Dispatcher 调用、死锁、后台线程直接操作 UI 的问题。
- 出现内存增长时,优先检查事件订阅、图像资源和窗口生命周期。
复盘问题
- 如果把《TreeView 虚拟化与懒加载》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《TreeView 虚拟化与懒加载》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《TreeView 虚拟化与懒加载》最大的收益和代价分别是什么?
