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 说明。
添加新页面
- 在
INavigationService.cs的PageKey枚举中添加新值 - 创建
ViewModels/NewThingViewModel.cs(继承ViewModelBase) - 创建
Views/NewThingView.axaml+.axaml.cs - 在
Program.cs中注册:services.AddSingleton<NewThingViewModel>() - 在
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:
- Light 和 Dark 变体
- 侧边栏颜色
- 语义颜色:Success、Warning、Error、Info
- 表面颜色和阴影
自定义资源
| 资源类型 | 使用方式 |
|---|---|
| Brushes | {StaticResource SuccessBrush} (通过 DynamicResource 引用主题色) |
| BoxShadows | CardShadowNone、CardShadowLarge、CardShadowSmall |
| 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() 在启动时调用:
- 读取
AppSettings.Language - 设置
CultureInfo.CurrentUICulture和Resources.Culture - 必须在
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.Hash 是 GitCommit.g.cs 中的常量,由 MSBuild 目标在构建前自动生成(git rev-parse --short HEAD)。