XplatWebWindow 1.4.1
dotnet add package XplatWebWindow --version 1.4.1
NuGet\Install-Package XplatWebWindow -Version 1.4.1
<PackageReference Include="XplatWebWindow" Version="1.4.1" />
<PackageVersion Include="XplatWebWindow" Version="1.4.1" />
<PackageReference Include="XplatWebWindow" />
paket add XplatWebWindow --version 1.4.1
#r "nuget: XplatWebWindow, 1.4.1"
#:package XplatWebWindow@1.4.1
#addin nuget:?package=XplatWebWindow&version=1.4.1
#tool nuget:?package=XplatWebWindow&version=1.4.1
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
.icofiles. - 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();
Navigation & Content
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.localTLDs 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:
- Create and configure your
XplatWindowexactly as you normally would. - Pass that existing window into a test suite.
- 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 Windowprotected XplatPage Page
[XplatE2ETest]
Marks a method on your suite as an executable E2E test case.
Supported method return types:
voidTaskValueTask
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 requiredata-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:
- 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" }));
- 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" }));
- 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
- Determine the path to the UI assets (usually a
wwwrootfolder copied to the output directory). - Call
SetVirtualHostNameToFolderMapping. - Call
Navigate("http://<your_host>/index.html").
Two-Way Communication
- C# registers an
OnJsMessageevent handler to listen for actions (e.g., button clicks). - JS calls
window.chrome.webview.postMessage(JSON.stringify({ action: 'doWork' })). - C# performs native work (e.g., file system access).
- 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 | Versions 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. |
-
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.