Is WebView2 the Right Successor to IE Mode? — The ActiveX Constraint and a Realistic Migration Design

· · WebView2, Windows, .NET, WPF, Windows Forms, IE Mode, ActiveX, Legacy Migration, Internal Systems, Technical Consulting

In an earlier article, How to Extend the Life of an IE Mode-Dependent Internal Web System, and How to Exit It, I wrote that IE Mode is only ever a time-limited stopgap, and that you should design your exit in parallel. When clients come to us for advice on that “exit,” WebView2 comes up constantly. “We want to display our internal web system inside a dedicated app.” “We only want to build one screen of a desktop app with web technology.” “I’ve heard Electron is heavy — is there an alternative?” — WebView2 comes up as an option in every one of these situations.

At the same time, WebView2 has quirks you need to understand before adopting it: how to distribute the runtime, where to put the user data folder, how to get the native side and the web side talking to each other. And above all, there’s a constraint that goes to the heart of any migration plan: ActiveX, which ran fine under IE Mode, does not run in WebView2. This article works through WebView2’s basic architecture, distribution, design, and security, and how it realistically fits together with an exit from IE Mode.

1. The bottom line up front

  • WebView2 is a control that embeds Chromium-based Microsoft Edge inside a Windows application. It’s usable from WinForms, WPF, WinUI, and Win32 C++, and it’s well suited to adding “just a bit of web UI” to an existing desktop app.1
  • As a rule, use the Evergreen runtime (a shared, automatically updated runtime). It ships standard with Windows 11, but Microsoft’s official recommendation is not to assume “it must already be there” — build a presence check and bootstrap step into your installer.2
  • For offline environments or factory/production lines that need to freeze a validated configuration, there’s Fixed Version (the runtime bundled with the app), but the bundled payload exceeds 250MB, and you take on the responsibility of shipping your own security updates. Don’t pick it lightly.3
  • The first thing people trip on is the user data folder (UDF). By default it’s created right next to the exe, which means an app installed under Program Files fails to start. Make it standard practice to explicitly point it at a folder under %LOCALAPPDATA%.4
  • For native-to-web integration, default to message exchange via PostWebMessageAsJson / WebMessageReceived, and restrict AddHostObjectToScript (exposing a COM object) to content you actually trust.1
  • ActiveX does not run inside WebView2. You can’t just “swap in WebView2 and be done” for an IE Mode-dependent page that uses ActiveX — you need to redesign so the work ActiveX used to do moves to the native side. This is the real core of any IE Mode exit plan.5

2. WebView2’s basic architecture

WebView2 consists of two pieces: the “SDK” (the API you embed in your app) and the “runtime” (the Edge-based execution environment installed on the client machine). It’s the same structure as the Visual C++ runtime or the .NET runtime — your app builds against the Microsoft.Web.WebView2 NuGet package and, at run time, uses whatever runtime is present on the client.3

Platform support is broad: you can use it from WinForms and WPF on .NET Framework 4.6.2+ / .NET Core 3.1+, from WinUI, and from Win32 C++. Being able to convert just one screen of an existing WinForms line-of-business app to WebView2 — an incremental approach — is a major advantage over switching the entire framework wholesale (as you would with Electron). For guidance on choosing the UI framework itself, see WinForms vs. WPF vs. WinUI — A Decision Table.

A minimal embedding looks like the code below (the same idea applies to WPF and WinForms alike).

var env = await CoreWebView2Environment.CreateAsync(
    browserExecutableFolder: null,   // Use the Evergreen runtime
    userDataFolder: Path.Combine(
        Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
        "KomuraSoft", "MyApp", "WebView2"));
await webView.EnsureCoreWebView2Async(env);
webView.CoreWebView2.Navigate("https://internal.example.co.jp/app/");

The key point is that we’re creating CoreWebView2Environment ourselves instead of leaving it to the defaults. The next two sections explain why.

The setup steps themselves are straightforward; for WinForms/WPF the flow looks like this:

  1. Add Microsoft.Web.WebView2 to the project via NuGet (the WebView2 control also appears in the designer’s toolbox)
  2. Place the control on your form/window
  3. Initialize it with EnsureCoreWebView2Async, as in the code above, and then call Navigate

There’s one API-level gotcha you need to nail down from the start here: webView.CoreWebView2 is null until initialization completes. A classic stumble is trying to subscribe to an event like CoreWebView2.WebMessageReceived += ... in a form’s constructor and getting a NullReferenceException. The standard pattern is to consolidate all initialization-dependent code to run “after awaiting EnsureCoreWebView2Async.” Assigning to the Source property implicitly kicks off initialization too, but for apps that need to specify environment settings (such as the UDF location), it’s safer to standardize on explicitly calling EnsureCoreWebView2Async(env) first.

Also, every WebView2 event fires on the UI thread. If you put heavy work (device access, file I/O) directly inside WebMessageReceived, the web side’s responsiveness freezes right along with it, so route native-side processing through async/await instead. The principles here map directly onto what I wrote in The UI Thread and async/await in WPF/WinForms — A One-Page Reference.

3. Runtime distribution — Evergreen and Fixed Version

Evergreen is the model where every WebView2 app shares a single runtime installed on the client, and that runtime updates itself automatically. Microsoft explicitly recommends it, because security patches get applied automatically and disk usage stays small.6

There are three things to watch for in practice.

  • Implement a presence check. It ships standard with Windows 11 and is widely distributed to Windows 10 as well, but machines without it still exist. In .NET you can check with CoreWebView2Environment.GetAvailableBrowserVersionString(), but on a machine with no runtime installed, that call itself throws (WebView2RuntimeNotFoundException), so wrap it in a try/catch, treat “exception thrown” as “not installed,” and build your setup so it then runs the bootstrapper (a small online installer) or the standalone installer.2
  • Design for keeping up with runtime updates. Even after the runtime updates, an already-running app keeps using the older version. The recommended pattern is to catch the NewBrowserVersionAvailable event and build a path that communicates “restart to pick up the update.”2
  • Align with your organization’s internal update governance. The Edge browser’s update policy and the WebView2 runtime’s update policy are separate things. In environments where Group Policy has frozen runtime updates, Evergreen’s underlying assumption — that the latest version is present — breaks down, so if you’re using newer APIs, add feature detection.6

3.2 Fixed Version (limited use)

Fixed Version bundles a specific runtime version with your app. Because it lets you freeze a validated configuration, it’s a reasonable choice for offline manufacturing equipment or environments with strict change management. However,

  • the bundled binaries exceed 250MB, so your distributable grows by that much2
  • since the runtime doesn’t auto-update, you take on the responsibility of shipping browser-engine vulnerability fixes through your own releases
  • neglect the updates, and “a line-of-business app carrying an old Chromium build” keeps lingering inside the company

— these are real costs. Only choose Fixed Version if the content you display is fully closed and under your own control, and you have a process that can fold runtime updates into your release cycle.

4. The first trap you hit — the user data folder

WebView2 stores cookies, cache, permissions, and the like in the user data folder (UDF). If you don’t specify a UDF location, it tries to create one in a default location — in most configurations, right next to the exe — which means an app installed under Program Files fails to write there and hits an initialization error. It works fine on a dev machine (debug run, a writable folder) but breaks the moment it’s installed — a classic bug that “only shows up once you actually ship it.”4

The fix is simple: always explicitly specify an app-dedicated folder under %LOCALAPPDATA%, as in the earlier code example. Also build the following into your design:

  • Keep the UDF separate per user and per app (don’t share it across multiple apps)
  • Don’t put it on a network drive (it causes slowdowns, corruption, and data loss)4
  • Decide up front how the UDF gets deleted — on uninstall, or via a “clear login info” feature (if operators don’t know it’s where cookies and site data live, it gets missed during things like wiping a departed employee’s machine)

Think of this as the WebView2-specific version of the “don’t write next to the exe” principle from Where to Store Local Data in a Business Windows App — A Decision Table.

5. Designing native-to-web integration

What turns WebView2 into more than “just a browser frame” is the two-way integration between native code and web content. There are mainly two mechanisms for this.1

5.1 Web messages (the default approach)

The native side sends JSON via PostWebMessageAsJson, and the web side receives it with window.chrome.webview.addEventListener("message", ...). In the other direction, it’s window.chrome.webview.postMessage(...) paired with the WebMessageReceived event. It’s loosely coupled, and you can inspect every exposed operation in one place, so make this your default integration mechanism first.

// Native → Web
webView.CoreWebView2.PostWebMessageAsJson(
    JsonSerializer.Serialize(new { type = "deviceStatus", connected = true }));

// Web → Native
webView.CoreWebView2.WebMessageReceived += (s, e) =>
{
    // Ignore messages from anything other than the expected origin (e.g., after navigating to an external site)
    if (!e.Source.StartsWith("https://internal.example.co.jp/", StringComparison.Ordinal))
        return;

    AppMessage? msg;
    try { msg = JsonSerializer.Deserialize<AppMessage>(e.WebMessageAsJson); }
    catch (JsonException) { msg = null; }
    if (msg?.Type is null)
        return;  // Discard malformed messages here (log if needed)

    // Only execute the operations allowed for each type
};

The web side (JavaScript) receives it like this. No special library is required — you just use the window.chrome.webview object that WebView2 injects.

// Receive a message from the native side
window.chrome.webview.addEventListener("message", (e) => {
    if (e.data.type === "deviceStatus") {
        updateStatusBadge(e.data.connected);
    }
});

// Send a request to the native side
document.getElementById("print-label").addEventListener("click", () => {
    window.chrome.webview.postMessage({ type: "printLabel", copies: 2 });
});

On the receiving side, as in the code above, check e.Source (the URI of the page that sent the message) first, and then rigorously enforce “check the message’s type against a whitelist, and ignore and log anything unexpected.” A single link or a single redirect can navigate the web side to an external site, so don’t skip the origin check just because you assume “what’s showing right now must be our own page.” Conversely, on the web side, adding a fallback for when window.chrome.webview doesn’t exist (i.e., when opened in an ordinary browser) lets you debug the web UI portion standalone in a regular browser, which improves development efficiency.

5.2 Exposing host objects (powerful, but limited use)

AddHostObjectToScript lets JavaScript call a .NET/COM object directly. Under the hood it’s the COM mechanism, and it’s an interesting sight to see COM technology, which we’ve worked with for a long time, still active in a place like this — but handing a native object directly to web content also means a much larger blast radius if that page is ever compromised. Limit exposure to content you manage yourselves, and keep the exposed methods to the bare minimum you need. The rule is: don’t register host objects on any WebView that might ever display an untrusted page.

5.3 Loading local content

When you bundle HTML/JS with the app and display it, the standard approach is to map a folder to a virtual host name with SetVirtualHostNameToFolderMapping rather than loading directly via file://. Since the content then has an origin, like https://appassets.example/, origin-dependent web APIs such as localStorage work normally, and you can also specify the cross-origin access level. Use a reserved domain that can’t actually exist (such as .example) for the host name, and start from the minimum access level necessary — DenyCors first.7

6. Exiting IE Mode with WebView2 — ActiveX doesn’t run

This is the most important section in this article. IE Mode manages to survive because a genuine IE11 (the Trident engine) runs inside Edge, and ActiveX controls and Browser Helper Objects run exactly as they always did.5 WebView2, on the other hand, is Chromium, and it has no mechanism for hosting ActiveX. In other words,

“Swapping an internal system running under IE Mode over to a WebView2-built shell” only works for screens that don’t depend on ActiveX.

You need to build your migration plan around that constraint. A realistic migration order looks like this:

  1. Inventory: Classify IE Mode-dependent pages into “screens that use IE-specific technology such as ActiveX” and “screens that are simply built the old way” (the site-list inventory approach from the IE Mode article applies directly here).
  2. Screens with no IE-specific dependency: Modernize them for standard browsers, and either display them in Edge itself or, if you want to integrate them into a business terminal app, put them in a WebView2 shell.
  3. ActiveX-dependent screens: Redesign so the functionality ActiveX used to provide (serial communication, file access, dedicated device control, and so on) moves to the native side (the WebView2 host app) and gets invoked over web messages. Think of it as flipping “ActiveX inside the browser” into “web UI inside the app, plus native-side processing.”
  4. The decision on whether to keep, wrap, or replace ActiveX itself can use the exact same criteria from Should You Keep, Wrap, or Replace ActiveX/OCX? — A Decision Table.

That step 3 redesign is where the real effort of a WebView2 rollout goes, and at the planning stage you need to set expectations accordingly: this is not as simple as “install WebView2 and you’re out of IE Mode.” Put another way, once you’ve done the design work to move ActiveX’s functionality into the host app, you get the best of both worlds — a UI that’s easier to build and maintain in-house using web technology, with distribution you can still control as a desktop app.

7. Implementation questions you’ll always be asked for an internal system

When you’re considering adopting WebView2, there are a handful of questions the business side always raises. Here they are, addressed in advance.

7.1 Printing and reports

The requirement “under IE, clicking the print button produced the report” can be met in WebView2 through two mechanisms.

  • Print the screen as-is: Bring up the print dialog with CoreWebView2.ShowPrintUI(), or print unattended with PrintAsync. It’s equivalent to browser printing, so your CSS print rules (@media print) apply exactly as they do in a browser.
  • Output as PDF: PrintToPdfAsync lets you save the currently displayed page to a PDF file. For a business flow like “save the report as a PDF and put it in a shared folder,” this option is better suited, since you can control the file name and save location on the native side.1

For reports that demand pixel-perfect column alignment — carbon-copy style multipart forms, for instance — also weigh whether it’s better to lean on native-side report output (the approaches laid out in How to Build Excel Report Output) rather than fighting web printing to get there.

7.2 File downloads and uploads

Downloads work like a browser by default, but for a business app, the standard practice is to intervene via the DownloadStarting event. That lets you fix the save location, allow or deny based on file extension, and suppress the default download UI in favor of your own app’s notification.1 Uploads (<input type="file">) open the OS file-picker dialog with no special implementation required.

7.3 Authentication and SSO

If your internal web system uses Integrated Windows Authentication (NTLM/Kerberos), it generally goes through in WebView2 just as it does in a browser. For older systems using Basic authentication, you can supply credentials via the BasicAuthenticationRequested event and skip showing a login screen entirely — though where you store those credentials is exactly the topic covered in the DPAPI article. If you want Microsoft Entra ID (formerly Azure AD) SSO to flow through the OS sign-in identity, consider enabling the environment option AllowSingleSignOnUsingOSPrimaryAccount.

Cookies are stored in the UDF, so the login state persists even after the app restarts. If you need a logout feature to reliably clear the session, add an implementation that explicitly deletes cookies via CookieManager.

7.4 Debugging during development

Since the inside of WebView2 is Chromium, F12 DevTools work exactly as they do during development (CoreWebView2Settings.AreDevToolsEnabled is enabled by default). And as noted earlier, if you build the web UI side so it also runs standalone in a browser, you can split the work: run UI development and debugging as ordinary web development, and only verify the native integration on top of WebView2. In other words, even once your web assets are locked inside the app, the development experience stays a web development experience.

8. Key points for security design

A WebView2 app is “an app with a browser built in,” so think about it using a browser-equivalent threat model.

  • Restrict the content you display: In NavigationStarting, check the destination against an internal-domain whitelist, and hand off any unexpected URL to the default browser (handle NewWindowRequested the same way).
  • Narrow anything that crosses a trust boundary: Keep the scope of exposed host objects and the operations allowed via web messages to a minimum. It’s also effective to separate a WebView that might display an external site from one that has native integration.
  • Tune user-facing features to match the environment: CoreWebView2Settings lets you keep only as much “browser-ness” as you actually need. Locking it down on kiosk terminals or shop-floor terminals cuts down on accidents.
  • If you use Fixed Version, own the update plan: As noted earlier, distributing vulnerability fixes becomes your app’s own responsibility.6

Here are the settings under CoreWebView2Settings that get adjusted most often. It’s worth including “what to do in the production build” as an item on your pre-release checklist.

Setting Default Typical for shop-floor terminals / production
AreDevToolsEnabled (F12 developer tools) Enabled Disable in production
AreDefaultContextMenusEnabled (right-click menu) Enabled Disable on screens where you don’t want users hitting “Back” or “Reload”
AreBrowserAcceleratorKeysEnabled (shortcuts like Ctrl+F5) Enabled Disable for kiosk use
IsStatusBarEnabled (link destination display) Enabled Per preference
IsZoomControlEnabled (Ctrl+wheel zoom) Enabled Disable on business screens whose layout breaks when zoomed
AreHostObjectsAllowed (host objects) Enabled Disable if unused

None of these are really about “disable it and you’re safe” so much as they’re tools for “closing off any entry point besides the operations your app intends to allow.” For raising the security baseline of Windows apps in general, also see The Minimum Security Checklist for Windows Apps.

9. Summarizing the adoption decision

Configuration Good fit for Watch out for
Display in Edge (browser) An ordinary internal web system No app integration or native interop possible
Existing app + WebView2 for part of the UI Screen-by-screen modernization, reusing web assets UDF, runtime distribution, integration design (this article)
WebView2 shell + migrated native functionality Exit path for ActiveX-dependent IE Mode assets Reimplementing ActiveX’s functionality is the real work
Electron and similar Cross-platform is a hard requirement Heavy if you’re Windows-only. Larger distributable, more memory
Full native rebuild (WPF, etc.) No existing web assets / offline-first Weigh against development cost

If you have “a Windows-only internal app and want a UI built with web technology,” WebView2, not Electron, should be your default choice. Since the runtime is shared with the OS, distribution stays light, and integrating with existing .NET assets is straightforward.

10. Summary

WebView2 is a technology that lets you embed a Chromium-based web UI as a component in a Windows app, and it’s a good fit for modernizing internal systems. The practical points at adoption time come down to three things: checking for the Evergreen runtime’s presence and keeping up with its updates, explicitly specifying the user data folder, and designing integration around web messages by default. And on the planning side, look squarely at the constraint that ActiveX doesn’t run, and put the redesign that moves ActiveX’s functionality to the native side at the center of your effort estimate.

If you’re keeping an eye on the IE Mode deadline and thinking “it’s about time for an exit,” the solid place to start is inventorying the target system and breaking down its ActiveX-dependent functionality. A lot of judgment calls here really do depend on looking at the actual system configuration, so if you’re unsure, feel free to get in touch.

KomuraSoft LLC handles consulting on exit-path design for IE Mode/ActiveX-dependent internal systems, incremental modernization using WebView2, and integrating web UI into existing Windows apps.

References

  1. Microsoft Learn, Overview of WebView2 APIs. Covers the overall picture of WebView2’s features: navigation management, loading local content, and host-to-web communication (web messages, host objects).  2 3 4 5

  2. Microsoft Learn, Distribute your app and the WebView2 Runtime. Covers distribution via bootstrapper/standalone installer, detecting an existing install, tracking updates via NewBrowserVersionAvailable, and the Fixed Version bundling procedure (250MB+).  2 3 4

  3. Microsoft Learn, Evergreen vs. fixed version of the WebView2 Runtime. Covers the difference between the two runtime distribution modes, standard inclusion in Windows 11, and the pros and cons of Fixed Version.  2

  4. Microsoft Learn, Manage user data folders. Covers the role of the user data folder, the read/write permissions a custom UDF needs, and how placing it on a network drive causes slowdowns, crashes, and data loss.  2 3

  5. Microsoft Learn, What is Internet Explorer (IE) mode?. Covers how IE Mode runs on the Trident (MSHTML) engine and supports ActiveX controls and Browser Helper Objects (i.e., Chromium-based WebView2 has no such support).  2

  6. Microsoft Learn, Development best practices for WebView2 apps. Covers recommending Evergreen, handling runtime updates, feature detection, and the need for regular updates when using Fixed Version.  2 3

  7. Microsoft Learn, Using local content in WebView2 apps. Covers loading local content via virtual host name mapping, the benefits of getting an origin, and specifying access kinds (such as DenyCors). 

Recent articles sharing the same tags. Deepen your understanding with closely related topics.

These topic pages place the article in a broader service and decision context.

This article connects naturally to the following service pages.

Author Profile

Profile page for the article author.

Go Komura

Representative of KomuraSoft LLC

Focused on Windows software development, technical consulting, and investigations into failures that are difficult to reproduce.

Back to the Blog