Skip to content

Commit 96c9a00

Browse files
committed
Archive OpenSpec change add-view-level-menus
1 parent 82cb632 commit 96c9a00

File tree

9 files changed

+93
-79
lines changed

9 files changed

+93
-79
lines changed

openspec/changes/add-view-level-menus/design.md renamed to openspec/changes/archive/2025-12-19-add-view-level-menus/design.md

File renamed without changes.

openspec/changes/add-view-level-menus/proposal.md renamed to openspec/changes/archive/2025-12-19-add-view-level-menus/proposal.md

File renamed without changes.

openspec/changes/add-view-level-menus/specs/module-template/spec.md renamed to openspec/changes/archive/2025-12-19-add-view-level-menus/specs/module-template/spec.md

File renamed without changes.

openspec/changes/add-view-level-menus/specs/navigation/spec.md renamed to openspec/changes/archive/2025-12-19-add-view-level-menus/specs/navigation/spec.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ The framework SHALL support registration of navigation guards that can intercept
3131
- **AND** 任一 guard 的 `CanNavigateToAsync` 返回 false
3232
- **THEN** 导航取消,当前视图保持不变
3333

34+
## ADDED Requirements
3435
### Requirement: ViewModel Navigation Lifecycle
3536
The framework SHALL provide ViewModel-level navigation interception and lifecycle hooks that can be overridden without extra registration code.
3637

openspec/changes/add-view-level-menus/specs/runtime/spec.md renamed to openspec/changes/archive/2025-12-19-add-view-level-menus/specs/runtime/spec.md

File renamed without changes.

openspec/changes/add-view-level-menus/tasks.md renamed to openspec/changes/archive/2025-12-19-add-view-level-menus/tasks.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,6 @@
1313
- [x] 1.9.2 新增/扩展运行时菜单树组装测试(建议新增 `tests/Modulus.Core.Tests/Runtime/MenuTreeTests.cs`):覆盖从 DB 菜单组装 `MenuItem.Children`,以及多 View 父菜单 `NavigationKey` 为空(仅展开不导航)
1414
- [x] 1.9.3 新增导航生命周期与拦截顺序测试(建议 `tests/Modulus.Hosts.Tests/NavigationViewModelLifecycleTests.cs`):覆盖 guards → 当前 VM `CanNavigateFrom` → 目标 VM `CanNavigateTo``OnNavigatedFrom/To` 的调用顺序与短路行为
1515
- [x] 1.9.4 新增 Avalonia 控件层级菜单行为测试(建议扩展 `tests/Modulus.UI.Avalonia.Tests/NavigationViewTests.cs`):覆盖父节点无 `NavigationKey` 时点击仅切换展开态、不触发导航命令
16-
- [ ] 1.10 增加“完整导航机制”回归测试(可选但推荐):在 `tests/Modulus.Hosts.Tests/ModulusApplicationIntegrationTests.cs` 基础上构造最小模块样例(单 View/多 View),验证从安装投影 → 运行时注册 → 导航解析的端到端链路(未实现)
16+
- [x] 1.10 增加“完整导航机制”回归测试(可选但推荐):在 `tests/Modulus.Hosts.Tests/ModulusApplicationIntegrationTests.cs` 基础上构造最小模块样例(单 View/多 View),验证从安装投影 → 运行时注册 → 导航解析的端到端链路
1717

1818

openspec/specs/module-template/spec.md

Lines changed: 12 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -49,44 +49,20 @@
4949
- **AND** 命令退出码为非 0
5050

5151
### Requirement: Module Project Structure
52-
5352
系统 SHALL 根据目标 Host 生成对应的模块项目结构。
5453

55-
#### Scenario: Avalonia 模块结构
56-
57-
- **WHEN** 创建 Avalonia 模块,名称为 "MyModule"
58-
- **THEN** 生成以下结构:
59-
```
60-
MyModule/
61-
├── MyModule.Core/
62-
│ ├── MyModule.Core.csproj
63-
│ ├── MyModuleModule.cs
64-
│ └── ViewModels/MainViewModel.cs
65-
├── MyModule.UI.Avalonia/
66-
│ ├── MyModule.UI.Avalonia.csproj
67-
│ ├── MyModuleAvaloniaModule.cs
68-
│ ├── MainView.axaml
69-
│ └── MainView.axaml.cs
70-
└── extension.vsixmanifest
71-
```
72-
73-
#### Scenario: Blazor 模块结构
74-
75-
- **WHEN** 创建 Blazor 模块,名称为 "MyModule"
76-
- **THEN** 生成以下结构:
77-
```
78-
MyModule/
79-
├── MyModule.Core/
80-
│ ├── MyModule.Core.csproj
81-
│ ├── MyModuleModule.cs
82-
│ └── ViewModels/MainViewModel.cs
83-
├── MyModule.UI.Blazor/
84-
│ ├── MyModule.UI.Blazor.csproj
85-
│ ├── MyModuleBlazorModule.cs
86-
│ ├── _Imports.razor
87-
│ └── MainView.razor
88-
└── extension.vsixmanifest
89-
```
54+
#### Scenario: Avalonia 模块包含 View 级菜单声明
55+
- **WHEN** 创建 Avalonia 模块
56+
- **THEN** 生成的默认 ViewModel(如 `MainViewModel`)包含 View 级菜单声明(例如 `[AvaloniaViewMenu]`
57+
- **AND** 生成的默认 View(如 `MainView.axaml`)与 ViewModel 通过约定绑定(无需额外 `IViewRegistry.Register` 代码)
58+
- **AND** 生成的 View 不包含无参构造函数,仅提供 DI 构造函数(例如 `MainView(MainViewModel vm)`
59+
- **AND** 生成的 View 使用 `Design.IsDesignMode` 分支支持设计态渲染(无需运行时 DI)
60+
- **AND** 模板包含覆写导航生命周期/拦截的示例代码(如 `CanNavigateFromAsync/CanNavigateToAsync`
61+
62+
#### Scenario: Blazor 模块包含 View 级菜单声明
63+
- **WHEN** 创建 Blazor 模块
64+
- **THEN** 生成的默认页面(如 `MainView.razor`)包含 View 级菜单声明(例如 `@attribute [BlazorViewMenu]`
65+
- **AND** 页面包含/继承支持 ViewModel 绑定的基类示例(使 ViewModel 可覆写导航生命周期/拦截)
9066

9167
### Requirement: NuGet Package References
9268

openspec/specs/navigation/spec.md

Lines changed: 58 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -7,46 +7,33 @@ TBD - created by archiving change enhance-navigation-view. Update Purpose after
77
The framework SHALL provide an `INavigationService` abstraction that enables programmatic navigation with interception capabilities across all hosts.
88

99
#### Scenario: Navigate to a registered view
10-
- **WHEN** a module calls `NavigateToAsync(navigationKey)`
11-
- **THEN** the navigation service resolves the target view/viewmodel
12-
- **AND** the content area displays the corresponding view
10+
- **WHEN** 模块调用 `NavigateToAsync(navigationKey)`
11+
- **THEN** 导航服务基于稳定 `navigationKey` 解析目标(而不是运行时全域扫描 Type)
12+
- **AND** 解析结果包含:所属模块、目标 View(或 Route)、目标 ViewModel、实例生命周期策略
13+
- **AND** 内容区域显示对应 View
1314

1415
#### Scenario: Navigate with typed ViewModel
15-
- **WHEN** a module calls `NavigateToAsync<TViewModel>()`
16-
- **THEN** the navigation service resolves TViewModel
17-
- **AND** creates or retrieves the view instance based on lifecycle settings
18-
- **AND** the content area displays the view
16+
- **WHEN** 模块调用 `NavigateToAsync<TViewModel>()`
17+
- **THEN** 导航服务通过注册表/约定映射解析 `TViewModel` 对应的 `navigationKey`
18+
- **AND** 导航行为与 `NavigateToAsync(navigationKey)` 等价
1919

2020
#### Scenario: Navigation exposes current state
21-
- **WHEN** navigation completes successfully
22-
- **THEN** `CurrentNavigationKey` reflects the active navigation target
23-
- **AND** the `Navigated` event is raised with navigation details
21+
- **WHEN** 导航成功完成
22+
- **THEN** `CurrentNavigationKey` 反映当前激活的 `navigationKey`
23+
- **AND** `Navigated` event MUST 被触发,且包含 `FromKey/ToKey` 与可选的 `View/ViewModel` 实例
2424

2525
### Requirement: Navigation Guards
2626
The framework SHALL support registration of navigation guards that can intercept and conditionally prevent navigation.
2727

2828
#### Scenario: Guard prevents navigation from current page
29-
- **WHEN** user triggers navigation to a new page
30-
- **AND** a registered guard's `CanNavigateFromAsync` returns false
31-
- **THEN** the navigation is cancelled
32-
- **AND** the current view remains displayed
33-
- **AND** no `Navigated` event is raised
29+
- **WHEN** 用户触发从 A 到 B 的导航
30+
- **AND** 任一 guard 的 `CanNavigateFromAsync` 返回 false
31+
- **THEN** 导航取消,当前视图保持不变
3432

3533
#### Scenario: Guard prevents navigation to target page
36-
- **WHEN** user triggers navigation to a new page
37-
- **AND** a registered guard's `CanNavigateToAsync` returns false
38-
- **THEN** the navigation is cancelled
39-
- **AND** the current view remains displayed
40-
41-
#### Scenario: Multiple guards are evaluated
42-
- **WHEN** multiple guards are registered
43-
- **THEN** all guards are evaluated sequentially
44-
- **AND** navigation proceeds only if all guards return true
45-
46-
#### Scenario: Guard receives navigation context
47-
- **WHEN** a guard is invoked
48-
- **THEN** it receives `NavigationContext` containing source key, target key, and options
49-
- **AND** can make decisions based on this context
34+
- **WHEN** 用户触发从 A 到 B 的导航
35+
- **AND** 任一 guard 的 `CanNavigateToAsync` 返回 false
36+
- **THEN** 导航取消,当前视图保持不变
5037

5138
### Requirement: Page Instance Lifecycle
5239
The framework SHALL support configurable page instance modes that control whether views are reused or recreated on each navigation.
@@ -325,3 +312,45 @@ Home 主页 SHALL 提供"快速创建模块"入口,方便用户快速开始开
325312
- **THEN** 打开模块创建向导对话框
326313
- **OR** 跳转到模块开发文档页面(如果向导未实现)
327314

315+
### Requirement: ViewModel Navigation Lifecycle
316+
The framework SHALL provide ViewModel-level navigation interception and lifecycle hooks that can be overridden without extra registration code.
317+
318+
#### Scenario: ViewModel can intercept NavigateFrom
319+
- **GIVEN** 当前 ViewModel 支持导航拦截(通过继承基类或实现接口)
320+
- **WHEN** 从当前页面导航到新页面
321+
- **THEN** 框架调用 `CanNavigateFromAsync(context)`
322+
- **AND** 若返回 false,则导航被取消
323+
324+
#### Scenario: ViewModel can intercept NavigateTo
325+
- **GIVEN** 目标 ViewModel 支持导航拦截(通过继承基类或实现接口)
326+
- **WHEN** 导航到目标页面
327+
- **THEN** 框架调用 `CanNavigateToAsync(context)`
328+
- **AND** 若返回 false,则导航被取消且不改变当前页面
329+
330+
#### Scenario: ViewModel receives lifecycle callbacks
331+
- **WHEN** 导航从 A 切换到 B 成功完成
332+
- **THEN** 框架调用 A 的 `OnNavigatedFromAsync(context)`
333+
- **AND** 调用 B 的 `OnNavigatedToAsync(context)`(包含参数)
334+
335+
### Requirement: Hierarchical Menu Navigation
336+
Navigation menus SHALL support a two-level hierarchy where module menus are top-level and view menus are second-level when the module contains multiple views.
337+
338+
#### Scenario: Single-view module shows only module menu
339+
- **GIVEN** 某模块仅包含 1 个 View(可导航页面)
340+
- **AND** 该 View 仍声明了 View 级菜单元数据
341+
- **WHEN** 导航菜单渲染
342+
- **THEN** 主导航仅显示模块级菜单项(名称来自模块级菜单声明)
343+
- **AND** 不显示二级 View 菜单
344+
345+
#### Scenario: Multi-view module shows module menu and view children
346+
- **GIVEN** 某模块包含多个 View(可导航页面)
347+
- **WHEN** 导航菜单渲染
348+
- **THEN** 主导航显示模块级菜单项(名称来自模块级菜单声明)
349+
- **AND** 该菜单项包含二级子项,每个子项名称来自对应 View 的菜单声明
350+
351+
#### Scenario: Multi-view module parent menu expands only
352+
- **GIVEN** 某模块包含多个 View(可导航页面)
353+
- **WHEN** 用户点击模块级父菜单项
354+
- **THEN** 菜单仅展开/收起以显示/隐藏子菜单
355+
- **AND** 不触发导航(父菜单项不包含可导航的 `NavigationKey`
356+

openspec/specs/runtime/spec.md

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,27 @@ Menu entries SHALL be projected to the database at install/update time and read
2323

2424
#### Scenario: Install or update module projects menus to database
2525
- **WHEN** 模块被安装或更新
26-
- **THEN** 安装器解析模块 host-specific 入口类型上的菜单属性(Blazor: `[BlazorMenu]`,Avalonia: `[AvaloniaMenu]`
27-
- **AND** 将菜单写入数据库表 `Menus``ReplaceModuleMenusAsync` 覆盖该模块现有菜单)
28-
- **AND** 写入的菜单 ID 采用 `{ModuleId}.{HostType}.{Key}.{Index}` 规则(允许多条同 Key 菜单用于排错)
26+
- **THEN** 安装器解析模块入口类型上的模块级菜单属性(Blazor: `[BlazorMenu]`,Avalonia: `[AvaloniaMenu]`
27+
- **AND** 安装器以 metadata-only 方式解析模块 UI 程序集中的 View 级菜单属性(新增)
28+
- **AND** 将菜单写入数据库表 `Menus`,并构建父子层级(父为模块级菜单,子为 View 级菜单,使用 `ParentId`
29+
30+
#### Scenario: Single-view module ignores view-level menus
31+
- **GIVEN** 模块 UI 程序集中可识别的 View 数量为 1
32+
- **AND** 该 View 声明了 View 级菜单元数据
33+
- **WHEN** 安装器投影菜单到数据库
34+
- **THEN** 数据库中仅写入模块级菜单项(`ParentId = null`
35+
- **AND** 不写入该 View 的二级菜单项(避免重复)
36+
37+
#### Scenario: Multi-view module projects view-level menus as children
38+
- **GIVEN** 模块 UI 程序集中可识别的 View 数量大于 1
39+
- **WHEN** 安装器投影菜单到数据库
40+
- **THEN** 数据库中写入 1 条模块级菜单项(`ParentId = null`
41+
- **AND** 为每个 View 写入 1 条二级菜单项(`ParentId = <模块级菜单Id>`
2942

3043
#### Scenario: Render menus reads from database only
3144
- **WHEN** Shell 渲染导航菜单
3245
- **THEN** 从数据库读取 `Menus``Modules`(仅 `IsEnabled=true` 且状态可加载)
33-
- **AND** 注册到 `IMenuRegistry`
46+
- **AND** 运行时将 DB 菜单组装为树结构注册到 `IMenuRegistry`
3447
- **AND** 渲染过程不进行任何 DLL 动态解析(不反射、不 metadata 扫描、不读取菜单属性)
3548

3649
### Requirement: Detail Content Fallback
@@ -330,17 +343,12 @@ Host SHALL 在启动时注册版本信息到 RuntimeContext。
330343
### Requirement: Installation without executing module code
331344
模块安装/更新 MUST 不执行第三方模块代码;菜单与元数据解析 MUST 使用 metadata-only 方式完成。
332345

333-
#### Scenario: Metadata-only attribute parsing
334-
- **WHEN** 安装器需要读取菜单属性
335-
- **THEN** 使用 metadata-only 解析(如 `System.Reflection.Metadata`
336-
- **AND** 不创建可执行的模块实例
346+
#### Scenario: Metadata-only view menu parsing
347+
- **WHEN** 安装器需要读取 View 级菜单声明
348+
- **THEN** 使用 `System.Reflection.Metadata` 解析程序集 custom attributes
349+
- **AND** 不创建任何可执行模块实例
337350
- **AND** 不触发模块程序集的静态初始化
338351

339-
#### Scenario: Failure fast on invalid metadata
340-
- **WHEN** 菜单属性缺失必填字段(如 `Route` / `DisplayName` / `Key`
341-
- **THEN** 安装失败并输出清晰诊断信息
342-
- **AND** 不写入不完整菜单到数据库
343-
344352
### Requirement: Loading trusts database validation state
345353
模块加载 MUST 信任数据库中的验证状态,不重复执行 manifest 验证,以优化启动性能。
346354

0 commit comments

Comments
 (0)