使用VirtualObjectListView时,未知对象数如何实现IVirtualListDataSource?
解决VirtualObjectListView动态行数问题及代码优化
一、GetObjectCount()的正确返回策略
1. 初始返回估算值
不要返回int.MaxValue或固定小数值,建议初始基于文件总大小估算行数(比如按每行平均80字符计算),或直接返回一个较大的保守值(如100000),这样列表会先显示滚动条并允许用户滚动触发加载。
2. 动态更新计数并通知列表
在LogSource中维护计数字段,初始设为估算值,然后在后台异步读取文件的过程中逐步更新:
- 读取新行时,可临时更新计数为「已读取行数 + 剩余文件大小估算行数」
- 完全读完所有文件后,将计数设为准确总行数
- 每次计数变化时,调用
OnObjectCountChanged()方法(需让LogSource继承自VirtualListDataSource而非直接实现IVirtualListDataSource),通知列表更新行数。
二、核心代码修改
1. 重构LogSource实现
internal class LogSource : VirtualListDataSource // 继承自VirtualListDataSource,获取通知能力 { private readonly List<string> _logFiles; private readonly List<LogLine> _cachedLines = new(); private long _totalLinesEstimate; // 初始估算总行数 private bool _isReadingComplete; private IEnumerator<LogLine>? _readEnumerator; // 保存读取状态,避免重复从头读文件 public class LogLine { /* 你的行数据结构 */ } public LogSource(List<string> logFiles) { _logFiles = logFiles; // 初始估算总行数:按文件大小/80字符计算,兜底设为1000避免无滚动条 _totalLinesEstimate = logFiles.Sum(f => new FileInfo(f).Length / 80); _totalLinesEstimate = Math.Max(_totalLinesEstimate, 1000); // 启动后台异步读取,避免阻塞UI Task.Run(ReadAllLinesAsync); } public override int GetObjectCount() { // 读取完成返回准确计数,否则返回估算值(不超过int最大值) return _isReadingComplete ? _cachedLines.Count : (int)Math.Min(_totalLinesEstimate, int.MaxValue); } public override object GetNthObject(int index) { if (index < _cachedLines.Count) return _cachedLines[index]; // 初始化读取枚举器 _readEnumerator ??= ReadLines().GetEnumerator(); // 读取到目标索引 while (_cachedLines.Count <= index && _readEnumerator.MoveNext()) { _cachedLines.Add(_readEnumerator.Current); // 每读1000行通知一次列表更新 if (_cachedLines.Count % 1000 == 0) OnObjectCountChanged(); } // 读取完成后更新最终计数 if (!_readEnumerator.MoveNext() && !_isReadingComplete) { _isReadingComplete = true; _totalLinesEstimate = _cachedLines.Count; OnObjectCountChanged(); } return index < _cachedLines.Count ? _cachedLines[index] : null; } public override void PrepareCache(int first, int last) { // 预加载目标索引后100行,提升滚动流畅度 var targetIndex = Math.Max(0, last + 100); _ = GetNthObject(targetIndex); } // 逐行读取核心逻辑,仅生成一次枚举 private IEnumerable<LogLine> ReadLines() { foreach (var path in _logFiles) { try { using var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 0x1000, FileOptions.Asynchronous | FileOptions.SequentialScan); using var streamReader = new StreamReader(fileStream); string? line; while ((line = streamReader.ReadLine()) != null) { if (!string.IsNullOrEmpty(line)) { // 替换为你的行处理逻辑 yield return new LogLine(/* 处理后的行数据 */); } } } catch (Exception ex) { // 处理文件读取异常,比如添加错误提示行 yield return new LogLine($"读取文件{path}出错: {ex.Message}"); } } } // 后台异步读取所有行,更新缓存与计数 private async Task ReadAllLinesAsync() { var enumerator = ReadLines().GetEnumerator(); while (enumerator.MoveNext()) { lock (_cachedLines) { _cachedLines.Add(enumerator.Current); } // 每读1000行切换到UI线程通知列表更新 if (_cachedLines.Count % 1000 == 0) { // WPF用Dispatcher,WinForms用Control.Invoke Application.Current.Dispatcher.Invoke(OnObjectCountChanged); } } // 读取完成后更新最终计数 _isReadingComplete = true; Application.Current.Dispatcher.Invoke(() => { _totalLinesEstimate = _cachedLines.Count; OnObjectCountChanged(); }); } }
2. 关键改进点说明
- 避免重复读取:用
_readEnumerator保存读取状态,确保每次只读取未处理的行,解决原代码重复从头读文件的问题。 - 异步后台读取:将文件读取放在后台线程,避免UI卡顿,同时定期更新列表计数。
- 正确通知计数变化:通过
OnObjectCountChanged()触发列表更新,解决初始计数为0导致无法加载的问题。 - 异常处理:添加文件读取异常捕获,避免单个文件出错导致整个查看器崩溃。
- 预加载优化:
PrepareCache中预加载额外行,提升滚动流畅度。
三、其他注意事项
- UI线程安全:更新列表计数必须在UI线程执行,WPF用
Dispatcher.Invoke,WinForms用Control.Invoke。 - 超大文件处理:如果行数超过
int.MaxValue,需考虑ObjectListView的索引限制,可分页或截断超出部分。 - 内存管理:若行数极多,可实现LRU缓存,只保留最近访问的行,避免内存溢出。
内容的提问来源于stack exchange,提问作者PaperBirdMaster
相关产品推荐
相关产品推荐

