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.
 
 
 
 

7.6 KiB

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 进行属性变更通知:

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 监听属性变化:

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 设置不一致 ⚠️

问题描述:

// 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

当前绑定:

<reactive:RoutedViewHost Router="{Binding Router}"/>

这个绑定会从 DataContext(MainWindowViewModel)获取 Router,可以正常工作。

建议:

  1. 选项 1(推荐): 如果 MainWindow 的主要功能都在 MainWindowViewModel 中,建议改为:

    public partial class MainWindow : ReactiveWindow<MainWindowViewModel>
    {
        public MainWindow(MainWindowViewModel viewModel, ...)
        {
            ViewModel = viewModel;
            // 不需要单独设置 DataContext
        }
    }
    

    这样更符合 ReactiveUI 的设计模式。

  2. 选项 2: 保持当前结构,但明确绑定路径:

    <reactive:RoutedViewHost Router="{Binding ViewModel.MainWindowViewModel.Router}"/>
    

    然后将 MainWindow.axaml 的绑定改为从 ViewModel.MainWindowViewModel 获取。

2. ViewLocator 使用 Activator.CreateInstance ⚠️

当前实现:

var view = Activator.CreateInstance(viewType) as IViewFor;

问题:

  • 没有使用依赖注入创建 View 实例
  • 如果 View 的构造函数需要参数,会失败

建议: 如果项目使用依赖注入,可以考虑从 DI 容器获取 View 实例:

// 从 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 的异步支持 💡

当前实现:

NavigateCommand = ReactiveCommand.Create<NavigationItem>(NavigateToPage);

建议: 如果命令需要异步操作或验证,可以使用:

NavigateCommand = ReactiveCommand.CreateFromTask<NavigationItem>(async item => 
{
    await NavigateToPageAsync(item);
});

4. 订阅的清理 💡

当前实现:

_screen.Router
    .CurrentViewModel
    .Subscribe(viewModel => { ... });

建议: 使用 WhenActivated 确保订阅在视图激活时创建,在停用时清理:

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 进行订阅管理

📝 最佳实践检查清单

  • ViewModels 继承 ReactiveObject
  • 路由 ViewModels 实现 IRoutableViewModel
  • Views 继承 ReactiveUserControl 或 ReactiveWindow
  • 使用 RaiseAndSetIfChanged 进行属性通知
  • 使用 ReactiveCommand 创建命令
  • 使用 IScreen 和 RoutingState 进行路由
  • 使用 RoutedViewHost 显示路由内容
  • 实现自定义 ViewLocator
  • 正确注册 ReactiveUI 服务
  • 使用响应式编程模式(WhenAnyValue 等)
  • 使用 WhenActivated 管理订阅(可选)
  • ViewLocator 支持依赖注入(可选)

检查完成日期: 2025年1月 检查工具: AI 代码分析