Skip to content

Commit 82cb632

Browse files
authored
Merge pull request #26 from AGIBuild/spec/add-view-level-menus
spec/add view level menus
2 parents 452cd15 + 8df9554 commit 82cb632

File tree

62 files changed

+2699
-324
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

62 files changed

+2699
-324
lines changed

Directory.Build.props

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,14 @@
1717
<Authors>Modulus Team</Authors>
1818
<Company>Agibuild</Company>
1919
<Copyright>Copyright © $([System.DateTime]::UtcNow.Year) Agibuild. All rights reserved.</Copyright>
20-
<PackageProjectUrl>https://github.com/user/Modulus</PackageProjectUrl>
20+
<PackageProjectUrl>https://github.com/AGIBuild/Modulus</PackageProjectUrl>
2121
<PackageLicenseExpression>MIT</PackageLicenseExpression>
2222
<PackageReadmeFile>README.md</PackageReadmeFile>
2323
<PackageIcon>icon.png</PackageIcon>
2424
<PackageTags>modulus;plugin;module;extensibility;avalonia;blazor;desktop;web</PackageTags>
25-
<RepositoryUrl>https://github.com/user/Modulus</RepositoryUrl>
25+
<RepositoryUrl>https://github.com/AGIBuild/Modulus</RepositoryUrl>
2626
<RepositoryType>git</RepositoryType>
27+
<PublishRepositoryUrl>true</PublishRepositoryUrl>
2728

2829
<!-- Build Properties -->
2930
<LangVersion>latest</LangVersion>

docs/getting-started.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ Modulus Module (Blazor) modulus-blazor [C#] Modulus/Module/Plugin/B
4343

4444
```bash
4545
# Clone the repository
46-
git clone https://github.com/user/Modulus.git
46+
git clone https://github.com/AGIBuild/Modulus.git
4747
cd Modulus
4848

4949
# Build the solution
@@ -281,6 +281,6 @@ Menus are read from the database at render time. If you upgraded from an older b
281281

282282
## Getting Help
283283

284-
- GitHub Issues: [Report bugs or request features](https://github.com/user/Modulus/issues)
285-
- Discussions: [Ask questions](https://github.com/user/Modulus/discussions)
284+
- GitHub Issues: [Report bugs or request features](https://github.com/AGIBuild/Modulus/issues)
285+
- Discussions: [Ask questions](https://github.com/AGIBuild/Modulus/discussions)
286286

docs/getting-started.zh-CN.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ Modulus Module (Blazor) modulus-blazor [C#] Modulus/Module/Plugin/B
4343

4444
```bash
4545
# 克隆仓库
46-
git clone https://github.com/user/Modulus.git
46+
git clone https://github.com/AGIBuild/Modulus.git
4747
cd Modulus
4848

4949
# 构建解决方案
@@ -281,6 +281,6 @@ dotnet restore
281281

282282
## 获取帮助
283283

284-
- GitHub Issues: [报告 bug 或请求功能](https://github.com/user/Modulus/issues)
285-
- Discussions: [提问](https://github.com/user/Modulus/discussions)
284+
- GitHub Issues: [报告 bug 或请求功能](https://github.com/AGIBuild/Modulus/issues)
285+
- Discussions: [提问](https://github.com/AGIBuild/Modulus/discussions)
286286

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
## Context
2+
本变更以“模块为一级菜单,View 为二级菜单”为目标,并提供默认基础设施使第三方模块开发者无需编写额外注册代码:
3+
- 菜单投影在安装/更新期完成,且 MUST 为 metadata-only(不执行模块代码)。
4+
- 运行时导航基于稳定 Key 查表解析目标,避免 Avalonia 运行时全域类型扫描。
5+
- ViewModel 可以通过继承基类(或实现接口)覆写 `NavigateFrom/NavigateTo` 拦截与生命周期回调。
6+
7+
## Goals / Non-Goals
8+
- Goals:
9+
- 提供模块级菜单(父)与 View 级菜单(子)的层级模型(DB `ParentId`)。
10+
- 单 View 模块主导航只显示模块级菜单;多 View 模块显示模块级菜单 + 子 View 菜单。
11+
- ViewModel 可覆写导航拦截与导航生命周期,无需额外注册。
12+
- 模板默认包含 View 级菜单声明示例。
13+
- Non-Goals:
14+
- 本变更不要求取消模块级菜单声明(仍作为父节点来源)。
15+
- 本变更不要求把所有菜单完全“零配置”(DisplayName/Icon/Order 仍建议显式声明)。
16+
17+
## Decisions
18+
### Decision: 引入 View 级菜单 Attribute(Host-specific)
19+
为保持安装期 metadata-only 可解析与 Host 行为一致,新增:
20+
- `BlazorViewMenuAttribute`:声明 View(组件)在模块导航中的菜单信息(key/display/icon/order 等)。
21+
- `AvaloniaViewMenuAttribute`:声明 ViewModel 在模块导航中的菜单信息(key/display/icon/order 等)。
22+
23+
说明:
24+
- Blazor 的 route 从 `RouteAttribute`(由 `@page` 生成)获取;`BlazorViewMenuAttribute` 不负责提供 route,仅提供菜单元数据。
25+
- Avalonia 的导航目标最终解析为 ViewModel 类型 + View 类型;`AvaloniaViewMenuAttribute` 放在 ViewModel 类型上可直接得到 ViewModel Type。
26+
27+
### Decision: 使用 DB `MenuEntity.ParentId` 形成层级菜单
28+
安装期写入:
29+
- 模块级菜单:`ParentId = null`
30+
- View 级菜单:`ParentId = <模块级菜单.Id>`
31+
32+
运行时从 DB 读取后构建 `MenuItem.Children`,UI 直接使用树结构渲染。
33+
34+
### Decision: 单 View 折叠规则(View 菜单声明仍要求存在,但不生效)
35+
- 模块内 View 数量 == 1:
36+
- 主导航仅显示模块级菜单(来自模块入口类型声明)
37+
- 忽略 View 级菜单(不写入 DB,或写入但运行时折叠;推荐安装期不写入,避免噪音)
38+
- 模块内 View 数量 > 1:
39+
- 模块级菜单作为父节点显示(名字来自模块声明)
40+
- View 级菜单作为子节点显示(名字来自 View 声明)
41+
42+
### Decision: ViewModel 导航拦截与生命周期(可覆写)
43+
`Modulus.UI.Abstractions` 引入:
44+
- `ViewModelBase`(建议):
45+
- `virtual Task<bool> CanNavigateFromAsync(NavigationContext context)` 默认 true
46+
- `virtual Task<bool> CanNavigateToAsync(NavigationContext context)` 默认 true
47+
- `virtual Task OnNavigatedFromAsync(NavigationContext context)` 默认 completed
48+
- `virtual Task OnNavigatedToAsync(NavigationContext context)` 默认 completed(包含参数)
49+
50+
并允许不继承场景:
51+
- `INavigationParticipant`(可选接口)与基类语义一致
52+
53+
导航服务拦截顺序(建议):
54+
1) 全局 `INavigationGuard``CanNavigateFromAsync` / `CanNavigateToAsync`
55+
2) 当前 ViewModel(如果支持)`CanNavigateFromAsync`
56+
3) 目标 ViewModel(如果支持)`CanNavigateToAsync`
57+
4) 发生切换后:当前 `OnNavigatedFromAsync` → 目标 `OnNavigatedToAsync`
58+
59+
### Decision: Avalonia View 使用 DI 构造(不提供无参构造)
60+
为统一“View 决定绑定哪个 ViewModel”的约定,并减少模板/示例中的样板代码:
61+
- View 类型 SHOULD 仅提供带依赖注入参数的构造函数(例如 `MyView(MyViewModel vm)`),不提供无参构造函数。
62+
- View 构造函数内负责完成绑定(如 `DataContext = vm`)。
63+
- 设计态(Designer)通过 `Design.IsDesignMode` 分支处理,保证 XAML 设计器可用:
64+
- 设计态可以创建一个轻量的 DesignTime ViewModel(或使用默认值),避免依赖运行时 DI/模块加载器。
65+
66+
### Decision: Module View / ViewModel MUST 由模块 CompositeServiceProvider 创建(Host 不直接创建模块类型)
67+
为保证模块隔离与卸载安全,以及让模块对象可以同时依赖“模块服务 + Host 服务”:
68+
- 导航创建模块 ViewModel 时 MUST 使用 `RuntimeModuleHandle.CompositeServiceProvider`(primary=模块,fallback=Host)。
69+
- UI 工厂创建模块 View 时 MUST 使用同一个 `RuntimeModuleHandle.CompositeServiceProvider`,以支持 View 构造函数注入模块 ViewModel(以及其它模块服务)。
70+
- Host 的根 `IServiceProvider` MUST NOT 直接构造模块类型(View/ViewModel/服务实现),避免跨 ALC 类型泄漏与卸载阻塞。
71+
- Host 仅应依赖共享域抽象(`Modulus.UI.Abstractions` 等)与稳定的 `navigationKey`
72+
73+
## Migration Plan
74+
- 先实现 DB 分层注册与 UI 渲染树结构(不会破坏现有单层菜单,只是添加 children)。
75+
- 为兼容历史 DB 中 `ParentId=null` 的扁平结构,运行时可以:
76+
- 当所有项 `ParentId==null` 时仍按原逻辑显示(等价)。
77+
- 模板更新只影响新创建模块,不影响现有模块。
78+
79+
## Open Questions
80+
- 多 View 模块的父菜单点击行为:仅展开/收起(不触发导航;父节点 `NavigationKey` 为空,由 UI 负责切换展开态)
81+
- View 级菜单 key 的规范:是否要求稳定且与 route/类型解耦?(推荐:`<viewKey>` 由开发者声明,DB id 仍由系统规则拼装)
82+
83+
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Change: add-view-level-menus(引入 View 级菜单声明 + 分层导航 + ViewModel 导航拦截)
2+
3+
## Why
4+
当前导航/菜单体系存在三个核心问题:
5+
- 菜单缺少“模块 → 视图”层级结构,无法在主导航中以模块为一级、模块内页面为二级进行呈现。
6+
- Avalonia 导航依赖运行时 Type 全域扫描(`Type.GetType` + 遍历 assemblies),性能不可控,且把“导航 key”与类型名强绑定,重构易破坏。
7+
- ViewModel 无法以统一、可覆写的方式参与 `NavigateFrom/NavigateTo` 拦截与生命周期回调,导致拦截逻辑分散在 Guard/Service/UI 里。
8+
9+
本变更提供一套默认基础设施:模块开发者只需要在每个 View(或其对应 ViewModel)上声明 View 级菜单信息,框架即可在安装期进行 metadata-only 投影,运行时按 Key 解析目标并完成 View/ViewModel 绑定,无需额外注册代码。
10+
11+
## What Changes
12+
- **新增** View 级菜单声明机制(metadata-only),并将 DB `Menus` 投影从扁平结构升级为“模块(父) → View(子)”层级结构(使用 `ParentId`)。
13+
- **修改** 导航解析:导航服务以稳定 `NavigationKey` 为入口,通过注册表/索引解析目标,避免 Avalonia 全域类型扫描。
14+
- **新增** ViewModel 导航生命周期与拦截点(可覆写 `CanNavigateFrom/CanNavigateTo/OnNavigatedFrom/OnNavigatedTo`),并与现有 `INavigationGuard` 协同工作。
15+
- **修改** 模块模板(VS/CLI):生成的示例 View/VM 默认包含 View 级菜单声明与导航生命周期覆写示例。
16+
17+
## Impact
18+
- Affected specs:
19+
- `openspec/specs/navigation/spec.md`
20+
- `openspec/specs/runtime/spec.md`
21+
- `openspec/specs/module-template/spec.md`
22+
- Affected code (implementation phase):
23+
- 安装期菜单投影:`src/Modulus.Core/Installation/ModuleInstallerService.cs``src/Modulus.Core/Installation/ModuleMenuAttributeReader.cs`
24+
- 运行时菜单组装:`src/Modulus.Core/Runtime/ModulusApplication.cs`
25+
- 导航:`src/Hosts/Modulus.Host.Avalonia/Services/AvaloniaNavigationService.cs``src/Hosts/Modulus.Host.Blazor/Services/BlazorNavigationService.cs`
26+
- 模板:`templates/**``src/Modulus.Cli/Templates/**`
27+
28+
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
## MODIFIED Requirements
2+
### Requirement: Module Project Structure
3+
系统 SHALL 根据目标 Host 生成对应的模块项目结构。
4+
5+
#### Scenario: Avalonia 模块包含 View 级菜单声明
6+
- **WHEN** 创建 Avalonia 模块
7+
- **THEN** 生成的默认 ViewModel(如 `MainViewModel`)包含 View 级菜单声明(例如 `[AvaloniaViewMenu]`
8+
- **AND** 生成的默认 View(如 `MainView.axaml`)与 ViewModel 通过约定绑定(无需额外 `IViewRegistry.Register` 代码)
9+
- **AND** 生成的 View 不包含无参构造函数,仅提供 DI 构造函数(例如 `MainView(MainViewModel vm)`
10+
- **AND** 生成的 View 使用 `Design.IsDesignMode` 分支支持设计态渲染(无需运行时 DI)
11+
- **AND** 模板包含覆写导航生命周期/拦截的示例代码(如 `CanNavigateFromAsync/CanNavigateToAsync`
12+
13+
#### Scenario: Blazor 模块包含 View 级菜单声明
14+
- **WHEN** 创建 Blazor 模块
15+
- **THEN** 生成的默认页面(如 `MainView.razor`)包含 View 级菜单声明(例如 `@attribute [BlazorViewMenu]`
16+
- **AND** 页面包含/继承支持 ViewModel 绑定的基类示例(使 ViewModel 可覆写导航生命周期/拦截)
17+
18+
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
## MODIFIED Requirements
2+
### Requirement: Navigation Service
3+
The framework SHALL provide an `INavigationService` abstraction that enables programmatic navigation with interception capabilities across all hosts.
4+
5+
#### Scenario: Navigate to a registered view
6+
- **WHEN** 模块调用 `NavigateToAsync(navigationKey)`
7+
- **THEN** 导航服务基于稳定 `navigationKey` 解析目标(而不是运行时全域扫描 Type)
8+
- **AND** 解析结果包含:所属模块、目标 View(或 Route)、目标 ViewModel、实例生命周期策略
9+
- **AND** 内容区域显示对应 View
10+
11+
#### Scenario: Navigate with typed ViewModel
12+
- **WHEN** 模块调用 `NavigateToAsync<TViewModel>()`
13+
- **THEN** 导航服务通过注册表/约定映射解析 `TViewModel` 对应的 `navigationKey`
14+
- **AND** 导航行为与 `NavigateToAsync(navigationKey)` 等价
15+
16+
#### Scenario: Navigation exposes current state
17+
- **WHEN** 导航成功完成
18+
- **THEN** `CurrentNavigationKey` 反映当前激活的 `navigationKey`
19+
- **AND** `Navigated` event MUST 被触发,且包含 `FromKey/ToKey` 与可选的 `View/ViewModel` 实例
20+
21+
### Requirement: Navigation Guards
22+
The framework SHALL support registration of navigation guards that can intercept and conditionally prevent navigation.
23+
24+
#### Scenario: Guard prevents navigation from current page
25+
- **WHEN** 用户触发从 A 到 B 的导航
26+
- **AND** 任一 guard 的 `CanNavigateFromAsync` 返回 false
27+
- **THEN** 导航取消,当前视图保持不变
28+
29+
#### Scenario: Guard prevents navigation to target page
30+
- **WHEN** 用户触发从 A 到 B 的导航
31+
- **AND** 任一 guard 的 `CanNavigateToAsync` 返回 false
32+
- **THEN** 导航取消,当前视图保持不变
33+
34+
### Requirement: ViewModel Navigation Lifecycle
35+
The framework SHALL provide ViewModel-level navigation interception and lifecycle hooks that can be overridden without extra registration code.
36+
37+
#### Scenario: ViewModel can intercept NavigateFrom
38+
- **GIVEN** 当前 ViewModel 支持导航拦截(通过继承基类或实现接口)
39+
- **WHEN** 从当前页面导航到新页面
40+
- **THEN** 框架调用 `CanNavigateFromAsync(context)`
41+
- **AND** 若返回 false,则导航被取消
42+
43+
#### Scenario: ViewModel can intercept NavigateTo
44+
- **GIVEN** 目标 ViewModel 支持导航拦截(通过继承基类或实现接口)
45+
- **WHEN** 导航到目标页面
46+
- **THEN** 框架调用 `CanNavigateToAsync(context)`
47+
- **AND** 若返回 false,则导航被取消且不改变当前页面
48+
49+
#### Scenario: ViewModel receives lifecycle callbacks
50+
- **WHEN** 导航从 A 切换到 B 成功完成
51+
- **THEN** 框架调用 A 的 `OnNavigatedFromAsync(context)`
52+
- **AND** 调用 B 的 `OnNavigatedToAsync(context)`(包含参数)
53+
54+
### Requirement: Hierarchical Menu Navigation
55+
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.
56+
57+
#### Scenario: Single-view module shows only module menu
58+
- **GIVEN** 某模块仅包含 1 个 View(可导航页面)
59+
- **AND** 该 View 仍声明了 View 级菜单元数据
60+
- **WHEN** 导航菜单渲染
61+
- **THEN** 主导航仅显示模块级菜单项(名称来自模块级菜单声明)
62+
- **AND** 不显示二级 View 菜单
63+
64+
#### Scenario: Multi-view module shows module menu and view children
65+
- **GIVEN** 某模块包含多个 View(可导航页面)
66+
- **WHEN** 导航菜单渲染
67+
- **THEN** 主导航显示模块级菜单项(名称来自模块级菜单声明)
68+
- **AND** 该菜单项包含二级子项,每个子项名称来自对应 View 的菜单声明
69+
70+
#### Scenario: Multi-view module parent menu expands only
71+
- **GIVEN** 某模块包含多个 View(可导航页面)
72+
- **WHEN** 用户点击模块级父菜单项
73+
- **THEN** 菜单仅展开/收起以显示/隐藏子菜单
74+
- **AND** 不触发导航(父菜单项不包含可导航的 `NavigationKey`
75+
76+
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
## MODIFIED Requirements
2+
### Requirement: Menu Projection
3+
Menu entries SHALL be projected to the database at install/update time and read from the database at render time.
4+
5+
#### Scenario: Install or update module projects menus to database
6+
- **WHEN** 模块被安装或更新
7+
- **THEN** 安装器解析模块入口类型上的模块级菜单属性(Blazor: `[BlazorMenu]`,Avalonia: `[AvaloniaMenu]`
8+
- **AND** 安装器以 metadata-only 方式解析模块 UI 程序集中的 View 级菜单属性(新增)
9+
- **AND** 将菜单写入数据库表 `Menus`,并构建父子层级(父为模块级菜单,子为 View 级菜单,使用 `ParentId`
10+
11+
#### Scenario: Single-view module ignores view-level menus
12+
- **GIVEN** 模块 UI 程序集中可识别的 View 数量为 1
13+
- **AND** 该 View 声明了 View 级菜单元数据
14+
- **WHEN** 安装器投影菜单到数据库
15+
- **THEN** 数据库中仅写入模块级菜单项(`ParentId = null`
16+
- **AND** 不写入该 View 的二级菜单项(避免重复)
17+
18+
#### Scenario: Multi-view module projects view-level menus as children
19+
- **GIVEN** 模块 UI 程序集中可识别的 View 数量大于 1
20+
- **WHEN** 安装器投影菜单到数据库
21+
- **THEN** 数据库中写入 1 条模块级菜单项(`ParentId = null`
22+
- **AND** 为每个 View 写入 1 条二级菜单项(`ParentId = <模块级菜单Id>`
23+
24+
#### Scenario: Render menus reads from database only
25+
- **WHEN** Shell 渲染导航菜单
26+
- **THEN** 从数据库读取 `Menus``Modules`(仅 `IsEnabled=true` 且状态可加载)
27+
- **AND** 运行时将 DB 菜单组装为树结构注册到 `IMenuRegistry`
28+
- **AND** 渲染过程不进行任何 DLL 动态解析(不反射、不 metadata 扫描、不读取菜单属性)
29+
30+
### Requirement: Installation without executing module code
31+
模块安装/更新 MUST 不执行第三方模块代码;菜单与元数据解析 MUST 使用 metadata-only 方式完成。
32+
33+
#### Scenario: Metadata-only view menu parsing
34+
- **WHEN** 安装器需要读取 View 级菜单声明
35+
- **THEN** 使用 `System.Reflection.Metadata` 解析程序集 custom attributes
36+
- **AND** 不创建任何可执行模块实例
37+
- **AND** 不触发模块程序集的静态初始化
38+
39+
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
## 1. Implementation
2+
- [x] 1.1 在 `Modulus.Sdk` 新增 View 级菜单 Attribute(Blazor/Avalonia),并补齐 XML 注释与使用示例
3+
- [x] 1.2 扩展安装期 metadata-only 菜单投影:读取模块级菜单(现有)+ 读取 View 级菜单(新增),按“模块父 → View 子”写入 `Menus`(填充 `ParentId`
4+
- [x] 1.3 实现“单 View 折叠规则”:View 数量为 1 时忽略 View 级菜单(仍允许声明),仅使用模块级菜单作为主菜单项
5+
- [x] 1.4 运行时从 DB 注册 `IMenuRegistry` 时构建层级 `MenuItem.Children`,确保 Avalonia/Blazor Shell 渲染使用树结构
6+
- [x] 1.5 Avalonia 导航从“类型扫描”改为“Key→Target 索引”,并引入 View↔ViewModel 约定绑定(无需模块手写 `IViewRegistry.Register`
7+
- [x] 1.5.1 Avalonia UI 工厂创建模块 View 时 MUST 使用 `RuntimeModuleHandle.CompositeServiceProvider`(支持 `MyView(MyViewModel vm)` 形式的 DI 构造与 Host 服务注入)
8+
- [x] 1.6 Blazor 导航补齐 Key→Route 解析(避免基于命名猜测),并保持与 DB 投影一致
9+
- [x] 1.7 新增 ViewModel 导航生命周期/拦截(可覆写 NavigateFrom/NavigateTo),并在导航服务中自动调用
10+
- [x] 1.8 更新 `modulus new` 与 VS 模板:默认生成的 View/VM 包含 View 级菜单声明与导航拦截示例
11+
- [x] 1.9 补充单元测试覆盖(必须):
12+
- [x] 1.9.1 扩展 `tests/Modulus.Core.Tests/Installation/ModuleInstallerServiceMenuProjectionTests.cs`:覆盖 View 级菜单投影、`ParentId` 层级写入、单 View 折叠(View 菜单存在但不生效)
13+
- [x] 1.9.2 新增/扩展运行时菜单树组装测试(建议新增 `tests/Modulus.Core.Tests/Runtime/MenuTreeTests.cs`):覆盖从 DB 菜单组装 `MenuItem.Children`,以及多 View 父菜单 `NavigationKey` 为空(仅展开不导航)
14+
- [x] 1.9.3 新增导航生命周期与拦截顺序测试(建议 `tests/Modulus.Hosts.Tests/NavigationViewModelLifecycleTests.cs`):覆盖 guards → 当前 VM `CanNavigateFrom` → 目标 VM `CanNavigateTo``OnNavigatedFrom/To` 的调用顺序与短路行为
15+
- [x] 1.9.4 新增 Avalonia 控件层级菜单行为测试(建议扩展 `tests/Modulus.UI.Avalonia.Tests/NavigationViewTests.cs`):覆盖父节点无 `NavigationKey` 时点击仅切换展开态、不触发导航命令
16+
- [ ] 1.10 增加“完整导航机制”回归测试(可选但推荐):在 `tests/Modulus.Hosts.Tests/ModulusApplicationIntegrationTests.cs` 基础上构造最小模块样例(单 View/多 View),验证从安装投影 → 运行时注册 → 导航解析的端到端链路(未实现)
17+
18+

0 commit comments

Comments
 (0)