Integrar WebView2 en WinForms con .NET Framework 4.6 – sin NuGet y sin una canalización de compilación moderna

Un informe PDF debe ser reemplazado por una interfaz web interactiva en un sistema ERP basado en WinForms, sin pipeline de compilación. Un informe sobre 4 obstáculos que pueden surgir en el camino.

Hay proyectos en los que desearías con solo teclear Install-Package Microsoft.Web.WebView2 en la consola del administrador de paquetes y listo. Y luego hay proyectos como el nuestro: una aplicación ERP que ha crecido durante años, basada en un framework propietario de .NET Framework 4.6 WinForms, distribuida a través de despliegue por Terminal Server (RDS) a decenas de usuarios en simultáneo. Para los formularios personalizados no hay un flujo de trabajo moderno de NuGet ni acceso abierto a la estructura del proyecto en Visual Studio. La tarea: sustituir informes en papel, impresos durante años, por UIs interactivas hechas en Angular, embebidas directamente en el cliente WinForms. ¿Cómo desplegar los componentes necesarios si no puedes simplemente recrear el proyecto de instalación?

Quien ya haya integrado WebView2 en un proyecto .NET «normal» conoce el camino estándar: paquete NuGet dentro, el runtime normalmente ya está disponible en el cliente, crear un WebView2Environment y listo. En un entorno legacy sin acceso al gestor de paquetes y con docenas de sesiones de RDS en el mismo servidor, prácticamente cada uno de estos pasos es diferente.

Por qué el camino estándar no funciona aquí

Tres condiciones determinan toda la solución:

  1. Sin NuGet / sin proceso de compilación. Los formularios personalizados no se compilan, sino que se entregan como código fuente C# en una carpeta y se cargan en tiempo de ejecución. Por lo tanto, las ensambladuras (assemblies) de WebView2 deben incluirse como referencias DLL estáticas.
  2. Operación multiusuario en RDS. Varios usuarios trabajan simultáneamente en el mismo Terminal Server. WebView2 crea, por defecto, una carpeta userDataFolder; sin separación limpia, las sesiones se sobrescribirían cookies, caché y estado entre sí.
  3. No hay garantía de que el runtime de WebView2 esté preinstalado en todos los clientes. Para una imagen RDS centralizada se necesita un despliegue controlado y con versión fija, en vez del runtime “Evergreen” que se actualiza solo y supone un riesgo incontrolable para un sistema ERP en producción.

Paso 1: Fixed Version Runtime en lugar de Evergreen

En entornos RDS el runtime Evergreen no es la mejor opción: se actualiza automáticamente en segundo plano, lo que puede llevar a un comportamiento inconsistente entre las sesiones en un servidor terminal centralizado. En su lugar, se utiliza el Fixed Version Runtime: se descarga una versión concreta del runtime WebView2 una sola vez, se almacena localmente en el servidor y se referencia explícitamente en el código.

var environmentOptions = new CoreWebView2EnvironmentOptions();
var browserExecutableFolder = @"C:\ErpSystem\WebView2Runtime\";
var environment = await CoreWebView2Environment.CreateAsync(
    browserExecutableFolder,
    userDataFolder,
    environmentOptions);

La variable browserExecutableFolder debe apuntar exactamente a la carpeta donde está la Fixed Version Runtime descomprimida; no a un directorio superior y tampoco directamente al .exe. Una ruta incorrecta aquí es uno de los obstáculos más comunes y normalmente se manifiesta en una poco útil COMException al crear la instancia del entorno.

Paso 2: Referenciación estática de DLL sin NuGet

Como no podemos usar ningún gestor de paquetes, las ensambladuras (assemblies) necesarias (Microsoft.Web.WebView2.Core.dll, Microsoft.Web.WebView2.WinForms.dll, Microsoft.Web.WebView2.Wpf.dll si fuera necesario, así como el correspondiente WebView2Loader.dll para la arquitectura objetivo) deben extraerse manualmente de un paquete NuGet e incluirse como referencias clásicas de proyecto. Importante:

  • La arquitectura (x86/x64) del WebView2Loader.dll debe coincidir con la plataforma objetivo del proyecto; de lo contrario, se produce una BadImageFormatException en tiempo de ejecución, no al compilar.
  • Las DLL deben estar marcadas como «Copy to Output Directory: Copy if newer» para que realmente se copien en el deployment.

Paso 3: Aislamiento de sesión mediante LocalApplicationData

Como varios usuarios trabajan simultáneamente en el mismo host RDS, cada sesión necesita su propia carpeta userDataFolder aislada. Solución: crear la carpeta dinámicamente por usuario de Windows bajo LocalApplicationData.

string userDataFolder = Path.Combine(
    Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
    "ErpSystemWebView2",
    Environment.UserName);

Directory.CreateDirectory(userDataFolder);

Un null o una userDataFolder vacía causa un E_ACCESSDENIED seguro, tan pronto como WebView2 intente escribir en un directorio sobre el que el proceso, bajo la identidad del usuario RDS correspondiente, no tiene permisos de escritura. La especificación explícita y personalizada de la ruta soluciona esto de manera fiable.

Paso 4: Bloqueo Mark-of-the-Web

Un problema que aparece especialmente durante despliegues: Windows marca los archivos copiados desde una carpeta compartida en red o desde Internet con el llamado «Mark of the Web» (MOTW), un flujo alternativo de datos NTFS que etiqueta el archivo como potencialmente inseguro. WebView2, con esta marca, a veces se niega a cargar correctamente contenidos locales.

La solución más fiable es eliminar el MOTW de los archivos relevantes tras el despliegue (por ejemplo, usando Unblock-File en un script PowerShell de deployment) en lugar de intentar evitar la marca de seguridad desde el propio código.

Paso 5: Comunicación bidireccional entre C# y Angular

El objetivo real —la UI interactiva— necesita un canal de comunicación bidireccional. De C# a JavaScript funciona mediante PostWebMessageAsJson:

webView.CoreWebView2.PostWebMessageAsJson(jsonPayload);

Para grandes volúmenes de datos (en nuestro caso unas 200 órdenes, unos 100 KB), hemos visto que ExecuteScriptAsync tras el evento NavigationCompleted es más fiable que PostWebMessageAsJson, ya que así se garantiza que la página Angular realmente está lista para recibir los datos.

El camino inverso, de JavaScript a C#, funciona mediante objetos visibles para COM:

webView.CoreWebView2.AddHostObjectToScript("host", hostObject);

Importante para .NET Framework 4.6: aquí se usa JavaScriptSerializer para la serialización, en vez de System.Text.Json, ya que este último solo está disponible a partir de .NET Core/5+ o versiones más recientes de .NET Framework.

En el lado de Angular, el acceso al host object se inicia (“pull-based”) desde ngOnInit, en lugar de esperar a eventos push, lo que hace el estado de carga más determinista y sencillo de depurar.

Resumen

Al final resultó un setup de WebView2 que, a primera vista, parece poco espectacular: un control de navegador embebido que intercambia datos con Angular. El camino hasta aquí no lo fue: runtime con versión fija en vez de Evergreen, referencias DLL estáticas en lugar de NuGet, userDataFolder específico por usuario en lugar de la ruta estándar, y la eliminación manual del Mark-of-the-Web. Ninguno de estos pasos es complicado por sí solo, pero cada uno de ellos interrumpe el despliegue o produce un error en tiempo de ejecución si solo copias las instrucciones de un tutorial moderno de .NET y lo aplicas en un entorno legacy RDS sin tener en cuenta el contexto. El informe impreso y estático ahora es una UI web interactiva con diseño tipo “carta” (“card”), que simplemente se percibe mucho más moderna y reactiva que solo controles WinForms.

 

Aviso sobre Cookies en WordPress por Real Cookie Banner