Runic.Application.Views.CsWebUi
0.12.1-preview.1
dotnet add package Runic.Application.Views.CsWebUi --version 0.12.1-preview.1
NuGet\Install-Package Runic.Application.Views.CsWebUi -Version 0.12.1-preview.1
<PackageReference Include="Runic.Application.Views.CsWebUi" Version="0.12.1-preview.1" />
<PackageVersion Include="Runic.Application.Views.CsWebUi" Version="0.12.1-preview.1" />
<PackageReference Include="Runic.Application.Views.CsWebUi" />
paket add Runic.Application.Views.CsWebUi --version 0.12.1-preview.1
#r "nuget: Runic.Application.Views.CsWebUi, 0.12.1-preview.1"
#:package Runic.Application.Views.CsWebUi@0.12.1-preview.1
#addin nuget:?package=Runic.Application.Views.CsWebUi&version=0.12.1-preview.1&prerelease
#tool nuget:?package=Runic.Application.Views.CsWebUi&version=0.12.1-preview.1&prerelease
Runic.Application.Views.CsWebUi
Present Runic Windows and Views with CS-WebUI.
New projects select it with dotnet new runic-app --host cswebui; it opens the
app in a browser window and is supported on a best-effort basis. The template
defaults to the native Runic Desktop host.
dotnet add package Runic.Application.Views.CsWebUi --prerelease
dotnet add package Microsoft.Extensions.DependencyInjection
using Microsoft.Extensions.DependencyInjection;
using MyApp; // the generated AddRunicViews()
using Runic.Application.Views;
using Runic.Application.Views.CsWebUi;
var services = new ServiceCollection();
services.AddScoped<MainViewModel>();
services.AddRunicViews(); // generated in <RootNamespace>.RunicViewsComposition
using var provider = services.BuildServiceProvider();
return RunicCsWebUiHost.Run(provider, async host =>
{
host.ValidateWindow<MainWindow>().ThrowIfInvalid(); // stops at startup when the Bridge is not registered
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 natively with
RunicDesktopHost.Run(provider, ...) from Runic.Application.Views.Desktop.
RunicCsWebUiHost.Run runs the asynchronous application on the thread pool
while the process main thread shows its windows and waits in WebUI's wait loop,
as CS-WebUI expects. It calls WebUiApplication.Clean() before it returns.
OpenWindowAsync creates a DI scope, resolves the Window's ViewModel, assigns
it to the Window's DataContext, attaches the generated Bridge, sets the
content root and size from RunicWindowOptions, and shows StartPage. The
Window owns the native window, scope, and attachments until it is disposed or
CloseAsync drains its accepted operations. Its Host is a
CsWebUiBridgeWindow<TViewModel>; Window exposes the underlying
WebUiWindow; CsWebUiPlatformServices.GetWebUiWindow(window) returns it for
any Window of this host without naming the ViewModel type, and null for
another host's. WaitForCloseAsync completes when WebUI reports that its
windows have closed: CS-WebUI reports closing for all of its windows at once,
so with several windows open it waits for all of them.
A browser window closes without asking the application, and WebUI reports the
close only after it happened, so CS-WebUI cannot keep a window open. A Window
with GuardClose opens unguarded, logs event 1053 WindowCloseGuardUnavailable
once, and its CanCloseAsync is not asked when the user closes the browser
window; the navigation regions' departure guards still run for navigation
inside the window. window.RequestCloseAsync() still asks CanCloseAsync
before it closes the window, because the application is the one closing. Use
Runic Desktop's embedded window when a user close must be able to keep unsaved
work open.
ValidateWindow<TWindow> checks at startup that the generated Bridge for the
ViewModel is registered and returns a RunicWindowValidationResult, the same
result Runic Desktop returns. OpenWindowAsync repeats the check before it
creates the native window. A missing AddRunicViews() throws a
RunicWindowConfigurationException (an InvalidOperationException) whose
diagnostic has the code bridge-not-registered, the same code Runic Desktop
reports, and a remediation, instead of a dependency-injection error. It is
logged as event 1050 through the provider's ILoggerFactory.
ValidateWindow asks the container through IServiceProviderIsService; a
container without it is checked only when OpenWindowAsync resolves the Bridge.
Both hosts share one lifetime contract, IBridgeWindow from
Runic.Application.Views: CloseAsync(timeout, cancellationToken) returns a
RunicWindowCloseResult, and disposal is asynchronous only. Accepted
operations can keep the Window's DI scope alive after the visible window
closes, so dispose the Window with await using before the application returns.
State snapshots and collection delta frames are written, in order, as WebSocket
messages to every connected client. Runic Desktop waits until the WebView has run
each frame. CS-WebUI cannot do that for several clients, so its writes block
instead until each socket accepts the frame or the write times out. A slow
browser therefore holds back delivery at the TCP level. Frames queue in the
bridge, which sends a recovery snapshot once the queue is full, and the window's
other clients wait for the same write. Other CS-WebUI windows are not held back:
WebUI before nightly 9264f7ae9 (webui-dev/webui#723) wrote every WebSocket
frame under one process-wide lock, so one stalled client delayed every window in
the process by up to 30 seconds per frame. CsWebUi 2.5.0-beta.4.8, the lowest
version this package accepts, includes the fix.
What a window opens
By default a window uses WebUI's recommended presentation: an installed browser
in app mode (Chrome, Edge and other Chromium-based browsers work best; Firefox
works without app mode), then the system default browser, then the platform
WebView. RunicCsWebUiHostOptions.UseWebView always uses the WebView: the Edge
WebView2 Runtime on Windows, GTK 3 with WebKitGTK 4.1 on Linux, or WKWebView on
macOS. Pass the options to Run, which keeps WebUI's main-thread loop:
return RunicCsWebUiHost.Run(provider, new RunicCsWebUiHostOptions { UseWebView = true }, async host =>
{
await using var window = await host.OpenWindowAsync<MainWindow>();
await window.WaitForCloseAsync();
return 0;
});
ConfigureWindow configures each WebUiWindow
before it is shown, for example SetPort, and ShowWindow replaces how it is
shown, for example to open a frontend development server's URL. To choose one
browser, set CSWEBUI_BROWSER_PATH to its executable (on macOS its .app
bundle) or call WebUiApplication.SetBrowserPath; WebUI then uses only that
browser, with no fallback. WebUI itself reads no browser variable such as
WEBUI_BROWSER_PATH.
On macOS this package has no native window mode, and none is planned: a window
is a browser window, or with UseWebView the WKWebView window that WebUI creates
and runs from its own main-thread loop. Runic does not run AppKit for it, so
CS-WebUI windows on macOS get no Runic window features (native menu bar, close
confirmation, file drops, window-owned platform services), as on the other
platforms, and Runic's CI does not exercise UseWebView there. An application
that needs a native macOS window uses Runic Desktop, whose RunicDesktopHost.Run
runs the Desktop event loop on the macOS main thread; the same Windows and
ViewModels run on both hosts. For native
windows and platform services, use
Runic.Application.Views.Desktop
instead. services.AddRunicPlatformServices() registers IWindowPlatformServices,
IFileDialogs, ITextClipboard and IDesktopFileLauncher per window here too, so
ViewModels that take them resolve on both hosts. CS-WebUI windows have no native
owner, so every operation, AnnounceAsync included, and the capability snapshot report
Unavailable(OwnerUnavailable). It also registers one IDesktopSettings,
IDesktopNotifications, IDesktopStatusIcon, IDesktopLoginStartup and
IDesktopInhibition, the same application services as Runic Desktop, whose
requests return Unavailable(ProviderNotConfigured): this package selects no
native provider. They need no window, so register a platform provider's service,
such as WindowsPlatformProvider.CreateSettings() or
new PortalApplication("org.example.Notes").CreateInhibition(), before
it to use one on CS-WebUI. The
Runic.Platform guide
lists each service's result on both hosts. A root ViewModel that implements IRunicFileDropTarget is
never called here: a browser window reports no dropped paths, so the page only
gets HTML5 File objects. There is no native menu bar either: the page renders
RunicCsWebUiHostOptions.MenuBar (see Menu bar).
When WebUI cannot show a window, because it finds no browser, the selected
browser does not exist or does not start, or the page does not connect within
the connection timeout, OpenWindowAsync throws a
RunicWindowConfigurationException, as Runic Desktop does when no presentation
can open. Its diagnostic names the browser, its path and the reason, with the
code browser-not-found, browser-not-started, browser-not-connected,
webview-not-started or, for another WebUI error such as an unusable
CSWEBUI_BROWSER_PATH, window-show-failed. The WebUiShowException (or
WebUiException) is its inner exception. The host logs the failure as event
2100 CsWebUiWindowShowFailed in the category Runic.Application.Views.CsWebUi,
or writes it to Trace without an ILoggerFactory. When the application does
not handle the exception, RunicCsWebUiHost.Run writes its message to standard
error and returns 1 instead of ending the process with an unhandled exception.
WebUI starts browsers detached and discards their output; run the browser with
the window's URL to see why it fails.
ValidateWindow reports, with the code browser-not-found, a
CSWEBUI_BROWSER_PATH that names no file, because WebUI uses that browser
without fallback and every open fails. It cannot check more without showing a
window: WebUI's recommended presentation falls back from an installed browser
to the system default browser and then to the WebView, and the WebView runtime
(for UseWebView too) is loaded only when a window shows. Those failures are
reported by OpenWindowAsync as above. ShowWindow and serve-only mode skip
the check.
Set RUNIC_APPLICATION_SERVE_ONLY=1 to start the local server without opening
anything. Each window then writes RUNIC_APPLICATION_URL=<url> to
standard output, ignoring UseWebView and ShowWindow, and keeps serving
until a line is read from standard input or the process ends, so a test
harness or remote browser can drive the unchanged application. Write the line
on Windows too; the
serve-only protocol
is the same as on Runic Desktop, and RunicServeOnlyApplication in
Runic.Application.Testing
implements it for browser tests. In serve-only mode ValidateWindow checks no
browser. On Linux with GTK 3 and WebKitGTK 4.1 installed, WebUI initializes
GTK when it creates a window, also in serve-only mode, and GTK ends the process
when no display opens. Give a headless run a display, for example with
xvfb-run.
Menu bar
A CS-WebUI window has no native menu bar, so the page renders the host's bar
with its own menubar component, as a Runic Desktop window with
DesktopMenuBarPresentation.Page does. Declare the menus with
RunicCsWebUiHostOptions.MenuBar, the same DesktopMenu model and rules as
Runic Desktop's DesktopHostOptions.MenuBar, and run them with the Window's
root ViewModel (IRunicMenuCommandSource):
return RunicCsWebUiHost.Run(provider, new RunicCsWebUiHostOptions
{
MenuBar =
[
new DesktopMenu("File", [new DesktopMenuItem("Save", "file.save") { Shortcut = new DesktopShortcut("S") }])
{ Role = DesktopMenuRole.File, AccessKey = 'F' },
],
}, async host => { ... });
The page reads the bar with createMenuBarController() from
@runic-artifex/views, or useMenuBar and injectMenuBar in the framework
adapters; see
In-page menu bar.
The routes are those of Runic Desktop
(specs/desktop/menu-bar-page.md),
from the same source, so the same frontend runs on both hosts:
placementis"page"on every platform while the host declares menus, and"none"with an empty bar.- An item is enabled while its
IsEnabledand its command'sCanExecute(CommandParameter)aretrue, followingCanExecuteChanged, andinvokerunsExecute(CommandParameter)in the ViewModel's model turn. The host asks the ViewModel for each id when the Window opens and afterSetMenuBar. There are no application commands, so an id the ViewModel returnsnullfor stays disabled. A failure is logged as event 2102,CsWebUiMenuCommandFailed. - WebUI reports no keys to .NET, so the page's controller matches the items'
shortcuts (
shortcuts"page"), andshortcuts: "app"leaves them to the page's own handler. host.SetMenuBar(menus)replaces the bar in every Window of the host, for example with new checked state, andhost.MenuBarreturns it.
Build output
The package's build targets copy runic-cswebui.js into www/ next to the
built frontend, also as runic-views.js. Load webui.js (served by WebUI) and
runic-views.js before the generated client in index.html; the same page then
runs on Runic Desktop, whose package copies its own client as runic-views.js.
A project that references both hosts sets RunicApplicationHost (cswebui or
desktop); see
the host of a page.
Pages that load runic-cswebui.js keep working. The script adapts
window.webui to the generated client's transport contract.
The script sends each Bridge call as soon as the page makes it, so several
calls can reach WebUI at once. Runic relies on WebUI's fix for concurrent calls
(webui-dev/webui#721): before it, native WebUI could assign one event slot to
two calls that arrive together. One call could then run with the other's
arguments or receive the other's reply, and one of them could receive no reply
at all. The fix is in WebUI nightly efac0e24d and in every CsWebUi since
2.5.0-beta.4.7. CsWebUi 2.5.0-beta.4.8, the lowest version this package
accepts, bundles WebUI nightly 2b35db13f864031543f61816c496b1035c026340,
which also removes the process-wide send lock (webui-dev/webui#723) and gives
navigation events their URL argument (webui-dev/webui#724). An application that
loads its own WebUI through CSWEBUI_NATIVE_LIBRARY or
WebUiNativeLibrary.SetLibraryPath needs efac0e24d or later for concurrent
calls, and 2b35db13f or later for the behaviour this package is tested with.
WebUI reports no version or commit that Runic could check, so the host logs
event 2101 CsWebUiNativeLibraryUnchecked (Warning) once, before the first
window opens, when such a library is configured.
Applications that keep their own copy of runic-cswebui.js should update it.
Since 0.11.0 the host sends Views state to the script's __runicViewsPush
function and carries out navigations through __runicContinueNavigation, both
as WebUI raw data. A copy from before 0.11.0 has no __runicViewsPush: WebUI
then tries to call it as a script, which fails, and the page's Views receive no
state pushes (a Content-Security-Policy reports the refusal). The build
replaces the copy in www/ on every build, so only a copy the application
keeps elsewhere, such as in a frontend's public/ folder, can be stale.
The window binds all WebUI events because only that binding delivers disconnects. The script stops WebUI from also sending a click event for every element with an id: those clicks would reach the server in parallel with the Bridge calls they start. Use Bridge commands rather than WebUI element bindings in a Views window.
With that binding, WebUI also cancels the page's own navigations after each connect until a Bridge call allows them, and reports each one to the server. The host carries out a same-origin one, such as a reload, a link to another page or a fragment, so the page does not stay put before its first Bridge call. A cross-origin navigation and a form submission stay cancelled.
A window-local CreateBridgeSession() retains one native binding for each route
name and swaps the active managed handler as Views change; an inactive route
returns the reply error released (Views protocol 3). CS-WebUI cannot remove a native route registration
before its window closes. Retained page references reuse routes; new page
identities still add routes.
Security
This host adds no admission of its own. Runic Desktop checks the Origin of
each connection, requires a per-surface session credential and binds an
installed browser with a client cookie; in a CS-WebUI window, who can connect
is decided by native WebUI alone: its local server, the token it writes into
its webui.js and its single-client default. Treat these settings as security
decisions:
SetPublic(true).ConfigureWindowcan call it on theWebUiWindow. The window's server is then reachable from the network, not only from this machine, and any client that can load the page can call the Bridge. Leave it off unless the application authenticates its users itself.Serve-only mode. With
RUNIC_APPLICATION_SERVE_ONLY=1the window writes its URL to standard output and waits for a client. The first local client to connect gets the window, and on a shared machine that can be another user's process. Use it for test harnesses on machines you control.Multiple clients.
WebUiApplication.SetConfiguration(WebUiConfiguration.MultiClient, true)lets every client that reaches the server share the window.Your own WebUI build. A library from
CSWEBUI_NATIVE_LIBRARYorWebUiNativeLibrary.SetLibraryPathmust include WebUI commitefac0e24d: with an older WebUI, Bridge calls that arrive together can run with each other's arguments or receive each other's replies. Runic is tested with2b35db13f, which CsWebUi 2.5.0-beta.4.8 bundles (see above). Runic cannot check a custom build, so the host logs event 2101 when one is configured.Content-Security-Policy. WebUI serves
SetRootFolderfiles without headers of its own, so give the page a policy with<meta http-equiv="Content-Security-Policy" content="…">. A<meta>policy cannot carryframe-ancestors,report-uriorsandbox. The Bridge needs'self'scripts and connections and no'unsafe-eval', as indefault-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'. The host sends Views state and navigations as data, whichrunic-cswebui.jsparses instead of evaluating. WebUI still evaluates script forWebUiWindow.RunJavaScript/ExecuteJavaScriptandWebUiEvent.RunJavaScript, and forSetSizeorSetPositionwhile a browser window is connected (the host sizes a window before it shows it). An application that uses them needs'unsafe-eval'.
Runic does not review native WebUI's admission. For a page that shows
untrusted content, or a machine shared with other users, prefer Runic Desktop
(Runic.Application.Views.Desktop), whose admission Runic implements and
documents.
See the Runic.Application.Views package guide for Windows, Views, generated clients, and build properties.
| 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
- CsWebUi (>= 2.5.0-beta.4.8 && < 3.0.0)
- CsWebUi.Native (>= 2.5.0-beta.4.8 && < 3.0.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.14)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.14)
- Runic.Application.Views (= 0.12.1)
- Runic.Platform (= 0.12.1)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.12.1-preview.1 | 0 | 10/27/2026 |
| 0.12.0-preview.1 | 0 | 10/27/2026 |
| 0.11.0-preview.1 | 1,647 | 10/25/2026 |
| 0.10.0-preview.1 | 3,446 | 10/22/2026 |
| 0.9.1-preview.1 | 584 | 10/21/2026 |
| 0.9.0-preview.1 | 122 | 10/21/2026 |
| 0.8.0-preview.6 | 819 | 10/19/2026 |
| 0.8.0-preview.5 | 206 | 10/17/2026 |
| 0.8.0-preview.4 | 601 | 10/17/2026 |
| 0.8.0-preview.3 | 602 | 10/15/2026 |
| 0.8.0-preview.2 | 105 | 10/15/2026 |
| 0.8.0-preview.1 | 143 | 10/15/2026 |
| 0.7.0-preview.9 | 464 | 10/14/2026 |
| 0.7.0-preview.8 | 100 | 10/12/2026 |
| 0.7.0-preview.7 | 55 | 10/12/2026 |