This document describes how the app is put together, subsystem by subsystem. For the user-facing overview see README.md.
- Overview
- Runtime & bootstrap
- diskpart execution layer
- Output parsing
- Domain models
- The view-model
- Dialogs & file pickers
- The view (Blazor UI)
- Elevation & the manifest
- Testing
- Build & run
- Packaging the release
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:
- Nothing runs behind your back. Every mutating operation is turned into visible
diskpartcommands that the user reviews and confirms before execution. - The UI is a thin view over the view-model.
Main.razorholds no logic — it rendersMainViewModelstate and forwards clicks to its commands, re-rendering whenever the view-model raisesPropertyChangedor a collection changes. - Services are injected. Everything arrives through the MAUI DI container, which keeps
MainViewModelunit-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.csis the first authored code that runs. Before anything else it points the environment variableWEBVIEW2_USER_DATA_FOLDERat%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.CreateMauiAppcallsAddMauiBlazorWebView()(plusAddBlazorWebViewDeveloperTools()in Debug) and registers the services and view-model in the DI container:DiskPartService,DiskPartParser,IDialogService,IFileDialogService, andMainViewModelas singletons;MainPageas transient.Appreceives theIServiceProviderand, inCreateWindow, resolvesMainPageand returns it as the window's content (there is no Shell). The window title and initial size are set here.MainPage.xamlis aContentPagewhose only child is aBlazorWebViewwithHostPage="wwwroot/index.html"and a root component ofMain.Main.razor@injects the singletonMainViewModel, subscribes to itsPropertyChangedand the collections'CollectionChanged(callingStateHasChanged), initializes the JS splitters, and firesRefreshAllCommandon first render to populate the lists.
Services/DiskPartService.cs is the only code that launches a process.
- Script model.
diskpartis 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 runsdiskpart /s <file>through CliWrap. - Encoding. The temp file is written as UTF-8 without a BOM — a leading BOM can make
diskpartreject the first command. - Output.
StandardOutputandStandardErrorare captured (ExecuteBufferedAsync) and combined; validation is disabled (CommandResultValidation.None) becausediskpartsignals problems through both exit codes and text, so everything is surfaced to the user rather than thrown. - Serialization. A
SemaphoreSlim(1, 1)guards execution so twodiskpartinstances never run concurrently. - Result. Returns a
DiskPartResult(bool Success, string Output). The temp file is deleted best-effort in afinally. 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):
- Find the separator row — the first line made only of dashes and spaces (and containing
at least
--). - Use the runs of dashes to record each column's start position.
- Slice every following data row at those positions until the next blank line; each field is trimmed. The last column extends to end-of-line.
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 computedCaption("Disk 0"),PartitionStyle(GPT/MBR), andSummary(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 byDiskPartService(Success+Output).MenuAction— one entry in a per-item popup:Label,Category(Info/Normal/Danger, which drives the button color), and aFunc<Task> Run.
ViewModels/MainViewModel.cs (a partial ObservableObject) holds all state and commands.
- State. Observable collections (
Disks,Volumes,Partitions,MenuActions) and observable properties (SelectedDisk/Volume/Partition,CommandScript,OutputLog,StatusText,IsBusy,IsElevated,IsActionMenuOpen,MenuTitle, …). - Two kinds of command. Read-only commands (
RefreshAll,DetailDisk,ListPartitions,DetailVolume) rundiskpartimmediately and show the output. Builder commands (AppendClean,AppendFormat,AppendDeleteVolume, …) only append the matching commands — with the correctselect …prefix — toCommandScriptviaAppendToScript. Nothing mutates a disk untilRunScript. - The run gate.
RunScriptAsyncconfirms (showing the exact script and a destructive-data warning) before callingDiskPartService, then refreshes the lists to reflect any change. - Selection. Setting
SelectedDisk(OnSelectedDiskChanged) clears and reloads that disk's partitions automatically. - Guards & busy tracking.
WithSelectedDisk/Volume/Partitionshort-circuit with an alert when nothing is selected.RunGuardedwraps everydiskpartinteraction in try/catch and a re-entrant busy counter (EnterBusy/ExitBusy) that drivesIsBusy. - Per-item menus.
ShowDiskActions/ShowVolumeActions/ShowPartitionActionsset the selection, fillMenuActionswith color-categorized entries, and open the popup;InvokeMenuActioncloses it and runs the chosenFunc<Task>.
Services/DialogService.csimplementsIDialogService(confirm / prompt / alert) by resolving the current window'sPageat call time (Application.Current.Windows[0].Page), so the view-model never holds aPagereference.Services/FileDialogService.csimplements open/save with the native WinRT pickers (FileOpenPicker/FileSavePicker). Because the app is unpackaged, each picker is associated with the window's HWND viaWinRT.Interop.InitializeWithWindowbefore 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:
- Header — title + live status on the left; elevation badge, busy spinner, and the square ⟳ refresh button on the right.
- Left column — Disks / Partitions / Volumes cards, each an
@foreachover the matching observable collection, with a per-row Actions button and aselectedclass on the current item. - Right column — the Actions panel (grouped, color-coded buttons with divider rules), the
Command preview (a
<textarea>two-way bound toCommandScript+ Open/Save/Run/Clear), and the Output console (<pre>). - Per-item popup — rendered when
IsActionMenuOpenis true: a dimmed overlay with a card whose buttons come fromMenuActions; a CSS class derived fromCategorycolors 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.csslays out the columns and cards with flexbox; thin splitter strips (.h-split/.v-split) sit between the resizable panels.wwwroot/js/splitter.jsattaches pointer handlers to each splitter, adjusts the previous element'sflex-basis(clamped to a minimum), and on release saves the size to the WebView'slocalStoragekeyed by adata-splitkey.splitter.initrestores 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 reallist disk/list volume/list partitionoutput, including the tricky cases: empty drive-letter cells, a multi-wordNo Mediastatus,0 Bvalues, GPT-vs-MBR detection, CRLF as well as LF line endings, rows whose id is not numeric, and empty / no-table input.- Model formatting — the
Caption/Summary/PartitionStylecomputed strings, including the omit-when-empty branches (zero free space, missing offset, no fields set).
dotnet testThe 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 Debugdotnet run -c Debug -f net10.0-windows10.0.19041.0The 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=trueThe 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