# 异步加载使用指南 本文说明应用和 Control 作者如何使用 AtomUI 的异步加载能力。状态模型、协调器时序、生命周期和兼容性由 [异步加载架构](../../architecture/systems/control-infrastructure/async-loading.md) 定义。 ## 选择加载模式 | 场景 | 协调器 | 典型 Control | | --- | --- | --- | | 输入变化触发搜索,后一次请求覆盖前一次 | `AsyncSearchLoadCoordinator` | AutoComplete、Mentions、Select | | 展开节点触发加载,同一节点复用在飞任务 | `AsyncExpandLoadCoordinator` | Cascader、TreeView、TreeSelect | 搜索类可以配置 debounce;展开类不使用 debounce,通过 in-flight 去重避免同一节点并发重入。 ## 配置现有 Control 搜索类 Control 通常提供: | 属性 | 作用 | | --- | --- | | `AsyncLoadDebounce` | 输入停止后等待多久再发送请求;`Zero` 表示立即发送 | | `AsyncLoadTimeout` | 单次调用的总超时,包括 debounce 等待 | | `OptionsAsyncLoader` / `OptionsLoader` | 实际加载器;`null` 表示不启用异步加载 | Select 的加载触发点是 Popup 打开,因此没有 `AsyncLoadDebounce`。展开类 Control 通常提供 `AsyncLoadTimeout` 和 `DataLoader`。 ### Select ```csharp mySelect.OptionsLoader = new MyAsyncLoader(); mySelect.AsyncLoadTimeout = TimeSpan.FromSeconds(5); ``` ```csharp public sealed class MyAsyncLoader : ISelectOptionsAsyncLoader { public async Task LoadAsync( object? context, CancellationToken cancellationToken) { var items = await _api.FetchUsersAsync(cancellationToken); return new SelectOptionsLoadResult { Data = items }; } } ``` ### AutoComplete debounce ```xml ``` 新的输入会接管 loading 状态。被覆盖的旧请求返回 `Skipped`,不会触发终态事件,也不能用旧结果更新 UI。 ### TreeView 节点加载 ```csharp myTree.DataLoader = new MyNodeLoader(); myTree.AsyncLoadTimeout = TimeSpan.FromSeconds(30); ``` ```csharp public sealed class MyNodeLoader : ITreeItemNodeLoader { public async Task LoadAsync( ITreeItemNode parent, CancellationToken cancellationToken) { var children = await _api.FetchChildrenAsync(parent.Id, cancellationToken); return new TreeItemLoadResult { Data = children, StatusCode = RpcStatusCode.Success }; } } ``` 同一节点的连续展开调用复用一个在飞 Task。首次成功后 item 标记为 `AsyncLoaded`,由 Control 决定后续是否需要再次加载。 ## 处理终态 Control 的加载结果事件统一暴露 `IsSuccess`、`StatusCode`、`UserFriendlyMessage` 和可选 `Data`。应用应基于公开结果 类型处理提示,不依赖协调器的内部 CancellationToken 组合方式。 ```csharp myCascader.ItemAsyncLoaded += (_, args) => { if (args.Result?.IsSuccess == true) { return; } switch (args.Result?.StatusCode) { case RpcStatusCode.Timeout: ShowToast("加载超时,请重试"); break; case RpcStatusCode.NetworkFailure: ShowToast("网络异常"); break; default: ShowToast(args.Result?.UserFriendlyMessage ?? "加载失败"); break; } }; ``` Loader 必须尊重传入的 `CancellationToken`。不要捕获并吞掉 `OperationCanceledException`;协调器需要根据 token 来源 区分 timeout、外部取消和请求覆盖。 ## 新搜索类 Control 接入 1. Control 拥有一个长期复用的 `AsyncSearchLoadCoordinator`。 2. 调用前把公开的 debounce 和 timeout 属性同步给协调器。 3. `Skipped` 直接返回,不清理由新请求接管的 loading 状态,不触发事件。 4. Success 更新数据;TimedOut、Cancelled 和 Faulted 映射到 Control 的公开结果类型。 5. template reapply、detach 或 owner 变化不能遗留旧事件、binding 或请求状态。 ```csharp private readonly AsyncSearchLoadCoordinator _coordinator = new(); private async Task LoadAsync(string? query) { if (Loader is null) { return; } _coordinator.DebounceInterval = AsyncLoadDebounce; _coordinator.Timeout = AsyncLoadTimeout; var outcome = await _coordinator.LoadAsync( query, (context, token) => Loader.LoadAsync(context, token)); if (outcome.IsSkipped) { return; } ApplyOutcome(outcome); } ``` ## 新展开类 Control 接入 1. 使用 `AsyncExpandLoadCoordinator`。 2. 节点对象默认使用 `ReferenceEqualityComparer.Instance`;只有稳定业务 ID 明确构成节点身份时才使用自定义 comparer。 3. 多个不同节点可以并发加载,同一节点复用在飞 Task。 4. 集合和 item 状态更新回到 UI 线程。 5. Control detach 时调用 `CancelAll()`。 ```csharp private readonly AsyncExpandLoadCoordinator _coordinator = new(ReferenceEqualityComparer.Instance); protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) { _coordinator.CancelAll(); base.OnDetachedFromVisualTree(e); } ``` ## 常见问题 **Debounce 是否计入 timeout?** 计入。Timeout 覆盖从 `LoadAsync` 调用开始到产生终态的总时间。 **超时后 loader 是否继续运行?** 协调器会取消传给 loader 的 token。实际底层操作必须正确消费该 token 才能及时停止。 **为什么 `Skipped` 不触发错误或完成事件?** 它表示旧搜索已经被新搜索取代。对外发布旧终态可能覆盖新结果或产生错误提示。 ## 源码入口 - `src/AtomUI.Controls.Shared/AsyncLoad/` - `src/AtomUI.Desktop.Controls/AutoComplete/AbstractAutoComplete.cs` - `src/AtomUI.Desktop.Controls/Select/Select.AsyncOptionsLoad.cs` - `src/AtomUI.Desktop.Controls/Cascader/CascaderView.AsyncItemDataLoad.cs` - `src/AtomUI.Desktop.Controls/TreeView/TreeView.AsyncItemDataLoad.cs`