mirror of
https://github.com/AtomUI/AtomUI.git
synced 2026-10-28 23:08:36 +08:00
统一文档目录的入口文件名为 `overview.md`,取代原先的 `index.md`。单 Control 目录本来就用 `overview.md`,此前只有目录级入口用 `index.md`,两套命名并存;现在全库一致。 改名覆盖 42 个入口文件:`docs/` 根、一级分类目录、Architecture 分组与跨模块系统目录、Module 目录、Control 平台与分类目录、Guide 与 Reference 子目录。`docs/AI/generated/index.md` 是手工 维护的导航页,一并改名。 生成产物 `docs/AI/generated/llms/**/index-cn.md` 保持原名,它属于 LLMs Generator 的输出契约。 同步更新 28 个正式文档与 `AGENTS.md` 中的入站链接,以及两处目录树示意图。改名后全部相对链接 可解析,LLMs Generator `verify` 通过(78 控件 / 159 文件),单 Control 四件套文件名契约未变。 新增 `docs/engineering/contributing/documentation-structure-guidelines.md` 承载入口命名、文档 拆分标准、事实所有权和链接规范。此前该规则只写在带日期的设计过程记录里,不适合作为长期来源。 `docs/superpowers/specs/2026-08-10-documentation-information-architecture-design.md` 保留原文 并加取代说明,其余结论继续有效。
570 lines
28 KiB
Markdown
570 lines
28 KiB
Markdown
# GalleryBase 源码展示模块架构设计
|
||
|
||
本文档定义 GalleryBase 中 ShowCase 源码展示模块的最新设计状态。该模块用于在 Gallery 示例卡片中打开当前示例的源码视图,让用户在不离开 ShowCase 页面、不打断示例运行状态的前提下查看示例 AXAML 和相关代码片段。
|
||
|
||
源码展示模块由 GalleryBase 运行时控件、产品侧源码片段 Provider 和 GalleryBase 专用 Source Generator 三部分组成。GalleryBase 只提供产品中立的 UI、交互和运行时契约;具体产品 Gallery 负责提供源码片段数据;Source Generator 负责把产品 ShowCase 源文件转换成 AOT 友好的强类型 catalog。
|
||
|
||
## 1. 模块定位
|
||
|
||
源码展示模块是 `AtomUI.Toolkits.GalleryBase` 的 ShowCase 辅助能力,服务于 `ShowCaseItem` 示例浏览场景。
|
||
|
||
模块职责:
|
||
|
||
- 在 `ShowCaseItem` 上提供查看源码入口。
|
||
- 使用共享 Drawer 展示源码,不把大段代码嵌入示例卡片内部。
|
||
- 使用多标签只读编辑器展示 AXAML、code-behind、ViewModel 等源码片段。
|
||
- 延迟加载源码片段和编辑器实例,避免增加 ShowCase 首屏创建成本。
|
||
- 通过产品侧 Provider 获取源码片段,不在 GalleryBase 中引用具体产品页面。
|
||
- 通过 Source Generator 生成产品侧源码片段 catalog,并按 `ShowCaseItem` 做源码切片,避免运行时文件扫描和反射扫描。
|
||
|
||
模块非职责:
|
||
|
||
- 不编辑源码,不提供保存、格式化、诊断、跳转定义或智能提示能力。
|
||
- 不替代控件文档、API reference 或 Design Token 文档。
|
||
- 不展示整个源码文件,除非该文件本身就是当前示例的最小片段。
|
||
- 不为了让片段可独立编译而引入整页、整文件或无关基础设施代码;源码片段目标是阅读完整,不是生成可复制即编译的小项目。
|
||
- 不从运行时文件系统读取 `ShowCases/**/*.axaml`、`.axaml.cs` 或 ViewModel `.cs`。
|
||
- 不在 GalleryBase 内写入 `AtomUIGallery.ShowCases.*` 或任何产品命名空间。
|
||
|
||
## 2. 设计语言
|
||
|
||
源码展示是 ShowCase 示例的辅助阅读层,不是主要内容层。示例卡片继续优先展示真实控件运行效果;源码入口作为卡片工具动作出现。
|
||
|
||
交互语言:
|
||
|
||
- 入口轻量:`ShowCaseItem` 只显示 icon-only 查看源码按钮。
|
||
- 阅读聚焦:源码内容在 Drawer 中展示,避免长代码撑开 Masonry 卡片。
|
||
- 上下文保留:打开 Drawer 不销毁当前示例、不切换页面、不重建 ShowCase 内容。
|
||
- 延迟成本:只有用户点击源码按钮后才解析片段、创建 Drawer 内容和代码编辑器。
|
||
- 多文件表达:同一示例可包含 AXAML、Code-behind、ViewModel 等片段标签;每个标签只展示当前 `ShowCaseItem` 的共性上下文和直接相关代码。
|
||
|
||
Drawer 而不是卡片内展开,是因为 Gallery 示例代码量差异很大。Drawer 提供稳定宽度、内部滚动、多标签和复制工具条,能承载 DataGrid、Form、TreeView 等复杂示例的代码阅读需求。
|
||
|
||
## 3. 模块分层
|
||
|
||
源码展示模块采用五层结构:
|
||
|
||
| 层级 | 所属项目 | 职责 |
|
||
|---|---|---|
|
||
| 入口层 | `AtomUI.Toolkits.GalleryBase` | `ShowCaseItem` 展示源码按钮,发出打开源码请求 |
|
||
| 宿主层 | `AtomUI.Toolkits.GalleryBase` | `GalleryShowCaseCodeDrawerHost` 承载单例 Drawer,接收打开请求 |
|
||
| 查看层 | `AtomUI.Toolkits.GalleryBase` | `GalleryShowCaseCodeDrawer` 和 `GalleryCodeViewer` 展示多标签只读源码 |
|
||
| 数据层 | 产品 Gallery 项目 | 实现 `IShowCaseCodeSnippetProvider`,从生成 catalog 查询片段 |
|
||
| 生成层 | `AtomUI.Toolkits.GalleryBase.Generator` | 扫描产品 AdditionalFiles,按 `ShowCaseItem` 提取 AXAML 与相关 C# 成员,生成 `ShowCaseCodeSnippetCatalog.g.cs` |
|
||
|
||
依赖方向:
|
||
|
||
```text
|
||
AtomUIGallery
|
||
-> AtomUI.Toolkits.GalleryBase
|
||
-> AtomUI.Toolkits.GalleryBase.Generator (Analyzer)
|
||
|
||
AtomUI.Toolkits.GalleryBase
|
||
-> AvaloniaEdit / TextMate integration
|
||
-> AtomUI.Desktop.Controls
|
||
|
||
AtomUI.Toolkits.GalleryBase.Generator
|
||
-> Microsoft.CodeAnalysis
|
||
```
|
||
|
||
GalleryBase 运行时不能依赖 generator 项目。Generator 只在编译期作为 Analyzer 被产品 Gallery 引用。
|
||
|
||
## 4. 运行时契约模型
|
||
|
||
源码展示运行时契约由 key、snippet、snippet group 和 provider 组成。
|
||
|
||
### 4.1 Snippet Key
|
||
|
||
`ShowCaseCodeSnippetKey` 标识一个 ShowCase 示例:
|
||
|
||
```csharp
|
||
public readonly record struct ShowCaseCodeSnippetKey(
|
||
string ViewTypeName,
|
||
string PanelKey,
|
||
int ItemIndex,
|
||
string? SourceKey = null);
|
||
```
|
||
|
||
字段语义:
|
||
|
||
| 字段 | 语义 |
|
||
|---|---|
|
||
| `ViewTypeName` | 当前 ShowCase 页面类型全名,例如 `AtomUIGallery.ShowCases.Button.ButtonShowCase` |
|
||
| `PanelKey` | 当前 `ShowCasePanel` 的稳定 key,使用 `Name`,例如 `ExamplesContent` |
|
||
| `ItemIndex` | 当前 panel 中 `ShowCaseItem` 的声明顺序索引 |
|
||
| `SourceKey` | 可选手工 key,用于复杂页面显式绑定特定源码片段 |
|
||
|
||
默认 key 使用 `ViewTypeName + PanelKey + ItemIndex`。`SourceKey` 只作为特殊页面的稳定覆盖项,不要求每个 `ShowCaseItem` 手写。参与默认源码匹配的 `ShowCasePanel` 必须有稳定 `Name`;没有 `Name` 的 panel 只能通过 `SourceKey` 显式绑定源码片段。
|
||
|
||
### 4.2 Snippet Group
|
||
|
||
一个示例返回一个 `ShowCaseCodeSnippetGroup`:
|
||
|
||
```csharp
|
||
public sealed record ShowCaseCodeSnippetGroup(
|
||
string Title,
|
||
IReadOnlyList<ShowCaseCodeSnippet> Snippets);
|
||
```
|
||
|
||
`Title` 用于 Drawer 标题。默认使用 `ShowCaseItem.Title` 的当前文本值;Provider 返回的标题只作为 fallback。
|
||
|
||
### 4.3 Snippet
|
||
|
||
每个标签页对应一个 `ShowCaseCodeSnippet`:
|
||
|
||
```csharp
|
||
public sealed record ShowCaseCodeSnippet(
|
||
string TabTitle,
|
||
string Language,
|
||
string Text,
|
||
string? SourceFilePath,
|
||
int StartLine,
|
||
int EndLine);
|
||
```
|
||
|
||
字段语义:
|
||
|
||
| 字段 | 语义 |
|
||
|---|---|
|
||
| `TabTitle` | 标签标题,例如 `AXAML`、`Code-behind`、`ViewModel` |
|
||
| `Language` | 语法高亮语言,例如 `axaml`、`xml`、`csharp` |
|
||
| `Text` | 展示的源码片段 |
|
||
| `SourceFilePath` | repo 相对路径,用于调试和测试 |
|
||
| `StartLine` / `EndLine` | 源文件行号范围 |
|
||
|
||
Generator 必须默认生成 `AXAML` 标签。`Code-behind` 和 `ViewModel` 标签由 item 级静态分析生成:只有当当前 `ShowCaseItem` 的 AXAML 直接引用事件处理器、Binding、`x:DataType` 或相关命名对象,并且 generator 能从同组 `.axaml.cs` / ViewModel `.cs` 中切出相关成员时才生成。没有相关内容时不生成空标签。Generator 不能自动展示整个 `.axaml.cs` 或 ViewModel 文件。
|
||
|
||
### 4.4 Provider
|
||
|
||
GalleryBase 定义产品侧 provider 契约:
|
||
|
||
```csharp
|
||
public interface IShowCaseCodeSnippetProvider
|
||
{
|
||
bool TryGetSnippetGroup(
|
||
ShowCaseCodeSnippetKey key,
|
||
out ShowCaseCodeSnippetGroup group);
|
||
}
|
||
```
|
||
|
||
Provider 注册在 GalleryBase 配置中:
|
||
|
||
```csharp
|
||
public sealed class GallerySourceCodeDisplayOptions
|
||
{
|
||
public bool IsEnabled { get; set; }
|
||
public IShowCaseCodeSnippetProvider? SnippetProvider { get; set; }
|
||
}
|
||
```
|
||
|
||
该 options 作为 `GalleryBaseOptions` 的子配置存在。未设置 Provider 或 `IsEnabled=false` 时,`ShowCaseItem` 不显示源码按钮。
|
||
|
||
## 5. 交互与状态模型
|
||
|
||
源码展示的核心状态属于 Drawer Host,不属于每个 `ShowCaseItem`。
|
||
|
||
状态:
|
||
|
||
| 状态 | Owner | 说明 |
|
||
|---|---|---|
|
||
| `IsCodeActionVisible` | `ShowCaseItem` | 当前 item 是否显示查看源码按钮 |
|
||
| `CurrentKey` | Drawer Host | 当前打开的源码 key |
|
||
| `IsOpen` | Drawer Host | Drawer 是否打开 |
|
||
| `IsLoading` | Drawer | 是否正在解析片段 |
|
||
| `SelectedSnippetIndex` | Drawer | 当前标签页 |
|
||
| `LoadedEditors` | Drawer | 已创建的 editor 缓存 |
|
||
|
||
打开流程:
|
||
|
||
```text
|
||
user clicks ShowCaseItem code button
|
||
-> ShowCaseItem creates ShowCaseCodeSnippetKey
|
||
-> request bubbles to nearest GalleryShowCaseCodeDrawerHost
|
||
-> host opens Drawer with title and loading state
|
||
-> host asks provider for ShowCaseCodeSnippetGroup
|
||
-> Drawer renders tabs
|
||
-> first tab creates GalleryCodeViewer
|
||
```
|
||
|
||
关闭流程:
|
||
|
||
```text
|
||
user closes Drawer
|
||
-> Drawer clears current group
|
||
-> disposes created GalleryCodeViewer instances
|
||
-> releases TextMate installations
|
||
-> keeps ShowCaseItem demo content untouched
|
||
```
|
||
|
||
切换标签流程:
|
||
|
||
```text
|
||
user selects tab
|
||
-> Drawer resolves snippet
|
||
-> creates GalleryCodeViewer for that tab if needed
|
||
-> existing editors stay cached while Drawer remains open
|
||
```
|
||
|
||
错误状态:
|
||
|
||
- Provider 查不到片段:Drawer 显示空状态,并提供当前 key 的调试信息。
|
||
- Snippet group 无标签:Drawer 显示空状态。
|
||
- 编辑器初始化失败:Drawer 显示纯文本 fallback,不吞掉错误上下文。
|
||
|
||
## 6. 视觉与主题模型
|
||
|
||
源码展示使用 AtomUI Drawer 承载,使用 AvaloniaEdit 作为只读代码查看器。Drawer 位于 Gallery Shell 级浮层,不参与 `ShowCasePanel` Masonry 布局。`GalleryShowCaseCodeDrawerHost` 必须包住 Shell 根布局,让 Drawer mask 覆盖导航区和内容区;不要只挂在右侧内容列上。
|
||
|
||
视觉结构:
|
||
|
||
```text
|
||
GalleryShowCaseCodeDrawerHost
|
||
ContentPresenter(page content)
|
||
Drawer
|
||
Header
|
||
Title
|
||
Copy current tab
|
||
Copy all
|
||
Close
|
||
Body
|
||
TabStrip
|
||
ContentControl
|
||
GalleryCodeViewer
|
||
```
|
||
|
||
主题规则:
|
||
|
||
- Drawer 背景、边框、标题、工具条间距使用 AtomUI SharedToken 和 GalleryBase Token。
|
||
- `GalleryCodeViewer` 使用等宽字体,默认字体族为 `Cascadia Code, Consolas, Menlo, Monospace`。
|
||
- 代码编辑器 TextMate theme 必须跟随当前主题模式:
|
||
- light 使用 TextMate `LightPlus`
|
||
- dark 使用 TextMate `DarkPlus`
|
||
- 当 `ActualThemeVariant` 是 Avalonia 内置 `ThemeVariant.Light` 或 `ThemeVariant.Dark` 时,优先按该值选择 TextMate theme。
|
||
- 当 `ActualThemeVariant` 是 AtomUI 生成的自定义变体,例如 `DaybreakBlue-Dark` 或 `DaybreakBlue-Dark-Compact`,不能用 `ActualThemeVariant == ThemeVariant.Dark` 判断暗色。`GalleryCodeViewer` 必须通过 `Application.IsDarkThemeMode()` 扩展方法读取当前应用暗色模式结果。
|
||
- `GalleryCodeViewer` 暴露 `LightSyntaxTheme` 和 `DarkSyntaxTheme`,默认值分别为 `LightPlus` 和 `DarkPlus`。产品 Gallery 可以在不改 GalleryBase 源码的前提下替换为 `VisualStudioLight`、`VisualStudioDark`、`AtomOneDark`、`Dracula` 等 TextMate 内置主题。
|
||
- `GalleryCodeViewer` 只安装一次 TextMate。主题变化时调用 `TextMate.Installation.SetTheme(...)` 切换主题,不销毁 `TextEditor`、不重建 document、不中断当前滚动位置。
|
||
- `Language` 变化只更新 grammar,`ActualThemeVariant` 或 syntax theme 属性变化只更新 TextMate theme,两条路径不能互相重建。
|
||
- 已打开的 Drawer 编辑器必须监听主题变体变化作为同步信号。纯 Avalonia 主题切换可由 `Application.ActualThemeVariantChanged` 覆盖;AtomUI 主题切换必须订阅 `IThemeManager.ThemeVariantProperty`,因为它在激活主题按算法更新后触发。不要直接订阅 `IThemeManager.IsDarkThemeModeProperty`,否则会早于主题激活流程,读到旧的 `ActivatedTheme`。
|
||
- 行号默认显示。
|
||
- 编辑器只读,允许文本选择和复制。
|
||
- 长代码在 Drawer 内部滚动,不让页面滚动条承载代码阅读。
|
||
|
||
源码展示模块可以新增 GalleryBase 专属 Token,例如:
|
||
|
||
| Token | 用途 |
|
||
|---|---|
|
||
| `CodeDrawerWidth` | Drawer 默认宽度 |
|
||
| `CodeToolbarPadding` | Drawer 内工具条 padding |
|
||
| `CodeViewerMinHeight` | 代码查看器最小高度 |
|
||
| `CodeViewerMaxHeight` | 代码查看器最大高度 |
|
||
| `CodeViewerFontSize` | 代码字体大小 |
|
||
|
||
Token 是否独立成类取决于实现时是否已有合适的 GalleryBase Token 承载。新增 Token 必须进入 GalleryBase theme provider 和生成资源。
|
||
|
||
## 7. 编辑器模型
|
||
|
||
源码展示选择 AvaloniaEdit 作为代码查看器,并通过 `AvaloniaEdit.TextMate` 接入 TextMate 语法高亮。
|
||
|
||
`GalleryCodeViewer` 是 GalleryBase 对 AvaloniaEdit 的封装:
|
||
|
||
```csharp
|
||
public class GalleryCodeViewer : TemplatedControl
|
||
{
|
||
public string? CodeText { get; set; }
|
||
public string Language { get; set; }
|
||
public bool ShowLineNumbers { get; set; }
|
||
}
|
||
```
|
||
|
||
封装职责:
|
||
|
||
- 创建和配置 `TextEditor`。
|
||
- 安装 TextMate registry。
|
||
- 根据 `Language` 设置 grammar。
|
||
- 同步 AtomUI 主题到 TextMate theme。
|
||
- 在 `Application.ActualThemeVariantChanged` 和 `IThemeManager.ThemeVariantProperty` 变化时重新应用 syntax theme;暗色判断读取当前激活主题的算法结果,避免主题状态来源分叉。
|
||
- 设置只读、行号、字体、换行和滚动策略。
|
||
- dispose 时释放 TextMate installation,取消事件订阅,清空 document 引用。
|
||
|
||
`ShowCaseItem` 和 Drawer 不直接操作 AvaloniaEdit API,避免第三方编辑器细节扩散到 ShowCase 控件。
|
||
|
||
## 8. 编译期生成模型
|
||
|
||
产品 Gallery 使用 `AtomUI.Toolkits.GalleryBase.Generator` 生成源码片段 catalog。Generator 以 Analyzer 方式被产品 Gallery 引用,不作为运行时依赖。
|
||
|
||
项目归属:
|
||
|
||
| 项目 | 归属 | 说明 |
|
||
|---|---|---|
|
||
| `src/AtomUI.Toolkits.GalleryBase` | 运行时 | 定义源码查看 UI、options、key、snippet、provider 契约 |
|
||
| `src/AtomUI.Toolkits.GalleryBase.Generator` | 编译期 | 读取产品 Gallery 的源码镜像 `AdditionalFiles` 并生成 catalog |
|
||
| `controlgallery/AtomUIGallery` | 产品侧 | 准备源码镜像输入、实现 provider、注册 options |
|
||
| `tests/AtomUI.Toolkits.GalleryBase.Generator.Tests` | 编译期测试 | 覆盖 AXAML 提取、诊断和生成物稳定性 |
|
||
|
||
Generator 项目使用 `netstandard2.0`,设置 `IsRoslynComponent=true`,并通过 `TreatAsLocalProperty` 隔离 `PublishAot`、`PublishTrimmed`、`RuntimeIdentifier` 等发布属性。产品项目引用 generator 时使用 Analyzer 引用,不把 generator assembly 带入运行时发布产物。
|
||
|
||
输入:
|
||
|
||
```text
|
||
controlgallery/AtomUIGallery/ShowCases/**/*.axaml
|
||
controlgallery/AtomUIGallery/ShowCases/**/*.axaml.cs
|
||
controlgallery/AtomUIGallery/ShowCases/**/ViewModels/*.cs
|
||
```
|
||
|
||
产品项目不要把真实 ShowCase AXAML、code-behind 和 ViewModel 源文件直接注册为 `AdditionalFiles`。真实 `.axaml` / `.cs` 文件已经有 Avalonia、Compile 和 IDE 语义角色,再叠加 `AdditionalFiles` 会污染 Rider 等 IDE 的设计时模型。
|
||
|
||
产品项目应在编译前把这些源文件复制到 `$(IntermediateOutputPath)` 下的源码镜像文件,例如 `.gallerysource`,然后只把镜像文件注册为 `AdditionalFiles`,并通过 `GallerySourceOriginalPath` metadata 传递真实源文件路径。Generator 使用该 metadata 做路径分类、同组文件匹配、诊断位置和 `SourceFilePath` 输出;镜像文件路径只作为 Roslyn `AdditionalText` 的物理载体。
|
||
|
||
`GallerySourceOriginalPath` 必须通过 `CompilerVisibleItemMetadata` 暴露给 Roslyn analyzer config,并且准备镜像 `AdditionalFiles` 的 MSBuild target 必须早于 `GenerateMSBuildEditorConfigFileShouldRun` 执行。只在 `BeforeTargets="CoreCompile"` 中追加 metadata 不够,因为此时 `GeneratedMSBuildEditorConfig` 可能已经生成,Generator 会读到空 metadata,进而把 `.gallerysource` 镜像路径当成真实路径并忽略 C# 镜像文件。
|
||
|
||
Generator 只读取 `AdditionalFiles`,不通过运行时文件系统、MSBuild project model 或 assembly metadata 反向扫描源码。
|
||
|
||
### 8.1 Item 级源码切片规则
|
||
|
||
Generator 以 `ShowCaseItem` 为切片单位,而不是以整个 ShowCase 页面为单位。每个 item 的源码片段必须由 AXAML 入口和 C# 依赖闭包共同决定:
|
||
|
||
- 读取 `x:Class` 得到 `ViewTypeName`。
|
||
- 读取 `gallery:ShowCasePanel`,使用 `Name` 作为 `PanelKey`。
|
||
- 按同一 panel 内声明顺序为 `gallery:ShowCaseItem` 分配 `ItemIndex`。
|
||
- 对迁移后的 item,提取 `ShowCaseItem.DeferredContentTemplate > DataTemplate` 内部节点。
|
||
- 对未迁移 item,提取 `ShowCaseItem` 的直接内容。
|
||
- 保留 AXAML 源文件相对路径和行号范围。
|
||
- 默认生成 `Language="axaml"`、`TabTitle="AXAML"`。
|
||
- 从 item AXAML 中收集 `x:DataType`、`{Binding ...}` 根路径、事件属性、`Name` / `x:Name`。
|
||
- 事件属性只在 code-behind 中存在同名方法时才视为事件处理器;普通字符串属性不进入 C# 依赖集合。
|
||
- 事件属性识别必须覆盖 Avalonia 常见事件入口,例如 `Click`、`Loaded`、`Unloaded`、`AttachedToVisualTree`、`DetachedFromVisualTree`、键盘事件、指针事件和 `Changed` / `Opened` / `Closed` 等后缀事件。
|
||
- Binding 根路径用于选择 ViewModel 成员,例如 `SelectedColorPreset.Name` 选择 `SelectedColorPreset`,不把 `Name` 当成独立 ViewModel 依赖。
|
||
- `Name` / `x:Name` 只作为 code-behind 依赖辅助输入,用于识别事件处理器或 helper 中访问的示例控件,不单独生成片段。
|
||
|
||
### 8.2 同组文件定位
|
||
|
||
ShowCase 源文件通常按以下结构组织:
|
||
|
||
```text
|
||
ShowCases/<Category>/<Control>/
|
||
Views/<Control>ShowCase.axaml
|
||
Views/<Control>ShowCase.axaml.cs
|
||
ViewModels/<Control>ViewModel.cs
|
||
```
|
||
|
||
Generator 按以下顺序定位同组文件:
|
||
|
||
1. code-behind 优先匹配 AXAML 同路径同名 `.axaml.cs`。
|
||
2. 如果同路径匹配失败,通过 `x:Class` 的类型名和 namespace 在 AdditionalFiles 中兜底匹配 partial class。
|
||
3. ViewModel 优先读取 item 或上层 `DataTemplate x:DataType`。
|
||
4. 如果 `x:DataType` 缺失,通过 code-behind 基类 `GalleryReactiveUserControl<TViewModel>` 推断。
|
||
5. 如果泛型推断失败,再按同组 `ViewModels/*ViewModel.cs` 约定兜底;兜底只能在唯一匹配时生效。
|
||
|
||
找不到 code-behind 或 ViewModel 不阻断 AXAML 片段生成。Generator 只跳过对应 C# 标签。
|
||
|
||
### 8.3 C# 成员切片规则
|
||
|
||
Generator 使用 Roslyn C# syntax tree 对 code-behind 和 ViewModel 做成员级切片。切片目标是“当前示例阅读完整”,不是“单片段可独立编译”。
|
||
|
||
Code-behind 片段包括:
|
||
|
||
- 当前 item AXAML 直接引用的事件处理器方法。
|
||
- 事件处理器调用的同类型 helper 方法。
|
||
- 这些方法访问的字段、属性和嵌套类型。
|
||
- 这些方法创建或引用的同文件局部记录类型、枚举或小型 DTO。
|
||
- 片段需要的 `using`、alias using、namespace 和包含类型声明。
|
||
|
||
ViewModel 片段包括:
|
||
|
||
- 当前 item Binding 根路径对应的 public / internal 属性。
|
||
- 属性使用的 backing field。
|
||
- 属性 setter 或 getter 调用的同类型 helper 方法。
|
||
- code-behind 事件转发到 ViewModel 的 public / internal 方法。
|
||
- 相关属性或方法依赖的同文件 record、enum、小型 DTO 和构造初始化。
|
||
- 构造函数中只与已选成员相关的初始化语句;如果无法安全拆分构造函数,保留完整构造函数但不引入只服务旧 API/Token metadata 的无关 lazy data 方法。
|
||
- 片段需要的 `using`、namespace 和包含类型声明。
|
||
|
||
以下内容默认不进入当前 item 的 C# 片段:
|
||
|
||
- 只服务旧 API/Token metadata 的初始化方法。
|
||
- 其他 `ShowCaseItem` 专用事件处理器、Binding 属性和 helper。
|
||
- 仅服务页面 Shell、Scenario tabs、路由、导航、语言刷新或 DataGrid lazy loading 的成员,除非当前 item AXAML 直接引用。
|
||
- 整个 `.axaml.cs` 或整个 ViewModel 文件。
|
||
|
||
### 8.4 依赖闭包与降级规则
|
||
|
||
成员切片采用有界递归:
|
||
|
||
```text
|
||
AXAML references
|
||
-> direct C# members
|
||
-> same-file member references
|
||
-> backing fields / helper methods / small related types
|
||
-> stop at framework, external assembly, unrelated ShowCase members
|
||
```
|
||
|
||
递归只跨同一 code-behind 文件和同一 ViewModel 文件,不跨产品项目中任意 C# 文件扩散。跨文件类型只保留引用表达式,不自动展开目标源码;后续如确实需要,可通过显式 SourceKey 或 include 规则扩展。
|
||
|
||
当 generator 无法可靠判断某个成员是否相关时,按以下顺序降级:
|
||
|
||
1. 保留当前 item 的 AXAML。
|
||
2. 保留能确定直接相关的 C# 成员。
|
||
3. 跳过不确定的 C# 标签,不生成空标签。
|
||
4. 在严格模式下报告诊断;默认模式不因 C# 切片不完整产生 warning 噪声。
|
||
|
||
严格模式由产品项目 MSBuild 属性控制,例如:
|
||
|
||
```xml
|
||
<GallerySourceCodeDisplayStrict>true</GallerySourceCodeDisplayStrict>
|
||
```
|
||
|
||
默认非严格模式适合日常开发和迁移期;严格模式适合 release 前检查源码展示完整性。
|
||
|
||
### 8.5 生成片段顺序
|
||
|
||
输出:
|
||
|
||
```csharp
|
||
internal static partial class ShowCaseCodeSnippetCatalog
|
||
{
|
||
public static bool TryGetSnippetGroup(
|
||
string viewTypeName,
|
||
string panelKey,
|
||
int itemIndex,
|
||
string? sourceKey,
|
||
out ShowCaseCodeSnippetGroup group)
|
||
{
|
||
// generated switch
|
||
}
|
||
}
|
||
```
|
||
|
||
每个 `ShowCaseCodeSnippetGroup.Snippets` 使用稳定顺序:
|
||
|
||
1. `AXAML`
|
||
2. `Code-behind`
|
||
3. `ViewModel`
|
||
|
||
没有内容的 C# 标签不生成。`AXAML` 是唯一默认必选标签;如果 item 没有可展示 AXAML 内容,则整个 group 不生成。
|
||
|
||
生成代码应使用显式 `switch` 或稳定表,不进行运行时文件系统读取、assembly scan 或 XML 解析。
|
||
|
||
源码片段数据只放在产品 Gallery 编译产物内的 generated C# catalog 中:
|
||
|
||
- 不生成运行时 JSON。
|
||
- 不生成单独源码资源包。
|
||
- 不把源码片段嵌入 GalleryBase runtime assembly。
|
||
- 不从安装目录、repo 工作区或 `avares://` 资源读取源码文件。
|
||
- `Text` 以 C# 字符串字面量或等价静态数据写入 generated code。
|
||
- `SourceFilePath` 使用 repo 相对路径,只用于调试、空状态和测试断言。
|
||
|
||
生成物命名应位于产品 Gallery namespace 下,例如 `AtomUIGallery.Generated.ShowCaseCodeSnippetCatalog`。GalleryBase 只认识 provider 契约,不认识生成物名称。
|
||
|
||
Generator 诊断遵循 [AtomUI 编译期诊断规范](../../engineering/development/compiler-diagnostics-guidelines.md),诊断 ID 使用 `ATOMUIGEN` 领域前缀并登记到诊断注册表。源码展示 generator 已使用 `ATOMUIGEN101` 覆盖默认源码匹配缺少 panel key 的场景,其他诊断按同一领域前缀扩展。
|
||
|
||
| 场景 | Severity | 行为 |
|
||
|---|---|---|
|
||
| AXAML 无法解析 | Warning | 跳过当前文件,提示文件路径和解析错误摘要 |
|
||
| 根节点缺少 `x:Class` | Warning | 跳过当前文件,提示补齐页面类型 |
|
||
| `ShowCasePanel` 缺少 `Name` 且没有显式 `SourceKey` | Warning | 跳过该 panel 的默认 key 生成 |
|
||
| `ShowCaseItem` 无法定位可展示内容 | Warning | 为该 item 不生成 snippet,并保留其他 item |
|
||
| 同一 key 产生重复 snippet group | Warning | 后出现的 key 不覆盖先出现的 key,提示重复来源 |
|
||
| 事件处理器名称在 code-behind 中不存在 | Hidden / Warning in strict mode | 默认只跳过该 handler;严格模式提示补齐 handler 或修正 AXAML |
|
||
| `x:DataType` 或推断 ViewModel 无法解析 | Hidden / Warning in strict mode | 默认只生成 AXAML 和可用 code-behind;严格模式提示补齐 `x:DataType` 或 ViewModel 约定 |
|
||
| Binding 根路径在 ViewModel 中找不到成员 | Hidden / Warning in strict mode | 默认跳过该 ViewModel 成员;严格模式提示 Binding 与 VM 不一致 |
|
||
| C# syntax tree 无法解析 | Warning | 跳过对应 C# 文件,保留其他文件片段 |
|
||
|
||
默认诊断策略偏低噪声:AXAML key 和 group 生成问题使用 Warning,因为它们会导致源码入口无法匹配;C# 切片不完整默认不阻断 ShowCase 源码展示,因为 AXAML 片段仍然可用。严格模式只在产品显式开启时提高 C# 关联问题的可见性。
|
||
|
||
## 9. 产品集成模型
|
||
|
||
具体产品 Gallery 负责接入生成 catalog:
|
||
|
||
```csharp
|
||
internal sealed class AtomUIGalleryShowCaseCodeSnippetProvider
|
||
: IShowCaseCodeSnippetProvider
|
||
{
|
||
public bool TryGetSnippetGroup(
|
||
ShowCaseCodeSnippetKey key,
|
||
out ShowCaseCodeSnippetGroup group)
|
||
{
|
||
return ShowCaseCodeSnippetCatalog.TryGetSnippetGroup(
|
||
key.ViewTypeName,
|
||
key.PanelKey,
|
||
key.ItemIndex,
|
||
key.SourceKey,
|
||
out group);
|
||
}
|
||
}
|
||
```
|
||
|
||
产品模块在 `UseGalleryBase(...)` 配置阶段注册:
|
||
|
||
```csharp
|
||
options.SourceCodeDisplay.IsEnabled = true;
|
||
options.SourceCodeDisplay.SnippetProvider =
|
||
new AtomUIGalleryShowCaseCodeSnippetProvider();
|
||
```
|
||
|
||
GalleryBase 不直接寻找 `ShowCaseCodeSnippetCatalog`,也不约定产品 catalog 的 namespace。产品侧 provider 是唯一桥接点。
|
||
|
||
## 10. 生命周期与资源边界
|
||
|
||
源码展示模块必须保持 ShowCase 页面延迟创建语义:
|
||
|
||
- `ShowCaseItem` 显示源码按钮不能触发 `MaterializeDeferredContent()`。
|
||
- Drawer 打开不能创建未 materialize 的示例控件树。
|
||
- Snippet 查询只读取生成 catalog,不访问当前 demo visual tree。
|
||
- Drawer 关闭时释放 editor、TextMate installation、事件订阅和 clipboard 状态。
|
||
- Drawer Host detach 时关闭 Drawer 并释放当前 group。
|
||
- Provider 不持有 ShowCase 页面或 `ShowCaseItem` 实例引用。
|
||
|
||
Clipboard 操作通过当前 `TopLevel` 获取剪贴板服务。没有可用 clipboard 时,复制按钮进入禁用或错误提示状态,不抛出未处理异常。
|
||
|
||
## 11. AOT、裁剪与 Browser 边界
|
||
|
||
源码展示模块必须满足 Gallery AOT 和 Browser 发布要求:
|
||
|
||
- 运行时不使用 `Assembly.GetTypes()`、`Activator.CreateInstance(...)`、反射字段扫描或运行时文件扫描。
|
||
- Source Generator 输出的 catalog 是普通 C# 代码,trimmer 可静态分析。
|
||
- Generator 项目使用 `netstandard2.0`,并隔离 `PublishAot`、`PublishTrimmed`、`RuntimeIdentifier` 等发布属性。
|
||
- Generator 的 C# 分析只在编译期使用 Roslyn syntax tree;运行时不保存 Roslyn 类型、语义模型或源码文件索引。
|
||
- TextMate grammar 资源必须通过包资源路径加载,并在 Browser 发布中验证。
|
||
- Drawer 和 editor 均按用户操作延迟创建,Browser 首屏不初始化 AvaloniaEdit。
|
||
- 生成 catalog 不应无限增长到包含整页或整文件源码;只保存与示例直接相关的 AXAML、code-behind 和 ViewModel 切片。
|
||
|
||
涉及 NativeAOT 发布时,除普通测试外必须执行 Gallery NativeAOT publish 验证。
|
||
|
||
## 12. 兼容性不变量
|
||
|
||
实现源码展示模块时必须保持以下不变量:
|
||
|
||
- 现有 ShowCase 示例视觉树不因源码功能变化而改变。
|
||
- 现有 `ShowCasePanel + ShowCaseItem` 双层延迟创建规则不变。
|
||
- `ShowCaseItem` 标题、描述、Badge、占整行和 deferred placeholder 语义不变。
|
||
- 未配置 Provider 的产品 Gallery 不显示源码按钮,也不产生运行时错误。
|
||
- Provider 查不到片段时不会阻断 ShowCase 页面展示。
|
||
- Drawer 始终作为浮层显示,不参与 Masonry 布局测量。
|
||
- GalleryBase 不引用产品项目。
|
||
- Source Generator 不生成产品运行时依赖之外的反射路径。
|
||
- Source Generator 生成的 `Code-behind` / `ViewModel` 标签必须来自 item 级依赖分析,不得退化为整文件展示。
|
||
- 当前 item 没有关联 C# 代码时不生成空 tab;Drawer 只显示实际存在的片段。
|
||
- Snapshot 测试中的 demo 内容提取规则仍以 demo XAML 为准,不把源码展示 UI 混入示例内容 snapshot。
|
||
|
||
## 13. 文档导航与验证策略
|
||
|
||
相关文档:
|
||
|
||
- [AtomUI.Toolkits.GalleryBase 模块概览](overview.md)
|
||
- [AtomUI.Toolkits.GalleryBase 设计文档](architecture.md)
|
||
- [GalleryBase ShowCase 控件设计](showcase-controls.md)
|
||
- [Gallery ShowCase Design Pattern](../../gallery/authoring/gallery-showcase-design-pattern.md)
|
||
- [AtomUI AOT 编程规范](../../engineering/development/aot-programming-guidelines.md)
|
||
- [AtomUI 编译期诊断规范](../../engineering/development/compiler-diagnostics-guidelines.md)
|
||
|
||
验证要求:
|
||
|
||
| 层级 | 验证 |
|
||
|---|---|
|
||
| Generator | generator tests 覆盖 AXAML 提取、panel key、item index、line range、空输入、事件 handler 切片、Binding 到 ViewModel 成员切片和无 C# 关联时不生成空 tab |
|
||
| Generator strict mode | generator tests 覆盖找不到事件处理器、找不到 ViewModel、Binding 成员缺失时默认低噪声和 strict warning 两种行为 |
|
||
| GalleryBase runtime | Drawer 打开/关闭、editor lazy 创建、provider 缺失、clipboard fallback |
|
||
| ShowCase 结构 | 全量迁移页面每个 `ShowCaseItem` 可匹配源码片段;典型页面覆盖 `AXAML + Code-behind`、`AXAML + ViewModel`、`AXAML + Code-behind + ViewModel` 三类组合 |
|
||
| AOT | Gallery NativeAOT publish 验证生成 catalog 和编辑器依赖 |
|
||
| 文档 | `git diff --check`,并确认本文件从 GalleryBase 模块入口可达 |
|