UWP 数据虚拟化实战:基于 IItemsRangeInfo 与 ISelectionInfo 构建高性能 XAML 列表数据源
示例工程【免费下载链接】Windows-universal-samplesAPI samples for the Universal Windows Platform.项目地址https://gitcode.com/gh_mirrors/wi/Windows-universal-samples点击查看免费下载导读本文以 Windows-universal-samples 仓库中的 XamlDataVirtualization 示例 为骨架系统讲解如何在 UWPUniversal Windows Platform的 XAML 列表控件ListView / GridView中实现数据虚拟化Data Virtualization。你将掌握两大核心接口的使用方法通过IItemsRangeInfo感知控件当前可视区与缓冲区的数据范围、维护按需加载的条目缓存通过ISelectionInfo在虚拟化场景下正确管理大规模列表的选中状态。读完本文你将能复刻一套以文件系统为数据源、具备滚动节流、请求取消与缓存复用能力的自定义虚拟化数据源并理解其底层工作原理。一、为什么要数据虚拟化大列表的内存与性能问题UWP 的ListView/GridView本身提供了UI 虚拟化UI virtualization——只实例化可视区域附近的容器Container滚动时复用Recycle这些容器因此 UI 元素的数量不会随数据量增长。但数据本身仍可能全部驻留在内存中如果你把一个包含 10 万个FileItem每项还带缩略图BitmapImage的ListT直接赋给ItemsSource内存占用将非常可观。数据虚拟化Data virtualization解决的是这一层问题数据源只按需取回当前视图真正需要的少量数据并用缓存管理这些数据块。示例 README 明确点出本示例的目标——implement a data source for XAML list controls that implements data virtualization具体包含两条主线见 README.md实现 IItemsRangeInfo让数据源理解列表当前在视图中或在缓冲区中是哪些条目并据此维护一个只包含这些条目的缓存实现 ISelectionInfo在数据虚拟化的前提下管理列表选中状态选中索引可能因数据源变化而失效。本示例的数据源建立在一个非常典型且可复现的场景上以本地图片库Pictures文件夹的文件列表作为数据源。示例通过StorageFolder.CreateFileQueryWithOptions建立文件查询借助GetFilesAsync(firstIndex, count)实现按索引范围分页读取——这与数据库的OFFSET/LIMIT分页本质相同是理解全文的最佳切入场景。前提说明本示例属于 UWP 功能示例集合Windows universal samples需要Visual Studio构建、Windows 10运行README 的 System requirements 一节注明 Client/Phone 为 Windows 10Server 为 Windows Server 2016 Technical Preview。数据源读取的是系统图片库Pictures 库内容如果运行后看不到任何结果多半是图片库中没有图片文件。二、示例整体结构两个递进的场景在动手读代码前先看清项目的组织方式。整个示例位于 Samples/XamlDataVirtualization解决方案与项目文件在 cs/DataVirtualization.sln 与 cs/DataVirtualization.csproj 下Samples/XamlDataVirtualization/cs/ ├── DataVirtualization.sln / DataVirtualization.csproj ├── MainPage.xaml / MainPage.xaml.cs # 场景导航框架 ├── Package.appxmanifest / SampleConfiguration.cs ├── Scenario1/ # 场景一仅 IItemsRangeInfo │ ├── FileDataSource.cs # 虚拟化数据源实现 IItemsRangeInfo │ ├── FileItem.cs # 文件条目模型文件名/大小/缩略图 │ ├── ItemCacheManager.cs # 核心按范围管理的条目缓存 │ ├── ItemIndexRangeExtensions.cs # ItemIndexRange 区间运算扩展 │ ├── ItemIndexRangeList.cs # 不连续区间的有序集合 │ └── Scenario1.xaml / Scenario1.xaml.cs # 展示页GridView 浏览图片库 └── Scenario2/ # 场景二IItemsRangeInfo ISelectionInfo ├── FileDataSource2.cs # 加入选择管理的数据源 └── Scenario2.xaml / Scenario2.xaml.cs # 展示页Extended 多选 强制重置按钮两个场景共用FileItem、ItemCacheManager等基础设施FileDataSource2是在FileDataSource之上的增量实现建议按 Scenario1 → Scenario2 的顺序阅读。三、核心接口与数据模型IItemsRangeInfo、ItemIndexRange 与 ISelectionInfo3.1 ItemIndexRange范围的最小单位ItemIndexRange是Windows.UI.Xaml.Data命名空间中的结构表示从FirstIndex开始、长度为Length的一段连续条目区间LastIndex FirstIndex Length - 1。它是IItemsRangeInfo回调、缓存管理与选中管理共同使用的核心单位。示例在 ItemIndexRangeExtensions.cs 中为其补充了一系列区间运算扩展方法这些方法是缓存算法的基石扩展方法作用Equals(ItemIndexRange)比较两个区间的起点与长度是否相同用于范围没变就快速返回ContiguousOrOverlaps判断两区间是否相邻或重叠相邻也算便于合并Intersects判断两区间是否有交集Combine合并两个区间为一个覆盖两者的新区间DiffRanges求两区间的交集并分别输出仅属于 A仅属于 B的部分Overlap返回两区间的交集无交集返回 null3.2 IItemsRangeInfo让数据源知道视图要看什么IItemsRangeInfo是数据虚拟化的核心接口只包含两个成员RangesChanged(ItemIndexRange visibleRange, IReadOnlyListItemIndexRange trackedItems)当列表控件的可见区域或缓冲区域变化时被调用。visibleRange是真正可见的条目范围trackedItems是控件当前跟踪的其他范围集合例如缓冲区buffer以及获得焦点的元素所在范围Dispose()数据源被释放时调用用于清理缓存。示例 FileDataSource.cs 中的实现非常简短但含义关键——它把视图说了算的范围直接交给缓存管理器去更新public void RangesChanged(ItemIndexRange visibleRange, IReadOnlyListItemIndexRange trackedItems) { // 可见范围已经包含在 trackedItems 的更大范围中所以只需把 trackedItems 交给缓存 itemCache.UpdateRanges(trackedItems.ToArray()); }注意注释中的要点visibleRange总被包含在trackedItems之中缓冲区包围可见区因此只需处理trackedItems即可让缓存覆盖可见 缓冲。3.3 ISelectionInfo虚拟化下的选择管理ISelectionInfo让列表控件在数据并未全部加载的前提下仍能正确管理多选状态SelectRange(ItemIndexRange range)/DeselectRange(ItemIndexRange range)选中/取消选中一段范围IsSelected(int index)查询某个索引是否被选中GetSelectedRanges()返回当前所有选中的区间列表。在 FileDataSource2.cs 中选中状态被组织成一个ItemIndexRangeList不连续区间的有序集合并用另一个ItemCacheManagerstringselectionCache缓存选中条目的稳定键Key而不是直接缓存数据对象public void SelectRange(ItemIndexRange range) { selection.Add(range); selectionCache.UpdateRanges(selection.ToArray()); } public void DeselectRange(ItemIndexRange range) { selection.Subtract(range); selectionCache.UpdateRanges(selection.ToArray()); } public bool IsSelected(int index) { foreach (ItemIndexRange range in selection) { if (index range.FirstIndex index range.LastIndex) return true; } return false; } public IReadOnlyListItemIndexRange GetSelectedRanges() { return selection.ToList(); }这里体现了虚拟化选择管理的一个关键设计用稳定标识StorageFile.FolderRelativeId即文件在文件夹内的相对 ID而非易变的索引来记住选中项。当文件系统变化导致列表内容重置时可以用 Key 重新映射出新的索引见下文重置与重新映射小节保证选中状态在数据源刷新后依然成立。四、缓存引擎 ItemCacheManager按需加载、滚动节流与请求取消ItemCacheManagerTItemCacheManager.cs是本示例最核心的复用组件两个场景共用。它承担了决定要取哪些数据、何时取、取回来后放哪里、并通知外部的全部工作。4.1 构造与回调模型public ItemCacheManager(fetchDataCallbackHandler callback, int batchsize 50, string debugName ItemCacheManager)callback委托delegate TaskT[] fetchDataCallbackHandler(ItemIndexRange range, CancellationToken ct)——由外部注入的实际取数逻辑示例中即从文件系统分页读文件。采用回调模型是为了让缓存算法与具体数据源解耦FileDataSource与FileDataSource2各自注入自己的取数回调而缓存逻辑完全复用batchsize单批最大取数条数示例默认 50内部还维护一个DispatcherTimer间隔 20ms用于滚动节流见 4.3。4.2 核心数据结构CacheEntryBlock、requests 与 cachedResults缓存内部用三个数据结构协同工作cacheBlocksListCacheEntryBlockT每个块记录一段连续区间FirstIndex / Length / Items[]存放已取回的数据requestsItemIndexRangeList记录应该取但还没有取的区间集合cachedResultsItemIndexRangeList记录已经取到的区间集合。索引器this[int index]遍历cacheBlocks返回对应条目若不在缓存中则返回default(T)即 null。这正是虚拟化的关键行为数据源对列表控件暴露的IList索引器先返回 null 占位待数据取回后再通过CollectionChanged事件以Replace动作通知控件更新该位置见 4.4。4.3 UpdateRanges范围更新 → 差额计算 → 节流取数UpdateRangesItemCacheManager.cs是每次视图滚动时的处理入口流程如下规范化区间用NormalizeRanges把传入的多个区间合并成一组互不连续discontiguous的有序区间快速失败HasRangesChanged对比新旧区间若完全一致直接返回避免无谓重建缓存重建缓存块按新区间创建新的CacheEntryBlock[]并把旧块与新区间的重叠部分通过Array.Copy迁移过来用DiffRanges计算重叠丢弃不再需要的部分——这就是缓存随视图滚动而滑动的实现计算待取清单用新区间减去已缓存区间得到requests触发取数调用startFetchData()。startFetchData与定时器fetchData一起实现了滚动节流throttling每次范围变化后不立即取数而是重置 20ms 定时器如果 20ms 内视图仍在滚动范围继续变化定时器被不断重置从而等滚动停稳后再集中取数避免快速滚动时发出大量无用请求。取数时fetchData方法从requests取第一块未取区间带上CancellationToken调用外部回调取回数据后逐条写入缓存若新旧条目不同!newItem.Equals(oldItem)更新缓存块并触发CacheChanged事件携带oldItem / newItem / itemIndex已取回的部分从requests中Subtract掉取消机制如果取数期间视图又滚走了当前请求的区间已不再被需要startFetchData/fetchData会调用cancelTokenSource.Cancel()fetchDataCallback内部的ct.ThrowIfCancellationRequested()会抛出OperationCanceledException被 try/catch 吞掉然后finally中递归调用fetchData()继续处理剩余请求——已取回的中间结果不写入缓存保证缓存只包含当前视图需要的数据。4.4 从缓存到列表控件CacheChanged → CollectionChanged → ReplaceItemCacheManager本身不依赖 UI 线程它通过CacheChanged事件把某索引的数据已就绪通知给数据源数据源再触发INotifyCollectionChanged// FileDataSource.cs private void ItemCache_CacheChanged(object sender, CacheChangedEventArgsFileItem args) { if (CollectionChanged ! null) { CollectionChanged(this, new NotifyCollectionChangedEventArgs( NotifyCollectionChangedAction.Replace, args.oldItem, args.newItem, args.itemIndex)); } }这是一个值得注意的设计细节它用Replace动作而不是Add/Insert来通知。因为列表的Count从一开始就是真实的文件总数见下一节数据是原地填充而非插入新项Replace既不会导致列表长度变化也不会引起容器重建与 UI 虚拟化天然兼容。五、场景一实现 IItemsRangeInfo 的文件系统虚拟化数据源5.1 数据源的三重身份FileDataSourceFileDataSource.cs同时实现三个接口构成虚拟化数据源的标准形态class FileDataSource : INotifyCollectionChanged, System.Collections.IList, IItemsRangeInfoSystem.Collections.IList以假装是一个完整的列表的方式向GridView提供Count与按索引取值——Count返回文件查询结果的总数真实总数索引器则从缓存取未命中返回 nullINotifyCollectionChanged当缓存数据就绪或集合内容变化如文件系统变动时通知控件IItemsRangeInfo接收控件反馈的视图范围驱动缓存更新。5.2 工厂方法异步初始化构造函数是私有的通过GetDataSoure(path)工厂创建——因为初始化需要异步获取StorageFolder并建立文件查询public static async TaskFileDataSource GetDataSoure(string path) { FileDataSource ds new FileDataSource(); StorageFolder f await StorageFolder.GetFolderFromPathAsync(path); await ds.SetFolder(f); return ds; }SetFolder中建立文件查询并订阅内容变化事件QueryOptions options new QueryOptions(); _queryResult _folder.CreateFileQueryWithOptions(options); _queryResult.ContentsChanged QueryResult_ContentsChanged; await UpdateCount();UpdateCount通过_queryResult.GetItemCountAsync()拿到文件总数_count随后触发一次Reset通知让列表控件重新读取Count。被注释的options.IndexerOption IndexerOption.DoNotUseIndexer提示这里使用的是默认索引器选项。5.3 取数回调按范围分页读取 缩略图加载private async TaskFileItem[] fetchDataCallback(ItemIndexRange batch, CancellationToken ct) { // 按索引范围分页获取 StorageFile IReadOnlyListStorageFile results await _queryResult.GetFilesAsync((uint)batch.FirstIndex, Math.Max(batch.Length, 20)).AsTask(ct); ListFileItem files new ListFileItem(); if (results ! null) { for (int i 0; i results.Count; i) { ct.ThrowIfCancellationRequested(); // 支持中途取消 FileItem newItem await FileItem.fromStorageFile(results[i], ct); files.Add(newItem); } } return files.ToArray(); }两点值得展开GetFilesAsync(firstIndex, maxCount)是StorageFileQueryResult提供的索引分页 API一次最多取Math.Max(batch.Length, 20)个文件——保证即使请求区间很小也有最低批量FileItem.fromStorageFileFileItem.cs除了读取文件名与大小还会通过GetThumbnailAsync(ThumbnailMode.SingleItem)生成缩略图BitmapImage。缩略图是典型的重数据只有视图需要时才加载这正是数据虚拟化要节约的资源。此外fromStorageFile用一个静态SemaphoreSlim(1)串行化属性读取请求避免并发争抢。5.4 文件系统变化处理跨线程回到 UI 线程并重置缓存ContentsChanged回调可能在非 UI 线程触发示例通过CoreDispatcher将其调度回 UI 线程后执行ResetCollectionprivate void QueryResult_ContentsChanged(IStorageQueryResultBase sender, object args) { if (!_dispatcher.HasThreadAccess) { var t _dispatcher.RunAsync(CoreDispatcherPriority.Normal, ResetCollection); } else { ResetCollection(); } }ResetCollection丢弃旧的ItemCacheManager、创建新实例并触发CollectionChanged的Reset通知让列表控件重新查询Count并按新视图范围重新驱动缓存。这一流程在场景二中会被扩展以同时重映射选中状态见第七节。5.5 场景一页面把数据源挂到 GridViewScenario1.xaml 中使用GridViewSelectionModeSingle且ShowsScrollingPlaceholdersFalse关闭滚动占位符否则在数据未就绪的位置会显示默认占位内容。其页面代码 Scenario1.xaml.cs 完成接线async void initdata() { StorageLibrary pictures await StorageLibrary.GetLibraryAsync(KnownLibraryId.Pictures); string path pictures.SaveFolder.Path; FileDataSource ds await FileDataSource.GetDataSoure(path); if (ds.Count 0) { Grid1.ItemsSource ds; } else { MainPage.Current.NotifyUser(Error: The pictures folder doesnt contain any files, NotifyType.ErrorMessage); } }页面还通过ContainerContentChanging事件在每个容器内容就绪时写入该条目的真实索引args.ItemIndex便于直观观察当前视图命中了哪些索引配合TRACE_DATASOURCE编译常量的调试输出可以非常直观地看到滚动时缓存如何滑动、请求如何按批发出。调试提示项目头注释说明——若在项目属性 Build 页的 Conditional compilation symbols 中加入TRACE_DATASOURCE调试时会在 Output 窗口输出RangesChanged fired、Fetching items、Inserting items into cache等追踪信息是理解虚拟化工作过程的利器。六、区间工具类ItemIndexRangeList 与扩展方法6.1 ItemIndexRangeList不连续区间的有序集合ItemIndexRangeListItemIndexRangeList.cs实现IListItemIndexRange维护一组互不连续、按 FirstIndex 升序排列的区间并保证在 Add/Subtract 后自动维持该不变式Add(range)把新区间并入集合若与已有区间相邻或重叠则自动Combine合并直到整个集合重新互不连续Subtract(range)从集合中减去一段区间必要时把一个区间拆成两段例如区间 [0..10] 减去 [4..6] 会变成 [0..3] 和 [7..10] 两段这也是requests和selection的通用数据结构构造时同样会执行NormalizeRanges规范化。它同时是ItemCacheManager内部requests/cachedResults以及场景二selection的实现基础。6.2 扩展方法与 DiffRanges 的用途ItemIndexRangeExtensions中的DiffRanges在UpdateRanges里扮演关键角色当旧缓存块与新区间求重叠时它一次性输出重叠部分可迁移仅旧区间所有可丢弃仅新区间所有需要新取三个结果缓存迁移逻辑据此用Array.Copy保留重叠数据、并在requests中记录需要新取的部分。七、场景二叠加 ISelectionInfo让选中状态在虚拟化下存活7.1 双缓存结构数据缓存 选择键缓存FileDataSource2FileDataSource2.cs在FileDataSource基础上实现ISelectionInfo构造函数中创建两个ItemCacheManager// 数据缓存FileItem文件名/大小/缩略图 this.itemCache new ItemCacheManagerFileItem(fetchDataCallback, 50); // 选择键缓存stringFolderRelativeId用于记住选中了哪些文件 this.selectionCache new ItemCacheManagerstring(fetchSelectionDataCallback, 50, selectionCache);选中范围的ItemIndexRangeList selection记录当前选中区间selectionCache负责按需取回这些选中索引对应的稳定键。选择键的取数回调fetchSelectionDataCallback有一个省 IO 的优化先查数据缓存itemCache[batch.FirstIndex]如果该文件已经在数据缓存中就直接用其Key否则才访问文件系统取FolderRelativeIdFileDataSource2.cs。7.2 重置与重新映射RemapSelection用 Key 找回索引这是场景二最具价值的设计当文件系统变化触发ResetCollection时先执行RemapSelectionFileDataSource2.cs把旧索引 旧键转换成新索引再重建缓存遍历旧选中区间逐个取回旧索引对应的键oldSelectionCache[origIndex]用_queryResult.FindStartIndexAsync(fileName)找到该文件在新结果中的大致位置再GetFilesAsync取一小批50 个核对FolderRelativeId是否精确匹配匹配成功的重新加入newSelection并写入newSelectionCache匹配失败说明文件已被删除从选中集合中剔除最后用newSelection / newSelectionCache替换旧的。这样即使列表内容因文件增删而整体重置用户之前选中的文件依然保持选中只要文件还在。页面上的 Force a reset 按钮Scenario2.xaml.cs就是用来手动触发ResetCollection、直观演示这一行为的。场景二的GridView使用SelectionModeExtended以支持多选。八、构建与运行8.1 系统要求目标要求ClientWindows 10ServerWindows Server 2016 Technical PreviewPhoneWindows 108.2 构建步骤遵循 README启动 Visual Studio选择File → Open → Project/Solution进入解压后的示例目录打开解决方案文件 cs/DataVirtualization.sln按CtrlShiftB或选择Build → Build Solution编译。8.3 运行与部署仅部署选择Build → Deploy Solution部署并运行按F5Debug → Start Debugging进入调试运行或按CtrlF5Debug → Start Without Debugging直接运行。8.4 运行预期与排障运行后应用使用StorageFolderAPI 读取**图片库Pictures 文件夹**的内容并显示在GridView中。如果界面上看不到任何结果最可能的原因是图片库中没有可枚举的图片文件README 明确提示了这一点。建议先向图片库放入若干张图片再运行滚动浏览时可通过TRACE_DATASOURCE的调试输出观察缓存的滑动与按批取数过程。九、相关资源导航示例本身XamlDataVirtualization/README.md场景一数据源实现FileDataSource.cs缓存引擎ItemCacheManager.cs区间运算扩展ItemIndexRangeExtensions.cs不连续区间集合ItemIndexRangeList.cs场景二数据源含选择管理FileDataSource2.cs同仓库可对比的 XAML 绑定相关示例XamlBind其 README 展示了 x:Bind 绑定模式相关接口参考外部文档IItemsRangeInfo、ISelectionInfoWindows.UI.Xaml.Data 命名空间十、要点回顾数据虚拟化 ≠ UI 虚拟化UI 虚拟化只节省容器数据虚拟化进一步节省数据对象本身的内存二者可叠加使用IItemsRangeInfo 是视图反馈通道RangesChanged告诉数据源当前可见 缓冲的索引范围数据源据此维护滑动缓存缓存引擎三件套cacheBlocks数据、requests待取、cachedResults已取配合 20ms 定时器实现滚动节流配合CancellationToken实现请求取消取数通过外部回调解耦Replace 通知填充占位索引器未命中返回 null数据就绪后用CollectionChanged的Replace动作原地填充与 UI 虚拟化兼容选择用稳定键而非索引ISelectionInfoFolderRelativeId键缓存让选中状态在数据源重置后仍能通过RemapSelection恢复实践落点本示例以图片库文件列表为数据源可直接作为文件夹浏览器 / 照片墙 / 文件缩略图列表类 UWP 应用的数据层参考实现。赞分享示例工程【免费下载链接】Windows-universal-samplesAPI samples for the Universal Windows Platform.项目地址https://gitcode.com/gh_mirrors/wi/Windows-universal-samples点击查看免费下载相关推荐突破数据渲染瓶颈react-window构建高性能虚拟列表的实战指南突破数据渲染瓶颈react window构建高性能虚拟列表的实战指南 你是否曾因前端页面加载 thousands 条数据而导致浏览器卡顿甚至崩溃当用户滚动列CLIP零样本分类概率不对角对比学习机制拆解与3步诊断法CLIP零样本分类概率不对角对比学习机制拆解与3步诊断法 CLIP Contrastive Language Image Pre Training把一张图人工智能基础模型大模型多模态计算机视觉未来展望ViT-B-32__openai在Immich生态系统中的发展路线图未来展望ViT B 32__openai在Immich生态系统中的发展路线图 ViT B 32__openai作为Immich自托管照片库的核心AI模型组件上一篇探索无人机群智能EGO-Planner-v2下一篇如何用OCAT自动挂载EFI分区并打开config.plist完整指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考