235 lines
7.2 KiB
Markdown
235 lines
7.2 KiB
Markdown
|
|
# 软件开发文档 - 06. 动态菜单与导航设计
|
|||
|
|
|
|||
|
|
本文档详细阐述了一套基于数据库驱动的动态菜单和参数化导航系统的设计方案,旨在与 `iNKORE.UI.WPF.Modern` 等现代化UI框架无缝集成。
|
|||
|
|
|
|||
|
|
## 1. 设计目标
|
|||
|
|
|
|||
|
|
1. **菜单动态化**:应用程序的导航菜单(结构、文本、图标)应由数据库定义,允许在不重新编译程序的情况下进行修改。
|
|||
|
|
2. **视图解耦**:菜单点击(导航发起者)与目标视图(导航接收者)之间不应存在直接引用。
|
|||
|
|
3. **参数化导航**:导航时必须能够安全、清晰地将参数(如一个具体的设备ID)传递给目标视图模型。
|
|||
|
|
4. **层级支持**:支持无限层级的父/子菜单结构。
|
|||
|
|
|
|||
|
|
## 2. 数据库设计 (`DMS.Infrastructure`)
|
|||
|
|
|
|||
|
|
我们创建一个自引用的 `Menus` 表来存储菜单的树状结构。
|
|||
|
|
|
|||
|
|
```csharp
|
|||
|
|
// 文件: DMS.Infrastructure/Entities/DbMenu.cs
|
|||
|
|
using SqlSugar;
|
|||
|
|
|
|||
|
|
namespace DMS.Infrastructure.Entities;
|
|||
|
|
|
|||
|
|
[SugarTable("Menus")]
|
|||
|
|
public class DbMenu
|
|||
|
|
{
|
|||
|
|
[SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
|
|||
|
|
public int Id { get; set; }
|
|||
|
|
|
|||
|
|
[SugarColumn(IsNullable = true)]
|
|||
|
|
public int? ParentId { get; set; }
|
|||
|
|
|
|||
|
|
public string Header { get; set; }
|
|||
|
|
|
|||
|
|
public string Icon { get; set; }
|
|||
|
|
|
|||
|
|
public string TargetViewKey { get; set; }
|
|||
|
|
|
|||
|
|
[SugarColumn(IsNullable = true)]
|
|||
|
|
public string NavigationParameter { get; set; }
|
|||
|
|
|
|||
|
|
public int DisplayOrder { get; set; }
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 3. 核心导航契约 (`DMS.WPF`)
|
|||
|
|
|
|||
|
|
这是整个导航机制的核心,定义了组件间如何通信。
|
|||
|
|
|
|||
|
|
### 3.1. `INavigatable` 接口
|
|||
|
|
|
|||
|
|
任何需要接收导航参数的ViewModel都必须实现此接口。
|
|||
|
|
|
|||
|
|
```csharp
|
|||
|
|
// 文件: DMS.WPF/Services/INavigatable.cs
|
|||
|
|
namespace DMS.WPF.Services;
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 定义了一个契约,表示ViewModel可以安全地接收导航传入的参数。
|
|||
|
|
/// </summary>
|
|||
|
|
public interface INavigatable
|
|||
|
|
{
|
|||
|
|
/// <summary>
|
|||
|
|
/// 当导航到此ViewModel时,由导航服务调用此方法,以传递参数。
|
|||
|
|
/// </summary>
|
|||
|
|
/// <param name="parameter">从导航源传递过来的参数对象。</param>
|
|||
|
|
Task OnNavigatedToAsync(object parameter);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3.2. `INavigationService` 接口与实现
|
|||
|
|
|
|||
|
|
重构后的导航服务,支持基于字符串键和参数的导航。
|
|||
|
|
|
|||
|
|
```csharp
|
|||
|
|
// 文件: DMS.WPF/Services/INavigationService.cs
|
|||
|
|
public interface INavigationService
|
|||
|
|
{
|
|||
|
|
Task NavigateToAsync(string viewKey, object parameter = null);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 文件: DMS.WPF/Services/NavigationService.cs
|
|||
|
|
public class NavigationService : INavigationService
|
|||
|
|
{
|
|||
|
|
private readonly IServiceProvider _serviceProvider;
|
|||
|
|
private readonly MainViewModel _mainViewModel;
|
|||
|
|
|
|||
|
|
public NavigationService(IServiceProvider sp, MainViewModel mainVm) { /*...*/ }
|
|||
|
|
|
|||
|
|
public async Task NavigateToAsync(string viewKey, object parameter = null)
|
|||
|
|
{
|
|||
|
|
if (string.IsNullOrEmpty(viewKey)) return;
|
|||
|
|
|
|||
|
|
var viewModelType = GetViewModelTypeByKey(viewKey);
|
|||
|
|
var viewModel = _serviceProvider.GetRequiredService(viewModelType) as BaseViewModel;
|
|||
|
|
|
|||
|
|
if (viewModel is INavigatable navigatableVm)
|
|||
|
|
{
|
|||
|
|
await navigatableVm.OnNavigatedToAsync(parameter);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
_mainViewModel.CurrentViewModel = viewModel;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private Type GetViewModelTypeByKey(string key)
|
|||
|
|
{
|
|||
|
|
return key switch
|
|||
|
|
{
|
|||
|
|
"DashboardView" => typeof(DashboardViewModel),
|
|||
|
|
"DeviceListView" => typeof(DeviceListViewModel),
|
|||
|
|
"DeviceDetailView" => typeof(DeviceDetailViewModel),
|
|||
|
|
_ => throw new KeyNotFoundException($"未找到与键 '{key}' 关联的视图模型类型。"),
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 4. 菜单构建与显示 (`DMS.WPF`)
|
|||
|
|
|
|||
|
|
### 4.1. `MenuItemViewModel`
|
|||
|
|
|
|||
|
|
这是 `ui:NavigationView` 直接绑定的数据对象。
|
|||
|
|
|
|||
|
|
```csharp
|
|||
|
|
// 文件: DMS.WPF/ViewModels/Items/MenuItemViewModel.cs
|
|||
|
|
public partial class MenuItemViewModel : ObservableObject
|
|||
|
|
{
|
|||
|
|
[ObservableProperty] private string _header;
|
|||
|
|
[ObservableProperty] private string _icon;
|
|||
|
|
public ObservableCollection<MenuItemViewModel> Children { get; } = new();
|
|||
|
|
public ICommand NavigateCommand { get; }
|
|||
|
|
|
|||
|
|
public MenuItemViewModel(string targetViewKey, object navParameter, INavigationService navService)
|
|||
|
|
{
|
|||
|
|
NavigateCommand = new RelayCommand(async () =>
|
|||
|
|
{
|
|||
|
|
await navService.NavigateToAsync(targetViewKey, navParameter);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 4.2. `IMenuService` (应用层/基础设施层)
|
|||
|
|
|
|||
|
|
此服务负责从数据库加载菜单并构建ViewModel树。
|
|||
|
|
|
|||
|
|
```csharp
|
|||
|
|
// 应用层接口: DMS.Application/Interfaces/IMenuService.cs
|
|||
|
|
public interface IMenuService
|
|||
|
|
{
|
|||
|
|
Task<List<MenuItemViewModel>> GetMenuItemsAsync();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 基础设施层实现: DMS.Infrastructure/Services/MenuService.cs
|
|||
|
|
public class MenuService : IMenuService
|
|||
|
|
{
|
|||
|
|
private readonly IRepositoryManager _repoManager;
|
|||
|
|
private readonly INavigationService _navigationService;
|
|||
|
|
|
|||
|
|
public MenuService(IRepositoryManager repoManager, INavigationService navigationService)
|
|||
|
|
{ /*...*/ }
|
|||
|
|
|
|||
|
|
public async Task<List<MenuItemViewModel>> GetMenuItemsAsync()
|
|||
|
|
{
|
|||
|
|
var allDbMenus = await _repoManager.Menus.GetAllAsync();
|
|||
|
|
// ... 此处编写逻辑,将 allDbMenus (扁平列表) ...
|
|||
|
|
// ... 转换为 MenuItemViewModel 的树状结构 (通过ParentId) ...
|
|||
|
|
// 示例:
|
|||
|
|
// var menuItem = new MenuItemViewModel(dbMenu.TargetViewKey, dbMenu.NavigationParameter, _navigationService);
|
|||
|
|
return new List<MenuItemViewModel>(); // 返回构建好的顶级菜单项
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 5. 目标视图模型实现 (`DMS.WPF`)
|
|||
|
|
|
|||
|
|
这是一个需要接收设备ID作为参数的ViewModel示例。
|
|||
|
|
|
|||
|
|
```csharp
|
|||
|
|
// 文件: DMS.WPF/ViewModels/DeviceDetailViewModel.cs
|
|||
|
|
public class DeviceDetailViewModel : BaseViewModel, INavigatable
|
|||
|
|
{
|
|||
|
|
private readonly IDeviceAppService _deviceAppService;
|
|||
|
|
|
|||
|
|
[ObservableProperty]
|
|||
|
|
private DeviceDetailDto _device;
|
|||
|
|
|
|||
|
|
public DeviceDetailViewModel(IDeviceAppService deviceAppService) { /*...*/ }
|
|||
|
|
|
|||
|
|
public async Task OnNavigatedToAsync(object parameter)
|
|||
|
|
{
|
|||
|
|
if (parameter is not int deviceId) return;
|
|||
|
|
|
|||
|
|
IsBusy = true;
|
|||
|
|
Device = await _deviceAppService.GetDeviceDetailAsync(deviceId);
|
|||
|
|
IsBusy = false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 6. UI绑定与启动 (`DMS.WPF`)
|
|||
|
|
|
|||
|
|
`MainViewModel` 负责加载菜单,`MainWindow.xaml` 负责显示。
|
|||
|
|
|
|||
|
|
```csharp
|
|||
|
|
// MainViewModel.cs
|
|||
|
|
public class MainViewModel : BaseViewModel
|
|||
|
|
{
|
|||
|
|
public ObservableCollection<MenuItemViewModel> MenuItems { get; } = new();
|
|||
|
|
|
|||
|
|
public MainViewModel(IMenuService menuService, /*...*/) { /*...*/ }
|
|||
|
|
|
|||
|
|
public override async Task LoadAsync()
|
|||
|
|
{
|
|||
|
|
var menus = await _menuService.GetMenuItemsAsync();
|
|||
|
|
foreach(var menu in menus) { MenuItems.Add(menu); }
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
```xml
|
|||
|
|
<!-- MainWindow.xaml -->
|
|||
|
|
<ui:NavigationView ItemsSource="{Binding MenuItems}">
|
|||
|
|
<ui:NavigationView.MenuItemTemplate>
|
|||
|
|
<DataTemplate>
|
|||
|
|
<!-- 使用 HierarchicalDataTemplate 支持子菜单 -->
|
|||
|
|
<ui:NavigationViewItem Header="{Binding Header}"
|
|||
|
|
Icon="{Binding Icon}"
|
|||
|
|
Command="{Binding NavigateCommand}"
|
|||
|
|
ItemsSource="{Binding Children}" />
|
|||
|
|
</DataTemplate>
|
|||
|
|
</ui:NavigationView.MenuItemTemplate>
|
|||
|
|
|
|||
|
|
<ContentControl Content="{Binding CurrentViewModel}" />
|
|||
|
|
</ui:NavigationView>
|
|||
|
|
```
|