source-guide
Adding a page
Build a device debug page from scratch and walk the whole MVVM convention.
A real task to walk the conventions: add a "nozzle vacuum debug" page.
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()
{
// subscribe only while the page is visible; the base class unsubscribes on leave
Subscribe(_io.Changes, OnIoChanged);
}
private void OnIoChanged(IoChange c)
{
if (c.Channel == IoMap.PartSensor) PartDetected = c.Value;
}
}Points:
CommunityToolkit.Mvvmsource generators — no hand-writtenINotifyPropertyChanged;- commands carry
CanExecute, which drives button state — noIsEnabledlogic in the view; - subscribe through the base class helper. Forgetting to unsubscribe is the number one cause of WPF memory leaks.
2. View
<!-- 07.PreciSim.App/Views/VacuumDebugView.xaml -->
<UserControl x:Class="PreciSim.App.Views.VacuumDebugView"
d:DataContext="{d:DesignInstance vm:VacuumDebugViewModel}">
<StackPanel Margin="16">
<ToggleButton Content="{DynamicResource Page.Vacuum}"
IsChecked="{Binding VacuumOn, Mode=OneWay}"
Command="{Binding ToggleVacuumCommand}" />
<TextBlock Text="{Binding PartDetected, Converter={StaticResource BoolToPresence}}" />
</StackPanel>
</UserControl>Plain {Binding} — no binding DSL. The code-behind contains InitializeComponent() and
nothing else.
3. Registration
// 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",
icon: Icons.Vacuum,
requiredRole: UserRole.Engineer, // permissions declared here, not checked inside the page
viewModel: typeof(VacuumDebugViewModel))4. Strings
<!-- Resources/Strings.en.resx -->
<data name="Page.VacuumDebug"><value>Vacuum debug</value></data>
<!-- Resources/Strings.zh-CN.resx -->
<data name="Page.VacuumDebug"><value>真空调试</value></data>Both resx files. A CI test compares the key sets and fails if one is missing — the same idea as the bilingual dictionary on this website.
5. Test
[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);
}The ViewModel has no WPF dependency, so this runs instantly and needs no UI thread. That is the practical payoff of MVVM, not architectural tidiness.
Checklist
- No
DispatcherorMessageBoxin the ViewModel (dialogs go throughIDialogService) - View code-behind is only
InitializeComponent() - Both resx files updated
- The page declares
requiredRole - One unit test that does not touch the UI
Last updated: Sep 21, 2026
Was this page helpful?