XplatWebWindow 1.4.1

dotnet add package XplatWebWindow --version 1.4.1
                    
NuGet\Install-Package XplatWebWindow -Version 1.4.1
                    
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="XplatWebWindow" Version="1.4.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="XplatWebWindow" Version="1.4.1" />
                    
Directory.Packages.props
<PackageReference Include="XplatWebWindow" />
                    
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 XplatWebWindow --version 1.4.1
                    
#r "nuget: XplatWebWindow, 1.4.1"
                    
#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 XplatWebWindow@1.4.1
                    
#: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=XplatWebWindow&version=1.4.1
                    
Install as a Cake Addin
#tool nuget:?package=XplatWebWindow&version=1.4.1
                    
Install as a Cake Tool

XplatWebWindow

A tiny, high-performance, cross-platform WebView framework for .NET using native WebView2 on Windows and WebKit (WKWebView) on macOS.

Quick Start

using XplatWebWindow.Core;

// 1. Create a window
using var window = new XplatWindow("My App", 1200, 800);

// 2. Optional: hide browser UI affordances in production
if (!System.Diagnostics.Debugger.IsAttached)
{
    window.SetBrowserUiEnabled(false);
}

// 3. Set up local asset mapping (bypasses CORS for ES modules)
string wwwroot = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "wwwroot");
window.SetVirtualHostNameToFolderMapping("app.assets", wwwroot);

// 4. Navigate to your UI
window.Navigate("http://app.assets/index.html");

// 5. Run the message loop (blocking)
window.Run();

API Reference

XplatWebWindow provides a simple API for building desktop applications using web technologies without the overhead of heavy frameworks like Electron or Chromium Embedded Framework (CEF).

Initialization

XplatWindow(string title, int width, int height)

Creates a new native OS window hosting a webview.

  • title: The text displayed in the window's title bar.
  • width: The initial width of the window in logical pixels.
  • height: The initial height of the window in logical pixels.

Note: On Windows, it handles per-monitor DPI awareness and automatically scales the initial dimensions.

using XplatWebWindow.Core;

// Create an 800x600 window titled "My App"
var window = new XplatWindow("My App", 800, 600);

Window Management

void Center()

Centers the window on the monitor where it currently resides. Call this after initialization but before running the message loop.

void SetTitle(string title)

Updates the text in the window's title bar.

void SetSize(int width, int height)

Resizes the window to the specified logical dimensions.

Size Size

Gets the current native window size in logical pixels.

var size = window.Size;
Console.WriteLine($"{size.Width}x{size.Height}");

void SetIcon(string iconPath)

Sets the application icon (Title bar and Taskbar/Dock).

  • Windows: Supports .ico files.
  • macOS: Supports .png, .jpg, etc.

void SetBrowserUiEnabled(bool enabled)

Enables or disables browser-style UI affordances for the hosted webview at runtime.

  • true: Leaves browser UI behavior enabled.
  • false: Hides browser-style affordances so the app feels more native.

When disabled, XplatWebWindow suppresses browser actions where supported, including:

  • context menus
  • developer tools / inspect
  • browser accelerator shortcuts such as find, refresh, print, zoom, and history navigation
  • pinch zoom and swipe navigation

Call this from the host application based on its own runtime or build configuration.

Platform note:

  • On Windows/WebView2, some browser UI settings are only guaranteed to take effect on the next navigation. If you change this after content is already loaded, navigate or reload once to fully apply the new behavior.

Example:

using var window = new XplatWindow("My App", 1200, 800);

if (!System.Diagnostics.Debugger.IsAttached)
{
    window.SetBrowserUiEnabled(false);
}

void SetDeveloperToolsEnabled(bool enabled)

Enables or disables developer tools support for the hosted webview at runtime.

  • true: Allows developer inspection where supported.
  • false: Disables developer tools integration.

This is separate from SetBrowserUiEnabled(bool). In development, a common setup is:

  • browser UI disabled
  • developer tools enabled

so the app still feels native while inspection remains available.

With developer tools enabled, use the platform-native inspection entry point:

  • Windows / WebView2: right-click and choose Inspect
  • macOS / WKWebView: right-click / control-click or use Safari Develop, depending on WebKit/macOS behavior

void Run()

CRITICAL: Starts the native OS message loop. This is a blocking call and must be the last method called in your application's entry point (Main).

Activation focus behavior:

  • On Windows and macOS, when the native window becomes active/key again (for example after Alt+Tab / app switching), the host forwards keyboard focus back into the embedded webview so JS keyboard shortcuts continue to work without requiring a mouse click.

void Close()

Requests that the native window close and the message loop exit. This is useful for automation, test runners, and coordinated app shutdown.

using var window = new XplatWindow("My App", 1200, 800);

Task.Run(async () =>
{
    await Task.Delay(1000);
    window.Close();
});

window.Run();

void Navigate(string url)

Navigates the webview to the specified URL.

window.Navigate("https://google.com");

event Action<bool>? OnNavigationCompleted

Fired after a navigation completes.

  • true: Navigation succeeded.
  • false: Navigation failed.
window.OnNavigationCompleted += success =>
{
    Console.WriteLine($"Navigation completed. Success = {success}");
};

event Action<Size>? OnWindowSizeChanged

Fired when the native window size changes. Width and height are reported in logical pixels.

window.OnWindowSizeChanged += size =>
{
    Console.WriteLine($"Window resized to {size.Width}x{size.Height}");
};

void SetVirtualHostNameToFolderMapping(string hostName, string folderPath)

Maps a virtual hostname (e.g., app.assets) to a local directory on disk. This is the required approach for loading local HTML/JS/CSS files, as it bypasses CORS restrictions that block ES modules (<script type="module">) when using file:/// URIs.

  • hostName: The virtual domain (e.g., app.assets). Avoid using .local TLDs to prevent DNS resolution delays on Windows.
  • folderPath: The absolute path to the local directory containing your assets.

Usage Pattern:

string wwwroot = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "wwwroot");
window.SetVirtualHostNameToFolderMapping("app.assets", wwwroot);
window.Navigate("http://app.assets/index.html");

JavaScript Interop

void SendMessageToJs(string message)

Sends a string message from C# to the JavaScript context.

  • C# Side:

    window.SendMessageToJs("Hello from .NET!");
    
  • JavaScript Side (Listening): The framework injects a polyfill so the syntax is identical across platforms.

    window.chrome.webview.addEventListener('message', event => {
        console.log(event.data); // Outputs: "Hello from .NET!"
    });
    

event Action<string>? OnJsMessage

Fired when JavaScript sends a message back to C#.

  • JavaScript Side (Sending):

    window.chrome.webview.postMessage("Hello from JS!");
    
  • C# Side (Listening):

    window.OnJsMessage += (message) => {
        Console.WriteLine($"Received: {message}");
    };
    

Task<string> ExecuteScriptAsync(string script, CancellationToken cancellationToken = default)

Executes JavaScript in the current page and returns the raw JSON result string from the underlying webview.

This is mainly useful for automation, diagnostics, or host-driven DOM inspection.

using System.Text.Json;

var raw = await window.ExecuteScriptAsync("document.title");
var title = JsonSerializer.Deserialize<string>(raw);
Console.WriteLine(title);

E2E Automation

XplatWebWindow now includes a small app-owned E2E layer. The model is:

  1. Create and configure your XplatWindow exactly as you normally would.
  2. Pass that existing window into a test suite.
  3. Run the suite from your app process.

This avoids needing a separate dotnet test host or a second automation process just to drive the embedded webview.

Core Types

abstract class XplatE2ETestSuite

Base type for defining E2E test classes against an existing XplatWindow.

It exposes:

  • protected XplatWindow Window
  • protected XplatPage Page

[XplatE2ETest]

Marks a method on your suite as an executable E2E test case.

Supported method return types:

  • void
  • Task
  • ValueTask

XplatE2ETestRunner.RunAsync(XplatE2ETestSuite suite, TextWriter? output = null, CancellationToken cancellationToken = default)

Runs all [XplatE2ETest] methods in declaration order and returns an XplatE2ERunResult.

XplatE2EHost.Attach(XplatWindow window, XplatE2EOptions? options = null)

Attaches automation helpers to an existing window and returns an XplatPage.

XplatPage

Provides Playwright-style page methods for the current document:

  • WaitForLoadAsync()
  • GotoAsync(string url)
  • Locator(string cssSelector)
  • GetById(string id)
  • GetByTestId(string testId)
  • EvaluateAsync<T>(string script)
  • WithOptions(XplatE2EOptions options)

XplatLocator

Provides element-level actions:

  • ClickAsync()
  • FillAsync(string value)
  • TextContentAsync()
  • IsVisibleAsync()
  • WaitForAsync()

Example: app-owned E2E suite

using XplatWebWindow.Core;
using XplatWebWindow.Core.E2E;

internal sealed class MyWindowTests : XplatE2ETestSuite
{
    public MyWindowTests(XplatWindow window)
        : base(window, new XplatE2EOptions
        {
            DefaultTimeout = TimeSpan.FromSeconds(5),
            PollInterval = TimeSpan.FromMilliseconds(25)
        })
    {
    }

    [XplatE2ETest]
    private async Task PageLoadsAsync()
    {
        await Page.WaitForLoadAsync();
        var heading = await Page.GetById("app-title").TextContentAsync();
        if (heading != "My App")
        {
            throw new InvalidOperationException($"Unexpected heading: {heading}");
        }
    }

    [XplatE2ETest]
    private async Task NativeRoundTripWorksAsync()
    {
        await Page.GetById("ping-button").ClickAsync();
        var status = await Page.GetById("status").TextContentAsync();
        if (status is null || !status.Contains("pong", StringComparison.OrdinalIgnoreCase))
        {
            throw new InvalidOperationException($"Unexpected status: {status}");
        }
    }
}

Example: running tests from your app

using XplatWebWindow.Core;
using XplatWebWindow.Core.E2E;

using var window = new XplatWindow("My App", 1200, 800);

var wwwroot = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "wwwroot");
window.SetVirtualHostNameToFolderMapping("app.assets", wwwroot);
window.Navigate("http://app.assets/index.html");

var exitCode = 1;
var runTask = Task.Run(async () =>
{
    try
    {
        var suite = new MyWindowTests(window);
        var result = await XplatE2ETestRunner.RunAsync(suite);
        exitCode = result.ExitCode;
    }
    finally
    {
        window.Close();
    }
});

window.Run();
await runTask;
Environment.Exit(exitCode);

Example: direct page automation without a suite

using XplatWebWindow.Core.E2E;

var page = XplatE2EHost.Attach(window, new XplatE2EOptions
{
    DefaultTimeout = TimeSpan.FromSeconds(3),
    PollInterval = TimeSpan.FromMilliseconds(25)
});

await page.WaitForLoadAsync();
await page.GetById("search-box").FillAsync("hello");
await page.Locator("button.submit").ClickAsync();
var text = await page.GetByTestId("result").TextContentAsync();

Notes

  • Locator(string) uses CSS selectors.
  • GetById(string) does not require data-testid.
  • GetByTestId(string) is optional convenience over [data-testid="..."].
  • EvaluateAsync<T> should be used for JSON-serializable return values such as strings, numbers, booleans, arrays, or simple objects.
  • Locator actions are DOM-driven, not OS-level keyboard or mouse synthesis.

Native Dialogs & System Integration

Task<string?> ReadClipboardTextAsync(CancellationToken cancellationToken = default)

Reads plain text from the system clipboard via native platform APIs.

  • Returns the clipboard text when available.
  • Returns an empty string when clipboard text is unavailable or clipboard content is non-text.
string text = await window.ReadClipboardTextAsync();
if (!string.IsNullOrWhiteSpace(text))
{
    Console.WriteLine(text);
}

void PickFile(string title, Action<string> onSelected)

Opens the native OS file picker dialog.

  • title: The title or message displayed on the dialog.
  • onSelected: A callback invoked with the absolute path of the selected file. If the user cancels, the callback is not fired.

void PickFolder(string title, Action<string> onSelected)

Opens the native OS folder picker dialog.

  • title: The title or message displayed on the dialog.
  • onSelected: A callback invoked with the absolute path of the selected folder.

void PickSaveFile(string title, string defaultFileName, string filter, Action<string> onSelected)

Opens the native OS save file dialog.

  • title: The title or message displayed on the dialog.
  • defaultFileName: Initial file name shown in the save dialog.
  • filter: Optional file pattern filter (for example *.txt;*.json).
  • onSelected: A callback invoked with the absolute selected save path. If the user cancels, the callback is not fired.

Task<string?> PickOpenFileAsync(string title, string filter = "")

Task-based wrapper for file open picker. Returns selected file path.

Task<string?> PickFolderAsync(string title)

Task-based wrapper for folder picker. Returns selected folder path.

Task<string?> PickSaveFileAsync(string title, string defaultFileName, string filter = "")

Task-based wrapper for save file picker. Returns selected save path.

void OpenExternal(string url)

Opens a URL in the user's default external browser.

void ShowFolder(string path)

Opens the specified folder path in the native OS file explorer (Windows Explorer / macOS Finder).

void ShowItemInFolder(string path)

Opens the native OS file explorer and highlights/selects the specific file or folder.


Drag and Drop

event Action<string>? OnFileDropped

Fired when the user drags and drops a local file into the webview window. The native layers automatically intercept the drop to prevent the browser from navigating away from your app.

  • path: The absolute path to the dropped file.
window.OnFileDropped += (path) => {
    Console.WriteLine($"File dropped: {path}");
};

High-Performance Binary Transfer

void RegisterBinary(string id, byte[] data)

Registers a binary buffer in native memory that can be fetched instantly from JavaScript without Base64 encoding overhead. This is ideal for transferring large datasets (e.g., Arrow IPC, images).

  • id: A unique string identifier for the buffer.
  • data: The raw byte array.

Usage Pattern:

  1. C# registers the buffer and notifies JS:
byte[] largeData = GetLargeData();
window.RegisterBinary("my_buffer_1", largeData);
window.SendMessageToJs(JsonSerializer.Serialize(new { type = "BINARY_READY", id = "my_buffer_1" }));
  1. JS fetches the buffer directly and notifies C# when done:
// On macOS, WKWebView custom schemes are app://
// On Windows, WebView2 uses http://
// We use app.binary to avoid colliding with the UI virtual host mapping on Windows.
const scheme = window.location.protocol === 'app:' ? 'app:' : 'http:';
const url = `${scheme}//app.binary/__binary__/my_buffer_1`;

const response = await fetch(url);
const arrayBuffer = await response.arrayBuffer();

// Free the memory on the native side
window.chrome.webview.postMessage(JSON.stringify({ action: "FREE_BINARY", id: "my_buffer_1" }));
  1. C# receives the message and frees the memory:
if (msg.Contains("FREE_BINARY")) {
    var doc = JsonDocument.Parse(msg);
    string id = doc.RootElement.GetProperty("id").GetString();
    window.RemoveBinary(id);
}

void RemoveBinary(string id)

Removes a previously registered binary buffer from native memory. It is critical to call this after JavaScript has finished fetching the data to prevent memory leaks.


Native HTTP Client

HttpClient (Class)

Provides a lightweight, cross-platform HTTP client that utilizes the native OS networking libraries (WinHttp on Windows, NSURLSession on macOS). This avoids bringing in the full .NET networking stack if you want to keep the app size small, while leveraging OS-level proxy and security settings.

Usage Pattern:

using XplatWebWindow.Core;

using var client = new HttpClient();
var headers = new Dictionary<string, string> { { "User-Agent", "MyApp/1.0" } };

try
{
    string response = await client.GetStringAsync("https://api.github.com/users", headers);
    Console.WriteLine(response);
}
catch (HttpRequestException ex)
{
    Console.WriteLine($"Error {ex.StatusCode}: {ex.Message}");
}

Memory Management

void Dispose()

XplatWindow implements IDisposable. Currently, this is a placeholder for future native cleanup routines. It is safe to rely on process termination for cleanup in most single-window app scenarios.


Common Agent Workflows

Loading Local UI

  1. Determine the path to the UI assets (usually a wwwroot folder copied to the output directory).
  2. Call SetVirtualHostNameToFolderMapping.
  3. Call Navigate("http://<your_host>/index.html").

Two-Way Communication

  1. C# registers an OnJsMessage event handler to listen for actions (e.g., button clicks).
  2. JS calls window.chrome.webview.postMessage(JSON.stringify({ action: 'doWork' })).
  3. C# performs native work (e.g., file system access).
  4. C# replies using window.SendMessageToJs(JSON.stringify({ result: 'success' })).

System & Process APIs

These APIs work on Windows and macOS. They use native OS interfaces — the same data sources as Task Manager (Windows) and Activity Monitor (macOS) — so values match those tools exactly.


XplatPlatformInfo (Static Class)

Host-level memory, CPU, and uptime. All methods are thread-safe.

MemorySnapshot GetMemory()

Returns a snapshot of physical and swap memory usage.

Field Type Description
TotalPhysical ulong Total installed RAM in bytes
UsedPhysical ulong RAM currently in use in bytes
TotalSwap ulong Total swap / page-file size in bytes
UsedSwap ulong Swap currently in use in bytes
var mem = XplatPlatformInfo.GetMemory();
Console.WriteLine($"RAM: {mem.UsedPhysical / 1_073_741_824.0:F1} / {mem.TotalPhysical / 1_073_741_824.0:F1} GB");
float SampleCpuPercent()

Returns overall CPU utilization as a percentage (0–100). Returns 0 on the first call (baseline establishment); subsequent calls reflect activity since the previous call.

XplatPlatformInfo.SampleCpuPercent(); // baseline
await Task.Delay(1000);
float cpu = XplatPlatformInfo.SampleCpuPercent();
Console.WriteLine($"CPU: {cpu:F1}%");
Dictionary<uint, uint> GetParentPidMap()

Returns a pid → parentPid map for all running processes. Useful for building a process tree.

ulong GetUptimeSeconds()

Returns system uptime in seconds (cross-platform via Environment.TickCount64).


XplatProcessSampler (Class)

Stateful, per-process CPU sampler. Create one instance and call Sample() repeatedly; CPU values are 0 on the first call and reflect activity since the previous call on all subsequent calls. Thread-safe.

IReadOnlyList<XplatProcessSnapshot> Sample()
Field Type Description
Pid uint Process ID
Name string Process name (without extension)
CpuPercent float CPU usage since last sample (0–100)
MemoryBytes ulong Resident/working-set memory in bytes
IsResponding bool false only for zombie processes (macOS)
ParentPid uint? Parent PID, or null for root processes
StartTimeUnix ulong Process start time as Unix timestamp (seconds)
var sampler = new XplatProcessSampler();
sampler.Sample(); // establish baseline
await Task.Delay(1000);
var procs = sampler.Sample();
foreach (var p in procs.OrderByDescending(p => p.CpuPercent).Take(5))
    Console.WriteLine($"{p.Name,-28} CPU:{p.CpuPercent,5:F1}%  RAM:{p.MemoryBytes / 1_048_576} MB");

XplatProcess (Static Class)

Stateless per-process operations.

XplatProcessDetail? GetDetail(uint pid)

Returns detailed information about a single process, or null if it doesn't exist or isn't accessible.

Field Type Description
Pid uint Process ID
Name string Process name
MemoryBytes ulong Resident memory in bytes
IsResponding bool Whether the process is alive / not a zombie
ParentPid uint? Parent PID
StartTimeUnix ulong Start time as Unix timestamp (seconds)
FullCmd IReadOnlyList<string> Full command line (or [exe] if unavailable)
Cwd string? Working directory inferred from executable path
Exe string? Full path to the executable
Dictionary<uint, string> GetNameMap()

Returns a pid → name map for all processes. Intended for enriching network connection data with process names.

void Kill(uint pid)

Terminates the specified process. Throws InvalidOperationException if pid is 0 or 4 (Windows system processes).

int KillTree(uint pid)

Terminates the process and all of its descendants. Returns 1 on success.


XplatNetInfo (Static Class)

Active TCP/UDP connection enumeration. Uses GetExtendedTcpTable / GetExtendedUdpTable on Windows and proc_pidfdinfo(PROC_PIDFDSOCKETINFO) on macOS — the same source as netstat -an.

IReadOnlyList<XplatNetConnection> GetConnections()

Returns all active TCP and UDP connections. Results are sorted by PID descending and capped at 5 000 entries.

Field Type Description
Protocol string "TCP" or "UDP"
LocalAddr string Local address and port, e.g. 127.0.0.1:8080 or [::1]:443
RemoteAddr string Remote address and port (*:* for UDP listeners)
State string TCP state (ESTABLISHED, LISTEN, TIME_WAIT, …) or LISTEN for UDP
Pid uint Owning process ID
var nameMap = XplatProcess.GetNameMap();
var conns   = XplatNetInfo.GetConnections();

foreach (var c in conns.Where(c => c.State == "ESTABLISHED"))
{
    nameMap.TryGetValue(c.Pid, out var name);
    Console.WriteLine($"{c.Protocol,-4} {c.LocalAddr,-25} → {c.RemoteAddr,-25}  [{c.Pid} {name}]");
}
Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net8.0

    • No dependencies.

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
1.4.1 82 6/2/2026
1.4.0 99 6/1/2026
1.3.5 95 5/25/2026
1.3.4 94 5/24/2026
1.3.3 93 5/23/2026
1.3.2 97 5/18/2026
1.3.1 92 5/15/2026
1.3.0 111 3/8/2026
1.2.6 98 3/7/2026
1.2.4 93 3/7/2026
1.2.3 101 3/6/2026
1.2.2 107 3/3/2026
1.2.1 140 3/1/2026
1.2.0 102 3/1/2026