Runic.Application.Views.Desktop
0.8.0-preview.5
dotnet add package Runic.Application.Views.Desktop --version 0.8.0-preview.5
NuGet\Install-Package Runic.Application.Views.Desktop -Version 0.8.0-preview.5
<PackageReference Include="Runic.Application.Views.Desktop" Version="0.8.0-preview.5" />
<PackageVersion Include="Runic.Application.Views.Desktop" Version="0.8.0-preview.5" />
<PackageReference Include="Runic.Application.Views.Desktop" />
paket add Runic.Application.Views.Desktop --version 0.8.0-preview.5
#r "nuget: Runic.Application.Views.Desktop, 0.8.0-preview.5"
#:package Runic.Application.Views.Desktop@0.8.0-preview.5
#addin nuget:?package=Runic.Application.Views.Desktop&version=0.8.0-preview.5&prerelease
#tool nuget:?package=Runic.Application.Views.Desktop&version=0.8.0-preview.5&prerelease
Runic.Application.Views.Desktop
Present Runic Windows and Views with Runic Desktop: native windows, embedded WebViews or browser presentations, and the Runic platform services.
dotnet add package Runic.Application.Views.Desktop --prerelease
dotnet add package Microsoft.Extensions.DependencyInjection
This is the default host of the runic-app project template: dotnet new runic-app
opens a native window with this host.
In a project named MyApp:
using Microsoft.Extensions.DependencyInjection;
using MyApp; // the generated AddRunicViews()
using Runic.Application.Views;
using Runic.Application.Views.Desktop;
using Runic.Desktop;
var services = new ServiceCollection();
services.AddScoped<MainViewModel>();
services.AddRunicViews();
await using var provider = services.BuildServiceProvider();
// Windows uses WebView2 and macOS WKWebView; Linux selects a toolkit explicitly.
var options = new DesktopHostOptions { Linux = new() { EmbeddedBackend = LinuxEmbeddedBackend.Gtk3WebKit41 } };
return RunicDesktopHost.Run(provider, options, async host =>
{
// Stops at startup when a registration, WebView runtime or window option is missing.
host.ValidateWindow<MainWindow>().ThrowIfInvalid();
await using var window = await host.OpenWindowAsync<MainWindow>(new RunicWindowOptions { Width = 800, Height = 600 });
await window.WaitForCloseAsync();
return 0;
});
public sealed partial class MainWindow : RunicWindow<MainViewModel>;
The same Window and async host => ... body run on CS-WebUI with
RunicCsWebUiHost.Run(provider, ...) from Runic.Application.Views.CsWebUi.
Entry points
RunicDesktopHost.Run(provider, options, application)starts aDesktopHostand runs the asynchronous application on the event loop the platform needs (DesktopEventLoop): the process main thread on macOS and with GTK 4. Call it fromMainbefore anyawaitand return its result. Ctrl+C and SIGTERM close the open windows, soWaitForCloseAsyncreturns, and cancelhost.Desktop.ShutdownRequested. A body without a window, such as one behind a status icon, setsDesktopHostOptions.HandlesShutdownWithoutWindowand ends its loop on that token; without that option, a signal that arrives while no Window is open or served ends the process with its default action.host.OpenWindowAsync<TWindow>(options, cancellationToken), fromRunic.Application.Views, creates the Window's DI scope, resolves its ViewModel, opens aDesktopSurfaceand its presentation, and attaches the generated Bridge. The surface servesRunicWindowOptions.ContentRoot(wwwby default) and opensStartPage;WidthandHeightreplace the window size. For other surface or window options, createnew RunicDesktopHost(provider, desktop) { SurfaceOptions = ..., WindowOptions = ... }insideDesktopEventLoop.Run.RunicDesktopHost.DefaultWindowOptionsopens the embedded WebView and falls back to an installed browser.RunicDesktopHost.Resourcesserves application files that are not part of the frontend build, such as icons from a cache, from each Window's own address. It is called with each Window as it opens, after its ViewModel is set, and returns that Window'sContentHandlerornull. The surface asks the handler only for a request its content has no file for, before 404, so the page loads<img src="icons/org.example.App.png">alike in a native window, in a browser after a fallback and underRUNIC_APPLICATION_SERVE_ONLY. The request path is decoded and can contain..: map it to known files rather than to the file system.var host = new RunicDesktopHost(provider, desktop) { Resources = window => window is DashboardWindow ? icons.ServeAsync : null, };host.OpenWindowAsync<TWindow>(options, activation, cancellationToken)onRunicDesktopHostopens a Window for a launch or a click: it opens the Window as above and presents it with the desktop's activation token or startup ID, such asApplicationLaunch.PlatformContextof a launch the running instance received. Wayland compositors such as KWin give a new window focus only with that token. A Window that is already open is brought forward withPlatformServices.PresentAsync(window, context)instead.host.ValidateWindow<TWindow>(options)checks, before any window opens, that the generated Bridge for the ViewModel is registered and thatDesktopHost.Validateaccepts the window options. It returns every problem as aRunicWindowDiagnosticwith a stable code and remediation: an errorbridge-not-registered, the Runic Desktop codes, and a warningviewmodel-not-registeredwhen the container does not report the ViewModel throughIServiceProviderIsService. A container can resolve services it does not report, so only the Bridge check fails; a container withoutIServiceProviderIsServiceis not checked.ThrowIfInvalid()throws aRunicWindowConfigurationException.OpenWindowAsyncruns the same checks before it creates a surface and throws the same exception; aDesktopConfigurationExceptionfrom Runic Desktop becomes itsInnerException. A missing Bridge is logged as event 1050 through the provider'sILoggerFactory.DesktopBridgeWindow<TViewModel>, the Window'sHost, owns the Window's scope, surface, and attachment. It exposesViewModel,Surface,Presentation,WaitForCloseAsync, andCloseAsync, which stops new operations and waits for accepted ones before releasing the scope.RunicDesktopHost.GetSurface(window)returns theSurfaceof any Window of this host without naming its ViewModel type, andnullfor another host's. It implementsIBridgeWindow, the lifetime contract shared with the CS-WebUI host, and returns the sameBridgeWindowCloseResult.A Window with
GuardClose(see Guarding a user close) passes itsCanCloseAsyncto the embedded window asDesktopWindowOptions.ConfirmCloseAsync, ahead of aConfirmCloseAsyncofWindowOptions, so the title-bar close button, Alt+F4 or the window manager asks the navigation regions' departure guards.window.RequestCloseAsync()is the same native close request and shares its decision;CloseAsync, disposal, Ctrl+C and SIGTERM do not ask. An explicitly selected browser, theEmbeddedThenBrowserfallback, or a custom window host without theCloseConfirmationcapability opens the Window unguarded for the user's close and logs event 1053WindowCloseGuardUnavailable;RequestCloseAsyncstill asksCanCloseAsyncthere.PlatformServices.AnnounceAsync(window, message, priority)asks the screen reader to speak a status message for a Window, such as "Saved", through its embedded window (DesktopWindow.AnnounceAsync; see Accessibility). WebKitGTK reports no ARIA live region to Orca, so on Linux this is how a page's status reaches a screen reader. A ViewModel takesDesktopWindowPlatformServicesand calls itsAnnounceAsync(message, priority). Both returnSuccess, orUnavailablewithOwnerUnavailablefor a Window without an embedded window (an installed browser, serve-only mode, another host, or a ViewModel's window that has not opened yet),OwnerClosedonce it closed, andBackendUnavailablefor a window host without announcements, such as GTK 4 before 4.14.RunicDesktopHost.Placementrestores each Window's size, position and maximized state from the last time it closed, andPlatformServices.GetWindowState(window)reads them to save. The callback is called with each Window as it opens, after its ViewModel is set, and returns the savedDesktopWindowPlacementornull; it becomesDesktopWindowOptions.Placement, which replaces the Window's size and keeps it on a monitor that exists now (see Window state and restore).GetWindowStatereturns the embedded window'sDesktopWindowState, also after the window closed and until the Window is disposed, so save it afterWaitForCloseAsyncreturns:var host = new RunicDesktopHost(provider, desktop) { Placement = window => window is MainWindow ? settings.MainWindowPlacement : null, }; await using var window = await host.OpenWindowAsync<MainWindow>(); await window.WaitForCloseAsync(); if (PlatformServices.GetWindowState(window) is PlatformResult<DesktopWindowState>.Success { Value: var state }) settings.MainWindowPlacement = state.Placement;A Window without a native owner, one on CS-WebUI or another host, one served under
RUNIC_APPLICATION_SERVE_ONLYor an installed browser after a fallback, reportsUnavailablewithOwnerUnavailable, asPresentAsyncdoes; a custom window host that reports no state reportsBackendUnavailable. For each change while the window is open, subscribe toDesktopWindow.StateChangedof the Window'sPresentation. A placement belongs to the machine and operating system that saved it: its units are each platform's.RunicDesktopHost.MenuCommandsruns the application's menu bar commands by id, and a Window's root ViewModel that implementsIRunicMenuCommandSourceruns its own while the Window is key (see Menu bar commands). Declare the menus inDesktopHostOptions.MenuBar; macOS shows them.A Window's root ViewModel that implements
IRunicFileDropTargetreceives the files the user drops onto the embedded window, with their native paths, in its model turn (see Receiving dropped files). A window served underRUNIC_APPLICATION_SERVE_ONLYor shown in an installed browser after a fallback receives none.services.AddRunicPlatformServices()registers native file dialogs, the text clipboard, and the file launcher for each window scope:IWindowPlatformServices,IFileDialogs,ITextClipboard, andIDesktopFileLauncher. A ViewModel takes them in its constructor before its window opens. They bind to the window's native owner when it opens, and the provider is selected from the backend that is actually running:- Windows:
Runic.Platform.Windows. - macOS:
Runic.Platform.MacOS. - Linux with GTK 3:
Runic.Platform.Linux. - Linux with GTK 4 (
WithGtk4()): xdg-desktop-portal dialogs and the file launcher throughRunic.Platform.Linux.Portal, parented withRunic.Platform.Linux.Gtk4, plus the GTK 4 clipboard. A GTK 4 process never loads the GTK 3 provider.
Until the window opens, every operation returns
Unavailable(OwnerUnavailable). It returns the same result when the window has no native owner, for example an installed browser after an embedded-window fallback. After the window closes, every operation returnsUnavailable(OwnerClosed). A Linux process without a GTK backend returnsUnavailable(ProviderNotConfigured).GetSnapshot()reports the same states per capability. The package references every provider package, but loads only the selected provider assembly. An application that targetsnet10.0-windows, such as a WPF application, gets an assembly that references onlyRunic.Platform.Windows, so it does not ship the Linux and macOS providers.dotnet runic doctornames the provider and, on Linux, checks for the session bus and xdg-desktop-portal.PlatformServices.ForWindow(owner)creates the same services for aDesktopWindowthat the application opened itself.services.AddScoped<NotesViewModel>(); services.AddRunicViews(); services.AddRunicPlatformServices(); public sealed partial class NotesViewModel(IFileDialogs files) : ObservableObject { [ObservableProperty] private string _text = ""; public async Task OpenAsync() { if (await files.OpenFileAsync(new() { Filters = [new("Notes", ["txt", "md"])] }) is not PickerResult<IReadFileLease>.Selected(var lease)) return; await using (lease) await using (var stream = await lease.OpenReadAsync()) using (var reader = new StreamReader(stream)) Text = await reader.ReadToEndAsync(); } // Replaces the file atomically, or leaves it unchanged. public async Task<bool> SaveAsync() { if (await files.SaveFileAsync(new("notes.txt") { Filters = [new("Notes", ["txt"])] }) is not PickerResult<ISaveFileLease>.Selected(var lease)) return false; await using (lease) { if (await lease.BeginWriteAsync(FileWritePolicy.RequireAtomicReplace) is not PlatformResult<IFileWriteTransaction>.Success(var transaction)) return false; await using (transaction) { await using (var writer = new StreamWriter(transaction.Content, leaveOpen: true)) await writer.WriteAsync(Text); return await transaction.CommitAsync() is FileCommitResult.Committed; } } } }Filters, the initial folder and
OpenFilesAsyncfor several files work with every provider; the Runic.Platform guide lists what each platform does with them.On Windows, Linux and for unsandboxed macOS selections, the save writes a staged sibling file and replaces the selection in one step. On Linux this also works inside Flatpak: the xdg-desktop-portal document view stages the sibling as a private host file and renames it onto the chosen file. A sandboxed macOS selection cannot stage a sibling yet, so there
BeginWriteAsyncreturnsUnavailable(AtomicReplaceUnavailable)before it changes the file. Linux can return the same result for a read-only or revoked grant, andCommitAsyncreturnsNotCommitted(Conflict)when the file changed or appeared since selection. Afalseresult therefore means the file was not changed: tell the user and offer Save As to another destination. Runic never falls back to overwriting the file in place. The portal guide describes the document-view mechanism and the GTK 3 opt-in. CS-WebUI'sAddRunicPlatformServices()registers the same services, which returnOwnerUnavailable.- Windows:
AddRunicPlatformServices()also registersIDesktopSettings, the desktop's appearance preferences (color scheme, accent color, high contrast, reduced motion). They belong to the application, not to a window, so there is one singleton, whichPlatformServices.CreateSettings()creates when it is first resolved and the service provider disposes. Windows reads its settings, macOS reads AppKit, and Linux reads xdg-desktop-portal with either GTK version, without loading GTK. On other systems every read returnsUnavailable(ProviderNotConfigured), and so does CS-WebUI's registration. A ViewModel or service takes it in its constructor and watches it off the model context:await foreach (var result in settings.WatchAsync(cancellationToken)) if (result is PlatformResult<DesktopAppearance>.Success { Value: var appearance }) await context.InvokeAsync(() => DesktopScheme = appearance.ColorScheme.ToString(), cancellationToken);The multi-window example stores the app's own theme in a settings file and follows this preference when the theme is "system". Register your own
IDesktopSettingsbefore callingAddRunicPlatformServices(), for example in tests, to replace it.AddRunicPlatformServices()also registersIDesktopNotifications, one singleton for the application, whichPlatformServices.CreateNotifications(applicationId)creates when it is first resolved and the service provider disposes. A ViewModel takes it in its constructor, with noOperatingSystembranch:services.AddRunicPlatformServices("org.example.Notes"); public sealed partial class NotesViewModel(IFileDialogs files, IDesktopNotifications notifications) : ObservableObject { private async Task SavedAsync(string name) => await notifications.ShowAsync(new DesktopNotification("saved", "Notes", $"Saved {name}.")); }The application ID is the one string you pass: on Windows the AppUserModelID your installer registers with the shell (an MSIX package's process uses the package's AppUserModelID instead), on Linux the reverse-DNS ID of the installed desktop entry (
org.example.Notesfororg.example.Notes.desktop); macOS uses the bundle identifier. Runic does not derive it from the assembly or the GTK application ID. The overload checks it for the running system and throwsArgumentExceptionwhen it is invalid there. Without an ID,AddRunicPlatformServices()lets the Linux portal infer the identity from the desktop session, which suits development, and Windows requests returnUnavailable(ProviderNotConfigured); so does CS-WebUI's registration. Creating the service sends nothing and opens no connection; the first request does, and success acknowledges submission, not that the desktop showed it. On Linux, a rejected desktop ID is reported asportal-identity-registration-failed, and portal diagnostics are logged as event 2003 through the provider'sILoggerFactory. Register your ownIDesktopNotificationsfirst to replace it, for example a fake in tests.AddRunicPlatformServices()also registersIDesktopStatusIcon,IDesktopLoginStartupandIDesktopInhibition, one singleton each for the application, created byPlatformServices.CreateStatusIcon(applicationId),CreateLoginStartup(applicationId)andCreateInhibition(applicationId). Inhibition keeps the system or display awake while an operation holds a lease:if (await inhibition.AcquireAsync(DesktopInhibitionEffects.SystemSleep, "Exporting documents") is PlatformResult<IDesktopInhibitionLease>.Success(var lease)) await using (lease) await ExportAsync();Windows uses a power request and macOS an IOKit power assertion; Linux asks the xdg-desktop-portal Inhibit interface without a parent window, so the request belongs to the application, not to a window. The application services need no native owner and work the same in an installed browser after a fallback; the window services report
OwnerUnavailablethere. The Runic.Platform guide lists every service's result without a native owner. RegisterIDesktopPrivilegedActionsyourself; it needs declared actions.DesktopBridgeWindow<TViewModel>.NativeOwner(aDesktopNativeOwner) is the presentation'sINativePickerOwner, which the services above bind to. The provider APIs that take it directly, such asWindowsPlatformProvider.CreateFileDialogs(owner)orPortalPlatformProvider.CreateFileDialogs(Gtk4PlatformProvider.CreatePortalWindowOwner(owner)), keep working for applications that compose providers themselves.LinuxPlatformProviderparents dialogs through GTK 3, so do not use it with a GTK 4 window.The owner runs provider callbacks on the window's native thread and becomes unavailable when the window closes or the surface replaces it. A presentation without native dispatch still has an owner, but it reports
IsAvailable == false. Examples are an installed browser after an embedded-window fallback, or a custom host without a native handle. Applications that open aDesktopWindowthemselves create one withnew DesktopNativeOwner(window).DesktopBridgeTransportconnects aDesktopSurfaceto aWindowContentSessiondirectly, for applications that compose a surface themselves.
The surface owns authenticated browser or embedded WebView sessions and
removable capability bindings; the application Window owns its ViewModel scope
and logical View lifetimes. DesktopHostOptions and DesktopWindowOptions
select the browser or embedded backend; the Runic Desktop guide lists the
native prerequisites for each platform.
The package's build targets copy runic-desktop-views.js into www/, also as
runic-views.js. Load webui.js (served by the surface) and runic-views.js
before the generated client in index.html; the same page then runs on
CS-WebUI, whose package copies its own client as runic-views.js. A project
that references both hosts sets RunicApplicationHost (desktop or
cswebui); see
the host of a page.
Pages that load runic-desktop-views.js keep working. runic({ desktop: true })
is not needed for a Views page. The adapter uses the same host-neutral client
contract as the CS-WebUI adapter, and its build defaults match
Runic.Application.Views.CsWebUi, so AddRunicViews() is generated in
<ProjectName>.RunicBridgeComposition.
The First Window on Runic Desktop example is a complete application. See the host selection guide to choose between this host and CS-WebUI.
An exception from the application body passed to Run closes the windows,
disposes the host and is rethrown by Run; Runic does not log it. The
crash reporting guide
shows how to log such crashes, capture a dump and keep NativeAOT symbols.
From a command line
An application with a command line, such as my-app ui next to other commands,
starts its Window from a
Runic.CommandLine command
with Runic Command Line 0.7.0-preview.2 or later (see its guide,
Start a UI host on the main thread). RunicDesktopHost.Run
and DesktopEventLoop.Run must be called on the process main thread before any
await, and a command runs asynchronously, so the command does not start the
host itself: it hands the host's start back to Main with
CommandExecutionContext.ContinueOnCallingThread, and Main returns
CommandApp.Run(args) instead of awaiting RunAsync. Run blocks until the
command completed, then runs the continuation on the thread that called it and
returns its result as the exit code:
using Runic.CommandLine;
using Runic.CommandLine.Generated;
// A synchronous Main, or no await before this line: the thread is the process main thread.
return new CommandApp(GeneratedCommandCatalog.Create()) { Name = "my-app" }.Run(args);
internal static class WindowCommands
{
[Command("ui", Description = "Open the main window.")]
internal static async Task Ui(CommandExecutionContext context,
[Option("--settings")] string? path = null, CancellationToken cancellationToken = default)
{
// Asynchronous work and its failures belong to the command, before the host starts.
AppSettings settings = await AppSettings.LoadAsync(path, cancellationToken);
context.ContinueOnCallingThread(() => DesktopEventLoop.Run(new DesktopHostOptions(), async desktop =>
{
await using var provider = new ServiceCollection()
.AddSingleton(settings)
.AddScoped<MainViewModel>()
.AddRunicViews()
.BuildServiceProvider();
var host = new RunicDesktopHost(provider, desktop);
await using var window = await host.OpenWindowAsync<MainWindow>();
await window.WaitForCloseAsync();
return 0;
}));
}
}
DesktopEventLoop.Run and new RunicDesktopHost(provider, desktop) let the
service provider be created and disposed asynchronously on the event loop.
RunicDesktopHost.Run(provider, options, application) works the same way in a
continuation for a provider built before it.
The continuation runs only when the command succeeded and was not cancelled,
after its output was written and its scope disposed, so capture values, such as
settings, rather than the command's scoped services. Help, usage errors and
the other commands never start the host, and a failed command returns its exit
code without opening a Window. Runic.CommandLine stops handling Ctrl+C before
the continuation starts; the Desktop host then handles Ctrl+C and SIGTERM as
described above. Under CommandApp.RunAsync, the hosting adapter's RunAsync
or a directly used CommandExecutor,
CommandExecutionContext.CanContinueOnCallingThread is false and
ContinueOnCallingThread throws.
Test such a command with CommandAppTester.Run from
Runic.CommandLine.Testing, which runs the continuation on the calling
thread, or start the application in serve-only mode with
RunicServeOnlyApplication from Runic.Application.Testing.
Automation
Set RUNIC_APPLICATION_CLOSE_AFTER_OPEN=1 to start a template-shaped
application without a user. With it set, RunicDesktopHost closes every
window it opens, as soon as the presentation has opened and, with
DesktopHostOptions.WaitForConnection, its bridge has connected. Before
closing, it writes:
- a warning to standard error, and event 2002 through a registered
ILoggerFactory, so an inherited setting is visible; RUNIC_APPLICATION_OPENED=<presentation>(for exampleEmbedded, or the browser it fell back to) to standard output.
An application shaped like the runic-app template then sees WaitForCloseAsync
return and exits normally. Applications that keep running after a window
closes, or that open further windows, are not meant to use it. The template
acceptance check uses it to run the GTK 4 template under Xvfb.
Under dotnet runic dev, a full page reload, such as Vite's full-reload
after an edit, closes the View's connection and fails a script in flight
(IOException, then No authenticated WebUI browser is connected until the
page reconnects), so a test waits for the View, for example for an element it
renders, instead of assuming that a call lands; @runic-artifex/vite-plugin-runic
pre-bundles its development client, so a cold Vite dependency cache no longer
reloads the first page.
Set RUNIC_APPLICATION_SERVE_ONLY=1 to drive the unchanged application from a
browser test, with the same contract as on
CS-WebUI.
RunicDesktopHost then opens no window or browser. Each window creates its
surface, writes RUNIC_APPLICATION_URL=<url> to standard output, and serves
until a line is read from standard input, a termination signal requests
shutdown, or the process ends; then WaitForCloseAsync returns. The URL is
the surface's capability URL, and the surface admits one browser. It takes
precedence over RUNIC_APPLICATION_CLOSE_AFTER_OPEN. As on CS-WebUI there is
no native owner: platform services report OwnerUnavailable, and a Window
with GuardClose opens unguarded (event 1053). NativeOwner throws, so bring
a Window forward with PlatformServices.PresentAsync(window, context), which
takes the IRunicWindow and reports OwnerUnavailable in this mode and for a
Window on another host. ValidateWindow makes the same
decision: in serve-only mode it skips the presentation checks, such as
webkitgtk-runtime-missing, graphical-session-missing and
presentation-unavailable, which describe what this machine lacks to present a
window, so an application that validates at startup also starts headless. It
still reports configuration errors, such as window-option-invalid,
browser-unsupported or gtk4-provider-missing, which fail every launch that
presents a window.
A served Window can end with its last browser tab, as a --browser command
that opens the URL in the default browser would. The Window's surface counts
its connected pages in ConnectionCount, and a connection event's handler
already sees the count with that event. A reload disconnects before or after
the page connects again, so wait a moment before closing:
var surface = RunicDesktopHost.GetSurface(window)!;
using var connections = surface.SubscribeConnectionEvents(invocation =>
{
if (invocation is not { Kind: PresentationEventKind.Disconnected, Surface.ConnectionCount: 0 }) return;
_ = Task.Run(async () =>
{
await Task.Delay(TimeSpan.FromSeconds(1));
if (surface.ConnectionCount == 0) await window.CloseAsync(TimeSpan.Zero);
});
});
OpenInBrowser(surface.Url);
await window.WaitForCloseAsync();
The serve-only protocol, the same on both hosts:
- URL. Each window writes one
RUNIC_APPLICATION_URL=<url>line to standard output once its surface serves. The first line is the first window's. - Stop. Writing a line to standard input ends serving for every window
serving at that moment; end of input, such as
</dev/null, does not. On Runic Desktop a window the application opens after that line serves until the next one; on CS-WebUI the line ends the application. On Runic Desktop on Linux and macOS, SIGTERM or Ctrl+C also requests shutdown. On Windows a test cannot send another process a termination signal, and killing it skips the application's cleanup, so write the line. - Timeouts. Bound both waits, and kill the process tree when the application does not exit after the line.
RunicServeOnlyApplication in
Runic.Application.Testing
implements it for browser tests. A headless Chromium, as in the
First Window on Runic Desktop
browser smoke, does not exercise the embedded WebView engine.
To drive the embedded View itself from an in-process test, run script in it through the window's surface and click the View's own controls, such as a dialog's button:
await using var window = await host.OpenWindowAsync<MainWindow>();
var surface = ((DesktopBridgeWindow<MainViewModel>)window.Host).Surface;
var title = await surface.ExecuteJavaScriptAsync(
"return document.querySelector('h1')?.textContent ?? ''", TimeSpan.FromSeconds(10));
ExecuteJavaScriptAsync needs a connected page; with
DesktopHostOptions.WaitForConnection the window has connected when
OpenWindowAsync returns.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. net10.0-windows7.0 is compatible. |
-
net10.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.12)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.12)
- Microsoft.Web.WebView2 (>= 1.0.4258.31)
- Runic.Application.Views (= 0.8.0-preview.5)
- Runic.Desktop (= 0.8.0-preview.5)
- Runic.Platform.Linux (= 0.8.0-preview.5)
- Runic.Platform.Linux.Gtk4 (= 0.8.0-preview.5)
- Runic.Platform.Linux.Portal (= 0.8.0-preview.5)
- Runic.Platform.MacOS (= 0.8.0-preview.5)
- Runic.Platform.Runtime (= 0.8.0-preview.5)
- Runic.Platform.Windows (= 0.8.0-preview.5)
- Tmds.DBus.Protocol (>= 0.95.1)
-
net10.0-windows7.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.12)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.12)
- Microsoft.Web.WebView2 (>= 1.0.4258.31)
- Runic.Application.Views (= 0.8.0-preview.5)
- Runic.Desktop (= 0.8.0-preview.5)
- Runic.Platform.Runtime (= 0.8.0-preview.5)
- Runic.Platform.Windows (= 0.8.0-preview.5)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on Runic.Application.Views.Desktop:
| Package | Downloads |
|---|---|
|
Runic.Application.Views.Wpf
Optional WPF child WebView presentation for existing Runic ViewModels. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.8.0-preview.5 | 78 | 10/17/2026 |
| 0.8.0-preview.4 | 711 | 10/17/2026 |
| 0.8.0-preview.3 | 696 | 10/15/2026 |
| 0.8.0-preview.2 | 101 | 10/15/2026 |
| 0.8.0-preview.1 | 149 | 10/15/2026 |
| 0.7.0-preview.9 | 473 | 10/14/2026 |
| 0.7.0-preview.8 | 88 | 10/12/2026 |
| 0.7.0-preview.7 | 54 | 10/12/2026 |