VpnHood.AppUi.Hosting.WebView
9.0.857
dotnet add package VpnHood.AppUi.Hosting.WebView --version 9.0.857
NuGet\Install-Package VpnHood.AppUi.Hosting.WebView -Version 9.0.857
<PackageReference Include="VpnHood.AppUi.Hosting.WebView" Version="9.0.857" />
<PackageVersion Include="VpnHood.AppUi.Hosting.WebView" Version="9.0.857" />
<PackageReference Include="VpnHood.AppUi.Hosting.WebView" />
paket add VpnHood.AppUi.Hosting.WebView --version 9.0.857
#r "nuget: VpnHood.AppUi.Hosting.WebView, 9.0.857"
#:package VpnHood.AppUi.Hosting.WebView@9.0.857
#addin nuget:?package=VpnHood.AppUi.Hosting.WebView&version=9.0.857
#tool nuget:?package=VpnHood.AppUi.Hosting.WebView&version=9.0.857
VpnHood.AppUi.Hosting.WebView
Shared, platform-neutral hosting for the VpnHood SPA (the web UI served by
VpnHoodAppWebHost and shown in a native web view). One controller holds all the
hosting business logic; each OS supplies only a thin adapter for the native web-view mechanics.
Why
Every platform used to re-implement the same logic — start the web server, compute the launch URL, show a spinner, show an error page, reload when the server self-heals, run the recovery state machine, forward resume. Only the native web-view calls actually differed. This project extracts the common part so a new platform (MAUI, Mac Catalyst, WinRT, …) is just one adapter.
Pattern: composition (Bridge), not a base class
Each OS host already inherits a platform type it can't give up (UIViewController, an Android
activity handler), and C# has no multiple inheritance. So
the shared logic is a controller (WebViewHost) that talks to a per-OS adapter
(IWebView). The host creates the adapter + controller and forwards native lifecycle calls.
OS host (UIViewController / Activity handler / Window / ContentPage)
├─ owns → WebViewHost (SHARED business logic — this project)
│ └─ drives → IWebView (the ONLY per-OS code)
└─ forwards native lifecycle (create / resume / destroy) to the host
Types
IWebView— the per-OS surface:Initialize,Load(Uri),SetLoading(bool),ShowError(string),Post(Action)(marshal to the UI thread), and eventsPageLoaded,LoadFailed,ContentProcessGone. Threading contract: the host calls these on the UI thread and expects the events on the UI thread. Adapters only report; they never decide.WebViewHost— owns serverInit, launch-URL computation (?nocache={SpaHash}plus an optionalLaunchUrlBuilderhook), resume→AppUiContext.NotifyResumed(), and reload after a failure or server restart.IWebViewPageHost— what the controller needs of the page's server:EnsureStarted(the address to load) andRestarted(bound again: load the page again).AppWebViewPageHostis the app's own web host as one, where the app runs in the host's process.WebViewHostOptions—LaunchUrlBuilder.
Keeping the server up (in VpnHoodAppWebHost)
Three small mechanisms, each tied to a concrete signal. There is deliberately no periodic connect probe: one that times out on a busy or dozing device restarts a healthy server, and every restart kills whatever the web view is loading at that moment (blank page, or the old "user interface is not responding" screen).
- Watchdog — a 5 s timer restarts the listener when
IsListeningis false. CavemanTcp clears the flag when its accept loop exits, so this only ever restarts a listener that is actually gone. After a successful restart,Restartedmakes the native host reload the SPA, including assets whose loading was interrupted after the main document finished. - Resume probe — the server subscribes to
AppUiContext.OnResumeditself. iOS suspends the process and can close the loopback socket meanwhile while the accept loop still believes it is listening, so one real connect per resume, restart if it fails. The host then reloads as in 1. - Load again after a failure (in
WebViewHost) —LoadFailed(main-document connection failure) orContentProcessGonemakes the host give the server one real connect check (a failed load is a concrete "unreachable" signal, and the flag can lie after an iOS suspension), then load the SPA again after 1 s (Android rebuilds its WebView before reporting). The delay is the only bound: a listener that keeps failing is retried every second instead of dead-ending on an error screen, and the watchdog has brought it back by then. The fatal error screen is reserved forStart()throwing (the server cannot bind at all).
Recovery lives in the native host; index.html has no bundle-error reload handler. A successful
server restart reloads the whole document after 1 s, coalescing with any pending failure reload.
This covers assets interrupted by a server outage; it does not detect arbitrary bundle failures
while the server remains healthy. A full reload resets the current route and unsaved UI state.
Adding / owning a platform
- Implement
IWebViewwrapping the native web view; raise the events from its navigation callbacks (drop cancelled/superseded loads, e.g. iOSNSURLErrorCancelled -999). - In the OS host: construct the adapter,
new WebViewHost(adapter, pageHost)- the page's server: the app's ownLocalWebHostin anAppWebViewPageHostwhen the app runs in this process, or on a desktop the address its service hands the window (DesktopWebViewPageHost, WebView.Windows) - callStart()on create,OnResume()from the platform's foreground/resume hook,Dispose()on teardown. - Keep OS-only chrome (safe area, status bar, tray icon, hardware back, window state) in the host.
Current adapters
| Platform | Adapter | Host | Web view |
|---|---|---|---|
| iOS | IosWebView (WebView.Ios) |
IosWebViewController |
WKWebView |
| Android | AndroidWebView (WebView.Android) |
AndroidWebViewMainActivityHandler |
Android.Webkit.WebView |
| Windows | WindowsWebView (WebView.Windows) |
WebViewMainWindow, run by WindowsWebViewUi - the IDesktopUi a Windows head names - over the service's address |
WebView2 in a Win32 window, no WPF |
A mobile head's classes
A head constructs none of the above. The OS creates its classes, so they derive from the web view
UI's own: on Android its Application from AndroidWebViewApplication and its launcher activity
from AndroidWebViewMainActivity; on iOS its AppDelegate from IosWebViewAppDelegate, which gives
every window scene an IosWebViewSceneDelegate with an IosWebViewController as its root. Each
starts the app from the head's init params (CreateInitParams), and the head carries only what
the OS reads - its attributes, its [Register] name, its access keys.
Build / verification status
- iOS — built Release and device-verified (launches, server starts, background→foreground recovers).
- Windows — WebView2 in a Win32 window, no WPF (2026-10-06):
WindowsWebViewUirun indevon the dev machine and the Windows VM shows the page with its focus on each activation, opens the log link in a window of its own, comes back when asked, ends on the host's cancel, shows the one message, and fits 1366x768 in landscape. The WPF window it replaced ran over a service (2026-09-23). Still to try: the runtime-missing fallback, a display change, the taskbar badge seen. - Android — build-verified (adapter + full
Client.Android.Webapp). Still smoke-test on a device (content-view swap, hardware back, background→foreground recovery).
Per-platform things to verify
- Android — content-view swap (loader → WebView on first
PageLoaded); WebView-version "update WebView" redirect (ResolveUrl); hardware back (API<33OnKeyDown+ API33+ back callback). The oldKillSpaServerdebug OnPause/OnResume hook was dropped (the watchdog supersedes it).LoadFailedis raised for main-frame connection errors; a dead render process rebuilds the WebView, then raisesContentProcessGone. - Windows — the WebView2 "runtime missing" fallback (hide the window, open the page in the
system browser) is
OnWebView2Unavailable; it keeps no tray, so closing the window ends it and a start in the tray (ui --tray) ends at once; its web view's profile is under the person's own folder, not the service's.
| 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
- VpnHood.AppLib.App (>= 9.0.857)
- VpnHood.AppUi.Common (>= 9.0.857)
NuGet packages (4)
Showing the top 4 NuGet packages that depend on VpnHood.AppUi.Hosting.WebView:
| Package | Downloads |
|---|---|
|
VpnHood.AppUi.Hosting.WebView.Android
Hosts the app web UI in an Android WebView: the IWebView adapter, the web-view clients, and the activity handler a head derives from |
|
|
VpnHood.AppUi.Hosting.WebView.Ios
Hosts the app web UI in an iOS WKWebView: the IWebView adapter and the view controller a head mounts as its root |
|
|
VpnHood.AppUi.Hosting.WebView.Windows
Hosts the app web UI in a Win32 window through WebView2, with no UI framework: the IWebView adapter, the main window, and WindowsWebViewUi, the UI a Windows head names to show the app that runs in its service |
|
|
VpnHood.AppUi.Hosting.Maui
Hosts the VpnHood app under a .NET MAUI UI on Android and Windows, in the app's own process: the platform's start, the Android activity's events, and the app's API for the MAUI pages |
GitHub repositories
This package is not used by any popular GitHub repositories.