跳转到主内容

SeatFlow.Presentation.Avalonia 层的 MVVM 架构、导航、主题、启动序列和关键服务

Presentation 层详解

SeatFlow.Presentation.Avalonia 是 SeatFlow 的跨平台桌面 UI 层,基于 Avalonia UI 12 构建,采用 MVVM 模式与 CommunityToolkit.Mvvm 8.4 源码生成器。

MVVM 架构

ViewModelBase

所有 ViewModel 继承自 ViewModelBase(扩展 ObservableObject),提供核心基础设施:

public abstract class ViewModelBase : ObservableObject
{
    // 静态 Dialog 服务(需在 App 启动时初始化)
    public static IDialogService Dialog { get; private set; }
    public static void InitializeDialogService(IDialogService dialog);

    // SafeExecuteAsync — 两个重载
    protected async Task<bool> SafeExecuteAsync(Func<Task> action, string? errorTitle = null);
    protected async Task<bool> SafeExecuteAsync(Func<CancellationToken, Task> action,
        TimeSpan timeout, string? errorTitle = null);

    // CanLeaveAsync — 导航离开前检查未保存更改
    public virtual Task<bool> CanLeaveAsync();
}

SafeExecuteAsync 提供:

  • 简洁重载:try-catch,自动错误对话框
  • 超时重载CancellationTokenSource 自动取消,超时对话框
  • 超时值应远低于 WatchdogService 阈值(45 秒)

CommunityToolkit.Mvvm 源码生成器

public class ExampleViewModel : ViewModelBase
{
    [ObservableProperty]              // 生成 public 属性 + OnXChanged 分部方法
    private string _name = string.Empty;

    [RelayCommand]                    // 生成 ICommand 属性
    private async Task SaveAsync() { }

    [ObservableProperty]
    [NotifyPropertyChangedFor(nameof(IsValid))]  // 依赖属性
    private int _count;
}

所有 ViewModel 使用源码生成器,无需手写 INotifyPropertyChanged 样板代码。

ViewLocator

ViewLocator 通过反射按命名约定自动解析 View:将 XXXViewModel 中的 "ViewModel" 替换为 "View",查找对应类型。这意味着 HomeViewModel 自动关联 HomeView,无需显式注册。

导航系统

INavigationService

位于 SeatFlow.Presentation.Avalonia/Services/INavigationService.cs

public interface INavigationService
{
    PageKey CurrentPage { get; }
    void NavigateTo(PageKey page);
    Task<bool> NavigateToAsync(PageKey page);  // 先调用 CanLeaveAsync()
}

PageKey 枚举

管理 10 个页面:

ViewModel View 用途
Home HomeViewModel HomeView 首页概览
MemberManagement MemberManagementViewModel MemberManagementView 成员管理(数据集)
VenueConfiguration VenueConfigurationViewModel VenueConfigurationView 会场配置
FreeformManagement FreeformManagementViewModel FreeformManagementView 自由点管理
StrategyConfiguration StrategyConfigurationViewModel StrategyConfigurationView 策略配置
SeatingArrangement SeatingArrangementViewModel SeatingArrangementView 排座执行与微调
SnapshotHistory SnapshotHistoryViewModel SnapshotHistoryView 快照历史
PluginManagement PluginManagementViewModel PluginManagementView 插件管理
Settings SettingsViewModel SettingsView 设置
About AboutViewModel AboutView 关于

MainShellViewModel

主壳 ViewModel,管理页面切换:

  • CurrentViewModel — 当前活动页 ViewModel
  • 页面切换使用过渡动画(RunTransitionAsync),引导模式下跳过动画(IsOnboardingActive 守卫)
  • SidebarWidth — 侧边栏宽度(展开 140px / 折叠 64px)
  • ToggleSidebar() — 侧边栏手动切换
  • 当窗口宽度 < 750px 时自动折叠

页面导航可见性

JSON 配置 Data/page_navigation.json(嵌入资源)控制哪些页面启用/禁用:

{ "version": "1.0", "pages": { "Home": true, "PluginManagement": false } }

MainShellViewModel 使用 IsPageEnabled("PageKeyName") 检查。禁用页面在侧边栏中以低透明度显示(Opacity=0.4),并附加 ToolTip 说明。

添加新页面

  1. INavigationService.csPageKey 枚举中添加新值
  2. 创建 ViewModels/NewThingViewModel.cs(继承 ViewModelBase
  3. 创建 Views/NewThingView.axaml + .axaml.cs
  4. Program.cs 中注册:services.AddSingleton<NewThingViewModel>()
  5. MainWindow.axaml 侧边栏添加导航按钮

UI 服务

位于 SeatFlow.Presentation.Avalonia/Services/

服务 接口 用途
NavigationService INavigationService 页面切换,NavigateToAsync 运行 CanLeaveAsync()
DialogService IDialogService 错误/信息对话框,需 SetTopLevel(TopLevel) 初始化
FileService IFileService 文件打开/保存选择器,需 SetTopLevel() 初始化
WatchdogService UI 线程卡顿检测,默认超时 45 秒
ArrangementCounterService IArrangementCounterService 排座次数内存计数,离开排座页面时上报到后端 API

WatchdogService

通过后台轮询检测 UI 线程卡顿:

  • UI 线程必须定期调用 Ping()App.axaml.cs 中 DispatcherTimer 每 3 秒执行)
  • 超时后:转储线程诊断到 err_<timestamp>.log,强制退出应用
  • 长时间操作(导出、导入)应保持在 WatchdogService 阈值以下

辅助窗口

窗口 文件 用途
DialogWindow Windows/DialogWindow.axaml 通用模态对话框(Confirm/Error/Warning/Info/MultiOption)
InputWindow Windows/InputWindow.axaml 单行文本输入对话框

DialogWindow 按钮绑定

按钮使用 Content="{x:Static lang:Resources.Common_OK}" 属性语法。 注意:在继承自 Window 的类中,Resources 解析为 Window.Resources(IResourceDictionary),需使用完全限定 Lang.Resources.xxx

行为 (Behaviors)

位于 SeatFlow.Presentation.Avalonia/Behaviors/

行为 文件 用途
CanvasZoomPan CanvasZoomPan.cs Canvas 缩放手势(平移/缩放)。拖放时通过 NaN 哨兵机制跳过平移
ZoomOnScroll ZoomOnScroll.cs Ctrl+滚轮缩放(需同时检查 KeyboardShortcutConfig.ZoomWithCtrlEnabled
ChineseInputNormalizer ChineseInputNormalizer.cs 全角数字/符号 → 半角自动转换
KeyboardShortcutHandler KeyboardShortcutHandler.cs 全局键盘快捷键——根据 KeyboardShortcutConfig 开关 + 当前页 ViewModel 分派 Ctrl+Z/Y/S/Delete/Esc 命令

图标系统

使用 FluentUI 图标:

<fic:FluentIcon Icon="{x:Static ficEnum:Icon.DataBarVertical20}" FontSize="18"/>

完整图标列表见 SeatFlow.Presentation.Avalonia/docs/Fluent_Icons.md

编译绑定

<!-- 项目设置中 AvaloniaUseCompiledBindingsByDefault=true -->
<UserControl xmlns:vm="clr-namespace:SeatFlow.Presentation.Avalonia.ViewModels"
             x:DataType="vm:HomeViewModel">    <!-- 必选 -->
    <TextBlock Text="{Binding Title}" />
</UserControl>
  • 始终在根元素设置 x:DataType
  • 编译绑定在 XAML 解析时验证类型安全性,运行时无反射开销
  • 转换器位于 Converters/BoolConverters.cs(Negate、TrueWhenNull 等)、ValueConverters.cs

主题系统

主题字典

App.axaml 定义 ResourceDictionary.ThemeDictionaries

  • LightDark 变体
  • 侧边栏颜色
  • 语义颜色:Success、Warning、Error、Info
  • 表面颜色和阴影

自定义资源

资源类型 使用方式
Brushes {StaticResource SuccessBrush} (通过 DynamicResource 引用主题色)
BoxShadows CardShadowNoneCardShadowLargeCardShadowSmall
Typography Typography.axaml 排版样式
Spacing Spacing.axaml 间距系统
Colors Colors.axaml 色板

原则:始终使用 Brush 资源,切勿硬编码十六进制颜色。

样式包含顺序

App.axaml ──→ FluentTheme
          ──→ Colors.axaml
          ──→ Spacing.axaml
          ──→ Typography.axaml
          ──→ 自定义控件样式

字体

全局 Window 样式设置 CJK 友好的字体回退链:

<FontFamily>Inter,Microsoft YaHei UI,PingFang SC,
    Noto Sans CJK SC,WenQuanYi Micro Hei,sans-serif</FontFamily>

i18n / 本地化

位于 SeatFlow.Presentation.Avalonia/Lang/

  • Resources.resx — 中性语言 (zh-CN),约 700 键
  • Resources.en-US.resx — 英文卫星资源
  • Resources.Designer.cs — 手动维护的强类型访问器类

XAML 用法(仅属性语法)

<TextBlock Text="{x:Static lang:Resources.Settings_Title}" />
<Button Content="{x:Static lang:Resources.Common_OK}" />

C# 用法

StatusMessage = Resources.Settings_Saved;
StatusMessage = string.Format(Resources.Snapshot_VenuesLoadedFmt, count);

管理工具

python3 scripts/i18n.py list                     # 列出所有键
python3 scripts/i18n.py add KEY --zh "中文" --en "EN"  # 添加键
python3 scripts/i18n.py sync                     # 从 .resx 重新生成 Designer.cs

语言切换

App.ApplyLanguageFromSettings() 在启动时调用:

  1. 读取 AppSettings.Language
  2. 设置 CultureInfo.CurrentUICultureResources.Culture
  3. 必须在 AvaloniaXamlLoader.Load(this) 之前调用,以确保 {x:Static} 正确定位

启动序列

App.axaml.cs 中完整的应用启动流程:

1. StartupGuard.CheckEnvironment()
   ├── 验证 .NET 运行时 >= 10
   └── 验证操作系统:Windows 10+ / macOS 12+ / Linux

2. App.Initialize()
   ├── ApplyLanguageFromSettings()  ← 设置 CurrentUICulture
   └── AvaloniaXamlLoader.Load(this) ← 加载 XAML

3. OnFrameworkInitializationCompleted
   ├── DI 解析 MainShellViewModel, MainWindow
   ├── 绑定 DataContext: MainWindow.DataContext = mainShellViewModel
   ├── IFileService.SetTopLevel(mainWindow)
   ├── IDialogService.SetTopLevel(mainWindow)
   ├── ViewModelBase.InitializeDialogService(dialogService)
   ├── 初始化 ViewModelBase 日志记录器
   ├── 启动 WatchdogService (3s DispatcherTimer)
   ├── 附加 ChineseInputNormalizer
   ├── 附加 KeyboardShortcutHandler(全局快捷键 Ctrl+Z/Y/S/Del/Esc)
   └── RestoreSettingsAsync()
       ├── 恢复主题
       └── 恢复窗口位置和大小

侧边栏

  • 展开宽度:140px | 折叠宽度:64px
  • MainShellViewModel.SidebarWidth 控制
  • 窗口宽度 < 750px 时自动折叠
  • ToggleSidebar() 手动切换

代码约定

DockPanel 子元素顺序

LastChildFill="True"(默认)时,最后个子元素填充剩余空间。Dock 子元素必须在填充元素之前:

<DockPanel>
    <Sidebar DockPanel.Dock="Left" />
    <ScrollViewer>    <!-- 填充剩余空间 -->
        <ContentPresenter />
    </ScrollViewer>
</DockPanel>

关于页面版本

版本格式:"{about.json.version}+{git commit hash}"

GitCommit.HashGitCommit.g.cs 中的常量,由 MSBuild 目标在构建前自动生成(git rev-parse --short HEAD)。