Files

293 lines
22 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Message 桌面版实现原理
本文档描述 Message 桌面版的内部实现范围、源码职责、状态流、生命周期、资源边界和维护规则。公共设计与 API 契约见 [Message 桌面版架构设计](overview.md),共用堆叠与计时算法见 [Feedback 堆叠基础设施](../../../../architecture/systems/control-infrastructure/feedback-stack.md),变化记录见 [Message Changelog](changelog.md)。涉及控件 Token 的实现应同时阅读 [Message Token 设计](token.md)。
## 1. 实现定位
本文档覆盖 Message 的控件实现、主题接入、状态同步和 Gallery 可见维护边界。具体属性注册、默认值、绘制细节和 AXAML selector 仍应直接阅读源码;本文只记录维护者必须理解的稳定结构和不变量。
## 2. 源码文件结构
主要源码文件:
- `src/AtomUI.Desktop.Controls/Message/IMessage.cs`
- `src/AtomUI.Desktop.Controls/Message/IMessageManager.cs`
- `src/AtomUI.Desktop.Controls/Message/Message.cs`
- `src/AtomUI.Desktop.Controls/Message/MessageCard.cs`
- `src/AtomUI.Desktop.Controls/Message/MessageCard.SemanticParts.cs`
- `src/AtomUI.Desktop.Controls/Message/MessageCardPseudoClass.cs`
- `src/AtomUI.Desktop.Controls/Message/MessageCardToken.cs`
- `src/AtomUI.Desktop.Controls/Message/MessageType.cs`
- `src/AtomUI.Desktop.Controls/Message/Themes/MessageCardTheme.axaml`
- `src/AtomUI.Desktop.Controls/Message/Themes/WindowMessageManagerTheme.axaml`
- `src/AtomUI.Desktop.Controls/Message/WindowMessageManager.cs`
- `src/AtomUI.Desktop.Controls/Primitives/FeedbackStack/FeedbackStackPresenter.cs`
- `src/AtomUI.Desktop.Controls/Primitives/FeedbackStack/FeedbackStackPanel.cs`
- `src/AtomUI.Desktop.Controls/Primitives/FeedbackStack/FeedbackLifetimeScheduler.cs`
- `src/AtomUI.Desktop.Controls/Primitives/FeedbackStack/FeedbackCardMotion.cs`
- `src/AtomUI.Desktop.Controls/Primitives/FeedbackStack/FeedbackCardMotionCoordinator.cs`
- `src/AtomUI.Desktop.Controls/Primitives/FeedbackStack/IFeedbackStackItem.cs`
- `src/AtomUI.Desktop.Controls/Message/WindowMessageManager.SemanticParts.cs`
- `src/AtomUI.Core/MotionScene/MotionExecutionState.cs`
职责边界:
- 控件主文件保留 public/protected API、Avalonia 属性注册、事件和主要生命周期入口。
- Theme 文件负责静态视觉结构、template part、selector 和资源绑定。
- Token 文件只提供组件视觉变量,不保存实例状态。
- Gallery 文件只展示用法和示例,不作为运行时逻辑 owner。
## 3. 核心类职责
- `Message`:控件核心或内部协作类型,维护 public surface 与主题可观察行为。
- `MessageCard`:控件核心或内部协作类型,维护 public surface 与主题可观察行为。
- `MessageCardToken`:internal 控件 Token scope,负责从全局 token 派生控件语义变量。
- `WindowMessageManager`:拥有稳定卡片集合、public Stack 配置、TopLevel host、生命周期调度与用户回调清理。
- `WindowFeedbackLayer`:作为窗口反馈 manager 的宿主,按最近成功提交的 `Show` 原子移动直接 manager 子项;不拥有卡片、
scheduler 或用户回调。
- `FeedbackStackPresenter`:消费稳定 ItemsSource,管理整体 hover 和 Message 静态背板状态。
- `FeedbackStackPanel`:按共享几何契约测量和排列,不拥有内容或生命周期。
- `FeedbackLifetimeScheduler`:按单调 deadline 调度有限时长项;空闲时没有活动 timer。
- `FeedbackCardMotion`:把 Message 的 Position 归一为 64 DIP translate/fade 和统一 Ant easing,不改变 card scale。
- `FeedbackCardMotionCoordinator`:协调当前 actor 的进入/退出状态、取消和完成提交,并在 retemplate、detach、dispose 时同步解除 transition 引用。
核心协作规则:
- 控件实例是 public API 和运行时状态 owner。
- Template part 是视觉协作对象,生命周期必须受 `OnApplyTemplate` 或模板加载流程管理。
- 数据对象、选项对象、任务对象或节点对象只保存业务数据,不应反向持有不可释放的视觉对象。
- 弹层、窗口、计时器、异步 loader 和全局管理器必须有明确关闭、解绑或释放路径。
## 4. 状态与数据流
Message 的状态流遵循下面路径:
```text
Public API / ItemsSource / Command / Event
-> 控件实例状态
-> internal state / effective state / pseudo-class
-> template part property / AXAML selector
-> renderer / popup / adorner / Gallery observable behavior
```
源码中的状态入口按以下语义维护:
- 内容与数据:`Show`、`MaxItems`、`Message`、`MessageType`、`Icon`。
- Stack 与生命周期:`IsStackEnabled`、`StackThreshold`、`IsPauseOnHover`、`IMessage.Expiration`、`DestroyAll()`;Stack
只参与视觉投影和实际 hover 暂停,不能改写或替代展示时长。
- 交互与状态:`IsClosed`、`IsClosing`、`IsMotionEnabled`。
- 视觉与布局:`Position`。
`IsClosing` 和 `IsClosed` 是 MessageCard 的 public 业务状态。卡片拥有一个共享 `FeedbackCardMotionCoordinator`;协调器分别
以 `MotionExecutionState` 约束进入和退出阶段,按 `Idle -> Pending -> Playing -> Completing -> Idle` 推进。关闭/禁用状态与
模板重套用都进入同一控制器;旧 actor 的执行会被取消且 completion 失效,当前 actor 立即接管目标状态。播放中的 Position /
Duration 变化不重启同一 actor,新值从下一次 motion 生效;Completing 只负责提交一次 `IsClosed=true`。
维护要求:
- 外部设置的 Avalonia 属性必须在模板应用前后保持一致。
- 集合、选择、展开、过滤、分页、上传任务或异步 loader 必须能处理 reset、replace 和 clear。
- 伪类和 internal state 必须从单一 owner 推导,避免双向同步导致循环更新。
- overview.md 的 API 契约说明应与源码实际状态流一致。
## 5. 生命周期与模板接入
生命周期规则:
- 构造阶段只注册必要状态,不依赖 template part。
- 模板应用时获取 part、建立事件订阅和绑定,并先释放旧 part 订阅。
- manager 的卡片 collection 在模板之外创建并保持稳定;新 `PART_Items` 只重新绑定该 collection,旧 presenter 立即解绑。
- 带宿主 manager 的跨 manager 层级只由 host layer 的直接子项顺序表达;激活使用 collection `Move`,不得通过
`Remove` + `Add` 触发 detach/attach,也不得覆盖 manager/card 的 `ZIndex`。
- manager 首次 attach 前允许 `Show`,卡片进入稳定集合,有限时长登记保持暂停。detach 时先暂停 scheduler,
并在视觉树级联完成后确认是否仍离树:持续离树关闭当时卡片,同轮重新入树的 host 迁移保留队列。
- `Dispose` 解绑 presenter 的集合与 hover 事件、释放 scheduler 和每张 card 的 owner/回调,再移除宿主层及安全区订阅。
模板重套用仅更换 presenter,不重新创建卡片。
- Gallery 页面分别惰性创建普通示例 manager 与 Stack 示例 manager;页面 detach 时两者都必须 `Dispose()` 并清空引用。
- 控件卸载、弹层关闭、窗口关闭、集合替换或 container recycle 时释放事件订阅和资源宿主。
- DynamicResource、TokenResourceBinder 或 C# binding 必须有明确 owner 和释放点。
- Browser 和 Desktop 宿主下的主题加载顺序不得影响 public API 语义。
稳定 template part 接入点:
- `PART_Frame`:承载根视觉、边框、背景或尺寸基线。
- `PART_HeaderContainer`:稳定模板协作入口,重命名前必须同步主题和实现。
- `PART_IconContent`:展示图标、状态图标或操作图标。
- `PART_Items`:`ItemsControl` 级稳定入口,承载共享 presenter 和 panel;不能再由 manager 直接修改 `Panel.Children`。
- `PART_Message`:稳定模板协作入口,重命名前必须同步主题和实现。
## 6. 交互与事件处理
Message 的交互事件应从输入源收敛到控件级语义事件:
- Pointer、keyboard、focus 和 command 事件不应绕过 Avalonia 基础控件语义。
- 没有弹层职责的路径不应引入额外 popup 或全局输入捕获。
- 集合类路径必须稳定处理 container prepare、clear、过滤、分组和虚拟化回收。
- 值提交或命令触发必须保持继承控件的事件顺序。
稳定事件路径包括 `MessageClosed`。事件参数和触发时机属于兼容边界。
## 7. 内部算法与关键流程
维护者需要重点关注以下流程:
- API 默认值到 effective state 的归一。
- Template part 重新应用时的状态回放。
- 主题资源、Token 和 SharedToken 计算后的视觉更新。
- ItemsSource、selection、checked、expanded、filter、paging 或 upload task 的集合同步。
- 动效启停、初始加载阶段 transition 抑制和卸载取消。
- MessageCard 的关闭请求、模板状态回放和最终 `IsClosed` 提交必须经过同一个关闭动效执行状态流。
- MessageCard 的内容 actor 使用共享 Feedback motion;按 Position 从对应边缘平移 64 DIP 并插值 opacity,退出反向执行,
scale 全程为 `1`。actor 进入/退出与外层队列位置 transform 不得写入同一属性。
- 入场和退出通过同一 motion 执行入口交接 actor;复用同一个 actor 时,取消请求之后必须等待旧 motion 的异步清理完成,
才能写入新目标。新模板的不同 actor 可以立即启动。等待期间仍响应当前执行的取消,重套模板、detach 或 dispose
不得留下继续启动旧执行的回调。
- FeedbackStackPanel 把展开位置表达为相对稳定顶部/底部锚点的 transform,使新增、移除和 Stack 切换只更新目标
transform;不得先改写屏幕位置再通过 Dispatcher 执行补偿动画。motion-disabled 时直接排列可见终态,并保持隐藏项
的有效展开 Bounds,以避免无意义的 transform 属性写入和 layout-invalid 重试。
- `Show` 在 UI thread 同步创建并登记卡片,加入稳定 collection 后更新 MaxItems 和 Stack 投影;不使用延迟队列重新取得旧模板的 children。
- Stack 判定使用活动项数量严格大于有效阈值;Message 折叠只保留最新真实卡片可见,并由 AXAML 中两个静态背板表达深度。
- 背板在展开期间仍跟随最新活动卡片的尺寸,通过 opacity 与 RenderTransform 的快速过渡表达显隐;收拢时不能从零
重新增长宽度。两层背板由零尺寸 Panel 叠放,不参与队列 extent 或命中;顶部、底部锚定及宽度变化过渡遵循共享几何契约。
- 生命周期调度只保存 deadline / remaining 与 card 协作引用。有限时长项无论 Stack 是否开启都登记 deadline;零时长项
不创建登记。只有 lifecycle detach 或实际 hover 才能暂停,折叠状态自身不能暂停。继续和 deadline 到期由一个
manager 级 scheduler 完成,不为每项创建 timer。
- scheduler 在本次唤醒结束时清空扫描临时列表,关闭回调完成后读取最新单调时刻再安排下一次唤醒;空闲 manager 不得
因复用列表保留已关闭卡片,也不得把回调耗时再次计入后续 deadline。
- `MaxItems` 淘汰先固定本批最旧活动项,再按创建顺序请求关闭;同步关闭或回调修改集合不得改变已选批次。
- `Show` 在 UI thread 同步创建卡片并加入稳定 collection,随后在生命周期登记、MaxItems 淘汰及其潜在用户回调之前激活
当前宿主 manager;重入到其他 manager 的后续 `Show` 必须保留为最终栈顶。
- `DestroyAll()` 固定调用时的卡片批次并倒序请求关闭;回调中新建的卡片不属于外层批次,manager dispose 后停止请求。
正在关闭项不会重复关闭,清理仍由 card 的 `MessageClosed` 单一路径提交。
- Gallery Stack 示例每次只同步创建一个零时长 Information 消息,并复用一个递增序号;Enabled 与 Threshold 直接更新
Stack 示例 manager 的现有投影,`DestroyAll()` 只清空该 manager 且不重置序号。普通示例 manager 保持运行时默认
`IsStackEnabled=false`,因此 Stack 示例配置不能泄漏到其他示例。配置行按 NumericUpDown 的行高垂直居中各子组,
`BadgeText="v6.1.9"` 交由 ShowCaseItem 的既有 RibbonBadge 模板呈现,不创建专用角标视觉。
实现文档不逐行解释私有方法。若某个私有算法成为稳定维护入口,应在本节补充算法不变量,而不是把代码复述为说明书。
## 8. 资源、性能与 AOT 边界
资源和 AOT 约束:
- 不通过运行时反射扫描 public API、Token 或 Gallery 示例数据。
- 不把可静态声明的模板结构迁移到 C# 动态创建。
- 异步加载、上传、弹层和窗口生命周期必须能取消或释放。
- 缓存对象必须与控件、窗口、弹层或数据 owner 生命周期一致。
- Source generator 生成文件不手工编辑;需要修改时改输入源或 generator。
性能边界:
- manager、ItemsControl 与 card collection 在 Stack 切换和重套模板之间保持稳定。
- manager 已位于反馈层末尾时,宿主激活只做一次 O(1) 尾项比较;切换 manager 时只对直接 manager 子项执行一次索引查找
和一次 collection `Move`。不得扫描 card、分配临时集合、创建 timer/任务/订阅或保存额外 manager 引用。
- `MeasureOverride` / `ArrangeOverride` 不允许 LINQ、临时数组、闭包或逐帧 transform 创建。
- 稳态布局必须复用缓存 transform;目标变化最多创建一个 transform 并交给 transition 插值,不增加逐帧 managed 回调。
- 没有有限时长活动项时 scheduler 不保持活动 timer;没有 Notification 进度需求时只安排最近 deadline。
- Gallery Stack 示例的零时长消息不创建 scheduler entry、异步循环、取消令牌或逐项 timer;两个示例 manager 都只在
第一次实际操作时创建,并在页面 detach 时释放。
- 折叠背板由 AXAML 静态创建,不随 show 次数增加视觉对象。
- 性能修改必须使用同一 Message 场景比较基线与优化后的 mean、median、P95,并证明主要指标无可测量回退。
## 9. 维护不变量
维护 Message 时不得破坏:
- Public API、默认值、事件顺序和 Gallery 可观察行为。
- Template part 名称、ControlTheme key、伪类和资源 key。
- 旧 template part、事件订阅、Popup/Flyout/Window host 和 collection view 的释放路径。
- Light/Dark、Browser/Desktop 和不同 SizeType 下的主题一致性。
- 控件文档、源码 public surface、Token 类型或生成数据与源码契约的一致性。
- `IsClosing` / `IsClosed` public 状态不得与 internal `MotionExecutionState` 合并;重复调度不得创建并行退出动效。
- `MaxItems <= 0` 必须保持无限语义;Stack 不能通过提前关闭旧项模拟折叠。
- Stack 不能隐式把有限时长改为永久展示,也不能在未 hover 时暂停 scheduler;Gallery 的永久 Stack 示例必须通过
`Expiration=TimeSpan.Zero` 显式表达。
- template detach、rehost、DestroyAll、用户回调异常与 dispose 均必须释放 scheduler entry、presenter、host 订阅、card owner 与 delegate。
## 10. 测试与验证
推荐验证:
- `CloseMotionExecutionTests` 验证关闭属性变化与模板重套用同时请求退出动效时只启动一次 motion,并只提交一次
`IsClosed=true`。
- Feedback motion 测试验证 64 DIP 方向、scale 恒等、完整时长 opacity 插值、Ant ease-in-out 参数以及取消后旧 actor
不提交 completion。
- `FeedbackCardMotionCoordinatorTests` 使用真实 actor 与可控时钟验证六种 Position 的入场中关闭,旧入场清理后退出目标
保持到完整时长结束,且关闭只完成一次。
- `FeedbackStackPanelTests` 覆盖阈值边界、最新项顺序、Message 背板、hover gap、运行时配置和 Top/Bottom 几何。
- `MessageStackAnimationTests` 用可控动画时钟验证背板在卡片收拢期间淡入、hover 反转连续、内容宽度变化、motion / Stack
开关,以及空队列和展开队列不被透明背板扩高。
- `FeedbackLifetimeSchedulerTests` 以可控时钟覆盖最近 deadline、剩余时长、普通/Stack 暂停、空闲停表及关闭回调耗时后的重调度;
`WeakReference` 用例验证 scheduler 保持存活时,最后一批已到期或已在关闭的项也能回收。
- manager 测试覆盖 Stack 开启但未 hover 的有限时长消息保持计时登记且未暂停,以及零时长 Stack 消息不创建 scheduler。
- `FeedbackManagerStackTests` 覆盖无动画批量淘汰的最旧优先顺序,以及关闭回调中的 Dispose、嵌套 DestroyAll 和新增消息;
`WeakReference` 用例验证 manager dispose 后的 manager、presenter、card 与回调 owner 对象图释放;跨 manager 激活还必须
覆盖 A/B/A 顺序、Notification 混合宿主、栈顶无操作快路径、单次 collection Move 和零 attach/detach。
- Gallery 行为测试覆盖普通与 Stack 示例 manager 隔离、Stack 控件初值、运行时切换、交替文案、局部 DestroyAll 和 detach 释放。
- 纯文档改动运行 `git diff --check` 并检查相对链接。
- 控件 API 或行为变更运行对应 `tests/AtomUI.Desktop.Controls.Tests` 或专用包测试。
- DataGrid 相关变更运行 `tests/AtomUI.Desktop.Controls.DataGrid.Tests`。
- Gallery 示例或源码片段变更运行 `tests/AtomUIGallery.Tests`。
- AOT、生成器或动态数据路径变更按 Gallery NativeAOT 发布流程验证。
## 11. Semantic Part 接入
Message 公开两个 owner 的 Semantic Part:`MessageCard`(notice)与 `WindowMessageManager`(list)。声明集中在与
owner 同目录的 `MessageCard.SemanticParts.cs` 与 `WindowMessageManager.SemanticParts.cs`,owner 本体保持 partial。
公共契约与逐 Part 说明见 [Message Semantic Part 契约](semantic-part.md)。
### 11.1 Descriptor 与 marker
| Owner | Part | SelectorClass | SelectorRoute | ContractType | 声明位置 |
| --- | --- | --- | --- | --- | --- |
| `MessageCard` | `wrapper` | `semantic-wrapper` | `/template/ .semantic-wrapper` | `DockPanel` | `MessageCardTheme.axaml` 内 `DockPanel#PART_HeaderContainer` |
| `MessageCard` | `icon` | `semantic-icon` | `/template/ .semantic-icon` | `IconPresenter`(`AtomUI.Controls.IconPresenter`) | `MessageCardTheme.axaml` 内 `IconPresenter#PART_IconContent` |
| `MessageCard` | `title` | `semantic-title` | `/template/ .semantic-title` | `Avalonia.Controls.SelectableTextBlock` | `MessageCardTheme.axaml` 内 `SelectableTextBlock#PART_Message` |
| `WindowMessageManager` | `listContent` | `semantic-list-content` | `/template/ .semantic-list-content` | `ItemsControl` | `WindowMessageManagerTheme.axaml` 内 `FeedbackStackPresenter#PART_Items` |
`root` 由生成器隐式加入 descriptor,不要求 `.semantic-root` marker。四个非 root Part 都是 owner 自身
`ControlTheme` 的静态模板节点,因此显式声明 `RuntimeCreated=false`,生成器按 owner 主题资产(derived/本体
Theme.axaml)做 marker 数量与 `ContractType` 兼容性静态校验,无需跨模板豁免。
marker 使用模板静态 class 属性语法书写,值是静态 `True`:
```xml
<DockPanel Name="PART_HeaderContainer" Classes.semantic-wrapper="True" LastChildFill="True">
```
`title` 的节点类型必须与 `ContractType` 精确一致:主题中无前缀的 `SelectableTextBlock` 解析为
`Avalonia.Controls.SelectableTextBlock`;AtomUI 的 `atom:SelectableTextBlock` 是另一类型,二者不可混用,否则
生成器 `ATOMUIGEN033` 类型不兼容校验会拒绝该 Part。
### 11.2 运行时创建路径
Message 没有任何运行时创建的 Part:
- `MessageCard` 由 `WindowMessageManager.Show` 以 `new MessageCard { ... }` 创建,但其模板由自身
`ControlTheme` 实例化,三个卡片级 marker 都随模板静态就位,root 为隐式 owner。
- `WindowMessageManager` 有两个 public 构造:`WindowMessageManager(TopLevel? host)` 与无参 `WindowMessageManager()`。
传入 host 时经 `InstallFromTopLevel` 挂到 TopLevel 反馈层并投影安全区外边距;传 `null` 或使用无参构造时控件不安装
到任何层,作为普通可放置控件由调用方放进自己的视觉树(上游把 list 改为内联容器的等价物),两种路径都在模板应用后
取得 `PART_Items`;`listContent` marker 静态声明在 manager 模板内。
卡片实例本身不携带任何 semantic class;`Show(IMessage, string[]? classes)` 的 `classes` 参数只追加用户业务 class,不参与
Semantic Part 身份。因此卡片增删、`MaxItems` 清理与超时/手动关闭只影响实例数量,不改变 marker 结构。
### 11.3 生命周期与布局
- MessageCard 模板重套用时重新取得 actor,旧执行取消、旧模板节点释放;marker 随新模板重建。
- manager 的稳定卡片集合与 scheduler 不依赖 presenter 实例。持续 detach 的延后关闭和 `Dispose` 的最终清理
遵循第 5 节生命周期规则;同轮 host 迁移不清空集合。
- `wrapper` 是 DockPanel,icon 停靠左侧、title 填充,间距来自 `MessageIconMargin`。
- `listContent` 实际为 `FeedbackStackPresenter`,公共 `ContractType` 为 `ItemsControl`。其内部 panel 维护项间距和
堆叠布局;应用通过生成 Style 定制尺寸、对齐,不能访问内部间距 API。
- `Grid#PART_StackHost.Margin` 绑定 owner `Padding`,默认取 `MessageTopMargin`。六种 Position 在模板应用和属性
修改时更新,StackHost 按方位紧贴队列,两个静态背板不增加 host 的 extent 或 hover 命中范围。
### 11.4 性能与 AOT
- 四个 marker 在模板初始化时各执行一次 `Classes.Set`,不建立 Binding 或持久订阅。
- 默认主题不消费 `.semantic-*`;未声明用户 Semantic Style 时不创建独立 selector activator。
- descriptor、`StyleType`、Part 常量与 XML namespace 映射全部由生成器静态产生,不引入反射、程序集扫描或运行时
AXAML 解析。