Build a desktop app from published packages
On this page
Use published NuGet/npm packages and generated clients in your application
repository. These examples match the template in the SDK snapshot these guides
are built from. Replace
<VERSION> with the catalog version
for Application, Desktop, templates, tools and frontend packages. Command Line
and Translations have independent versions on their product pages.
Create the package consumer #
For a Svelte/Bun/ReactiveUI application with GTK 4 on Linux:
dnx Runic.Create@<VERSION> -- MyApp --frontend svelte --package-manager bun --host desktop-gtk4 --view-models reactiveui
cd MyApp
dotnet tool restore
dotnet runic dev
Install .NET 10 and the frontend tools reported by dotnet runic doctor.
The creator takes its destination with --output (-o), as dotnet new does;
--directory is its former name. Its --format json option selects JSON output. Commit generated versions and lockfiles. The
creator previews other choices; see
existing-app setup to add Runic to an app.
desktop-gtk4 references Runic.Application.Views.Desktop and
Runic.Desktop.Gtk4, which brings the GTK 4 and portal platform providers.
Linux needs GTK 4.12 or newer and WebKitGTK 6.0. desktop selects GTK
3/WebKitGTK 4.1 on Linux. Both use WebView2 on Windows and WKWebView on macOS;
see choosing a host and MVVM library and
host selection for native prerequisites.
Let the build produce clients and assets #
RunicApplicationFrontendWindowProject opts the project into the packaged
build targets:
<RunicApplicationFrontendWindowProject>true</RunicApplicationFrontendWindowProject>
Declare the Window and Views in C#. dotnet build generates clients under
Frontend/src/generated, runs the frontend build, and copies Frontend/dist
into www beside the application. Ignore generated clients; missing imports
before the first .NET build are expected. Rebuild after contract or package changes.
Since SDK 0.7.0-preview.9, the page loads webui.js and the host-neutral
runic-views.js, and Vite's runic() from @runic-artifex/vite-plugin-runic
with base: "./" builds relative asset URLs for each surface path. Before
0.7.0-preview.9, runic({ desktop: true }) loaded the Desktop bootstrap;
those pages keep working.
App code imports public @runic-artifex/views; generated modules own its
generated-code entries.
The default asset bundle is the www directory: RunicWindowOptions serves
www next to the application, starting at index.html, unless you set
another content root.
For an embedded archive, follow Embed and serve assets.
Keep one scope and model context per Window #
The generated AddRunicViews() registers Bridges and Views; the application
registers its ViewModels and services. The Window's root ViewModel is scoped.
For ReactiveUI, register its model context before constructing commands:
services.AddRunicReactiveModelContext();
services.AddScoped<WorkspaceViewModel>();
services.AddScoped<WelcomeViewModel>();
services.AddScoped<CounterViewModel>();
// Generated by the build: the Window and View Bridges and the View locator.
services.AddRunicViews();
host.OpenWindowAsync<TWindow>() creates the asynchronous DI scope, resolves
the root model and attaches its Bridge to the presentation. Register
window-bound services as scoped. Model state uses the model context; native UI
uses its dispatcher. ViewModel state and threads
explains the model context.
Start the native event loop before asynchronous work #
Use the template's entry point before any top-level await:
var options = new DesktopHostOptions { DiagnosticSink = ReportDiagnostic }.WithGtk4(ApplicationIdentity.Id);
...
return RunicDesktopHost.Run(provider, options, RunAsync);
This keeps GTK 4 on Linux's process main thread. RunAsync runs on the event
loop and returns when the Window closes; dispose the Window and other native
resources inside it, while the event loop is active. Retain the diagnostic sink
for missing runtimes and fallback.
Bind native services to the opened presentation #
services.AddRunicPlatformServices() registers IFileDialogs,
ITextClipboard, IDesktopFileLauncher and IWindowPlatformServices for
each Window. They bind to the Window's native owner when it opens, with the
provider for the running backend, so a GTK 4 process uses the portal file
dialogs and never the GTK 3 provider:
// Per-window file dialogs, clipboard and file launcher from the provider for the running backend. A
// ViewModel can take IFileDialogs, ITextClipboard or IWindowPlatformServices in its constructor; they
// bind to the native window when it opens and report OwnerUnavailable until then, as in tests.
services.AddRunicPlatformServices();
To call a provider yourself, the open Window's Host is a
DesktopBridgeWindow<TViewModel> with its NativeOwner; see
desktop services for the providers.
The owner verifies the presentation and dispatches work on its native thread.
Browser fallback has IsAvailable == false; expose unavailability with an app
alternative. Keep native handles inside dispatched C# callbacks. Dispose native
resources while their owner and event loop are available.
For folder selection, use IFileDialogs.OpenDirectoryAsync and retain the
selected directory lease while C# work uses its local path. See
directory selection for ownership
and unavailable-provider handling.
Own shutdown and verify the published output #
Use Presentation.RequestCloseAsync on the Window's DesktopBridgeWindow
host for app close buttons when they should
follow ConfirmCloseAsync. A close policy can refuse an active mutation or
await saving a draft. It requires an embedded presentation with
DesktopPresentationPolicy.RequestedOnly; the template's
EmbeddedThenBrowser fallback cannot promise native confirmation.
Forced CloseAsync and disposal bypass confirmation. Bridge invocation completion
does not establish accepted domain-task recovery. Retain and await those tasks
in asynchronous model/service disposal before releasing resources. See
operations and cancellation
and window close lifecycle.
dotnet publish -c Release -r linux-x64
Distribute the whole publish directory, including www; the frontend needs no
Node.js or package manager at runtime. A framework-dependent publish still needs
the matching .NET runtime, and embedded hosting still needs the native libraries.
Run the published native output as well as the development host. Check initial
state, a long operation with responsive Cancel, unavailable native services,
and close during recovery. Managed or browser-only checks do not verify the
native host.
For domain DTOs shared with a CLI, see serializer and bridge contract boundaries.