Runic.Application.Views.Wpf 0.12.0

dotnet add package Runic.Application.Views.Wpf --version 0.12.0
                    
NuGet\Install-Package Runic.Application.Views.Wpf -Version 0.12.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Runic.Application.Views.Wpf" Version="0.12.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Runic.Application.Views.Wpf" Version="0.12.0" />
                    
Directory.Packages.props
<PackageReference Include="Runic.Application.Views.Wpf" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Runic.Application.Views.Wpf --version 0.12.0
                    
#r "nuget: Runic.Application.Views.Wpf, 0.12.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Runic.Application.Views.Wpf@0.12.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Runic.Application.Views.Wpf&version=0.12.0
                    
Install as a Cake Addin
#tool nuget:?package=Runic.Application.Views.Wpf&version=0.12.0
                    
Install as a Cake Tool

Runic.Application.Views.Wpf

Optional WPF child presentation for a Runic web View. Targets .NET 10 and Windows. The containing WPF application owns its dispatcher, shell, navigation engine, ViewModels and services. Toolkit, ReactiveUI, or plain observable models can use the same presentation adapter. The API is stable since 0.9.0; its former experimental ID RUNICWPF001 is retired, so remove its suppressions. RunicWebView.CreateWindowHostFactory(), for the lower-level API, is a Runic Desktop custom window host, stable since 0.10.0; remove suppressions of its former ID RUNICDESK100. The XAML namespace is https://runic-artifex.eu/xaml/views, with the prefix rv.

Host a web View in XAML

RunicViewHost presents an existing ViewModel through its generated web View:

<UserControl xmlns:rv="https://runic-artifex.eu/xaml/views"
             xmlns:local="clr-namespace:MyApp">
  <rv:RunicViewHost ViewModel="{Binding}">
    
    <local:NativeEditorView />
  </rv:RunicViewHost>
</UserControl>

Give it the application's service provider once, on the window, or on the host from a View that received its provider (host.Services = services):

RunicViewHost.SetServices(mainWindow, services);

The property is the inherited Services of ViewHost in Runic.Navigation.Wpf, so a window that also hosts native navigation sets the provider once, with either RunicViewHost.SetServices or ViewHost.SetServices.

The host owns the hosting lifecycle:

  • It opens when it is loaded with a ViewModel and Services, and waits for the native child's Loaded before starting the presentation.
  • It closes when it is unloaded or ViewModel becomes null. Changing ViewModel, Services, ModelContext, ContentRoot, StartPage, SurfaceOptions, ContentSecurityPolicy, WebContent or Desktop closes the session and opens a fresh one.
  • Changes run one at a time. A newer change cancels an open in progress, and changes that queue behind a running one collapse into the latest.
  • Closing releases the Bridge session first, which drains its accepted operations, then its Desktop surface and its own DesktopHost, and only then removes and disposes the native child.
  • When the presentation can't open (no WebView2 Runtime, no Bridge registered for the model, a missing frontend), the host shows its content, the fallback, sets State to Failed and keeps the exception in Error. ReloadAsync() retries, and also reconnects an open View through a fresh session.

Screen readers see the web View as a pane that holds the page's document; set AutomationProperties.Name (and HelpText) on the host to name that pane, for example AutomationProperties.Name="Note editor". Tab and Shift+Tab move between the page's controls and the WPF controls around the host.

State may gain values in a minor release; handle values you don't know. The host manages its own Child: put the fallback in its content instead, and derive from it only to style it.

ContentRoot (default www) is the built frontend's directory relative to the application's base directory, and StartPage the page to open in it (default index.html), as RunicWindowOptions.StartPage is for a Runic Window. With RunicApplicationFrontendDelivery=Embedded, a host without ContentRoot serves the frontend the build embedded in the application instead. Set SurfaceOptions for anything else. ModelContext defaults to the IRunicModelContext registered in Services; pass the navigator's dispatcher context when navigation and the web View present the same model. State, Error and StateChanged report progress; Surface is the open presentation's DesktopSurface, and WaitForPresentationAsync() returns when no change is pending. State and Error are read-only dependency properties: use a Trigger or DataTrigger on State in XAML, and the CLR StateChanged event in code. The host borrows the model, services and context: it never creates or disposes a ViewModel, a scope or a navigation entry.

WebContent sets what the page may do beyond the application's origins, as DesktopWindowOptions.WebContent does for a Desktop window: external navigation, downloads and developer tools. null, the default, is DesktopWebContentPolicy.Default.

ContentSecurityPolicy sends a Content-Security-Policy with the HTML of the presentation's surface, unless SurfaceOptions set one of their own (DesktopSurfaceOptions.ContentSecurityPolicy). It is a header of its own next to the surface's frame-ancestors policy, and WebView2 enforces both. The Bridge needs 'self' scripts and connections and no 'unsafe-eval', as in default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'; only DesktopSurface.ExecuteJavaScriptAsync, which evaluates a string in the page, needs 'unsafe-eval'. A value with a line break or another control character is rejected.

Share one Desktop host between hosts

By default each RunicViewHost starts its own DesktopHost, with its own listener, when it opens, and disposes it when it closes. To serve every host from one listener, create a DesktopHost in the application, set it as each host's Desktop, directly or through a Style, and dispose it after the hosts that use it have unloaded:

_desktop = await DesktopHost.StartAsync(new DesktopHostOptions { LoggerFactory = loggerFactory });
editorHost.Desktop = _desktop;
previewHost.Desktop = _desktop;

// On shutdown, after the hosts have unloaded:
await _desktop.DisposeAsync();

Each host then creates its own surface on the shared DesktopHost, presents it through its own RunicWebView, and releases only that surface when it closes; it never disposes the shared host. The shared host's WindowHostFactory option is not used for these surfaces. A host whose Desktop was disposed before it opens fails to open: State becomes Failed and the fallback shows.

Lower-level API

RunicViewHost composes RunicWebView, DesktopHost and CreateWpfViewAsync. Use them directly only when you need another lifecycle. The view and host live as long as the presentation, so keep them in fields and dispose them in this order when the control unloads, rather than with await using in the method that opens them:

var webView = new RunicWebView();
editorContainer.Content = webView;
// The native child exists only after layout: open from its Loaded event.
webView.Loaded += async (_, _) =>
{
    _desktop = await DesktopHost.StartAsync(new DesktopHostOptions
    {
        WindowHostFactory = webView.CreateWindowHostFactory(),
    });
    _view = await services.CreateWpfViewAsync(_desktop,
        new DesktopSurfaceOptions { Content = new DesktopContent.Directory(Path.Combine(AppContext.BaseDirectory, "www")) },
        existingEditorViewModel, modelContext: existingDispatcherContext);
    await _view.OpenAsync();
};

// On unload: session, then host, then the native child.
await _view.DisposeAsync();
await _desktop.DisposeAsync();
editorContainer.Content = null;
webView.Dispose();

You then also serialize swaps, cancel an open on unload and fall back yourself.

Register generated Bridges using AddRunicViews() from the application's generated composition. A logical RunicView<TModel> is sufficient; no additional Runic Window or navigation engine is required. The frontend uses the existing Desktop Views client and generated connect...() function.

Referencing this package turns Bridge generation on, as the other host adapters do. In a single-project application, declare the View next to the WPF code and keep the frontend in Frontend/: the build writes the client to Frontend/src/generated, builds the frontend and copies Frontend/dist to www/. WPF's temporary *_wpftmp markup-compile project reuses the generated code instead of generating again. Views and ViewModels cannot depend on WPF types; the generator skips such types. A ViewModel may still have WPF-typed members and methods that the Bridge does not expose, such as a [RunicIgnore] ImageSource; one that the Bridge would expose is reported as RUNICBRIDGE005 at that member. When the Views live in a WPF-free model project that references Runic.Application.Views, that project generates and builds the frontend, and the WPF application copies its dist to www/. The application can then set RunicApplicationFrontendGenerationEnabled to false. If the model's composition has the application's default name (<application RootNamespace>.RunicViewsComposition, common in apps from 0.7.0-preview.6), the application's AddRunicViews() call uses it. An application that also declares Views gets RUNICBRIDGE018 until one of the two compositions is renamed. See the Runic.Application.Views README's build properties for the frontend folder and diagnostics.

The adapter uses DesktopHost's existing asset server, authenticated surface, WebSocket transport and Views session. Its HwndHost reuses Runic Desktop's native WebView2 controller and native loader, including origin-bound document start credentials, permission policy and the web content policy: external links open in the default browser, downloads are refused and developer tools are available only in Development (see Links, new windows, downloads and developer tools). WpfBridgeView and RunicViewHost open with the defaults unless you set their WebContent, which the next OpenAsync (or the host's next open) applies; a surface you open yourself through RunicWebView.CreateWindowHostFactory() takes DesktopWindowOptions.WebContent. Install Microsoft Edge WebView2 Runtime on the Windows machine. It creates no separate WPF window or UI thread.

CreateWpfViewAsync borrows the exact supplied model and service provider. It does not create or dispose a DI scope, ViewModel, navigator, or application-owned model context. Supply the shared dispatcher model context when navigation and web bindings present the same model. Disposing the child or unloading its control closes presentation admission promptly and drains accepted operations before releasing its session. Keep application services alive until that completion. Reloading the page reconnects within the same presentation; a new view after unload uses a fresh surface/session.

When the model is a page that a RunicNavigator owns, such as the content a NavigationHost presents, the child's session registers with that navigator (since 0.11): retiring the page cancels its operations declared RunicOperationRetirement.Cancel, and the navigator disposes the page after its Cancel and Drain operations ended; see Operations and navigation entries. Unloading the child still asks its remaining operations to cancel, as before, so in a child view a Drain operation runs only until the view unloads.

Each factory presents one surface at a time in its control. WPF owns layout, DPI, visibility, focus routing and the containing window. The child advertises focus only; it does not expose a top-level native handle or minimize, maximize, move, or resize the shell. Window layout options, including DesktopWindowOptions.Placement, report warnings and are configured on the WPF shell instead; the WPF Window saves and restores its own placement with RestoreBounds, WindowState and its LocationChanged, SizeChanged and StateChanged events, and the child presentation reports no DesktopWindow.State. Files dropped on the child WebView do not reach the WPF shell's Drop event, which the WebView's window covers; the borrowed ViewModel receives them, with their paths, when it implements IRunicFileDropTarget. WpfBridgeView.OpenAsync always opens the embedded WebView2: a child control has no browser fallback, and the WPF window owns the layout that window options set. Standard HWND airspace constraints apply; WPF overlays cannot draw over the child WebView. Tab enters the document's first or last tab stop. WebView2 moves that focus asynchronously: the document's focused element changes shortly after TabInto returns, not during the call. Tab on the document's last tab stop and Shift+Tab on its first return focus to the next or previous WPF control, through WPF's own traversal (MoveFocus); when WPF has no control to move to, focus cycles inside the document. Native authentication, reload, focus, tab-in/tab-out and unload checks run on Windows; Linux can compile the Windows target and run the linked portable session checks.

Platform services for a WPF window

A ViewModel shared with Runic Desktop takes IWindowPlatformServices (or its FileDialogs, Clipboard and FileLauncher), which AddRunicPlatformServices() binds to each Runic window. In WPF, create them for the window that owns the dialogs:

// On the UI thread, before building the provider:
var mainWindow = new MainWindow();
services.AddSingleton(WpfPlatformServices.ForWindow(mainWindow));

Call ForWindow on the window's thread; the services themselves work from any thread and run their native calls on the window's dispatcher, so dialogs are modal to it. Until the window has a native handle (it is shown, or WindowInteropHelper.EnsureHandle() ran) every operation reports OwnerUnavailable, as for a window that closed before the call. When the window closes, the services wait for operations in progress, release the file access they acquired, and report OwnerClosed; they need no disposal. Each call creates services with their own lifetime.

AnnounceAsync(message, priority) raises a UI Automation notification from the window's automation peer, as Runic Desktop's WebView2 window does, which Narrator and NVDA speak: Polite messages are queued and Assertive ones replace those not yet spoken. While the page of a RunicWebView in the window (a RunicViewHost's, for example) has the keyboard focus, the page's Bridge says the message instead, in a visually hidden live region it adds to the page (div[data-runic-announcer] at the end of <body>): the focus is then in WebView2's browser process, and NVDA speaks a notification only from the application that has the focus. The page and its Content-Security-Policy need no change (Runic.Desktop, "Why WebView2 announces through the page"). It returns Success once the window raised it or the page received it, OwnerUnavailable before the window has a handle and OwnerClosed after it closed. Send each message one way: a live region of the page's own reaches the screen reader too.

Clipboard is also an IDesktopClipboard, with HTML, RTF, PNG images and files, and SetBadgeAsync and SetProgressAsync show a badge and progress on the window's taskbar button, as for a Runic Desktop window. The services hook the window's HwndSource for Explorer's TaskbarButtonCreated, so the badge and progress return after Explorer restarts.

ShowContextMenuAsync(items, options) opens a WPF ContextMenu and returns the chosen item, null when the user dismissed it or a later menu of the window replaced it; cancelling the token closes it. Entries follow the Windows menu rules: AccessKey underlined, shortcuts as hints in InputGestureText, check and radio items with the check mark, disabled entries and menus grayed. Without options.X and Y it opens at the pointer; with them at that page point of the RunicWebView with the keyboard focus, or the window's first one, in CSS pixels times the page zoom. The snapshot reports platform.contextMenu available and no platform.menuBar: the WPF window owns its chrome, so a menu bar is the application's own WPF Menu. A ViewModel in a RunicViewHost gets the same menus at its control through its Runic window's services: the hosted Desktop window reports DesktopWindowCapabilities.ContextMenu and the placement None.

Bring the window forward for a second launch

A WPF application that keeps one copy running with ApplicationInstance.ClaimAsync brings its window forward when another launch arrives with WpfPlatformServices.PresentAsync(window), from any thread:

// For each launch the running instance receives:
var result = await WpfPlatformServices.PresentAsync(mainWindow);

It shows a hidden window, or one not shown yet, with Window.Show() and restores a minimized one to its previous size, then asks Windows to put it in the foreground on the window's dispatcher, as DesktopPlatformServices.PresentAsync does for Runic Desktop windows. Windows lets the running instance take the foreground because the forwarding launch allows it (AllowSetForegroundWindow); otherwise the taskbar button flashes and the result is Failed with PermissionDenied. A closed window, or one whose dispatcher shut down, reports OwnerClosed. Window.Activate() alone neither restores the window nor reports a refusal.

Product Compatible and additional computed target framework versions.
.NET net10.0-windows7.0 is compatible. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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.0 0 10/27/2026
0.11.0 1,245 10/25/2026
0.10.0 2,505 10/22/2026
0.9.1 448 10/21/2026
0.9.0 131 10/21/2026
0.8.0-preview.6 585 10/19/2026
0.8.0-preview.5 180 10/17/2026
0.8.0-preview.4 472 10/17/2026
0.8.0-preview.3 452 10/15/2026
0.8.0-preview.2 84 10/15/2026
0.8.0-preview.1 116 10/15/2026
0.7.0-preview.9 374 10/14/2026
0.7.0-preview.8 92 10/12/2026
0.7.0-preview.7 59 10/12/2026