This document describes how the app is put together, subsystem by subsystem. For the user-facing overview see README.md.
DiskPart UI is a single-window .NET MAUI desktop app that targets Windows only
(net10.0-windows10.0.19041.0) and runs unpackaged (WindowsPackageType=None). It is a
thin, transparent front-end over the diskpart command-line tool.
Architecture is textbook MVVM:
BlazorWebView (WebView2)
Main.razor ──@inject──► MainViewModel ──► DiskPartService ──► diskpart.exe
(Razor UI) (state + commands) │ (CliWrap)
▲ ▲ ├─► DiskPartParser (text → models)
│ └─ PropertyChanged / CollectionChanged ├─► IDialogService (native MAUI dialogs)
│ → StateHasChanged └─► IFileDialogService (WinRT open/save)
└─ js/splitter.js (drag-to-resize + localStorage)
The UI is .NET MAUI Blazor Hybrid: a native MAUI ContentPage (MainPage) hosts a
BlazorWebView, which renders the Main Razor component in an embedded WebView2. The app is
still a native Windows process, so diskpart execution, elevation, and the WinRT file pickers
all work exactly as in a XAML MAUI app.
Design principles:
diskpart commands that the user reviews and confirms before execution.Main.razor holds no logic — it renders
MainViewModel state and forwards clicks to its commands, re-rendering whenever the
view-model raises PropertyChanged or a collection changes.MainViewModel unit-testable and free of view concerns.Dependencies: CliWrap (process execution), CommunityToolkit.Mvvm (ObservableObject,
[ObservableProperty], [RelayCommand]), and Microsoft.AspNetCore.Components.WebView.Maui
(the BlazorWebView). No other third-party packages.
Platforms/Windows/App.xaml.cs is the first authored code that runs. Before anything else
it points the environment variable WEBVIEW2_USER_DATA_FOLDER at
%LOCALAPPDATA%\DiskPartUI\WebView2. Do not remove this. WebView2 otherwise defaults its
user-data folder to the directory holding the executable; once the app is installed under
Program Files that folder is read-only, and the WebView fails on launch with "We couldn't
create the data directory". It has to happen in the constructor, before any WebView exists.MauiProgram.CreateMauiApp calls AddMauiBlazorWebView() (plus
AddBlazorWebViewDeveloperTools() in Debug) and registers the services and view-model in the
DI container: DiskPartService, DiskPartParser, IDialogService, IFileDialogService, and
MainViewModel as singletons; MainPage as transient.App receives the IServiceProvider and, in CreateWindow, resolves MainPage and
returns it as the window's content (there is no Shell). The window title and initial size are
set here.MainPage.xaml is a ContentPage whose only child is a BlazorWebView with
HostPage="wwwroot/index.html" and a root component of Main.Main.razor @injects the singleton MainViewModel, subscribes to its PropertyChanged
and the collections' CollectionChanged (calling StateHasChanged), initializes the JS
splitters, and fires RefreshAllCommand on first render to populate the lists.Services/DiskPartService.cs is the only code that launches a process.
diskpart is a batch tool: RunScriptAsync(string script) normalizes the
script (trims each line, drops blanks, appends a trailing newline) and writes it to a
temporary file, then runs diskpart /s <file> through CliWrap.diskpart reject the first command.StandardOutput and StandardError are captured (ExecuteBufferedAsync) and
combined; validation is disabled (CommandResultValidation.None) because diskpart signals
problems through both exit codes and text, so everything is surfaced to the user rather
than thrown.SemaphoreSlim(1, 1) guards execution so two diskpart instances never
run concurrently.DiskPartResult(bool Success, string Output). The temp file is deleted
best-effort in a finally.RunCommandsAsync(params string[]) is a convenience overload that joins commands into a
script.Services/DiskPartParser.cs converts diskpart's fixed-width text tables into typed models.
diskpart prints tables like:
Disk ### Status Size Free Dyn Gpt
-------- ------------- ------- ------- --- ---
Disk 0 Online 931 GB 0 B *
The algorithm (ParseTable):
--).ParseDisks / ParseVolumes / ParsePartitions map the sliced fields to DiskInfo,
VolumeInfo, PartitionInfo by column index and skip rows without a numeric id. This
position-based slicing is resilient to the differing column widths seen across Windows
versions, and tolerates empty cells (a volume with no drive letter, No Media, 0 B, etc.).
Plain immutable objects in Models/ (init-only properties):
DiskInfo — number, status, size, free, dynamic/GPT flags, plus computed Caption
("Disk 0"), PartitionStyle (GPT/MBR), and Summary (the •-joined one-liner shown
in the list).VolumeInfo — number, letter, label, file system, type, size, status, info + Caption
/ Summary.PartitionInfo — number, type, size, offset + Caption / Summary.DiskPartResult — the record returned by DiskPartService (Success + Output).MenuAction — one entry in a per-item popup: Label, Category (Info / Normal /
Danger, which drives the button color), and a Func<Task> Run.ViewModels/MainViewModel.cs (a partial ObservableObject) holds all state and commands.
Disks, Volumes, Partitions, MenuActions) and
observable properties (SelectedDisk/Volume/Partition, CommandScript, OutputLog,
StatusText, IsBusy, IsElevated, IsActionMenuOpen, MenuTitle, …).RefreshAll, DetailDisk,
ListPartitions, DetailVolume) run diskpart immediately and show the output.
Builder commands (AppendClean, AppendFormat, AppendDeleteVolume, …) only append the
matching commands — with the correct select … prefix — to CommandScript via
AppendToScript. Nothing mutates a disk until RunScript.RunScriptAsync confirms (showing the exact script and a destructive-data
warning) before calling DiskPartService, then refreshes the lists to reflect any change.SelectedDisk (OnSelectedDiskChanged) clears and reloads that
disk's partitions automatically.WithSelectedDisk/Volume/Partition short-circuit with an alert
when nothing is selected. RunGuarded wraps every diskpart interaction in try/catch and a
re-entrant busy counter (EnterBusy/ExitBusy) that drives IsBusy.ShowDiskActions / ShowVolumeActions / ShowPartitionActions set the
selection, fill MenuActions with color-categorized entries, and open the popup;
InvokeMenuAction closes it and runs the chosen Func<Task>.Services/DialogService.cs implements IDialogService (confirm / prompt / alert) by
resolving the current window's Page at call time (Application.Current.Windows[0].Page),
so the view-model never holds a Page reference.Services/FileDialogService.cs implements open/save with the native WinRT pickers
(FileOpenPicker / FileSavePicker). Because the app is unpackaged, each picker is
associated with the window's HWND via WinRT.Interop.InitializeWithWindow before it is
shown.Components/Main.razor is the whole UI — a two-column flex layout with a modal overlay for
the popup. It renders the injected MainViewModel and forwards clicks to its commands:
@foreach over the matching
observable collection, with a per-row Actions button and a selected class on the
current item.<textarea> two-way bound to CommandScript + Open/Save/Run/Clear),
and the Output console (<pre>).IsActionMenuOpen is true: a dimmed overlay with a card
whose buttons come from MenuActions; a CSS class derived from Category colors them
(blue/gray/red) to match the Actions panel.Commands are invoked via the generated IAsyncRelayCommand / IRelayCommand properties
(Vm.AppendCleanCommand.ExecuteAsync(null), Vm.ShowDiskActionsCommand.Execute(disk), …).
Because the component subscribes to the view-model's change events, any state the commands
mutate re-renders automatically. Confirm/prompt dialogs still surface as native MAUI page
dialogs (via IDialogService), shown over the WebView.
Resizing & persistence are handled in the browser, not C#:
wwwroot/app.css lays out the columns and cards with flexbox; thin splitter strips
(.h-split / .v-split) sit between the resizable panels.wwwroot/js/splitter.js attaches pointer handlers to each splitter, adjusts the previous
element's flex-basis (clamped to a minimum), and on release saves the size to the WebView's
localStorage keyed by a data-splitkey. splitter.init restores saved sizes on
startup; the two columns default to a clean 50/50 until dragged.diskpart requires elevation, so Platforms/Windows/app.manifest requests
requireAdministrator. Running unpackaged is what lets that Win32 manifest take effect, so the
app self-elevates (one UAC prompt) at launch. ElevationHelper.IsElevated() reports the
current state for the header badge and the "not elevated" warnings.
Unit tests live in tests/ as a separate xUnit project (DiskPartUI.Tests).
Rather than reference the Windows MAUI app (which would drag in the WinUI/RID/packaging
model), the test project targets plain net10.0 and compiles the files under test
directly via linked <Compile Include="..\…" /> items — they are pure logic with no MAUI
dependencies, so the suite is fast and portable. The app project excludes tests/** from its
own compile so the two never collide.
Covered:
DiskPartParser — parsing real list disk / list volume / list partition output,
including the tricky cases: empty drive-letter cells, a multi-word No Media status, 0 B
values, GPT-vs-MBR detection, CRLF as well as LF line endings, rows whose id is not numeric,
and empty / no-table input.Caption / Summary / PartitionStyle computed strings,
including the omit-when-empty branches (zero free space, missing offset, no fields set).dotnet test
The side-effecting layers — process launch, dialogs, file pickers, elevation, and the UI — are integration concerns and are verified by running the app.
dotnet build -c Debug
dotnet run -c Debug -f net10.0-windows10.0.19041.0
The project multi-targets nothing else — it is Windows-only because diskpart is. See
README.md for the Visual Studio path and the admin-debugging note.
Release builds are published self-contained so the download needs no .NET or Windows App SDK runtime installed:
dotnet publish DiskPartUI.csproj -c Release -f net10.0-windows10.0.19041.0 -r win-x64 --self-contained true -p:WindowsAppSDKSelfContained=true
The installer is an Inno Setup script,
installer/DiskPartUI.iss, which packs that publish folder into
bin\DiskPartUI-v<version>-setup.exe:
"C:\Program Files (x86)\Inno Setup 6\ISCC.exe" installer\DiskPartUI.iss
(winget install JRSoftware.InnoSetup puts ISCC.exe under
%LOCALAPPDATA%\Programs\Inno Setup 6\ instead — adjust the path to match your install.)
MSIX is deliberately not used: a packaged app cannot request requireAdministrator, which
diskpart needs. The installer therefore sets PrivilegesRequired=admin and installs
per-machine into Program Files.
installer/Test-Installer.ps1 smoke-tests the result end to end.
It silently installs; asserts the app files, the self-contained runtime, the Start Menu shortcut,
the uninstaller and the Programs-and-Features entry all exist; launches the installed app and
checks that it stays running, that WebView2 wrote its data under LocalAppData, and that nothing
was written beside the executable; then silently uninstalls and confirms the cleanup.
That launch step matters: v1.0.0 shipped an installer whose app could not start at all, because the original test only exercised install and uninstall. Installing without running proves very little. The script needs an elevated shell:
powershell -ExecutionPolicy Bypass -File installer\Test-Installer.ps1