24 KiB
Upload 桌面版实现原理
本文档定义 Upload 桌面版的内部实现范围、源码职责、状态流、生命周期、资源边界和维护规则。公共设计与 API 契约见 Upload 桌面版架构设计,拖动输入的专项状态与平台边界见 Upload 拖动上传设计,变化记录见 Upload Changelog,控件视觉变量见 Upload Token 设计。
1. 实现定位
Upload 的实现定位是上传状态协调器,而不是固定上传按钮、拖拽框或列表模板的集合。根控件只拥有文件状态、上传队列、Form 值投影和生命周期释放;UploadTrigger、UploadDropZone、UploadList 只作为可组合动作入口或视图层协作对象。
本文档只记录维护者必须遵守的稳定实现结构和不变量。具体属性注册、AXAML selector、ControlTheme 节点和列表项视觉细节仍以源码为准。
2. 源码文件结构
源码结构按“状态 owner、输入入口、输入管线、队列协调、列表视图、主题模板”分层。
| 路径 | 职责 |
|---|---|
src/AtomUI.Desktop.Controls/Upload/Upload.cs |
保留 public/protected API、Avalonia 属性注册、构造、display source 桥接、IFormItemAware 和顶层上传协调。 |
src/AtomUI.Desktop.Controls/Upload/UploadAppendContentItem.cs |
internal picture append visual slot,用于把 TriggerContent 放入 PictureCard/PictureCircle 的同一 wrap flow;不进入 Files。 |
src/AtomUI.Desktop.Controls/Upload/Upload.FileSelection.cs |
封装文件选择和目录选择动作,并把同一个 IsMultipleEnabled 值投射到两种 picker 的 AllowMultiple。 |
src/AtomUI.Desktop.Controls/Upload/IUploadStorageProviderAdapter.cs |
隔离 Avalonia storage picker 调用,向文件选择和目录选择提供可测试的 typed StorageItem 边界。 |
src/AtomUI.Desktop.Controls/Upload/Upload.InputPipeline.cs |
连接 Upload 状态 owner 与输入管线,负责 UI 线程 option 快照、typed commit、批次事件和 ReplaceExisting source handoff。 |
src/AtomUI.Desktop.Controls/Upload/UploadInputPipeline.cs |
串行执行 picker、directory、drop 和 programmatic 批次,在一次 worker 边界内协调遍历与准入,再把数量策略和集合变更交给 UI commit。 |
src/AtomUI.Desktop.Controls/Upload/UploadInputBatchOperation.cs |
分别持有单个批次的 typed StorageItem 与 source lease ownership,验证 transfer,并在完成事件前释放全部未移交资源。 |
src/AtomUI.Desktop.Controls/Upload/UploadStorageItemEnumerator.cs |
在目录展开前应用顶层多选限制,再仅使用 IStorageFolder.GetItemsAsync() 按稳定顺序展开 storage items,并隔离目录分支错误。 |
src/AtomUI.Desktop.Controls/Upload/UploadInputCandidate.cs |
非 owning 地引用批次已拥有的 storage file,并提供基础元数据读取入口。 |
src/AtomUI.Desktop.Controls/Upload/UploadFileAdmissionService.cs |
在非 UI 执行上下文执行 AllowedFileTypes 和 AdmissionPolicy,生成明确的接受或拒绝结果。 |
src/AtomUI.Desktop.Controls/Upload/UploadStorageFileSource.cs |
以 IStorageFile lease 实现 IUploadFileSource,允许 transport 在 lease 有效期内打开读取流。 |
src/AtomUI.Desktop.Controls/Upload/Upload.AutoRemove.cs |
管理成功自动移除的延迟任务、取消和生命周期释放。 |
src/AtomUI.Desktop.Controls/Upload/UploadFileItem.cs |
public 文件状态模型,承载文件元数据、状态、进度、错误、结果和 pending 文案。 |
src/AtomUI.Desktop.Controls/Upload/UploadSourceKind.cs |
public 触发来源枚举,区分文件选择和目录选择。 |
src/AtomUI.Desktop.Controls/Upload/UploadFileValueMode.cs |
public Form 值投影枚举,控制提交全部文件、成功文件或上传结果。 |
src/AtomUI.Desktop.Controls/Upload/UploadTrigger.cs |
public 可组合触发器,查找最近的 Upload 并触发文件或目录选择。 |
src/AtomUI.Desktop.Controls/Upload/UploadDropZone.cs |
public 可组合拖拽区,拥有 DragDrop 协商、状态和 Drop 快照。 |
src/AtomUI.Desktop.Controls/Upload/UploadDefaultDropArea.cs |
public 默认拖动视觉,只保存图标、标题、副标题和动效属性。 |
src/AtomUI.Desktop.Controls/Upload/UploadQueue.cs |
internal 上传队列协调器,映射 UploadFileItem 与 FileUploadTask。 |
src/AtomUI.Desktop.Controls/Upload/UploadList.cs |
文件列表视图,渲染 effective 文件视图并维护列表滚动。 |
src/AtomUI.Desktop.Controls/Upload/AbstractUploadListItem.cs |
列表项基类,只绑定 UploadFileItem 并发出 item action,不拥有任务状态。 |
src/AtomUI.Controls.Shared/Net/FileUploadScheduler.cs |
共享上传调度器,跟踪 pending/running 任务、并发和取消。 |
src/AtomUI.Controls.Shared/Net/FileUploadTask.cs |
共享上传任务对象,承载传输回调和 cancellation 状态。 |
src/AtomUI.Controls.Shared/Net/IUploadFileSource.cs |
public 文件内容读取契约,不要求本地路径。 |
src/AtomUI.Controls.Shared/Net/IUploadFileSourceLease.cs |
internal AtomUI-owned source lease 标记,只由接受该 lease 的 owner 释放。 |
主题结构:
| 路径 | 职责 |
|---|---|
src/AtomUI.Desktop.Controls/Upload/Themes/UploadTheme.axaml |
根模板,连接 TriggerContent、list 和 picture display source,并保持触发区与列表的稳定间距。 |
src/AtomUI.Desktop.Controls/Upload/Themes/UploadTriggerTheme.axaml |
触发器 shell,只承载用户内容和点击表面。 |
src/AtomUI.Desktop.Controls/Upload/Themes/UploadDropZoneTheme.axaml |
拖拽行为 shell,只承载用户内容和内容对齐。 |
src/AtomUI.Desktop.Controls/Upload/Themes/UploadDefaultDropAreaTheme.axaml |
默认拖动视觉,保持 Frame、内容 presenter、Token 和动效结构。 |
src/AtomUI.Desktop.Controls/Upload/Themes/UploadListTheme.axaml |
列表 shell,内部拥有自动隐藏的 atom:ScrollViewer 和滚动边界。 |
src/AtomUI.Desktop.Controls/Upload/Themes/PictureList/* |
Picture 列表项视觉,pending 内容优先读取 UploadFileItem.PendingText。 |
src/AtomUI.Desktop.Controls/Upload/Themes/PictureShapeList/* |
PictureCard/PictureCircle 列表布局和 item 视觉;append slot 由 display source 承载并进入同一 wrap flow。 |
3. 核心类职责
| 类型 | 稳定职责 | 禁止职责 |
|---|---|---|
Upload |
Files 状态 owner、上传协调、Form 适配、生命周期释放、trigger/drop/list 组合入口。 |
不创建第二份任务列表,不把 trigger 伪造成文件项。 |
UploadFileItem |
public 文件状态模型,保存文件元数据、上传状态、进度、错误、结果和自定义数据。 | 不持有视觉控件、队列、transport 或 owner 控件。 |
UploadQueue |
管理 UploadFileItem.Id 与 FileUploadTask 的映射,转发 scheduler 状态到文件项。 |
不操作模板 part、列表容器或 Gallery 视图。 |
UploadTrigger |
根据 SourceKind 调用最近 Upload 的文件或目录选择方法。 |
不保存文件集合,不直接打开业务上传 transport。 |
UploadDropZone |
处理 DragDrop 路由、Copy/None 协商、Drop 快照和输入批次创建。 | 不读取文件元数据、枚举目录或维护任务状态。 |
UploadDefaultDropArea |
通过既有 ControlTheme 渲染默认拖动界面。 | 不设置 DropTarget、不读取 DataTransfer、不查找 Upload owner。 |
UploadInputPipeline |
统一处理 picker、directory、drop 和 programmatic 输入;只对 StorageItem 用户输入应用顶层多选限制。 | 不渲染控件,不执行网络传输。 |
UploadList |
渲染 effective 文件视图,维护 ListMaxHeight 和 ListScrollBarVisibility;Picture shape 派生列表可追加 display-only visual slot。 |
不创建、删除、隐藏真实上传文件状态。 |
AbstractUploadListItem |
从 UploadFileItem 投射 item 视觉状态并发出 remove/preview 等动作请求。 |
不反向持有 scheduler task 或复制 item 状态。 |
FileUploadScheduler |
共享并发调度、pending/running 跟踪、取消和 transport 替换。 | 不引用 Upload、UploadList 或其他视觉控件。 |
4. 状态与数据流
Upload state flows through one path:
Public API / Trigger / DropZone
-> UploadInputPipeline
-> StorageItem enumeration / metadata / admission
-> Upload UI count policy / typed commit
-> Files collection
-> UploadQueue/FileUploadScheduler
-> UploadFileItem.Status/Progress/Result
-> UploadList item containers
实现中只能有一个文件状态 owner:
Upload.Files是唯一 public 文件列表入口。Files为null时,Upload可以使用内部 fallback collection,但该 collection 仍是唯一 effective 文件集合。- 绑定场景下,
ResetAsync和 Form 写入必须清空或更新集合内容,不替换外部绑定的集合实例。 - 禁止保留
_allTaskList、TaskInfoList、CurrentTaskList或同类复制集合。 - 禁止从
UploadList、item container 或 picture trigger 反向创建业务任务状态。
队列状态只在 UploadQueue 和 FileUploadScheduler 内部存在:
UploadQueue通过UploadFileItem.Id查找对应FileUploadTask。- scheduler 回调只更新对应
UploadFileItem.Status、Progress、ErrorMessage和Result。 RemoveFileAsync、ResetAsync和 detach 在 AtomUI 可控制的路径中等待相关队列工作退出,再释放 source lease;ReplaceExisting先在 UI commit 中替换集合,再让被观察的清理任务取消实际移除项,旧 source 在执行退出前保持有效。- 外部集合 remove/reset、
Files属性替换和同步 Form Set/Clear 已经先改变了集合;Upload必须立即从 accepted-source map 取走被移除项,再由被观察的清理任务等待对应 queue cancellation,最后释放 lease。 MaxConcurrentTasks、UploadTransport和 cancel-all 维护操作必须经由同一个 scheduler maintenance gate 串行执行;取消令牌不得让已出队的 Pending task 留在无 owner 状态。
Form 与验证状态:
FileValueMode控制IFormItemAware.GetFormValue的投影结果。IFormItemAware.SetFormValue接收IEnumerable<UploadFileItem>并更新Files内容。IFormItemAware.SetFormValue和ClearFormValue保持同步集合语义;被替换或清除项的 queue cancellation 与 source lease 释放由具名、被观察的后台清理任务完成。- error 状态以 Avalonia
DataValidationErrors为准,不新增 Upload 专用 error owner。
5. 组合结构模型
控件角色图
Upload (public coordinator)
-> TriggerContent slot
-> UploadTrigger (public action entry)
-> UploadDropZone (public drop entry)
-> UploadDefaultDropArea (public default visual)
-> UploadList / UploadPictureShapeList (public/internal-observable list view)
-> atom:ScrollViewer (template-stable auto-hide list scroll boundary)
-> ItemsPresenter#PART_ItemsPresenter (template-stable)
-> AbstractUploadListItem derived container (internal-observable)
-> UploadAppendContentItem visual slot (picture display source only)
协作节点
| 节点 | 类型 | 来源 | 生命周期 owner | 影响的 public API | 稳定性 | Agent 使用边界 |
|---|---|---|---|---|---|---|
Upload |
控件 | Upload.cs |
自身 | Files、TriggerContent、UploadTransport、FileValueMode |
public | 可作为用户 API 和实现入口。 |
UploadTrigger |
控件 | UploadTrigger.cs / UploadTriggerTheme.axaml |
visual tree | UploadSourceKind、SelectFilesAsync、SelectDirectoriesAsync |
public | 可公开示例,不能持有任务状态。 |
UploadDropZone |
控件 | UploadDropZone.cs / UploadDropZoneTheme.axaml |
visual tree | DragState、DirectoryDropMode、输入批次 |
public | 唯一 DragDrop owner,drop 后必须交给统一输入管线。 |
UploadDefaultDropArea |
控件 | UploadDefaultDropArea.cs / UploadDefaultDropAreaTheme.axaml |
visual tree | DropIcon、Header、SubHeader |
public | 只负责默认视觉,不处理 DataTransfer。 |
UploadInputPipeline |
internal service | Upload 输入实现 | Upload |
picker/drop/programmatic input | private | 统一目录、准入、数量和 lease 生命周期。 |
UploadList |
控件 | UploadList.cs / UploadListTheme.axaml |
Upload 或用户布局 |
ListMaxHeight、ListScrollBarVisibility |
internal-observable | 用于理解布局和滚动,不作为第二状态 owner。 |
atom:ScrollViewer |
模板节点 | UploadListTheme.axaml |
UploadList template |
列表滚动 | template-stable | 使用 AtomUI lite/auto-hide 滚动条,只包裹列表,不包裹 trigger。 |
AbstractUploadListItem |
item container | AbstractUploadListItem.cs |
items control container lifecycle | remove/preview/action 事件 | internal-observable | container recycle 必须释放 item 绑定和事件。 |
UploadAppendContentItem |
internal visual slot | UploadAppendContentItem.cs |
Upload display source |
TriggerContent |
internal | 只能承载 picture append 入口,不能写入 Files 或持有任务状态。 |
UploadQueue |
internal service | UploadQueue.cs |
Upload |
上传状态和取消 | private | 不暴露给用户,不引用视觉控件。 |
6. 生命周期与模板接入
Upload 的生命周期释放必须成对设计,不能依赖 GC 或视觉树自然释放。
| Acquire | Release |
|---|---|
| upload task cancellation token | task completion, cancel, remove, reset, detach |
| success auto-remove delay | remove, reset, detach, status changes away from Success |
| trigger parent lookup | detached visual tree |
| drop-zone drag session | leave、drop、disable 或 detached visual tree |
| input batch typed StorageItem/source sets | reject、cancel、failed cleanup、accepted item transfer、reset 或 detach;所有 transfer 验证当前 owner |
accepted UploadFileInfo source lease |
remove、external collection change、Files replacement、Form Set/Clear、reset 或 detach;释放前等待对应 upload execution 退出 |
| generated list container bindings | container recycle |
模板和集合接入规则:
OnApplyTemplate重新应用时先释放旧 template part 订阅,再接入新 part。Files集合替换时解绑旧集合 change 订阅,接入新集合,保留新旧集合共有 id 的 source ownership,并把仅存在于旧集合的 source 移交给取消清理任务。- item container 准备时只绑定当前
UploadFileItem;container recycle 时必须释放旧 item 的订阅和 action handler。 UploadTrigger和UploadDropZone查找 owner 时只依赖当前 visual ancestor;脱离 visual tree 后不缓存旧 owner。- detach 时取消全部输入批次、上传任务、pending auto-remove delay、drag/drop 会话和 C# binding。
- collection/Form 通知、批次完成事件或上传取消事件抛出异常时,清理路径必须继续完成 task cancellation、accepted-source map 移交和 lease 释放,再向等待方传播单个异常或聚合异常。
7. 交互与事件处理
交互入口必须统一收敛到 Upload 的 public 方法:
- 文件按钮点击:
UploadTrigger.SourceKind=Files调用Upload.SelectFilesAsync。 - 目录按钮点击:
UploadTrigger.SourceKind=Directories调用Upload.SelectDirectoriesAsync。 - 拖拽提交:
UploadDropZone同步取得一次 StorageItem 快照,再交给UploadInputPipeline。 - 文件 picker、目录 picker 与 Drop 共享
Upload.IsMultipleEnabled契约;picker 打开时把该值写入AllowMultiple,StorageItem 管线在处理批次时快照该值。程序化EnqueueFilesAsync不应用顶层输入限制。 - 手动移除:列表项发出 remove 请求,
Upload.RemoveFileAsync负责取消任务、取消 auto-remove 并从Files移除。 - 表单写入与清空:同步更新当前 effective 集合,并把被移除 source 移交给被观察的取消清理任务;不得直接释放仍可能被 transport 使用的 lease。
事件处理不得通过 suppression flag、延时刷新或强制重算来修正状态顺序。若出现状态不同步,优先修正 owner、事件顺序或释放路径。
8. 内部算法与关键流程
8.1 入队流程
- 文件选择、目录选择、拖动或程序化入口创建统一输入批次。
UploadInputPipeline在 UI 线程取得包含IsMultipleEnabled的不可变 option snapshot;StorageItem 批次先按稳定顺序限制顶层项目,再在单一 worker 边界中展开目录并取得受控IUploadFileSource。程序化文件批次直接进入文件准入。- 管线在非 UI 执行上下文应用
AllowedFileTypes和AdmissionPolicy。 - UI commit 根据当前 effective file count 和批次 option snapshot 计算数量策略,再把接受文件转换为
UploadFileItem;typed batch operation 在集合变更前验证 ownership,并只为实际保留项执行 source-to-Upload transfer。 - 数量拒绝或尚未提交的文件仍由批次释放 lease;
AutoUpload=true时已提交 item 与 file info 进入UploadQueue。 - queue/scheduler 回调只更新对应
UploadFileItem,列表通过绑定观察变化。 - 批次逐项释放全部未移交资源并聚合清理异常,再在 UI 线程触发一次
InputBatchCompleted;事件处理返回后,正常 Task 完成,取消或失败 Task 向等待方传播对应异常。
拖动协商、Drop 快照、目录遍历、平台矩阵和 StorageItem 生命周期的完整算法见 Upload 拖动上传设计。
8.2 移除与 reset 流程
RemoveFileAsync(id)查找对应UploadFileItem,取消上传任务和 auto-remove delay,再从 effectiveFiles移除。ResetAsync()取消全部运行和 pending 工作,取消全部 auto-remove delay,清空 effectiveFiles,不替换绑定集合实例。- 外部集合 remove/reset、
Files替换和 Form Set/Clear 无法延迟调用方已经执行的集合变化;这些路径先同步状态,再逐项等待UploadQueue.CancelAsync,并只在该调用返回后释放对应 source lease。 - detach 使用 reset/cancel 语义释放资源,但不应触发用户不可预期的业务上传重试。
ResetAsync和 detach 将各清理阶段视为独立 acquire/release pair;非取消异常不得阻止后续队列清理或 source 释放。
8.3 成功自动移除流程
- 当 item 状态进入
Success且SuccessAutoRemoveDelay不为空时,按 item id 注册延迟取消源。 - 延迟到期后通过
RemoveFileAsync移除文件,确保与手动移除共享同一释放路径。 - item 状态离开
Success、手动移除、reset、detach 时取消对应延迟。
9. 资源、性能与 AOT 边界
资源边界:
- 异步上传任务、取消源、delay、drag/drop 事件、collection change 和 item container 绑定必须有确定释放点。
- DragOver 不读取文件值或元数据;Drop 只物化一次顶层 StorageItem 数据。
- 输入批次通过 arrival gate 串行执行;每个批次只建立一次显式 worker dispatch,目录展开、元数据读取和准入按候选稳定顺序处理,不创建无界并发任务;数量决策与集合变更保持在同一个 UI commit 边界。
UploadInputBatchOperation不保存泛化IDisposable;StorageItem 和 source lease 使用独立的引用相等 owner set,释放遍历不得因单个异常提前中止。UploadFileItem不持有视觉控件,避免文件项生命周期反向保留控件树。UploadQueue不捕获Upload外的视觉对象;回调只写 item 状态。- scheduler 的取消完成语义必须等待 transport execution task 真正退出,source lease 才能被上层释放。
- scheduler maintenance 必须先把全部 Pending task 转入 Cancelled、向全部 Running task 发出取消,再观察调用方取消令牌;并发 transport/并发度变更按调用顺序生效。
- C# binding 仅用于 AXAML 无法表达的动态关系,并必须挂到明确 owner 上释放。
性能边界:
- 列表滚动由
UploadList内部atom:ScrollViewer管理,并启用 lite/auto-hide;避免用户外层ScrollViewer把 trigger、drop-zone 和 list 一起滚动。 UploadList依赖 ItemsControl container 生命周期,不为每次进度变化重建列表项。- 大量文件状态更新时只更新对应
UploadFileItem属性,不替换Files集合。
AOT 边界:
- 不通过运行时反射扫描 public API、Token、API 契约摘要或上传模型。
- 拖动输入使用 typed DataTransfer、StorageItem 和显式策略,不依赖平台私有反射或动态发现。
- 新增 public 类型应显式引用并由源码、Gallery 和测试覆盖。
- Source generator 生成文件不手工编辑;LLMS 产物也不在本次运行时代码任务中手工修改。
10. 维护不变量
维护 Upload 时不得破坏以下不变量:
Files是唯一文件状态 owner。- 不得引入与
Files平行的任务集合,也不得把 picture trigger 伪装成文件项。 - trigger、drop-zone、list、item container 都不能保存第二份业务任务状态。
UploadDropZone是唯一 DragDrop 行为 owner;UploadDefaultDropArea必须保持纯视觉职责。IsMultipleEnabled只能限制 picker 与 Drop 的顶层 StorageItem 数量;目录内部展开、EnqueueFilesAsync和最终MaxCount各自保持独立语义。AdmissionPolicy不得在 UI 线程执行,也不得访问 Avalonia 控件;策略异常与策略显式拒绝必须使用不同 rejection reason。- 取消和批次级失败通过
UploadInputBatchStatus表达,不得创建空名称或虚假UploadRejectedItem。 - ownership transfer 必须由 typed batch operation 验证;不得重新引入
ownsFileSources、queueAlreadyCancelled或通用 transfer callback。 - 默认 DropZone/DropArea ControlTheme、模板视觉树、Token、布局和渲染结果不得因输入管线重构改变。
- PictureCard/PictureCircle 的上传入口只能通过
EffectivePictureItems中的 display append slot 呈现,确保与图片项处于同一 wrap flow。 RemoveFileAsync、外部集合变更、Files替换、Form Set/Clear、ResetAsync和 detach 必须以各自时序释放上传任务、source lease、auto-remove delay、集合订阅和 container 绑定。DataValidationErrors是 error 状态来源,Upload 不维护独立 error 机制。- AXAML-first binding 是默认选择;C# binding 必须说明 AXAML 不能表达的原因和释放 owner。
- Gallery 示例、控件文档和测试必须使用同一套 public contract。
11. 测试与验证
重构任务按以下层级验证:
| 变更范围 | 验证 |
|---|---|
| public API | UploadRedesignContractTests 检查属性、默认值、batch status/failure、AdmissionDecision factory 和 raw Exception 移除。 |
| 单一状态 owner | UploadFileStateTests 覆盖 add、remove、reset、集合替换和外部绑定集合保持。 |
| 队列生命周期 | UploadSchedulerTests 覆盖 running 计数、完成释放、cancel all、维护操作序列化、取消中途一致性和 transport 替换。 |
| 触发器与拖拽 | 行为测试覆盖文件/目录触发、Copy/None 协商、Drop-only 数据读取、嵌套路由和 owner 委托。 |
| 输入管线与生命周期 | fake StorageItem 覆盖目录模式、worker-thread 准入、数量策略、typed adopt/transfer、取消、异常聚合、ReplaceExisting、detach 和 lease 恰好释放一次。 |
| 平台拖动 | Windows、macOS、X11 和 Wayland 跨进程验证文件、目录、效果反馈和异步读取。 |
| 渲染兼容 | Light/Dark、不同缩放和自定义 Content 的截图、Measure、Arrange 与 Bounds 保持基线。 |
| 成功自动移除 | UploadAutoRemoveTests 覆盖 delay 到期、remove、reset、detach 和状态变更取消。 |
| Form 与验证 | UploadFormValueTests 覆盖 FileValueMode、set/clear 和 DataValidationErrors。 |
| Gallery 示例 | UploadShowCasePageTests 和 snapshot 覆盖示例、本地化和源码片段。 |
| 文档卫生 | git diff --check 和关键术语扫描。 |
文档验证命令:
rg -n "UploadFileItem|UploadTrigger|UploadDropZone|UploadDefaultDropArea|UploadInputPipeline|SuccessAutoRemoveDelay|UploadFileValueMode" docs/controls/desktop/data-entry/upload
git diff --check