A reusable WinForms (.NET Framework) UI dev kit that hosts a single, shared Chromium instance (WebView2) inside a legacy WinForms app, for declarative HTML/CSS/JS screens with modern layout and hardware acceleration — without the memory cost of a WebView2 control per screen.
This repository grew out of a real migration (ComicRack CE — see docs/ARCHITECTURE.md and
src/DevKit.ComicRackConsumer), but src/DevKit itself has no ComicRack-specific code, references,
or assumptions. If you're bringing this to a different legacy WinForms app, everything below
applies to you; skip src/DevKit.ComicRackConsumer entirely.
| Project | Assembly | What it is |
|---|---|---|
src/DevKit |
WebViewDevKit.dll |
The dev kit itself. No app-specific references. This is what you reference from a new host app. |
src/DevKit.TestHarness |
DevKit.TestHarness.exe |
Standalone WinForms exe exercising the two hardest sharing cases (route-swapping within one Form, reparenting out to a second Form and back). Good starting point to see the API in use outside any host app. |
src/DevKit.BridgeTests |
DevKit.BridgeTests.exe |
Headless (off-screen Form) integration test for the native↔web bridge round trip. Exit code 0/1, safe for CI. |
src/DevKit.ComicRackConsumer |
ComicRackConsumer.dll |
The first real consumer: wires the dev kit into ComicRack CE's live Pages and Library tabs via reflection. The one project allowed to reference ComicRack assemblies. Read this for a worked example, not as part of the kit. |
1. One shared host, many "screens." SharedWebViewHost.Instance is a process-wide singleton
wrapping exactly one WebView2 control. A "screen" in your app calls
AttachAndNavigateAsync(container, route) (or AttachAndNavigateToPathAsync(container, path) to
serve your own HTML instead of the core SPA's hash router) to reparent the single control into its
own container and navigate it. Never construct a second WebView2 — that's the whole discipline
this kit exists to enforce. WebViewEnvironmentService guarantees exactly one
CoreWebView2Environment per process and logs a warning (DevKitLog) if that's ever violated.
2. Multiple independently-toggleable screens need the Reparented event. If screen A is
showing the host and the user switches to screen B, the host silently moves away from A. Subscribe
to SharedWebViewHost.Instance.Reparented (fired with the old container) to restore your own
native fallback control's visibility when you lose the host. And always call
AttachAndNavigateAsync/AttachAndNavigateToPathAsync again whenever your screen turns itself back
on — it's idempotent and cheap, but skipping it on re-toggle is a real bug (the shared host doesn't
auto-reattach on its own). This is the single most common bug class once a second screen exists —
see docs/ARCHITECTURE.md's Phase 4 notes for the two real bugs this caused and how they were fixed.
3. Native fallback is a feature, not an afterthought. Every migrated screen in
DevKit.ComicRackConsumer keeps its original native control alive as a sibling inside the same
container — Controls.Add + Dock=Fill, native control's own .Visible flag flipped, never
removed/recreated. Toggling "off" just means .Visible = true on the native control again. This is
the dev-kit's whole answer to "leave WinForms consumers an opt-out from the Chromium dependency":
there is no separate abstraction to learn, just don't tear down what was there before you attach.
4. The bridge is thin and consumer-defined. WebBridge (SharedWebViewHost.Instance.Bridge)
only understands {channel, payload|items} envelopes and raw COM host objects — it has zero
knowledge of your app's data shapes.
AddHostObject(name, obj)/RemoveHostObject(name)— expose a[ComVisible(true)],ClassInterfaceType.AutoDualobject aschrome.webview.hostObjects.sync.<name>in JS. Define your own interface first (IYourBridge), perIComicPagesBridge/IComicLibraryBridgefor the worked example — this keeps the COM-visible surface intentional.PostBatch<T>(channel, items)— send many items in ONE call. Prefer this over one bridge call per row; it's cheap to get right up front and expensive to retrofit.PostMessage(channel, payload)/MessageReceivedevent — single native→web push, and web→native messages respectively.- On the JS side,
Web/bridge.jsprovidesDevKitBridge.on(channel, fn)/send/sendBatch. Real gotcha already hit twice:bridge.js's owndevkit.readysend only pings native — nothing posts a matching message back to the page, soDevKitBridge.on('devkit.ready', render)never fires. Call your page'srender()directly instead of waiting on that round trip (seelibrary.html/comicpages.htmlfor the fixed pattern), and register host objects before navigating, not after (a race otherwise lets page script run before the host object exists).
5. Diagnostics. DevKitLog is an in-memory + Debug.WriteLine + event logger with no file I/O
by design (a WinForms-embedded plugin often has nowhere sane to assume a log file lives). Add your
own file sink by subscribing DevKitLog.Logged — see ConsumerDiagnostics.EnsureInstalled() for a
worked example, including the idempotent-guard-called-from-every-entry-point pattern (a static
constructor only runs the first time that type is touched, which silently produces zero log output
for every other screen if you rely on it — call EnsureInstalled() explicitly instead).
MemoryDiagnosticPanel reports Process.WorkingSet64 + live msedgewebview2.exe count; wire it
into a debug-only panel to catch a leaked second instance early.
6. User-data folder isolation. WebViewEnvironmentService.HostAppName (default "WebViewDevKit")
names the folder under %LocalAppData% where the shared browser profile lives. If more than one app
on the same machine embeds this dev kit, set HostAppName to something app-specific before your
first call that touches the environment (EnsureInitializedAsync/AttachAndNavigateAsync/etc.) so
the two apps don't share a WebView2 profile.
- Reference
src/DevKit/DevKit.csproj(or the builtWebViewDevKit.dll+ itsMicrosoft.Web.WebView2NuGet dependency). - Put your HTML/CSS/JS under a
Web/folder withCopyToOutputDirectorycontent items (seeDevKit.csproj's ownContent Include="Web\**\*.*"— cross-project content items merge into one output folder automatically if a consumer project does the same, no manual step needed). - Call
await SharedWebViewHost.Instance.AttachAndNavigateToPathAsync(yourContainer, "yourpage.html")from wherever your screen becomes visible. - Define a
[ComVisible(true)]interface + class for whatever native data/actions your page needs, register it viaSharedWebViewHost.Instance.Bridge.AddHostObject(...)before navigating. - Keep your native control (if any) as a sibling, subscribe
Reparentedto restore it, re-attach on every re-toggle.
src/DevKit.TestHarness is the smallest working example of steps 1–3 with no app integration at all
— read HarnessForm.cs/SettingsHarnessForm.cs first if you want to see the pattern in isolation
before reading the ComicRack-specific consumer.