You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
243 lines
7.6 KiB
243 lines
7.6 KiB
|
1 month ago
|
# ReactiveUI 架构检查报告
|
||
|
|
|
||
|
|
## 📋 检查日期
|
||
|
|
2025年1月(当前时间)
|
||
|
|
|
||
|
|
## ✅ 符合 ReactiveUI 最佳实践的项目
|
||
|
|
|
||
|
|
### 1. ViewModels 继承结构 ✅
|
||
|
|
- **MainWindowViewModel**: 正确继承 `ReactiveObject` ✅
|
||
|
|
- **RoutableViewModel**: 正确继承 `ReactiveObject` 并实现 `IRoutableViewModel` ✅
|
||
|
|
- **所有页面 ViewModel**: 正确继承 `RoutableViewModel` ✅
|
||
|
|
- DashboardPageViewModel ✅
|
||
|
|
- UsersPageViewModel ✅
|
||
|
|
- SettingsPageViewModel ✅
|
||
|
|
- ReportsPageViewModel ✅
|
||
|
|
- HelpPageViewModel ✅
|
||
|
|
- DialogHostPageViewModel ✅
|
||
|
|
- IconsPageViewModel ✅
|
||
|
|
- EditorPageViewModel ✅
|
||
|
|
|
||
|
|
### 2. Views 继承结构 ✅
|
||
|
|
- **MainWindow**: 正确继承 `ReactiveWindow<AppViewModel>` ✅
|
||
|
|
- **所有页面 View**: 正确继承 `ReactiveUserControl<ViewModel>` ✅
|
||
|
|
- DashboardPageView ✅
|
||
|
|
- UsersPageView ✅
|
||
|
|
- SettingsPageView ✅
|
||
|
|
- ReportsPageView ✅
|
||
|
|
- HelpPageView ✅
|
||
|
|
- DialogHostPageView ✅
|
||
|
|
- IconsPageView ✅
|
||
|
|
- EditorPageView ✅
|
||
|
|
|
||
|
|
### 3. 响应式属性通知 ✅
|
||
|
|
所有 ViewModels 都正确使用 `RaiseAndSetIfChanged` 进行属性变更通知:
|
||
|
|
```csharp
|
||
|
|
public string Title
|
||
|
|
{
|
||
|
|
get => _title;
|
||
|
|
set => this.RaiseAndSetIfChanged(ref _title, value);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 4. IScreen 和 RoutingState 使用 ✅
|
||
|
|
- **AppViewModel**: 正确实现 `IScreen` 接口,提供 `RoutingState` ✅
|
||
|
|
- **MainWindowViewModel**: 正确使用 `IScreen` 进行路由导航 ✅
|
||
|
|
- **路由导航**: 使用 `Router.Navigate.Execute(viewModel)` 进行导航 ✅
|
||
|
|
|
||
|
|
### 5. ReactiveCommand 使用 ✅
|
||
|
|
- **MainWindowViewModel**: 正确使用 `ReactiveCommand` 创建命令 ✅
|
||
|
|
- NavigateCommand ✅
|
||
|
|
- CloseTabCommand ✅
|
||
|
|
- SelectTabCommand ✅
|
||
|
|
- **其他 ViewModels**: 也正确使用 ReactiveCommand ✅
|
||
|
|
|
||
|
|
### 6. 响应式编程模式 ✅
|
||
|
|
正确使用 `WhenAnyValue` 监听属性变化:
|
||
|
|
```csharp
|
||
|
|
this.WhenAnyValue(x => x.SelectedNavigationItem)
|
||
|
|
.Where(item => item != null)
|
||
|
|
.Subscribe(item => ...);
|
||
|
|
```
|
||
|
|
|
||
|
|
### 7. ViewLocator 实现 ✅
|
||
|
|
- **自定义 ViewLocator**: 正确实现 `IViewLocator` 接口 ✅
|
||
|
|
- **命名约定**: 遵循 ViewModel/View 命名约定 ✅
|
||
|
|
- **注册**: 在 App 初始化时正确注册到 Splat ✅
|
||
|
|
|
||
|
|
### 8. 路由系统集成 ✅
|
||
|
|
- **RoutedViewHost**: MainWindow.axaml 正确使用 `<reactive:RoutedViewHost Router="{Binding Router}"/>` ✅
|
||
|
|
- **路由状态监听**: 正确监听 `Router.CurrentViewModel` 变化 ✅
|
||
|
|
|
||
|
|
### 9. 依赖注入配置 ✅
|
||
|
|
- **ReactiveUI 服务注册**: 正确注册 IScreen 和 ViewModel ✅
|
||
|
|
- **服务生命周期**: 使用适当的生命周期(Singleton/Transient) ✅
|
||
|
|
|
||
|
|
### 10. XAML 绑定 ✅
|
||
|
|
- **ReactiveWindow/ReactiveUserControl**: 所有 XAML 文件正确使用 reactive 命名空间 ✅
|
||
|
|
- **绑定语法**: 使用标准的 Avalonia 绑定语法 ✅
|
||
|
|
- **命令绑定**: 正确绑定 ReactiveCommand ✅
|
||
|
|
|
||
|
|
## ⚠️ 潜在问题和建议
|
||
|
|
|
||
|
|
### 1. MainWindow ViewModel 设置不一致 ⚠️
|
||
|
|
**问题描述**:
|
||
|
|
```csharp
|
||
|
|
// MainWindow.axaml.cs
|
||
|
|
public partial class MainWindow : ReactiveWindow<AppViewModel>
|
||
|
|
{
|
||
|
|
public MainWindow(AppViewModel appViewModel, ...)
|
||
|
|
{
|
||
|
|
ViewModel = appViewModel; // ✅ 正确
|
||
|
|
DataContext = appViewModel.MainWindowViewModel; // ⚠️ 这里设置了 DataContext
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**分析**:
|
||
|
|
- `ReactiveWindow<AppViewModel>` 的 `ViewModel` 属性应该是 `AppViewModel` ✅
|
||
|
|
- 但是 `DataContext` 被设置为 `MainWindowViewModel`
|
||
|
|
- 在 XAML 中,绑定会使用 `DataContext`(MainWindowViewModel),这可以正常工作
|
||
|
|
- 但 ReactiveUI 的最佳实践是:如果使用 `ReactiveWindow<AppViewModel>`,应该通过 `ViewModel.MainWindowViewModel.Router` 来访问 Router
|
||
|
|
|
||
|
|
**当前绑定**:
|
||
|
|
```xml
|
||
|
|
<reactive:RoutedViewHost Router="{Binding Router}"/>
|
||
|
|
```
|
||
|
|
这个绑定会从 `DataContext`(MainWindowViewModel)获取 Router,可以正常工作。
|
||
|
|
|
||
|
|
**建议**:
|
||
|
|
1. **选项 1**(推荐): 如果 MainWindow 的主要功能都在 MainWindowViewModel 中,建议改为:
|
||
|
|
```csharp
|
||
|
|
public partial class MainWindow : ReactiveWindow<MainWindowViewModel>
|
||
|
|
{
|
||
|
|
public MainWindow(MainWindowViewModel viewModel, ...)
|
||
|
|
{
|
||
|
|
ViewModel = viewModel;
|
||
|
|
// 不需要单独设置 DataContext
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
这样更符合 ReactiveUI 的设计模式。
|
||
|
|
|
||
|
|
2. **选项 2**: 保持当前结构,但明确绑定路径:
|
||
|
|
```xml
|
||
|
|
<reactive:RoutedViewHost Router="{Binding ViewModel.MainWindowViewModel.Router}"/>
|
||
|
|
```
|
||
|
|
然后将 MainWindow.axaml 的绑定改为从 ViewModel.MainWindowViewModel 获取。
|
||
|
|
|
||
|
|
### 2. ViewLocator 使用 Activator.CreateInstance ⚠️
|
||
|
|
**当前实现**:
|
||
|
|
```csharp
|
||
|
|
var view = Activator.CreateInstance(viewType) as IViewFor;
|
||
|
|
```
|
||
|
|
|
||
|
|
**问题**:
|
||
|
|
- 没有使用依赖注入创建 View 实例
|
||
|
|
- 如果 View 的构造函数需要参数,会失败
|
||
|
|
|
||
|
|
**建议**:
|
||
|
|
如果项目使用依赖注入,可以考虑从 DI 容器获取 View 实例:
|
||
|
|
```csharp
|
||
|
|
// 从 ServiceProvider 获取 View(如果已注册)
|
||
|
|
var serviceProvider = Locator.Current.GetService<IServiceProvider>();
|
||
|
|
if (serviceProvider != null)
|
||
|
|
{
|
||
|
|
view = serviceProvider.GetService(viewType) as IViewFor;
|
||
|
|
}
|
||
|
|
// 如果 DI 中没有,再使用 Activator
|
||
|
|
if (view == null)
|
||
|
|
{
|
||
|
|
view = Activator.CreateInstance(viewType) as IViewFor;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3. ReactiveCommand 的异步支持 💡
|
||
|
|
**当前实现**:
|
||
|
|
```csharp
|
||
|
|
NavigateCommand = ReactiveCommand.Create<NavigationItem>(NavigateToPage);
|
||
|
|
```
|
||
|
|
|
||
|
|
**建议**:
|
||
|
|
如果命令需要异步操作或验证,可以使用:
|
||
|
|
```csharp
|
||
|
|
NavigateCommand = ReactiveCommand.CreateFromTask<NavigationItem>(async item =>
|
||
|
|
{
|
||
|
|
await NavigateToPageAsync(item);
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
### 4. 订阅的清理 💡
|
||
|
|
**当前实现**:
|
||
|
|
```csharp
|
||
|
|
_screen.Router
|
||
|
|
.CurrentViewModel
|
||
|
|
.Subscribe(viewModel => { ... });
|
||
|
|
```
|
||
|
|
|
||
|
|
**建议**:
|
||
|
|
使用 `WhenActivated` 确保订阅在视图激活时创建,在停用时清理:
|
||
|
|
```csharp
|
||
|
|
this.WhenActivated(disposables =>
|
||
|
|
{
|
||
|
|
_screen.Router
|
||
|
|
.CurrentViewModel
|
||
|
|
.Subscribe(viewModel => { ... })
|
||
|
|
.DisposeWith(disposables);
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
但这需要在 View 中实现 `IActivatableView` 接口。
|
||
|
|
|
||
|
|
## 📊 总体评分
|
||
|
|
|
||
|
|
| 检查项 | 状态 | 评分 |
|
||
|
|
|--------|------|------|
|
||
|
|
| ViewModels 继承结构 | ✅ | 10/10 |
|
||
|
|
| Views 继承结构 | ✅ | 10/10 |
|
||
|
|
| 响应式属性通知 | ✅ | 10/10 |
|
||
|
|
| IScreen/RoutingState | ✅ | 9/10 |
|
||
|
|
| ReactiveCommand | ✅ | 9/10 |
|
||
|
|
| 响应式编程模式 | ✅ | 8/10 |
|
||
|
|
| ViewLocator | ✅ | 8/10 |
|
||
|
|
| 路由系统集成 | ✅ | 9/10 |
|
||
|
|
| 依赖注入配置 | ✅ | 9/10 |
|
||
|
|
| XAML 绑定 | ✅ | 10/10 |
|
||
|
|
|
||
|
|
**总体评分**: **9.0/10** ⭐⭐⭐⭐⭐
|
||
|
|
|
||
|
|
## 🎯 结论
|
||
|
|
|
||
|
|
项目**基本完全遵循 ReactiveUI 最佳实践**,架构设计良好。主要符合点:
|
||
|
|
|
||
|
|
1. ✅ 正确的继承结构(ReactiveObject, ReactiveUserControl, ReactiveWindow)
|
||
|
|
2. ✅ 完整的路由系统集成(IScreen, RoutingState, RoutedViewHost)
|
||
|
|
3. ✅ 响应式编程模式(RaiseAndSetIfChanged, ReactiveCommand, WhenAnyValue)
|
||
|
|
4. ✅ 自定义 ViewLocator 实现
|
||
|
|
5. ✅ 完整的依赖注入集成
|
||
|
|
|
||
|
|
**主要建议**:
|
||
|
|
1. 考虑统一 MainWindow 的 ViewModel 设置(选项 1 更推荐)
|
||
|
|
2. 改进 ViewLocator 以支持依赖注入
|
||
|
|
3. 考虑使用 `WhenActivated` 进行订阅管理
|
||
|
|
|
||
|
|
## 📝 最佳实践检查清单
|
||
|
|
|
||
|
|
- [x] ViewModels 继承 ReactiveObject
|
||
|
|
- [x] 路由 ViewModels 实现 IRoutableViewModel
|
||
|
|
- [x] Views 继承 ReactiveUserControl 或 ReactiveWindow
|
||
|
|
- [x] 使用 RaiseAndSetIfChanged 进行属性通知
|
||
|
|
- [x] 使用 ReactiveCommand 创建命令
|
||
|
|
- [x] 使用 IScreen 和 RoutingState 进行路由
|
||
|
|
- [x] 使用 RoutedViewHost 显示路由内容
|
||
|
|
- [x] 实现自定义 ViewLocator
|
||
|
|
- [x] 正确注册 ReactiveUI 服务
|
||
|
|
- [x] 使用响应式编程模式(WhenAnyValue 等)
|
||
|
|
- [ ] 使用 WhenActivated 管理订阅(可选)
|
||
|
|
- [ ] ViewLocator 支持依赖注入(可选)
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
**检查完成日期**: 2025年1月
|
||
|
|
**检查工具**: AI 代码分析
|