跳到正文
PreciSim
source-guide

怎么加一个页面

从零加一个设备调试页,走完 MVVM 的完整套路。

这一篇用一个真实任务把 MVVM 的约定走一遍:加一个「吸嘴真空调试」页

1. ViewModel

// 07.PreciSim.App/ViewModels/VacuumDebugViewModel.cs
public partial class VacuumDebugViewModel : PageViewModelBase
{
    private readonly IDigitalIo _io;
 
    public VacuumDebugViewModel(IDigitalIo io, ILogger<VacuumDebugViewModel> logger)
        : base(logger)
    {
        _io = io;
    }
 
    [ObservableProperty] private bool _vacuumOn;
    [ObservableProperty] private bool _partDetected;
 
    [RelayCommand(CanExecute = nameof(CanToggle))]
    private async Task ToggleVacuumAsync(CancellationToken ct)
    {
        await _io.WriteAsync(IoMap.VacuumValve, !VacuumOn);
        VacuumOn = !VacuumOn;
    }
 
    private bool CanToggle() => !IsBusy && ConnectionState == DeviceState.Connected;
 
    protected override void OnActivated()
    {
        // 页面可见时才订阅 IO 事件,离开时自动退订(基类处理)
        Subscribe(_io.Changes, OnIoChanged);
    }
 
    private void OnIoChanged(IoChange c)
    {
        if (c.Channel == IoMap.PartSensor) PartDetected = c.Value;
    }
}

要点:

  • CommunityToolkit.Mvvm[ObservableProperty] / [RelayCommand]不手写 INotifyPropertyChanged
  • 命令带 CanExecute,按钮的可用状态由它驱动,不在 View 里写 IsEnabled 的逻辑
  • 订阅走基类的 Subscribe,页面切走自动退订——忘了退订是 WPF 内存泄漏的头号原因

2. View

<!-- 07.PreciSim.App/Views/VacuumDebugView.xaml -->
<UserControl x:Class="PreciSim.App.Views.VacuumDebugView"
             d:DataContext="{d:DesignInstance vm:VacuumDebugViewModel}">
  <StackPanel Margin="16" Orientation="Vertical">
    <ToggleButton Content="真空"
                  IsChecked="{Binding VacuumOn, Mode=OneWay}"
                  Command="{Binding ToggleVacuumCommand}" />
    <TextBlock Text="{Binding PartDetected, Converter={StaticResource BoolToPresence}}" />
  </StackPanel>
</UserControl>

要点:用标准 {Binding},不引入任何绑定 DSL。 代码后置文件里只允许有 InitializeComponent(),别的一行都不写。

3. 注册

// 07.PreciSim.App/Startup/ViewRegistration.cs
services.AddTransient<VacuumDebugViewModel>();
services.AddTransient<VacuumDebugView>();
 
// 07.PreciSim.App/Navigation/PageCatalog.cs
new PageEntry(
    id: "debug.vacuum",
    titleKey: "Page.VacuumDebug",         // 中英文文案在 Resources/Strings.*.resx
    icon: Icons.Vacuum,
    requiredRole: UserRole.Engineer,      // 权限分级在这里声明,不在页面里判断
    viewModel: typeof(VacuumDebugViewModel))

4. 文案

<!-- Resources/Strings.zh-CN.resx -->
<data name="Page.VacuumDebug"><value>真空调试</value></data>
<!-- Resources/Strings.en.resx -->
<data name="Page.VacuumDebug"><value>Vacuum debug</value></data>

两个 resx 都要加。 CI 里有一条测试会比对两份资源的键集合,缺一个就失败—— 和这个网站的中英文字典是同一套思路。

5. 测试

[Fact]
public async Task ToggleVacuum_WritesToIo()
{
    var io = new FakeDigitalIo();
    var vm = new VacuumDebugViewModel(io, NullLogger<VacuumDebugViewModel>.Instance);
 
    await vm.ToggleVacuumCommand.ExecuteAsync(null);
 
    Assert.True(io.LastWrite(IoMap.VacuumValve));
    Assert.True(vm.VacuumOn);
}

ViewModel 不依赖 WPF,所以这类测试跑得飞快,不需要 UI 线程。 这就是坚持 MVVM 的实际回报,不是为了架构好看。

检查清单

  • ViewModel 里没有 Dispatcher、没有 MessageBox(弹窗走 IDialogService
  • View 的代码后置只有 InitializeComponent()
  • 两份 resx 都加了文案
  • 页面声明了 requiredRole
  • 有一个不依赖 UI 的单元测试
最后更新: 2026/9/21
这页有帮助吗?