Choosing a host and MVVM library

On this page

The creator and dotnet new runic-app ask two questions that shape a project: which host opens the Window, and which library the ViewModels use. The defaults are Runic Desktop and ReactiveUI. Keep them unless one of the reasons below applies. Either way the Window declaration, the Views, the generated clients and the frontend are the same, and switching later changes a few lines.

Choice Template option Choose it when
Runic Desktop (default) --host desktop You are building a desktop app. It is the first-class host.
Runic Desktop with GTK 4 --host desktop-gtk4 Your Linux users run GTK 4 and WebKitGTK 6.
CS-WebUI --host cswebui A browser window is enough and you want the smallest runtime.
ReactiveUI (default) --view-models reactiveui You are starting fresh, or already use ReactiveUI. It is the primary path.
CommunityToolkit.Mvvm --view-models toolkit Your team or existing ViewModels use CommunityToolkit.Mvvm.

Unfamiliar words, such as Window, View, Host or model context, are defined in the glossary.

Window host #

Runic Desktop (default) #

Runic Desktop opens each Window as a native window with the platform's embedded WebView: WebView2 on Windows, WKWebView on macOS and WebKitGTK on Linux. It is Runic's first-class host; Desktop features and fixes come first.

ViewModels get native file dialogs, the clipboard and the file launcher by taking IFileDialogs, ITextClipboard or IWindowPlatformServices in their constructor. The template registers them, and the provider is selected from the backend that is running, so a GTK 4 process never loads 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.

When the WebView runtime is missing, the template's Window falls back to an installed browser and writes the reason to standard error; dotnet runic doctor lists what a machine needs. --host desktop uses GTK 3 with WebKitGTK 4.1 on Linux. --host desktop-gtk4 uses GTK 4.12 or newer with WebKitGTK 6.0 and xdg-desktop-portal file dialogs, and behaves like desktop on Windows and macOS. Ship a Desktop application covers publishing, signing and packaging for each platform.

CS-WebUI (best effort) #

CS-WebUI opens a browser window: an installed browser in app mode, or the platform WebView when no browser is found. Chrome, Edge and other Chromium-based browsers work best; Firefox works without app mode.

CS-WebUI is a supported second host on a best-effort basis. Where something cannot work well with it, Runic limits or diagnoses the capability instead of holding back Runic Desktop. A browser window has no native owner, so the platform services resolve but report OwnerUnavailable. In exchange its runtime is smaller: in one Linux measurement the same NativeAOT application was 5.16 MiB with CS-WebUI and 10.42 MiB with Runic Desktop, excluding the browser.

Runic Desktop CS-WebUI
Window Native window with the embedded WebView; installed browser as fallback Installed browser in app mode; platform WebView as fallback
File dialogs, clipboard, file launcher Native, through AddRunicPlatformServices() Report OwnerUnavailable
Linux runtime GTK 3 and WebKitGTK 4.1, or GTK 4.12 and WebKitGTK 6.0 A Chromium-based browser, or GTK 3 and WebKitGTK 4.1
.NET package Runic.Application.Views.Desktop Runic.Application.Views.CsWebUi
Host script in index.html runic-views.js (before 0.7.0-preview.9: runic-desktop-views.js) runic-views.js (before 0.7.0-preview.9: runic-cswebui.js)
Support First-class Best effort

Switching host later #

The application body is the same on every host. It opens the Window and waits until it closes:

static async Task<int> RunAsync(IRunicWindowHost host)
{
    await using var window = await host.OpenWindowAsync<WorkspaceWindow>(new RunicWindowOptions { Width = 1000, Height = 700 });
    await window.WaitForCloseAsync();
    return 0;
}

Only the entry point names the host. Runic Desktop runs the body on the event loop the platform needs:

return RunicDesktopHost.Run(provider, options, RunAsync);

CS-WebUI runs the same body:

return RunicCsWebUiHost.Run(provider, RunAsync);

To switch, change that line and its using, the host package in the project file, and, before SDK 0.7.0-preview.9, the host script in index.html. Since 0.7.0-preview.9, both hosts also provide their Views client as runic-views.js. A page that loads it, as the template does, names no host and needs no change. Generate a project with the other --host in the creator to compare the files.

ViewModel library #

ReactiveUI (default) #

ReactiveUI is Runic's primary MVVM path and the template default. Runic targets ReactiveUI 26 with its ReactiveUI.Primitives flavour, chosen for NativeAOT, through Runic.Application.Views.ReactiveUI. Commands are ordinary ReactiveCommands; the template's Counter page is one read-only property, one settable property and one command:

public int Count
{
    get => _count;
    private set => this.RaiseAndSetIfChanged(ref _count, value);
}

public int Step
{
    get => _step;
    set => this.RaiseAndSetIfChanged(ref _step, value);
}

public ReactiveCommand<RxVoid, RxVoid> IncrementCommand { get; }

AddRunicReactiveModelContext() makes every command deliver its results on the Window's model context, so a command needs no scheduler argument:

services.AddRunicReactiveModelContext();
services.AddScoped<WorkspaceViewModel>();

If you know ReactiveUI from System.Reactive: RxVoid replaces Unit, ISequencer replaces IScheduler, and awaiting a command needs using ReactiveUI.Primitives.Signals;. Applications that must stay on System.Reactive use Runic.Application.Views.ReactiveUI.Reactive instead; never reference both flavours in one application. Navigation has a ReactiveUI adapter with observable region state, Back and result commands, and leave guards asked through an Interaction; see pages and navigation.

CommunityToolkit.Mvvm #

CommunityToolkit.Mvvm is supported with its own idiomatic path: source-generated partial properties and relay commands, with no Runic adapter package. The same Counter page reads:

[ObservableProperty]
public partial int Count { get; private set; }

[ObservableProperty]
public partial int Step { get; set; }

[RelayCommand]
private void Increment() => Count += Step;

What stays the same #

ReactiveUI CommunityToolkit.Mvvm
.NET package Runic.Application.Views.ReactiveUI CommunityToolkit.Mvvm
Window RunicWindow<TViewModel> RunicWindow<TViewModel>
Views ReactiveRunicView<TViewModel> RunicView<TViewModel>
Commands ReactiveCommand.Create, CreateFromTask [RelayCommand]
Model context AddRunicReactiveModelContext() Provided by the host
Navigation helpers Runic.Navigation.ReactiveUI Region properties and PropertyChanged

In both, the C# setter's accessibility decides what the frontend may set: a public setter adds set<Property> to the generated client, and a private setter keeps the property read-only. Command bodies run on the Window's model context, so setting a property after await is correct in both; see ViewModel state and threads.

Packages for each choice #

The template references what each choice needs. For the defaults:

<PackageReference Include="Runic.Application.Views.ReactiveUI" Version="<VERSION>" />
<PackageReference Include="Runic.Application.Views.Desktop" Version="<VERSION>" />

--host desktop-gtk4 adds Runic.Desktop.Gtk4, --host cswebui references Runic.Application.Views.CsWebUi instead, and --view-models toolkit references CommunityToolkit.Mvvm instead of the ReactiveUI adapter. Each host package brings Runic.Application.Views and its build targets. The frontend installs @runic-artifex/views and the binding for its framework. To add Runic to a project you already have, follow add Runic to an existing app.

Next steps #