跳转到主内容

SeatFlow 基于 JSON 声明式配置的引导系统设计,包括启动引导、页面引导、演示数据注入和窗口状态同步

引导系统

概述

SeatFlow 的引导系统帮助新用户快速上手核心功能的操作。系统完全由 JSON 配置驱动,分为两种触发模式:启动引导(首次启动时触发完整工作流)和页面引导(首次访问某页面时触发)。

架构

Guide 控件 (CodeWF)
  步骤列表 + 高亮定位 + 前进/后退 + 完成/关闭
       │
       │ StepOpening 事件 → 解析 Target
       ▼
OnboardingService
  ├── StartOnboarding() — 启动引导
  ├── TryShowPageGuide(page) — 页面引导
  └── BuildStepsFromDefs() — JSON → GuideStepOption 转换
       │
       ▼
onboarding_config.json(嵌入资源)
  { startupPhases: [...], pageGuides: {...} }

关键文件

文件 作用
Data/onboarding_config.json 唯一的数据源——所有阶段、步骤、资源键引用
Services/IOnboardingService.cs 接口定义
Services/OnboardingService.cs 核心实现——加载 JSON、构建步骤、处理事件
Services/OnboardingPhaseDefinition.cs 数据模型
Views/MainWindow.axaml Guide 控件声明
Views/MainWindow.axaml.cs 事件处理器转发到 IOnboardingService
App.axaml.cs 启动时调用 CheckAndStartOnboardingAsync()
ViewModels/MainShellViewModel.cs 导航完成后触发 TryShowPageGuide
ViewModels/SettingsViewModel.cs "重新开始引导"按钮
Core/Models/AppSettings.cs IsFirstLaunch + CompletedPageGuides 字典
Styles/Guide.axaml Guide 控件的 ControlTheme

两种触发模式

启动引导(startupPhases)

  • 触发时机:首次启动时,IsFirstLaunch == true 或设置文件不存在
  • 触发路径App.axaml.cs → CheckAndStartOnboardingAsync()
  • 范围:~24 步完整工作流,覆盖 9 个阶段
  • 完成标记AppSettings.IsFirstLaunch = false(启动时立即持久化,崩溃安全)

触发流程:

App.axaml.cs → CheckAndStartOnboardingAsync()
  ├── 检测 IsFirstLaunch == true 或设置文件不存在
  ├── 立即持久化 IsFirstLaunch = false(崩溃安全)
  └── Dispatcher.UIThread.Post(Background) → onboarding.StartOnboarding()
       ├── 加载 JSON 配置 + 平铺步骤列表
       ├── 设置 MainShellViewModel.IsOnboardingActive = true
       ├── 展开侧边栏,导航到 Home
       ├── 订阅 Guide.StepOpening 事件
       └── Dispatcher.UIThread.Post(Loaded) → Guide.Show()

页面引导(pageGuides)

  • 触发时机:用户首次进入某个页面
  • 触发路径MainShellViewModel.SchedulePageGuideCheck()(导航完成后触发)
  • 范围:仅展示该页面的关键操作
  • 完成标记:持久化到 AppSettings.CompletedPageGuides 字典

触发流程:

MainShellViewModel.SchedulePageGuideCheck()
  └── Dispatcher.UIThread.Post(Background) → onboarding.TryShowPageGuide(page)
       ├── 守卫:IsActive == false(启动引导或已有页面引导活跃则跳过)
       ├── 守卫:该 page 在 pageGuides 中有定义
       ├── 守卫:CompletedPageGuides 中不存在
       ├── 设置 IsActive = true, _currentPageGuide = pageKey
       ├── 构建该页面的步骤列表
       └── Guide.Show()

JSON 配置结构

顶层结构

{
  "version": "3.4",
  "startupPhases": [ OnboardingPhaseDefinition, ... ],
  "pageGuides": {
    "FreeformManagement": OnboardingPhaseDefinition,
    "PluginManagement": OnboardingPhaseDefinition
  }
}

OnboardingPhaseDefinition

字段 类型 必填 说明
page string? 要导航到的页面(PageKey 枚举名称)。null = 留在当前页
seedData bool 跨阶段导航时是否注入演示数据。默认 false
steps array OnboardingStepDefinition 数组

OnboardingStepDefinition

字段 类型 默认值 说明
titleKey string (必填) Resources.resx 中标题文本的键名
descKey string (必填) Resources.resx 中描述文本的键名
target string "" 目标控件的 x:Name,支持分号分隔候选项;空字符串 = 居中模态
placement string "Right" 弹窗方向:Top/Bottom/Left/Right/Center
showMask bool true 是否显示深色遮罩
showArrow bool true 是否显示指向箭头

启动引导步骤一览

当前启动引导包含 9 个阶段共 ~24 步:

阶段 页面 seedData 步数 目标控件
欢迎 Home 2 (居中模态), ToggleSidebarButton
成员管理(导入) MemberManagement false 2 ExportTemplateButton, ImportButton
成员管理(更新) MemberManagement true 3 UpdateFromFileButton, StudentListBox, NewStudentRow
会场配置 VenueConfiguration true 3 NewVenueButton, LayoutTypePanel, SaveVenueButton
策略配置 StrategyConfiguration true 4 StrategyListBox, EditEnabledSwitch, (居中), SaveAllButton
排座生成 SeatingArrangement true 4 VenueListBox, DatasetListBox, GenerateButton, ExportButton
快照历史 SnapshotHistory true 2 VenueComboBox, SnapshotListBox
快捷键设置 Settings false 3 KeyboardShortcutsSection, UndoShortcutSwitch, SaveSettingsButton
结束 Home 1 (居中模态)

工作流阶段转换

MemberManagement 分两次进入(中间隔 Home 过渡阶段):

  • Phase 1: MemberManagement(seedData: false)→ ImportButton 可见,无演示数据
  • 代码内 Home 往返:Phase 2 过渡时自动执行 OnboardingNavigateTo(Home)OnboardingNavigateTo(MemberManagement),强制页面离开-重入
  • Phase 2: MemberManagement(seedData: true)→ 注入演示数据后,UpdateFromFileButton 可见

页面引导一览

页面 步数 目标控件
FreeformManagement 3 ImportCsvButton, AddPointButton, SaveLayoutButton
PluginManagement 2 PluginListBox, PluginEnabledSwitch

演示数据注入(v3.2)

声明式控制

通过 OnboardingPhaseDefinition.SeedData(JSON bool,默认 false)声明式控制。HandleStepOpening 在检测到阶段过渡时仅当 phase.SeedData == true 才调用 SeedPageData()

原运行状态标志 _memberManagementDataSeeded 已完全删除,改为纯 JSON 声明式控制。

注入详情

页面 注入内容 延迟策略
MemberManagement 6 名示例学生 → Students ObservableCollection + 演示数据集 → SavedDatasets 同步
VenueConfiguration 执行 NewVenueCommand 创建演示会场 DispatcherPriority.Background
StrategyConfiguration 选中 Strategies[0](首个策略) DispatcherPriority.Background
SeatingArrangement 演示会场+数据集 + 4×3 座位预览 DispatcherPriority.Background
SnapshotHistory 1 个演示快照 → Snapshots 同步

延迟注入的必要性

多个 ViewModel 在构造函数中 fire-and-forget 启动异步初始化(_ = LoadXxxAsync())。这些异步方法完成后会创建全新 ObservableCollection,覆盖同步注入的数据。DispatcherPriority.Background 是最低优先级的调度,保证在异步 continuation 之后执行。

注入原则

  • 零磁盘痕迹:引导结束无任何残留数据文件
  • 不依赖 Infrastructure 层:仅使用 Core 模型 + ViewModel public API
  • 不修改 ViewModel:纯外部注入(例外:MemberManagementViewModel 暴露了 SetSuppressDatasetLoad()ResetDirtyState() 两个 internal 方法)

完成与清理

完成流程

OnboardingService.CompleteOnboardingAsync()
  ├── 取消订阅 StepOpening 事件
  ├── 捕获 wasPageGuide(在 await 前保存,防竞态)
  ├── 立即设置 IsActive = false, _currentPageGuide = null
  ├── 若是页面引导 → 持久化到 AppSettings.CompletedPageGuides
  ├── 若是启动引导 → 调用 vm.CompleteOnboardingAsync() 恢复 UI
  └── 关闭 Guide 控件,清空 StepsSource

ClearPageData 清理

CompleteOnboardingAsync() 中调用 ClearPageData()

页面 清理内容
SeatingArrangement 清除 SeatItemsVenueItemsDatasetItemsHasGenerated
SnapshotHistory 清除 SnapshotsVenues
MemberManagement 使用 _memberManagementDemoInjected 静态标志判断——仅在实际注入过演示数据时才清理

_memberManagementDemoInjected 静态标志防止误清用户已导入的真实数据。

关闭确认

用户点击 × 或按 Esc:

  • 若已正常完成(_isCompleting == true)→ 直接关闭
  • 否则弹出 ShowConfirmAsync 确认对话框
  • 确认 → 执行 CompleteOnboardingAsync()
  • 取消 → 调用 Guide.Show() 恢复显示

窗口状态同步(v3.1)

MainWindow 订阅 Activated/Deactivated 事件,转发到 OnboardingService

事件 行为
Deactivated(最小化/Alt+Tab) _isWindowObscured=trueGuide.Close() 静默关闭 Popup(无确认对话框,无完成标记)
Activated(恢复) 重置标志,从保留的 CurrentIndex 恢复 Guide

这解决了 Guide 的 3 个 Popup(ShouldUseOverlayLayer=False,原生 OS 窗口)在窗口最小化时可能残留为孤儿窗口的问题。

导航顺序(Phase 1 修复)

HandleStepOpening 必须在解析目标控件的 x:Name 之前导航到新页面。

正确顺序:导航 → 解析目标
错误顺序:解析目标 → 导航

原始顺序(解析 → 导航)导致 ContentPresenter.Child 引用旧页面,使每个阶段第一步的 NameScope 查找失败。

配合 OnboardingNavigateTo 的同步 CurrentViewModel 设置(通过 IsOnboardingActive 守卫跳过 RunTransitionAsync 动画),目标控件在首次尝试时即可正确解析。

阶段点按页面呈现(v3.4)

引导卡片左下角的步骤指示点默认按每个子步骤渲染(~24 个点),视觉上过于密集。v3.4 改为按阶段(9 个点)呈现。

实现原理

查阅 Guide 控件(CodeWF.AvaloniaControls)源码后发现:

  1. Guide.StepCountDirectProperty 且仅注册 getter、CLR setter 为 private,外部无法修改
  2. RefreshStepCollection() 每次 StepsSource 赋值时强制重置 StepCount = _activeSteps.Count
  3. SyncIndicator() 在每次步骤切换时执行 Indicator.StepCount = StepCountIndicator.ActiveIndex = CurrentIndex
  4. GoTo(int index) 内部校验 index < StepCount——若设 StepCount=9 会导致步骤导航在第 9 步后失效

方案

OnStepOpened 事件中(SyncIndicator() 执行 之后),通过 Dispatcher.UIThread.Post(..., Background) 延迟覆盖 _guide.Indicator.StepCount_guide.Indicator.ActiveIndex

private void OnStepOpened(object? sender, GuideStepEventArgs e)
{
    int phaseCount = _activePhaseBoundaries.Count - 1;
    int phaseIndex = GetPhaseIndex(e.Index);
    var guide = _guide;

    Dispatcher.UIThread.Post(() =>
    {
        if (guide.Indicator is null) return;
        guide.Indicator.StepCount = phaseCount;
        guide.Indicator.ActiveIndex = phaseIndex;
    }, DispatcherPriority.Background);
}
  • Background 优先级保证在所有同步操作之后执行
  • 不影响 Guide 内部的步骤导航(Next/Previous 仍按 ~24 步切换)
  • GetPhaseIndex() 利用已有的 _activePhaseBoundaries 数组查找当前步骤所属阶段

详见 ADR-011: 全局键盘快捷键系统

添加/修改引导步骤

添加新步骤

只需编辑 2 个文件,不修改 C# 代码:

  1. onboarding_config.json 中添加步骤定义
  2. Resources.resxResources.en-US.resx 中添加对应文本
  3. Resources.Designer.cs 中添加强类型访问器属性

如果新步骤需要高亮某个控件,先在对应的 .axaml 文件中给控件添加 x:Name

示例——在 MemberManagement 阶段添加一步"保存数据集":

// 在 Members 阶段的 steps 数组末尾添加
{ "titleKey": "Guide_Members_Save_Title",
  "descKey": "Guide_Members_Save_Desc",
  "target": "SaveButton",
  "placement": "Bottom" }

然后在 resx 中添加对应的 <data> 条目。

添加新页面的页面引导

pageGuides 字典中添加新条目:

"SnapshotHistory": {
  "page": "SnapshotHistory",
  "steps": [
    { "titleKey": "Guide_Snapshot_XXX_Title", "descKey": "Guide_Snapshot_XXX_Desc", "target": "..." }
  ]
}

键名必须与 PageKey 枚举值完全匹配(区分大小写)。

修改现有步骤

只需编辑 JSON 中的字段,或修改 resx 中的文本内容。无需改 C# 代码。

删除步骤

从 JSON 中移除步骤定义即可。旧的 resx 键可以保留(无害)。

目标控件清单

MainWindow 中(可跨页面访问)

x:Name 控件 用途
ToggleSidebarButton Button 侧边栏折叠/展开

MemberManagementView

x:Name 控件 用途
ExportTemplateButton Button 导出导入模板
ImportButton Button 导入学生数据
UpdateFromFileButton Button 从文件更新学生数据
StudentListBox ListBox 学生列表
NewStudentRow Border 新增学生行
SaveButton Button 保存数据集

VenueConfigurationView

x:Name 控件 用途
NewVenueButton Button 新建会场
LayoutTypePanel StackPanel 布局类型选择
SaveVenueButton Button 保存会场

StrategyConfigurationView

x:Name 控件 用途
StrategyListBox ListBox 策略列表
EditEnabledSwitch ToggleSwitch 启用/禁用策略
SaveAllButton Button 保存全部策略

SeatingArrangementView

x:Name 控件 用途
VenueListBox ListBox 会场选择
DatasetListBox ListBox 数据集选择
GenerateButton Button 生成座位
ExportButton Button 导出结果
SaveSnapshotButton Button 保存为快照
UndoButton Button 撤销操作

SnapshotHistoryView

x:Name 控件 用途
VenueComboBox ComboBox 选择会场
SnapshotListBox ListBox 快照列表
RollbackButton Button 回滚快照

FreeformManagementView

x:Name 控件 用途
ImportCsvButton Button 导入自由布局
AddPointButton Button 添加坐标点
SaveLayoutButton Button 保存自由布局

SettingsView

x:Name 控件 用途
SaveSettingsButton Button 保存设置
KeyboardShortcutsSection StackPanel 键盘快捷键卡片标题行
UndoShortcutSwitch ToggleSwitch Ctrl+Z 快捷键开关

PluginManagementView

x:Name 控件 用途
PluginListBox ListBox 插件列表
PluginEnabledSwitch ToggleSwitch 启用/禁用插件

资源键命名约定

引导文本的命名模式:Guide_{Page}_{Action}_{Type}

键名 说明
Guide_Members_Import_Title 成员管理页面,导入操作,标题
Guide_Venue_Layout_Desc 会场配置页面,布局操作,描述
Guide_Strategy_Conflict_Title 策略配置页面,冲突提示,标题

注意:代码不做任何键名拼接或推断,BuildStepsFromDefs() 仅通过 ResourceManager.GetString(jsonKey) 进行纯粹查找。

文本风格指南

引导步骤的文本应遵循指令式风格——直接告诉用户要做什么:

推荐(指令式) 避免(描述式)
"点击此按钮从文件导入学生名单" "首先需要导入学生数据。点击「导入」按钮..."
"在此下拉框选择会场" "在此页面可以查看历史记录、预览座位表..."
"点击此开关启用策略" "使用开关启用/禁用策略"

每一步的 title 提示要执行的操作,description 提供简短的上文说明。

DI 注册

// Program.cs
services.AddSingleton<IOnboardingService, OnboardingService>();
services.AddSingleton<IOnboardingStarter>(sp =>
    (IOnboardingStarter)sp.GetRequiredService<IOnboardingService>());

services.AddSingleton<MainWindow>();           // 依赖 IOnboardingService
services.AddSingleton<MainShellViewModel>();    // 依赖 IOnboardingService
services.AddSingleton<SettingsViewModel>();     // 依赖 IOnboardingService(通过 IOnboardingStarter 桥接)

IOnboardingStarter 作为 IOnboardingService 的受限接口,仅暴露 CheckAndStartOnboardingAsync(),供 App.axaml.cs 启动时调用。IOnboardingService 的完整接口暴露给 ViewModel。

测试验证

由于引导系统依赖 UI 交互,主要通过以下方式验证:

验证方式 说明
完整性校验 交叉验证 JSON 中的资源键、目标控件名、页面引用
构建验证 dotnet build 确保编译通过
测试回归 dotnet test 确保现有测试无回归
手动验证 删除 AppSettings.json → 启动 → 验证启动引导;进入页面 → 验证页面引导;切换英文语言 → 验证英文文本

相关文档