Framework-neutral system dialog contracts with separate GUI-framework adapters.
Install only the adapter package for your UI stack. Each adapter depends on the core contracts package automatically.
| Package | UI stack | Install |
|---|---|---|
| Gui.SystemDialogs.Sharp | Contracts only | dotnet add package Gui.SystemDialogs.Sharp |
| Gui.SystemDialogs.Wpf.Sharp | WPF | dotnet add package Gui.SystemDialogs.Wpf.Sharp |
| Gui.SystemDialogs.WinUI.Sharp | WinUI 3 | dotnet add package Gui.SystemDialogs.WinUI.Sharp |
| Gui.SystemDialogs.Maui.Sharp | .NET MAUI (Windows) | dotnet add package Gui.SystemDialogs.Maui.Sharp |
| Gui.SystemDialogs.Avalonia.Sharp | Avalonia 12 | dotnet add package Gui.SystemDialogs.Avalonia.Sharp |
| Gui.SystemDialogs.Consolonia.Sharp | Consolonia / Avalonia 11 | dotnet add package Gui.SystemDialogs.Consolonia.Sharp |
| Gui.SystemDialogs.ProGpu.Sharp | ProGPU | dotnet add package Gui.SystemDialogs.ProGpu.Sharp |
| Gui.SystemDialogs.Windows.Sharp | Shared Windows helpers | Usually pulled in transitively by WinUI / MAUI |
Versioning:
0.1.0is the first public release. Earlier0.0.xbuilds were pre-release validation publishes.
using Gui.SystemDialogs.Sharp;
using Gui.SystemDialogs.Wpf.Sharp;
IFilePickerService filePicker = new WpfFilePickerService();
var path = await filePicker.OpenFileAsync(new OpenFileDialogOptions
{
Title = "Open document",
Filters =
[
new FileDialogFilter { DisplayName = "Text files", Patterns = ["*.txt"] },
new FileDialogFilter { DisplayName = "All files", Patterns = ["*.*"] },
],
});FileDialogFilter is a record with required init-only members, so both DisplayName and
Patterns must be supplied through an object initializer. Patterns are globs (*.txt) and are
handed to the underlying dialog unchanged — a bare extension such as .txt will not match.
Avalonia resolves the owner window internally or via a Func<TopLevel?> passed to the constructor:
using Gui.SystemDialogs.Avalonia.Sharp;
IFilePickerService filePicker = new AvaloniaFilePickerService();IFilePickerService— open / open-many / save (local paths only)IFolderPickerService— select folderIApplicationExitService— request application/process exit- Option/filter models (
FileDialogFilter,OpenFileDialogOptions, …)
The contracts assembly must not reference GUI, Windows, WinRT, HWND, or framework types.
| Project | Role |
|---|---|
Gui.SystemDialogs.Windows.Sharp |
HWND ownership + WinRT filter mapping |
Gui.SystemDialogs.Wpf.Sharp |
WPF pickers + exit + WpfFileFilterFormat |
Gui.SystemDialogs.WinUI.Sharp |
Windows App SDK pickers + exit |
Gui.SystemDialogs.Maui.Sharp |
Windows-targeted MAUI pickers + exit |
Gui.SystemDialogs.Avalonia.Sharp |
Avalonia 12 storage pickers + exit |
Gui.SystemDialogs.Consolonia.Sharp |
Consolonia / Avalonia 11 pickers + exit |
Gui.SystemDialogs.ProGpu.Sharp |
ProGPU pickers + exit |
Gui.SystemDialogs.Avalonia.Sharp and Gui.SystemDialogs.Consolonia.Sharp contain almost identical
code, because Consolonia is an Avalonia backend and both therefore map onto IStorageProvider. This
duplication is intentional — do not merge the projects or share source files between them. They track
different Avalonia majors (Avalonia 12.x vs Avalonia 11.x by way of Consolonia) and must be free to
diverge without coordination: a shared file would have to compile against both API versions at once the
moment either side moves.
The same rule applies to every adapter pair. Adapters are independent implementations; what keeps them honest is the conformance suite below, not shared code.
FilterMappingConformance in tests/Gui.SystemDialogs.TestSupport states the guarantees that every
adapter owes a caller of the neutral contract, expressed in terms of which file types the dialog ends up
accepting:
- an empty filter list leaves the dialog unconstrained;
- caller-supplied filters are never widened with an "all files" entry the caller did not ask for;
- every pattern the caller supplied reaches the dialog;
- an explicit wildcard filter survives mapping;
- filters carrying no usable pattern degrade to "any file".
Each adapter test project supplies a probe and runs the whole set. Add a new adapter, add its probe.
Gui.SystemDialogs.Sharp
↑
├── Gui.SystemDialogs.Avalonia.Sharp
├── Gui.SystemDialogs.Consolonia.Sharp
├── Gui.SystemDialogs.ProGpu.Sharp
├── Gui.SystemDialogs.Wpf.Sharp
└── Gui.SystemDialogs.Windows.Sharp
↑
├── Gui.SystemDialogs.WinUI.Sharp
└── Gui.SystemDialogs.Maui.Sharp
Requirements: .NET 10 SDK, Python 3 (for the boundary check).
python eng/verify-contract-boundary.py
dotnet test Gui.SystemDialogs.Sharp.slnx -c Release --filter Category!=Smoke
dotnet pack Gui.SystemDialogs.Sharp.slnx -c Release -o artifacts/packages
There is no <Version> property anywhere in the repository. MinVer
derives the version from the nearest reachable git tag (tags are prefixed with v), so the tag is the
single source of truth and the repository can never disagree with what was published.
- on a tagged commit —
v0.1.1produces0.1.1; - on any later commit — a prerelease such as
0.1.2-alpha.0.3.
A prerelease from a local pack is therefore expected, not a misconfiguration. AssemblyVersion stays
pinned at 0.0.0.0 to keep assembly identity stable; FileVersion and AssemblyInformationalVersion
follow the tag, and the informational version carries the commit hash.
Override the derived version for a dry run:
dotnet pack Gui.SystemDialogs.Sharp.slnx -c Release -o artifacts/packages -p:MinVerVersionOverride=0.1.2-dev
Anything that computes the version needs the full history:
actions/checkoutmust run withfetch-depth: 0, otherwise MinVer silently reports0.0.0-alpha.0.
All packages share one version, with one exception: Gui.SystemDialogs.ProGpu.Sharp always carries a
-preview suffix. Tag v0.1.2 publishes seven packages as 0.1.2 and that one as 0.1.2-preview.
ProGPU.WinUI has no stable release yet, and an adapter cannot honestly be more stable than what it
binds to — this is exactly what NU5104 reports, so the package is marked prerelease rather than the
warning being suppressed. Releases stay in lockstep: a ProGPU dependency bump is published as an
ordinary repository release, which re-publishes the other packages unchanged at the new version.
To retire the exception once ProGPU.WinUI is stable, delete the ProGpuAlwaysPreRelease target from
Gui.SystemDialogs.ProGpu.Sharp.csproj; nothing else changes.
FlaUI-driven common dialogs in tests/Gui.SystemDialogs.Wpf.Sharp.SmokeTests. Opt-in via env var (skipped otherwise):
set GUI_SYSTEMDIALOGS_SMOKE=1
dotnet test tests/Gui.SystemDialogs.Wpf.Sharp.SmokeTests -c Release
CI runs on every push and pull request to main. Packages are published to NuGet.org when a version tag is pushed:
git tag v0.1.1
git push origin v0.1.1
The tag alone determines the published version. Before packing, the workflow requires a matching
## [0.1.1] heading in CHANGELOG.md, runs the contract-boundary check and the full test
suite, and then asserts that the produced .nupkg really carries the tagged version. Write the release
notes first, or the publish fails.
Set the NUGET_API_KEY secret in the GitHub nuget environment before the first publish.
MIT — see LICENSE.