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.